M0 (the stream-json spike) was completed earlier but its code was left uncommitted in a sibling worktree and never landed on any branch. This commits it verbatim as the M1 baseline: - NucleicCore: AgentEvent/JSONValue model, AgentBackend protocol, ApprovalCoordinator, ProcessHost, Transcript writer/reader, ClaudeStreamDecoder, in-process MCP approval server, ClaudeCodeBackend - nucleic-spike CLI + fake-claude wire stub - 41 hermetic tests (golden + contract + drift) and live captures under fixtures/claude/2.1.167/ - PLAN.md / ADAPTERS.md updated with M0 findings; docs/M0_RESULTS.md Built and tested with `swift build/test --build-system native` (iCloud codesign workaround). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
6.1 KiB
Nucleic — M0 Spike Results (2026-06-12)
M0 is done: the normalized AgentEvent contract is implemented, tested hermetically, and
proven against the real claude CLI (2.1.167), including the full interactive-approval
round-trip. This doc records what was built, what the live captures pinned, and what changed
in the design docs as a result.
What was built
Swift package (Package.swift, Swift 6, strict concurrency):
| Component | File | Doc it implements |
|---|---|---|
JSONValue lenient JSON model |
Sources/NucleicCore/JSONValue.swift |
BACKEND_PROTOCOL §3.1 |
AgentEvent + payloads (Codable, {"type":…} discriminator) |
Sources/NucleicCore/AgentEvent.swift |
BACKEND_PROTOCOL §3 |
AgentBackend protocol, capabilities, specs |
Sources/NucleicCore/Backend.swift |
BACKEND_PROTOCOL §2 |
Approvals model + Decision |
Sources/NucleicCore/Approval.swift |
BACKEND_PROTOCOL §4 |
RiskClassifier |
Sources/NucleicCore/RiskClassifier.swift |
BACKEND_PROTOCOL §4 |
ApprovalCoordinator (first-responder-wins, always-rules) |
Sources/NucleicCore/ApprovalCoordinator.swift |
RUNTIME §5 |
ProcessHost (line-framed stdio) |
Sources/NucleicCore/ProcessHost.swift |
ADAPTERS §0.1 |
TranscriptWriter/Reader (canonical seq) |
Sources/NucleicCore/Transcript.swift |
BACKEND_PROTOCOL §6, RUNTIME §2.1 |
Claude stream-json → AgentEvent decoder |
Sources/NucleicCore/Claude/ClaudeStreamDecoder.swift |
ADAPTERS §1.4 |
| In-process MCP approval server (HTTP, bearer-token) | Sources/NucleicCore/Claude/MCPApprovalServer.swift |
ADAPTERS §1.2 |
ClaudeCodeBackend |
Sources/NucleicCore/Claude/ClaudeCodeBackend.swift |
BACKEND_PROTOCOL §7.1 |
nucleic-spike CLI |
Sources/nucleic-spike/ |
PLAN M0 |
fake-claude wire-protocol stub |
Sources/fake-claude/ |
OBSERVABILITY B.4 |
| 41 tests (Tier 0 golden + Tier 2 contract + drift) | Tests/NucleicCoreTests/ |
OBSERVABILITY Part B |
swift build --build-system native # see "Build notes" below
swift test --build-system native
.build/arm64-apple-macosx/debug/nucleic-spike --prompt "…" --cwd <repo> --capture --approvals interactive
Live-capture findings (the residual 🟡s from ADAPTERS §6, now pinned)
Captures live in fixtures/claude/2.1.167/ (native NDJSON + normalized golden events,
recorded by the spike on synthetic /tmp repos — no proprietary content). The
RealCaptureDriftTests suite decodes them on every test run; re-recording on a CLI upgrade
is the drift report.
- stdin user-message envelope — CONFIRMED. The nested form works:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":…}]}}. Both live turns ran with it. - Approval round-trip — CONFIRMED end-to-end. Our HTTP MCP server shows
status:"connected"in the init event; the gatedWriteproduced atools/call(approve)with{tool_name, input, tool_use_id}—tool_use_idis present and matches the subsequenttool_useblock id. The{"behavior":"allow","updatedInput":…}reply unblocked the tool; the{"behavior":"deny","message":…}reply produced an errortool_resultcarrying our message verbatim, and no file was written. - ⚠️
--permission-mode defaultmust be pinned. The host's user-leveldefaultMode(e.g."auto") auto-accepts edits before the permission tool is consulted, silently bypassing Nucleic's approval flow. The adapter now passes--permission-mode defaultwheneverApprovalPolicy == .interactive. This is the most important M0 finding. stream_eventpartial shapes — CONFIRMED (message_start,content_block_start,text_deltadeltas,content_block_stop,message_stop), interleaved with wholeassistantmessages exactly as ADAPTERS §1.4 predicted.thinkingblock shape — CONFIRMED:{"type":"thinking","thinking":"…","signature":"…"}.- New native shapes (CLI 2.1.167, not in the docs) — passed through as
.rawby the lenient decoder, exactly as designed; candidates to model later:{"type":"system","subtype":"status",…}(pre-turn status){"type":"system","subtype":"post_turn_summary",…}(turn summary/needs_action){"type":"rate_limit_event",…}(rate-limit telemetry — likely worth surfacing in UI)
- Child inherits host config. The spawned CLI loaded the user's global plugins/MCP
servers (init
toolslist included Cloudflare/Stripe plugin tools). For Nucleic sessions we likely want a more hermetic child (e.g.--strict-mcp-config, curated--settings) — deferred to M1. - Decoder fix from live data: a denied
Writeinitially synthesized a phantomfileChange. Synthesis now waits for the successfultool_result(deniedWriteEmitsNoFileChangeregression test).
Adapter ordering note (amends nothing, clarifies ADAPTERS §0.4)
Approval events (HTTP channel) and stdout events are emitted by independent tasks; the
adapter guarantees per-channel order plus causality (approvalRequested →
approvalResolved → the post-reply toolResult), not a total order. The canonical total
order is assigned at the single-writer ingest (TranscriptWriter/SessionController,
RUNTIME §2.1) — observed in practice when the fake-claude stub raced its own stdout.
Build notes (this machine)
- The repo lives in iCloud Drive; macOS stamps files with
com.apple.provenancexattrs, which the new SwiftBuild backend's codesign step rejects ("resource fork, Finder information, or similar detritus"). Workaround:swift build/test --build-system native(deprecated flag, still functional in 6.3.2). Revisit when the package moves out of iCloud or SwiftBuild handles the xattr. codexis not installed here; the Codex adapter (M3) was untouched.
What M1 should pick up first
SessionControlleractor + the single-writer ingest pipeline (RUNTIME §2) — the spike currently plays that role inline.WorktreeManager(create/diff/integrate, WORKTREE_MANAGER) — wholly unstarted.- Hermetic child config (finding 7) and modeling
rate_limit_event(finding 6). - GRDB metadata store; the spike persists only the JSONL transcript.