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. - 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:
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 --verboseand 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
codexin its JSON/execmode and map its approval/sandbox model onto the sameApprovalRequest. 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
execdoes not — interactive approvals requirecodex 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
SessionControllerand 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/HostMsgset,seq-cursor catch-up, first-responder-wins approvals, per-client verbosity throttling, and the pairing / E2EE (SecureChannel) design. The protocol is a projection ofAgentEvent+ 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
approvein 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
SessionControlleractor 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_totalcounter is the CLI-drift early warning. - Hermetic test pyramid: pure unit (golden event normalization, decision mapping, state
machine, replay invariant) → component tests with a
FakeBackendand real temp git repos → adapter contract tests via fake CLI binaries that speak the real wire protocols (noclaude/codexneeded) → 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-sdksdk.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, notdecision/reason. - Codex app-server (
openai/codexsource, commit7a19b14): real methodsinitialize→thread/start→turn/start→turn/interrupt/thread/resume; v2 approval decision enumaccept/acceptForSession/decline/cancel; streaming deltas +turn/steerexist; token usage via separatethread/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 onturn.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
- M0 — Spike ✅ (done 2026-06-12): spawn
claudein stream-json, parse events intoAgentEvent, print to console. Prove the event contract; capture the residual fixtures above. Shipped as theNucleicCorepackage +nucleic-spikeCLI +fake-claudetest stub with 41 hermetic tests — see docs/M0_RESULTS.md. - M1 — Single-session macOS MVP: one project, create worktree, run a Claude session, transcript + diff + interactive approvals, merge/discard back.
- M2 — Parallel + dashboard: many concurrent sessions, status badges, the "agents at once" view, robust cleanup.
- M3 — Codex backend: second backend against the same protocol; backend picker.
- M4 — iPhone over LAN: sync server + Bonjour, iOS client monitors + approves on the same network.
- M5 — Cloudflare relay + notifications: Worker/DO relay, E2EE pairing, APNs approvals from anywhere.
- 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.