# Nucleic — Plan

A macOS app (codename **Nucleic**) that runs multiple Claude Code / Codex sessions in
parallel, each isolated in its own git worktree + branch, with a SwiftUI cockpit to start,
watch, approve, and merge them. A companion iPhone app connects to the Mac as a remote
client to monitor sessions and answer approval prompts on the go.

The Mac is the **host** (does all real work: spawns CLIs, owns the repos). The iPhone is a
**thin remote client**. This host/client split is the spine of the whole design.

## Locked decisions

| Decision | Choice |
| --- | --- |
| Agents wrapped | Both Claude Code + Codex, behind one pluggable backend protocol |
| Concurrency model | Parallel git worktrees (the defining feature) |
| Agent integration | Wrap the CLI as a subprocess; parse `stream-json` output |
| Platforms | macOS (host) + iPhone (remote client) |
| iPhone role | Remote client to the Mac (monitor + approve) |
| Mac ↔ iPhone transport | LAN first; cloud relay fallback added later |
| Approvals | Interactive, per-session |
| Session resume | **Native CLI resume + a local transcript as the UI source of truth** |
| Distribution | **Deferred** — decide signing/notarization/MAS later |
| Relay hosting | **Build on Cloudflare, but later** — LAN-only for the first usable version |

## Document set

This file is the hub and overview. Each layer is over-specified in its own doc:

| Doc | Layer | Locks down |
| --- | --- | --- |
| [docs/BACKEND_PROTOCOL.md](docs/BACKEND_PROTOCOL.md) | Agent abstraction | `AgentBackend`, the normalized `AgentEvent` model, capability matrix, approval round-trip, per-CLI adapter mappings |
| [docs/ADAPTERS.md](docs/ADAPTERS.md) | Adapter internals | Claude in-process MCP approval server, Codex app-server JSON-RPC client, argv/JSON/method names, source-confirmed wire contracts |
| [docs/SYNC_PROTOCOL.md](docs/SYNC_PROTOCOL.md) | Mac ↔ iPhone | `ClientMsg`/`HostMsg`, `seq`-cursor catch-up, E2EE pairing (`SecureChannel`), first-responder approvals, verbosity throttling |
| [docs/WORKTREE_MANAGER.md](docs/WORKTREE_MANAGER.md) | Git | Worktree lifecycle, diff plumbing, merge/rebase/squash/PR + conflicts, crash reconciliation |
| [docs/LOCKING.md](docs/LOCKING.md) | Concurrency | Parent-scoped file locks (dedicated `LockManager`), all-or-nothing acquisition + demotion queue, merge-into-parent release (mediated + git poll), nested worktrees + cascade re-target |
| [docs/RUNTIME_ARCHITECTURE.md](docs/RUNTIME_ARCHITECTURE.md) | Integration | Object graph, single-writer event pipeline, GRDB schema, approval bridging, Swift 6 concurrency |
| [docs/UX_MACOS.md](docs/UX_MACOS.md) | macOS UX | Navigation, dashboard, session detail, approval bar, create/approve/integrate flows |
| [docs/UX_IOS.md](docs/UX_IOS.md) | iPhone UX | Notification-first IA, approval flows, connectivity states, biometric gating |
| [docs/OBSERVABILITY_AND_TESTING.md](docs/OBSERVABILITY_AND_TESTING.md) | Cross-cutting | Local-first redaction-aware observability + hermetic test pyramid with fake-CLI fixtures |

**The spine across all of them:** one normalized `AgentEvent` stream with a canonical monotonic
`seq`, written once by the `SessionController` (single writer), with the local transcript as the
source of truth — UI, sync, and persistence are all subscribers. Every backend/transport
difference is a declared *capability*, never an assumption. The single seam that unifies two very
different approval mechanisms is `ApprovalCoordinator.await(...) -> Decision`.

## Architecture

```
┌───────────────────────── macOS host ─────────────────────────┐
│  SwiftUI app                                                  │
│  ┌──────────────┐   ┌────────────────────┐  ┌──────────────┐  │
│  │ Session UI   │   │ AgentBackend layer │  │ Git/Worktree │  │
│  │ (panes,diff, │◄─►│  ClaudeCodeBackend │  │   manager    │  │
│  │  approvals)  │   │  CodexBackend      │  └──────┬───────┘  │
│  └──────┬───────┘   └─────────┬──────────┘         │          │
│         │                     │ spawns + stream-json│         │
│         │            ┌────────▼────────┐    ┌───────▼───────┐ │
│         │            │ child processes │    │  git worktrees│ │
│         │            │ claude / codex  │    │  + branches   │ │
│         │            └─────────────────┘    └───────────────┘ │
│         │                                                     │
│  ┌──────▼──────────────── Sync server ──────────────────────┐ │
│  │  Network.framework (Bonjour/LAN)  +  relay client (later)│ │
│  └──────────────────────────┬──────────────────────────────┘ │
└─────────────────────────────┼────────────────────────────────┘
                              LAN  ╲  relay fallback (Cloudflare, later)
                               │    ╲
                        ┌──────▼─────────────┐
                        │   iPhone client     │
                        │ monitor + approve   │
                        └─────────────────────┘
```

## Core domain model

A small, backend-agnostic model everything else is built on.

- **Project** — a registered git repo (root path, default branch, per-project config:
  setup script, approval defaults, default backend).
- **Session** — one agent run. Belongs to a Project, owns a **Worktree**, has a backend
  type, status (`idle / running / awaiting-approval / awaiting-input / finished / error`),
  and a transcript.
- **Worktree** — `path`, `branch`, base commit, dirty/clean, ahead/behind counts.
- **TranscriptEvent** — the normalized event stream (see backend section). Stored as JSONL
  on disk; metadata in the DB. **This local transcript is the UI's source of truth.**
- **ApprovalRequest** — tool name, input, risk, owning session; resolved with allow/deny
  (+ optional "always for this tool/session").

## Pluggable agent backend (highest-leverage abstraction)

One protocol normalizes two genuinely different CLIs:

```swift
protocol AgentBackend {
    func start(in worktree: Worktree, prompt: String, opts: RunOptions) -> AsyncStream<AgentEvent>
    func send(_ input: AgentInput)          // follow-up turns
    func respond(to: ApprovalRequest, _ decision: Decision)
    func interrupt()
    func resume(sessionID: String) -> AsyncStream<AgentEvent>
}
```

`AgentEvent` is a unified enum:
`.assistantText, .toolUse, .toolResult, .approvalRequest, .usage, .turnComplete, .error`.
Each backend translates its native output into it.

- **ClaudeCodeBackend** — spawn
  `claude -p --output-format stream-json --input-format stream-json --verbose` and parse the
  JSONL events. For interactive approvals, register a tiny **in-process MCP "approval"
  server** and pass `--permission-prompt-tool`; Claude Code calls it on each gated tool, and
  we block that call until the UI resolves it. This is the supported way to get interactive
  per-session approvals from the CLI rather than scraping a TTY. Resume via
  `--resume <session_id>`.
- **CodexBackend** — spawn `codex` in its JSON/`exec` mode and map its approval/sandbox
  model onto the same `ApprovalRequest`. Codex's approval semantics differ (sandbox +
  ask-for-approval levels), so the adapter normalizes them — exactly why the protocol earns
  its keep.

Build **ClaudeCodeBackend first and fully**, then add Codex against the same protocol once
the event/approval contract is proven.

> The full event model, approval round-trip, capability matrix, and per-CLI adapter mappings
> are over-specified in [docs/BACKEND_PROTOCOL.md](docs/BACKEND_PROTOCOL.md). **Key finding:**
> Claude Code supports interactive approvals in headless mode, but Codex `exec` does not —
> interactive approvals require `codex app-server` (JSON-RPC), so that is the default Codex
> adapter. The protocol models this as a per-backend *capability*.
>
> Message-by-message adapter internals — the Claude in-process MCP approval server, the Codex
> app-server JSON-RPC client, argv/JSON examples, and a confidence-tagged validation backlog —
> are in [docs/ADAPTERS.md](docs/ADAPTERS.md). The single seam that unifies two very different
> approval mechanisms is `ApprovalCoordinator.await(...) -> Decision`.

### Resume strategy

Use each CLI's **native resume** (`--resume`/session files) to restart the agent's own
context, but treat **our local JSONL transcript as the UI source of truth** for rendering
history. On resume we replay our transcript into the UI immediately and reattach the live
event stream; we don't depend on the CLI to re-emit past turns. We can also import Claude
Code's own session files (`~/.claude/projects`) to bootstrap or recover.

## Git / worktree manager

The Conductor-defining feature. Wraps `git` via subprocess (plain git over libgit2/SwiftGit2
for simplicity and predictable behavior).

- Create session → `git worktree add .nucleic/worktrees/<slug> -b nucleic/<slug>` off the
  chosen base.
- Run an optional **per-project setup script** in the new worktree (e.g. `npm install`)
  before the agent starts.
- Track diff vs base, ahead/behind, conflicts.
- Finish a session → review diff → **merge / rebase / squash back**, open a PR (`gh`), or
  discard. Clean up worktree + branch.
- Guardrails: worktree count limits, disk-usage checks, orphan cleanup on crash.

> Fully specified in [docs/WORKTREE_MANAGER.md](docs/WORKTREE_MANAGER.md): the lifecycle
> state machine, exact diff plumbing (base-SHA anchor + working-tree capture so uncommitted
> agent work shows), finalize/integrate (merge/rebase/squash/PR) with conflict handling, and
> crash reconciliation against `git worktree list`.

## macOS UI (SwiftUI)

- **Sidebar**: projects → sessions, with live status badges.
- **Session detail**: transcript (assistant text, collapsible tool calls), a **live diff
  view**, log tail, and an inline **approval bar** when `awaiting-approval`.
- **New-session sheet**: pick project, base branch, backend (Claude/Codex), initial prompt.
- **Dashboard**: all running sessions at a glance — the "10 agents at once" view.
- Menu-bar item + native notifications when a session needs attention or finishes.

> Information architecture, key screens (dashboard, session detail, approval bar), and the
> create/approve/integrate flows are wireframed in [docs/UX_MACOS.md](docs/UX_MACOS.md). The
> UI holds no canonical state — it observes the `SessionController` and sends intents, so Mac
> and iPhone are two renderers of the same authority.

## Approvals flow (interactive, per-session)

Approval originates in the backend (MCP approval tool for Claude; sandbox callback for
Codex) → becomes an `ApprovalRequest` → surfaced in the session UI **and** pushed to any
connected iPhone → first responder wins → decision flows back and unblocks the agent.
Support "always allow this tool in this session" to cut noise, persisted per-session. A
pre-configured policy layer can come later; the model already supports it.

## Mac ↔ iPhone transport

- **LAN (first)**: Bonjour + Network.framework (`NWListener`/`NWConnection`). Direct,
  low-latency when co-located. This is all v1 needs.
- **Relay fallback (later, on Cloudflare)**: a hosted WebSocket relay — a **Worker + Durable
  Object** per paired-device room (the DO holds connections and brokers messages).
- **Security**: device pairing (QR / code) and **end-to-end encryption** so the relay only
  ever sees ciphertext — it must never be able to read code or transcripts.
- **Protocol**: a small message set, identical over LAN and relay (only the transport swaps):
  `session.list`, `session.subscribe`, `event` (streamed transcript deltas),
  `approval.request` / `approval.respond`, `input.send`.

> Fully specified in [docs/SYNC_PROTOCOL.md](docs/SYNC_PROTOCOL.md): the
> `ClientMsg`/`HostMsg` set, `seq`-cursor catch-up, first-responder-wins approvals,
> per-client verbosity throttling, and the pairing / E2EE (`SecureChannel`) design. The
> protocol is a projection of `AgentEvent` + the approval round-trip, and is
> transport-agnostic so LAN (v1) and the Cloudflare relay (later) share one message layer.

## iPhone app

Subscribes to the session list + live events, renders transcripts and diffs read-friendly,
and — the key interaction — **answers approval prompts** (push notifications via the relay,
once relay exists) so you can unblock an agent from your phone. Mirrors a subset of the Mac
UI; no local git/CLI.

> Information architecture, screens, and the notification-first approval flows are wireframed
> in [docs/UX_IOS.md](docs/UX_IOS.md). The phone is a pure projection of host state (scope
> `approve` in v1: view + approve + send follow-up input), is notification-first (Live
> Activity / widget / actionable pushes), gates risky approvals behind biometrics, and is
> explicit about connectivity (no optimistic actions; stale data is always labeled).

## Persistence

- **GRDB (SQLite)** for projects/sessions/approvals metadata — predictable, fast, good for a
  write-heavy event log. (SwiftData is the alternative.)
- **Transcripts as JSONL on disk** (mirrors how Claude Code stores sessions), referenced
  from the DB and serving as the UI source of truth.

> The object graph, single-writer event pipeline, GRDB schema, approval bridging, resume
> wiring, and Swift 6 concurrency model are specified in
> [docs/RUNTIME_ARCHITECTURE.md](docs/RUNTIME_ARCHITECTURE.md). The `SessionController` actor
> is the single writer of each session's state; UI, sync, and DB all subscribe.

## Observability & testing

- **Local-first, redaction-aware observability**: OSLog with privacy specifiers (structural
  metadata public, content private), signpost tracing over the event pipeline, an in-app
  Diagnostics panel, and an opt-in per-session native-stream capture that doubles as a test
  fixture. An `unknown_events_total` counter is the CLI-drift early warning.
- **Hermetic test pyramid**: pure unit (golden event normalization, decision mapping, state
  machine, replay invariant) → component tests with a `FakeBackend` and real temp git repos →
  **adapter contract tests via fake CLI binaries** that speak the real wire protocols (no
  `claude`/`codex` needed) → opt-in nightly **live smoke** that re-records fixtures and emits a
  drift diff. Plus Noise/E2EE security tests and a Swift 6 strict-concurrency + TSAN gate.

> Full design in [docs/OBSERVABILITY_AND_TESTING.md](docs/OBSERVABILITY_AND_TESTING.md). The
> core loop: a debugging capture today is a regression fixture tomorrow, and a CLI wire-format
> change trips the same drift signal from both the running app and nightly CI.

## Tech stack

Swift 6 + SwiftUI · `Network.framework` · `Foundation.Process` (or swift-subprocess) for
child processes · GRDB · swift-async-algorithms for event streams · `git` / `gh` CLIs ·
Cloudflare Worker + Durable Object relay (later).

## Protocol validation status

A source-validation pass (2026-06-11) resolved the load-bearing adapter unknowns against primary
sources, so M0 starts from confirmed contracts rather than guesses. Detail in
[docs/ADAPTERS.md](docs/ADAPTERS.md) §0.

**Confirmed from source:**
- **Claude permission tool** (SDK docs "Custom permission prompt tool", archived 2025-05-31 +
  `@anthropic-ai/claude-agent-sdk` `sdk.d.ts`): tool receives `{tool_name, input, tool_use_id?}`;
  returns a **JSON-stringified** `{behavior:"allow",updatedInput}` / `{behavior:"deny",message}`
  inside an MCP text block — `behavior`/`message`, **not** `decision`/`reason`.
- **Codex app-server** (`openai/codex` source, commit `7a19b14`): real methods `initialize` →
  `thread/start` → `turn/start` → `turn/interrupt` / `thread/resume`; v2 approval decision enum
  `accept`/`acceptForSession`/`decline`/`cancel`; streaming deltas + `turn/steer` exist; token
  usage via separate `thread/tokenUsage/updated`.
- **Codex exec** (`codex-rs/exec/src/exec_events.rs`, rust-v0.139.0): a **distinct snake_case
  schema** from app-server's camelCase → needs its own decoder; usage inline on `turn.completed`.

**Residual (needs a live capture in M0, not docs):** Claude's stdin user-message envelope, the
`thinking` block + `stream_event` partial shapes, verbatim golden sample lines for both CLIs, and
Codex exec's exact fail-closed behavior on a gated action. These are precisely what the M0
fixtures harness produces.

**M0 capture pass (2026-06-12):** all Claude-side residuals are now pinned against the real CLI
(2.1.167) — stdin envelope, `thinking`/`stream_event` shapes, and the full approval round-trip,
plus one critical finding: the adapter must pass `--permission-mode default` or the host's
user-level `defaultMode` (e.g. `"auto"`) silently bypasses the permission tool. Golden captures
live in `fixtures/claude/2.1.167/`. Details in [docs/M0_RESULTS.md](docs/M0_RESULTS.md).

**M3 Codex pass (2026-06-20):** the Codex backends are built and **live-validated against
codex-cli 0.141.0**. The app-server protocol was regenerated (`codex app-server generate-ts`)
and the wire shapes confirmed byte-for-byte; a real turn + resume were driven end-to-end. Both
the interactive app-server path (`CodexAppServerBackend`) and the unattended `codex exec`
fallback (`CodexExecBackend`) ship, with golden captures in `fixtures/codex-appserver/0.141.0/`
and `fixtures/codex-exec/0.141.0/`. Codex is selectable via the model picker (a `gpt-*` SKU
routes to Codex). Remaining: Codex parity for Nucleic Control features (containers, conflict
locks, git interceptor, `host_exec`). Details in [docs/ADAPTERS.md](docs/ADAPTERS.md) §2.7.

## Milestones

1. **M0 — Spike** ✅ *(done 2026-06-12)*: spawn `claude` in stream-json, parse events into
   `AgentEvent`, print to console. Prove the event contract; capture the residual fixtures
   above. Shipped as the `NucleicCore` package + `nucleic-spike` CLI + `fake-claude` test
   stub with 41 hermetic tests — see [docs/M0_RESULTS.md](docs/M0_RESULTS.md).
2. **M1 — Single-session macOS MVP**: one project, create worktree, run a Claude session,
   transcript + diff + interactive approvals, merge/discard back.
3. **M2 — Parallel + dashboard**: many concurrent sessions, status badges, the
   "agents at once" view, robust cleanup.
4. **M3 — Codex backend**: second backend against the same protocol; backend picker.
5. **M4 — iPhone over LAN**: sync server + Bonjour, iOS client monitors + approves on the
   same network.
6. **M5 — Cloudflare relay + notifications**: Worker/DO relay, E2EE pairing, APNs approvals
   from anywhere.
7. **M6 — Polish**: per-project setup scripts, PR creation, policy approvals, usage/cost
   display, then revisit **distribution** (signing / notarization / MAS).

## Deferred / revisit later

- **Distribution** — signing, notarization, and whether to ship outside the Mac App Store
  (App Sandbox is likely impractical given arbitrary subprocess + repo access). Decide near
  M6.
- **Cloud relay** — built on Cloudflare, but only after LAN sync (M4) is solid.
