Files
nucleic/PLAN.md
T

311 lines
20 KiB
Markdown

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