Files
nucleic/PLAN.md
T

20 KiB

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 Agent abstraction AgentBackend, the normalized AgentEvent model, capability matrix, approval round-trip, per-CLI adapter mappings
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 Mac ↔ iPhone ClientMsg/HostMsg, seq-cursor catch-up, E2EE pairing (SecureChannel), first-responder approvals, verbosity throttling
docs/WORKTREE_MANAGER.md Git Worktree lifecycle, diff plumbing, merge/rebase/squash/PR + conflicts, crash reconciliation
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 Integration Object graph, single-writer event pipeline, GRDB schema, approval bridging, Swift 6 concurrency
docs/UX_MACOS.md macOS UX Navigation, dashboard, session detail, approval bar, create/approve/integrate flows
docs/UX_IOS.md iPhone UX Notification-first IA, approval flows, connectivity states, biometric gating
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.
  • Worktreepath, 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:

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. 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. 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: 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. 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: 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. 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. 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. 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 §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 initializethread/startturn/startturn/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.

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 §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.
  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.