# 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 |

```sh
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.

1. **stdin user-message envelope — CONFIRMED.** The nested form works:
   `{"type":"user","message":{"role":"user","content":[{"type":"text","text":…}]}}`.
   Both live turns ran with it.
2. **Approval round-trip — CONFIRMED end-to-end.** Our HTTP MCP server shows
   `status:"connected"` in the init event; the gated `Write` produced a
   `tools/call(approve)` with `{tool_name, input, tool_use_id}` — **`tool_use_id` is
   present** and matches the subsequent `tool_use` block id. The
   `{"behavior":"allow","updatedInput":…}` reply unblocked the tool; the
   `{"behavior":"deny","message":…}` reply produced an error `tool_result` carrying our
   message verbatim, and no file was written.
3. **⚠️ `--permission-mode default` must be pinned.** The host's user-level
   `defaultMode` (e.g. `"auto"`) auto-accepts edits *before* the permission tool is
   consulted, silently bypassing Nucleic's approval flow. The adapter now passes
   `--permission-mode default` whenever `ApprovalPolicy == .interactive`. This is the most
   important M0 finding.
4. **`stream_event` partial shapes — CONFIRMED** (`message_start`, `content_block_start`,
   `text_delta` deltas, `content_block_stop`, `message_stop`), interleaved with whole
   `assistant` messages exactly as ADAPTERS §1.4 predicted.
5. **`thinking` block shape — CONFIRMED:** `{"type":"thinking","thinking":"…","signature":"…"}`.
6. **New native shapes (CLI 2.1.167, not in the docs)** — passed through as `.raw` by 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)
7. **Child inherits host config.** The spawned CLI loaded the user's global plugins/MCP
   servers (init `tools` list 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.
8. **Decoder fix from live data:** a *denied* `Write` initially synthesized a phantom
   `fileChange`. Synthesis now waits for the successful `tool_result`
   (`deniedWriteEmitsNoFileChange` regression 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.provenance` xattrs,
  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.
- `codex` is not installed here; the Codex adapter (M3) was untouched.

## What M1 should pick up first

1. `SessionController` actor + the single-writer ingest pipeline (RUNTIME §2) — the spike
   currently plays that role inline.
2. `WorktreeManager` (create/diff/integrate, WORKTREE_MANAGER) — wholly unstarted.
3. Hermetic child config (finding 7) and modeling `rate_limit_event` (finding 6).
4. GRDB metadata store; the spike persists only the JSONL transcript.
