311 lines
20 KiB
Markdown
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.
|