docs(teardowns): commit the Claude Code teardown series

db6beb978105 · AtlantisPleb · · parent f3bad5277b67

docs(teardowns): commit the Claude Code teardown series

Six reports comparing Claude Code against the OpenAgents coder across the
query loop, tool surface and sandboxing, subagent orchestration, terminal
UI, context compaction and memory, and permissions/cost/telemetry. They are
the evidence behind the structural-gap list in the autoimprovement plan —
absent compaction, absent prompt history, a regex refusal table where a
parser belongs — so the plan cited paths that were not in the repository.

Add an index naming the reading order and the boundary these reports do not
draw for themselves: a gap is a candidate for the measured loop, not a
verdict, and some of them are deliberate positions rather than backlog.
Normalize one absolute home path to the ~-relative form the others use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7q2vA5LJroLTR6ZFbRq6j
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified docs/coder/autoimprove.md
  • added docs/teardowns/cc/01-architecture-query-loop.md
  • added docs/teardowns/cc/02-tool-surface-sandboxing.md
  • added docs/teardowns/cc/03-subagents-fleet-orchestration.md
  • added docs/teardowns/cc/04-terminal-ui-components-theme.md
  • added docs/teardowns/cc/05-context-compaction-memory-skills.md
  • added docs/teardowns/cc/06-permissions-cost-telemetry-bridge.md
  • added docs/teardowns/cc/README.md

Diff

8 files changed, +424 -3

docs/coder/autoimprove.md modified +3 -3

@@ -84,9 +84,9 @@ plugins, `password-recovery` for file forensics.

84 84
85 85
### 2.3 Harness and runtime: what the coder is missing structurally
86 86
87
The Claude Code teardown series (local working set, `docs/teardowns/cc/`,
88
verifiable directly against `packages/openagents-cli/src/`) names the
89
structural gaps in cost order:
87
The Claude Code teardown series (`docs/teardowns/cc/`, verifiable directly
88
against `packages/openagents-cli/src/`) names the structural gaps in cost
89
order:
90 90
91 91
- **No compaction.** The coder has per-result output caps and nothing else;
92 92
  long tasks pay quadratic transcript replay. `schemelike-metacircular-eval`
docs/teardowns/cc/01-architecture-query-loop.md added +72

@@ -0,0 +1,72 @@

1
# Teardown 01 — Architecture, Lifecycle, Query Loop & Entrypoints
2
3
**Claude Code** (`~/work/projects/repos/cc`) vs **OpenAgents Coder** (`packages/openagents-cli/src`)
4
5
## Component / Subsystem Breakdown
6
7
| Concern | Claude Code | OpenAgents Coder |
8
|---|---|---|
9
| Bootstrap | `entrypoints/cli.tsx` (302 ln) | `main.ts` (70 ln) → `cli.ts` (4,599 ln) |
10
| App shell | `main.tsx` (4,683 ln) + `replLauncher.tsx` + ink TUI | `runtime.ts` (Effect Layer graph) + `coder-ui.ts` (raw ANSI) |
11
| Query loop | `query.ts` (1,729 ln), `QueryEngine.ts` (1,295 ln) | `coder-thread.ts` (1,195 ln), `coder-session.ts` (1,308 ln) |
12
| Tool execution | `services/tools/StreamingToolExecutor.ts` + `utils/queryHelpers.ts` | inline in `coder-thread.ts` (`merge()` fan-out) |
13
| Sub-agents | `Task.ts` + `tasks/*` (6 task types) | `coder-delegate.ts` (1,994 ln) + `coder-child-gateway.ts` |
14
| Persistence | `utils/sessionStorage.ts` (local JSONL) | `coder-transcript.ts` (server-owned threads API) |
15
16
## Claude Code Implementation Details
17
18
### Entrypoints
19
20
`entrypoints/cli.tsx` sets `COREPACK_ENABLE_AUTO_PIN=0`, raises heap for remote containers, applies ablation-baseline env vars **at module scope** (BashTool/AgentTool capture them into module consts at import time — `init()` would be too late), then `main()` dispatches through dynamic imports so `--version` costs zero module evaluations. Fast paths exist for `--dump-system-prompt`, Chrome native host, `--computer-use-mcp`, `cc://` URL rewrite, and macOS deep links (detected via LaunchServices' overwritten `__CFBundleIdentifier`). `main.tsx:585 main()` then does Windows PATH-hijack defense (`NoDefaultCurrentDirectoryInExePath`), SIGINT routing that defers to print-mode handlers, and renders `screens/REPL.tsx` (5,005 ln) through a lazy-loaded ink tree. Compile-time dead-code elimination comes from `feature('FLAG')` (`bun:bundle`) — gated strings are physically absent from external builds — while *runtime* gates (statsig, env) are snapshotted once per query in `query/config.ts:buildQueryConfig()`. A second entrypoint family, `entrypoints/sdk/` (`agentSdkTypes.ts`, `coreSchemas.ts`, `controlSchemas.ts`), exposes the same loop as an embeddable SDK, plus `entrypoints/mcp.ts` for MCP-server mode.
21
22
### The query loop state machine
23
24
`query.ts:219 query()` wraps `queryLoop()`, an `AsyncGenerator<StreamEvent|Message|TombstoneMessage, Terminal>`. Three state classes are deliberately separated:
25
26
1. **Immutable params** — destructured once (`systemPrompt`, `canUseTool`, `fallbackModel`, `maxTurns`…). Never reassigned.
27
2. **Mutable cross-iteration `State` struct** — `messages`, `autoCompactTracking`, `maxOutputTokensRecoveryCount`, `hasAttemptedReactiveCompact`, `turnCount`, `pendingToolUseSummary`, `transition`. Continue sites write `state = {...state, x}` instead of nine loose assignments.
28
3. **`QueryConfig` snapshot** — sessionId + gates frozen at entry, so "a pure reducer can take (state, event, config)".
29
30
Each iteration: content-replacement budget enforcement → **microcompact** (operates purely by `tool_use_id`, invisible to prompt cache, composes with cached-MC) → **context-collapse projection** (read-time replay of a commit log; runs *before* autocompact so granular context survives if collapse alone gets under threshold) → **autocompact** with a `consecutiveFailures` circuit breaker → streaming model call (`deps.callModel`) inside a `while(attemptWithFallback)` retry wrapper → `StreamingToolExecutor` drains tool_use blocks → results appended → loop while any `toolUseBlock`s existed → `handleStopHooks` (a stop hook can veto completion and inject another iteration).
31
32
The `Terminal` return type enumerates exits: `'completed' | 'blocking_limit' | 'image_error' | 'model_error' | 'aborted_streaming' | 'prompt_too_long' | 'stop_hook_prevented'`, plus *transition* reasons (`max_output_tokens_escalate`, `reactive_compact_retry`, `collapse_drain_retry`) that mutate state and `continue` instead of returning. `task_budget.remaining` is carried across compaction boundaries so the server can count spend after history is summarized away.
33
34
### Supporting machinery
35
36
- **`StreamingToolExecutor`**: tracks each block as `queued→executing→completed→yielded`; concurrency-*safe* tools run in parallel, non-concurrent tools take exclusive access with order preserved; results buffer and emit in arrival order; a `siblingAbortController` kills sibling subprocesses on Bash error without aborting the parent turn; `discard()` drops stale executors after a mid-stream model fallback so orphaned `tool_results` can't be yielded.
37
- **DI seam**: `query/deps.ts` — `{callModel, microcompact, autocompact, uuid}`, typed as `typeof fn` so signatures stay synced; scope "intentionally narrow to prove the pattern."
38
- **`QueryEngine.ts`**: class owning conversation lifecycle (messages, readFileState cache, usage totals, permission denials, discovered skills) for the SDK/headless path; `ask()` at :1186 wraps it. `queryTracking.chainId/depth` tags every analytics event.
39
- **Tasks** (`Task.ts`): `TaskType = local_bash | local_agent | remote_agent | in_process_teammate | local_workflow | monitor_mcp | dream`; `TaskStatus = pending|running|completed|failed|killed` with `isTerminalTaskStatus` guarding dead-teammate message injection.
40
- **Prefetch overlap**: memory prefetch opened with `using` (disposed on all generator exits), skill discovery started per iteration and consumed post-tools — I/O hidden under the stream.
41
42
## OpenAgents Coder Implementation State
43
44
Entry is Effect-native: `cli.ts` defines 58 `Command.make` subcommands under a tagged `CliError` union (JSON error output with exit codes via `--json`), wired to `NodeRuntime.runMain` over `runtime.ts`'s composition of ~25 Layers (fetch transport + network policy, OS keychain credentials, git runner, computer-pairing stack). The `coder` command (`cli.ts:1896`) takes prompt/plain/offline/dev/resume/reasoning/model/child/concurrency flags; `--dev` auto-starts a local dev server behind a spinner.
45
46
There is **no monolithic query loop**. The closest analog:
47
48
- **`ReplySource`** (`coder-session.ts:283`): `reply(prompt, signal): AsyncIterable<ReplyChunk>` plus optional `steer()`, `cycleBackend()`, `history()`, `cycleReasoning()`, `useTools()`, `describeContext()`. `DummyReplySource` exercises every chunk kind offline.
49
- **`ThreadReplySource.reply()`** (`coder-thread.ts:562+`): per-turn budget reset → retrieval → steps until text-only answer or `MAX_TOOL_STEPS = 100` (then a forced `mustAnswer` user message: "answer now… say plainly what is still unfinished"). All calls in a round run concurrently via `merge()`; results push back in call order. Steering splices queued prompts into the wire transcript mid-turn and yields a `steered` chunk so the UI relocates the reader's entry. `finally { await this.refresh() }` reads spend even on interrupt.
50
- **`CoderSession.run()`** (`coder-session.ts:1025`): owns entries (`assistant|reasoning|tool|you` with `settled` flags), withdraws an empty opening caret entry when the turn opens with reasoning/tool use, routes chunks to entries, drains the pending queue (`submit(mode: "steer"|"queue")`).
51
- **Fleet** (`coder-delegate.ts`): `DelegateHarness` implemented by `SelfHarness`, `DevinHarness` (ACP), `ClaudeCodeHarness`, `CodexHarness`, `OpencodeHarness`. Fleet caps concurrency, refuses `fleet_full` *before* queue overflow, registers children as visible `pending` tasks, writes JSONL transcripts to a `0700` tmpdir, retries with `resumeSessionId`. **`coder-child-gateway.ts`** lends the session's thread grant over a loopback HTTP endpoint, flattening tool exchanges to plain turns (the proxy can't carry paired `function_call`/`_output` items) and never exposing the token to the child.
52
- **Transcript** (`coder-transcript.ts`): append-only POST to `/api/v1/threads/{id}/events`, background pump with backoff, "must never cost the session anything" — enqueue is sync, persistent failure surfaces one notice, never throws into the loop. Vocabulary: `turn.user | turn.reasoning | tool.ran | turn.assistant`.
53
54
## Detailed Gap Analysis
55
56
1. **No context management.** CC has five cooperating mechanisms (content-replacement budget, microcompact, autocompact+circuit-breaker, collapse projection, snip tombstones) plus cross-compact budget accounting. Coder has *none* — grep finds zero compaction logic. A long session walks into the provider context ceiling with no recovery, and `MAX_TOOL_STEPS=100` rounds will get there quickly.
57
2. **Loop robustness.** CC recovers from mid-stream model fallback (tombstoning orphans, discarding executors), escalates `maxOutputTokens`, resizes images, detects prompt-too-long, and lets stop hooks force continuation. Coder's failure mode is `throw ThreadUnavailable` → fleet-level retry with resume; there is no in-loop degradation, no fallback model, no partial-result salvage.
58
3. **No formal terminal state.** CC's `Terminal` union drives lifecycle notifications, analytics, and retry policy. Coder's `reply()` just ends; callers cannot distinguish "answered," "hit budget," "provider died," or "user aborted" except by exception type.
59
4. **Concurrency semantics.** CC classifies tools safe/exclusive and preserves result order under parallelism; Coder runs every round fully parallel — correct for its current read-only-ish toolset, unsafe once write tools contend (child 2's territory, but the loop is where it must be enforced).
60
5. **Testing/DI seams.** CC's `deps` injection makes the 1,700-line loop unit-testable without network. Coder's model call and budget refresh are hardwired in `ThreadReplySource`; tests lean on `DummyReplySource`, which exercises rendering, not loop policy.
61
6. **Entrypoint surface.** CC ships SDK (`ask()`/`QueryEngine`), headless `-p` with stream-json, MCP serve, deep links. Coder ships TUI + `--plain` + one-shot prompt; `ReplySource` is SDK-shaped but not exported as a stable library surface, and there is no machine-readable output mode.
62
7. **Trade-off worth keeping:** Coder's server-owned transcript and grant-lending gateway are *architecturally better* than CC's local-file persistence for cost control and multi-machine resume. Do not port CC's persistence model; port only the loop's self-management.
63
64
## Actionable Porting Recommendations
65
66
1. **Add a compaction stage to `ThreadReplySource.reply()`** before each step: estimate tokens; past a threshold, summarize the oldest completed steps into one injected system note and drop their wire messages (the server thread retains raw `tool.ran`/`turn.*` events, so nothing is lost durably — this is cheaper for Coder than for CC precisely because history lives server-side). Start with tool-output elision keyed by callId — the exact microcompact trick.
67
2. **Give `ReplySource.reply()` a terminal value**, not just termination: `AsyncIterable<ReplyChunk, TurnTerminal>` with `reason: 'completed'|'budget'|'provider_error'|'aborted'|'max_steps'`. Map these onto `CoderSession` notices and fleet retry decisions, mirroring `query.ts`'s Terminal/transition split.
68
3. **Extract `threadDeps`** (`callModel`, `refreshBudget`, `uuid`) out of `ThreadReplySource`, typed `typeof fn` à la `query/deps.ts`, so loop policy is testable without a proxy.
69
4. **On provider failure, fall back before failing**: catch stream errors in-loop and offer the existing tier-cycling machinery as an automatic downgrade (one retry), reserving `ThreadUnavailable` for exhaustion.
70
5. **Port executor discipline**: extend `CoderTool` with `concurrencySafe: boolean`; in the round runner, run safe tools via `merge()`, serialize unsafe ones, and emit results in call order regardless of finish order.
71
6. **Post-turn validators (stop-hook analog)**: an optional `validate?: (turn) => 'accept'|{inject: string}` on `ReplySource`, enabling checks like "export file exists" that can force exactly one more step.
72
7. **Headless SDK mode**: export `openThread`/`CoderSession` from package root and add `openagents coder --print --output-format json` emitting the `ReplyChunk` stream — this unlocks CI/embedding use with near-zero new logic, since the loop is already an AsyncIterable.
docs/teardowns/cc/02-tool-surface-sandboxing.md added +76

@@ -0,0 +1,76 @@

1
# Claude Code Teardown 02: Tool Surface, Schemas & Sandboxing
2
3
Scope: `~/work/projects/repos/cc` (`Tool.ts`, `tools.ts`, `tools/`, `native-ts/`, `schemas/`, `utils/sandbox/`) versus `packages/openagents-cli/src/` (`coder-tools.ts`, `coder-tool-families.ts`, `coder-tool-budget.ts`, `coder-shell.ts`, `coder-plugins.ts`, `coder-plugin-engine.ts`, `coder-capability.ts`) and `plugins/` (Rust PDK + guest crates).
4
5
## Component Breakdown
6
7
| Subsystem | Claude Code | OpenAgents Coder |
8
|---|---|---|
9
| Tool contract | `Tool.ts` (792 ln), `buildTool()` | `CoderTool` interface in `coder-tools.ts` |
10
| Tool registry | `tools.ts`: presets, deny filters, pool assembly | `cli.ts` `declareTools()` closure |
11
| Built-in tools | 40+ under `tools/` | 4 factories + `capability` |
12
| Shell safety | tree-sitter parse, sandbox runtime, permissions | static regex refusal table |
13
| Extensibility | MCP client + plugin scaffolding | WASM plugin host + Rust PDK |
14
| Result sizing | per-tool `maxResultSizeChars` | per-model-family token budgets |
15
16
## Claude Code Implementation
17
18
**The Tool contract** (`Tool.ts`) is a ~40-member type parameterized over `<Input, Output, Progress>`:
19
20
- *Semantics predicates*: `isConcurrencySafe(input)`, `isReadOnly(input)`, `isDestructive(input)`, `isEnabled()`, `isOpenWorld(input)`, `interruptBehavior(): 'cancel' | 'block'`, `isSearchOrReadCommand(input)` — the query loop uses these to decide parallel dispatch, permission prompts, and interruption behavior.
21
- *Schemas*: `inputSchema` (zod), optional hand-written `inputJSONSchema` override, `outputSchema` (zod), `strict?: boolean` (structured-output mode), `inputsEquivalent(a,b)` for transcript dedup.
22
- *Permissions*: `checkPermissions(input, ctx): Promise<PermissionResult>` where `PermissionResult` is a discriminated union — `behavior: 'allow' | 'ask' | 'deny' | 'passthrough'`, carrying `updatedInput` (permission may rewrite arguments) and reasons. `getPath(input)` and `preparePermissionMatcher(input)` let hook `if` patterns like `"Bash(git *)"` match against parsed commands rather than bare names.
23
- *Presentation*: `prompt({getToolPermissionContext, tools, agents})`, `userFacingName`, background color, plus JSX renderers (`renderToolUse*`) — tools own their Ink UI.
24
- *Economy*: `maxResultSizeChars`, `shouldDefer`, `alwaysLoad`, `searchHint`, `aliases`.
25
- *Context*: `ToolUseContext` carries `abortController`, `readFileState: FileStateCache`, app-state accessors, MCP clients/resources, `requestPrompt`, denial-tracking state, and `contentReplacementState` (aggregate tool-result budgeting across a thread).
26
27
`buildTool(def)` fills fail-closed defaults: `isConcurrencySafe -> false` (assume unsafe), `isReadOnly -> false` (assume writes), `checkPermissions -> allow` (defer to the general system), `toAutoClassifierInput -> ''` (skip the security classifier unless overridden).
28
29
**Registry** (`tools.ts`): `getAllBaseTools()` composes ~45 tools with environment-conditioned inclusion (`USER_TYPE === 'ant'`, embedded bfs/ugrep replacing Glob/Grep, worktree mode, todo v2, swarms). `assembleToolPool(permissionContext, mcpTools)` merges MCP tools and **sorts with built-ins as a contiguous prefix** because the server's cache policy places a breakpoint after the last prefix-matched built-in — interleaving would bust downstream prompt caches. `filterToolsByDenyRules` strips denied MCP servers entirely.
30
31
**Per-tool depth**: BashTool alone spans 18 files (1,143-ln core): `bashSecurity.ts` blocks command-substitution vectors ($(), <(), Zsh `=cmd` equals-expansion that hides the real binary from deny rules, glob qualifiers, heredoc-in-substitution), `sedEditParser`/`sedValidation` catch edits smuggled through sed, `bashPermissions.ts` strips env-var prefixes and safe wrappers before rule matching, `shouldUseSandbox.ts` consults settings, GrowthBook flags and `excludedCommands`. FileEdit/Write enforce read-before-write through the shared `FileStateCache` (LRU, 100 entries / 25 MB, an `isPartialView` flag forcing explicit Read after auto-injection). AgentTool is 2,370 lines (fork/resume/memory/subagent contexts). AskUserQuestionTool structures mid-turn interaction.
32
33
**Deferral**: MCP tools are always deferred; `ToolSearchTool` (471 ln) matches queries against name+`searchHint` and injects full schemas inside a `<functions>` block, making them callable thereafter. This keeps hundreds of MCP tools out of the initial prompt.
34
35
**Sandboxing**: `utils/sandbox/sandbox-adapter.ts` bridges `@anthropic-ai/sandbox-runtime`: filesystem read/write restriction configs, network host patterns, a violation store, per-platform dependency checks, and settings integration. The comment on `excludedCommands` is doctrine: *"a user-facing convenience feature, not a security boundary… the sandbox permission system is the actual control."*
36
37
**native-ts/**: pure-TS reimplementation of three native modules (nucleo-based `file-index`, `color-diff`, `yoga-layout`) to drop compiled dependencies.
38
39
**Extensibility**: `services/mcp/` (~20 files: auth, channel allowlists, elicitation handlers) plus `plugins/bundled/index.ts` — which registers *nothing*; it is scaffolding awaiting migration of bundled skills.
40
41
## OpenAgents Coder State
42
43
**Contract** (`coder-tools.ts`): `CoderTool = { name, description, parameters: Record<string, unknown>, run(args, signal): Promise<string> }`. Raw JSON Schema, no zod, no output schema, no permission method, no predicates. Documented decision: refusals return text the model can act on; throwing kills the turn. Four factories: `delegate` (fan-out N children across lanes, abort wires to `registry.stopAll()`, per-child numbering), `skill` (enum-constrained catalog reader), `openagents` (CLI passthrough taking an `args` string vector), `shell`.
44
45
**Shell** (`coder-shell.ts`, 175 ln): `/bin/sh -c`, stdin closed, stdout/stderr merged in arrival order, timeout SIGKILL (120 s default, 600 s ceiling), abort wired to SIGKILL, a `dropped` character counter past `OUTPUT_LIMIT`, and a cut notice naming how much is missing. Safety is `REFUSED`: nine regexes (deleting a root or home, formatting a filesystem, writing raw devices, repartitioning, stopping the machine, fork bombs, recursive mode changes on roots, direct device redirects) each paired with a human reason. No parser, no sandbox, no permission rules.
46
47
**Family-aware declarations** (`coder-tool-families.ts`): `toolFamilyOf(model)` returns `default | gemini | local`; measured emphasis sentences are appended per tool per family (Gemini gets batching/token-economy instructions backed by Terminal Bench data). **Per-family result budgets** (`coder-tool-budget.ts`): exhaustive `FamilyBudget` records (`contextWindowTokens`, `resultTokens`, `charactersPerToken`, `because`); unknown families fall back to the *smallest* row with a surfaced `substituted` flag; `budgetedResult()` cuts the middle with an explicit omission notice. Applied in every harness (`coder-thread.ts:783` et al.).
48
49
**WASM plugin host** (`coder-plugins.ts` + `coder-plugin-engine.ts`): manifest-first loading; SHA-256 digest pin verified *before compile*; `inspect()` proves the module's import list is covered by declared capabilities (pure compute means zero imports; mounts mean exactly `openagents.read_file` + `openagents.list_dir`, plus a bounded `read_file_range`); read-only mounts canonicalize paths, refuse absolute paths/`..`/symlinks, bound bytes-per-file and entries-per-listing; `${workspace}` mount expansion; `packet-v0` ABI; typed `{code, reason}` refusals both directions. The engine seam mandates engines enforce their own limits ("an engine that cannot kill a runaway guest is not enforcing anything"); the Node default runs one `worker_threads` worker per invocation, terminated at timeout. The Rust PDK (`plugins/pdk`) generates the whole ABI from `plugin_entry!(handle)` over serde types. Eleven guest crates ship prebuilt `.wasm` artifacts with checked-in digests.
50
51
**Capability discovery** (`coder-capability.ts`): one standing `capability` tool; no embeddings — returns the whole catalog and the model picks by exact name; unmatched requests are recorded to `~/.openagents/capability-gaps.jsonl` for a future registry loop.
52
53
**Session wiring** (`cli.ts` ~2515–2600): `declareTools()` rebuilds `[shell, skill?, openagents, delegate?, capability, ...visiblePlugins()]` each turn. Plugins are **turn-scoped**: reader-pinned (`/plugin load`) or *warm* (matched by retrieval or invoked within two turns), instances cached so revival is free. `PluginApproval` is currently `ask: () => "allow"` — an auto-consent placeholder. `summarizeToolCall` renders calls as typed command lines rather than JSON.
54
55
## Gap Analysis
56
57
1. **No file primitives as model tools.** CC's Read/Edit/Write/Glob/Grep/NotebookEdit (~4,000 ln) enforce read-before-write via shared cache state, handle images, and give ripgrep-backed search. OA's lane edits files through `shell` and delegates file-heavy work to child harnesses (the Claude/Codex lanes bring their own tools). Consequence: the OA-native lane has no structural protection against blind writes, and no fast indexed search.
58
2. **No permission architecture.** CC's `PermissionResult` union, rule sources (allow/deny/ask x origin), additional working directories, modes, and denial tracking have no OA counterpart — only the shell blocklist and an auto-approving `PluginApproval`. Nothing stands between the model and `run()`.
59
3. **No OS sandbox for shell.** CC wraps Bash in `sandbox-runtime` (filesystem/network restrictions, violation auditing). OA's regex table is a blocklist against named footguns, not confinement; a destructive command outside the nine patterns executes unrestricted.
60
4. **No hooks/interceptors.** CC's PreToolUse hooks can rewrite inputs or deny calls; `schemas/hooks.ts` types the whole event surface. OA has no seam between declaration and execution.
61
5. **No concurrency metadata.** Without `isConcurrencySafe`, OA cannot parallelize independent tool calls within a turn (it fans out via `delegate` instead — a deliberate but coarse substitute).
62
6. **No deferral protocol for third-party catalogs.** CC defers MCP tools behind ToolSearch. OA's warm-window achieves similar token economy for plugins, and `capability` covers discovery — arguably cleaner — but there is no path for hundreds of external tools.
63
7. **Schema rigor.** No zod validation layer, no `strict` structured outputs, no output schemas; plugin manifests do carry typed I/O schemas, so the concept exists at the WASM boundary only.
64
65
Counter-gaps (where OA leads): import-inspection-before-instantiate is stronger than CC's empty plugin scaffolding; per-family token budgets with honest cut notices exceed CC's flat `maxResultSizeChars`; family-tuned descriptions are measurement-driven; digest pinning gives provenance receipts CC does not offer for MCP.
66
67
## Actionable Recommendations
68
69
1. **Port `PermissionResult` as a seam now** (before UI exists): add optional `checkPermissions?(args): Promise<{behavior:'allow'|'deny'|'ask', updatedInput?}>` to `CoderTool`, defaulting to allow; route `shell` and `pluginTool` through it. Cheap today, expensive after call sites multiply.
70
2. **Add a read-before-write guard**: track `path -> last-read` in the session (the `FileStateCache` idea minus the LRU), and have the `shell` tool warn when a write-target file was never read this session. One map buys most of the invariant.
71
3. **Encode fail-closed defaults in a `makeTool` factory** once the count passes ~10: `readOnly = false`, `concurrencySafe = false` — mirroring `TOOL_DEFAULTS` so omissions fail safe.
72
4. **Add `isReadOnly`/`isConcurrencySafe` predicates** and let the runner issue concurrent calls over safe tools; the delegate fan-out pattern generalizes.
73
5. **Optional seatbelt for shell**: wrap `/bin/sh` in `sandbox-exec` (macOS) with a write-confined profile when available, keeping the regex table as a fast pre-filter — adopting CC's doctrine that convenience lists are not boundaries.
74
6. **PreToolUse interceptor seam**: `(args) => args | refusal` invoked by the harness before `run`, hosting future permission checks and audit logging without touching each tool.
75
7. **Keep warm-window deferral**, but extend `capability`'s gap log consumer so unmatched requests can trigger registry publication — closing CC's MCP reach without MCP's prompt-token cost.
76
8. **Preserve the inspection-first plugin model** as the documented differentiator; port CC's `maxResultSizeChars` only as a per-plugin manifest field (plugins already declare limits).
docs/teardowns/cc/03-subagents-fleet-orchestration.md added +53

@@ -0,0 +1,53 @@

1
# Teardown 03 — Subagents & Fleet Orchestration
2
3
**Claude Code** (`~/work/projects/repos/cc`) vs **OpenAgents Coder** (`packages/openagents-cli/src`)
4
5
## Component / Subsystem Breakdown
6
7
| Concern | Claude Code | OpenAgents Coder |
8
|---|---|---|
9
| Task state machine | `Task.ts` (125 ln) + `tasks/*` (~10.8k ln, 7 types) | `coder-tasks.ts` (374 ln, 1 type) |
10
| Subagent engine | `tools/AgentTool/runAgent.ts` (973 ln, in-process) | `coder-delegate.ts` (1,994 ln, external harnesses) |
11
| Spawn plumbing | `tools/shared/spawnMultiAgent.ts` (1,093 ln) | `DelegateFleet.submit()` |
12
| Execution backends | `utils/swarm/backends/` (tmux, iTerm2, in-process) | `DelegateHarness` impls (opencode, claude, codex, Devin/ACP) |
13
| Inter-agent comms | `utils/teammateMailbox.ts` (1,183 ln) + `SendMessageTool` (997 ln) | none |
14
| Shared blackboard | `TaskCreate/Get/List/Update/Stop/Output` tools | none |
15
| Orchestrator persona | `coordinator/coordinatorMode.ts` (369 ln) | none |
16
| Fleet rendering | React components + `pillLabel.ts` | `coder-fleet.ts` (272 ln, pure text) + `coder-ui.ts` sidebar |
17
18
## Claude Code Implementation Details
19
20
**The dual registries.** CC separates two things both called "tasks." The *runtime registry* (`AppState.tasks`) holds live executions: `TaskStateBase` (id, type, status, startTime, `outputFile`, `outputOffset`, `notified`) extended by seven discriminated unions — `local_shell`, `local_agent`, `remote_agent`, `in_process_teammate`, `local_workflow`, `monitor_mcp`, `dream`. IDs are prefixed per type (`a`/`b`/`r`/`t`/`w`/`m`/`d`) plus 8 chars of `crypto.randomBytes` base36 (36⁸ ≈ 2.8T, chosen to resist symlink brute-force on shared output dirs). Each type implements `Task { name, type, kill(taskId, setAppState) }`; `tasks.ts` dispatches only `kill` polymorphically. The *coordination blackboard* (`utils/tasks.ts`) is a persisted task list with `pending/in_progress/completed` statuses and owners, exposed to the model via TaskCreate/TaskUpdate so multiple agents can self-assign work.
21
22
**The subagent engine.** `runAgent()` is an async generator that recursively re-enters the full query loop *in the parent process*, parameterized by an `AgentDefinition` (from `loadAgentsDir.ts`, 755 ln: zod-validated `.claude/agents/*.md` frontmatter — tools allowlist, model, permission modes, hooks, effort levels, memory scope). Variants: `forkSubagent.ts` clones parent context into the child; `resumeAgent.ts` resumes a prior agent transcript including content-replacement state; `worktreePath` gives git-isolation per child. Built-ins (`builtInAgents.ts`) compose with user agents: explore, plan, general-purpose, verification (gated), plus coordinator-mode worker agents loaded lazily to break import cycles.
23
24
**Cache economics as architecture.** `shouldInjectAgentListInMessages()` moves the dynamic agent list out of the tool description into an `agent_listing_delta` attachment because the mutable description was **~10.2% of fleet cache_creation tokens** — any plugin load or permission change busting the tool-schema cache. This measurement drove a protocol change, not a tweak.
25
26
**Swarm mode.** `spawnMultiAgent.ts` resolves teammate model (`inherit` alias → leader model), picks a backend from the cached registry (`tmux` | `iterm2` | `in-process`), creates a colored pane, seeds a file mailbox. `inProcessRunner.ts` (1,552 ln) runs teammates in-process with isolated context and `onIdleCallbacks` so the leader waits without polling. Mailboxes carry typed envelopes: idle notifications, permission requests/responses, sandbox permissions, plan approvals. `SendMessageTool` extends delivery across sessions via UDS sockets and a bridge. `coordinatorMode.ts` swaps the whole persona: a coordinator system prompt, an `ASYNC_AGENT_ALLOWED_TOOLS` minus internal-tools worker allowlist, and a permission-free scratchpad directory for cross-worker knowledge. Completion flows back as `<task-notification>` XML injected into the leader's message queue — background agents return a task ID immediately and interrupt the leader later.
27
28
## OpenAgents Coder Implementation State
29
30
**One task shape, five statuses.** `CoderTaskRegistry` holds `CoderTask` records: `pending/running/completed/failed/stopped` (`stopped` deliberately ≠ `failed`), insertion-ordered, with `CoderTaskProgress` using exactly CC's token scheme (latest cumulative input replaces; output sums) and `MAX_RECENT_ACTIVITIES = 5` — direct lineage. Mutators no-op on unknown IDs; register-before-start makes launch failures visible; `prune()` drops terminal-read tasks after `STOPPED_DISPLAY_MS`; unread badges apply only to background children.
31
32
**Fleet as harness multiplexer.** `DelegateHarness` normalizes four external CLIs (opencode, claude, codex, Devin-over-ACP) into five event kinds (`session/tool/tokens/text/error`) via pure line parsers. `DelegateFleet` enforces `maxConcurrent` with an 8× queued bound, refuses with result-codes rather than throws (`fleet_full`, `empty_prompt`), appends the raw event stream to `<tmp>/openagents-coder-delegations/<id>.jsonl` whether watched or not, retries provider failures with backoff and session-resume, and honors abort → `registry.stopAll()`. Lanes are harness+model pairs (`CHILD_LANES`); the delegate tool sends one prompt to ≤32 children with `identify()` prepending "You are child N of M," reports aggregated outcomes, and blocks until all finish. `coder-child-gateway.ts` mints model-pinned `ChildGrant`s served on a loopback proxy (flatten-to-text turns, one grant-refresh retry); `computer-agents.ts` adds an Effect-layered ACP process service with byte-capped, scrubbed output. Rendering is pure text (`coder-fleet.ts`) consumed identically by TUI, `--plain`, and headless, with a focus-navigable sidebar and a live child-transcript screen in `coder-ui.ts`. Test coverage is genuine: fake harnesses drive scheduling/cancellation without a model.
33
34
## Detailed Gap Analysis
35
36
1. **No true background delegation.** OA's `delegate` tool awaits every child before returning; CC returns a task ID and injects `<task-notification>` later. OA's `unread` badge informs the *human* only — the model can never react to a finished child mid-turn. This is the largest behavioral gap.
37
2. **No in-process subagent.** CC re-enters its own loop with scoped tools/context at zero process cost; OA can only spawn foreign CLIs. Its own model cannot recurse, so no explore/plan-style cheap helpers exist.
38
3. **No user-defined agents.** Nothing answers `.claude/agents/*.md`; `AgentCatalogEntry` covers ACP inventory, not declarative presets (model/lane/tools/timeouts).
39
4. **No inter-agent communication.** No mailboxes, no SendMessage, no teams, no permission-over-mailbox, no idle callbacks. A child gets one prompt and returns one string; follow-ups require new children.
40
5. **No shared blackboard.** No persistent owner-bearing task list for cross-child coordination.
41
6. **No per-child isolation.** CC's `isolation: "worktree"` has no analog; OA children share `cwd` (docs say "in this repository").
42
7. **No coordinator persona**, scratchpad gating, or worker tool allowlists (though `coder-memory.ts` inherit/harvest already mirrors the scratchpad spirit).
43
44
Deliberate, defensible divergences: one registry instead of two (simpler, honest); pure-text rendering instead of per-task React trees; refusal-as-result instead of exceptions; transcripts-always-written. These are worth keeping.
45
46
## Actionable Porting Recommendations
47
48
1. **Make delegation interruptible** (highest value): when `count × estimated cost` warrants, return task IDs immediately, keep children running in `DelegateFleet`, and push a completion notice into `coder-thread.ts` at the next turn boundary — CC's `<task-notification>` pattern mapped onto the existing `unread`/registry machinery.
49
2. **Add `.openagents/agents/*.md`**: frontmatter → `{lane, cwd, timeoutMs, toolFamily allowlist}` folded into `DelegationRequest`; inject the catalog as an attachment-style delta, copying CC's measured cache-bust rationale verbatim.
50
3. **Add an in-process child runner**: wrap the existing `coder-thread` loop as an async-generator harness implementing `DelegateHarness`, constrained by `coder-tool-budget.ts` — registers in the same registry, renders in the same fleet, no process spawn.
51
4. **Worktree flag**: `git worktree add` in `execute()`, report branch in the fleet row (CC's `WORKTREE_BRANCH_TAG`).
52
5. **Child mailboxes**: per-child append-only JSONL plus harness stdin where available; start with `pendingUserMessages` drained on turn boundaries, then permission round-trips.
53
6. **Coordinator profile**: a system-prompt variant enumerating lanes and child capabilities over `coder-memory` scratchpads — a cheap, high-leverage port.
docs/teardowns/cc/04-terminal-ui-components-theme.md added +62

@@ -0,0 +1,62 @@

1
# Teardown 04 — Terminal UI, Ink Components, Keybindings & Theme
2
3
**Claude Code** (`~/work/projects/repos/cc`) vs **OpenAgents Coder** (`packages/openagents-cli/src`)
4
5
> Mapping note: the brief names `coder-status.ts`, `coder-prompt.ts`, and `coder-events.ts` as the OpenAgents counterparts. Those files do not exist. Their concerns are folded into one module: the status line, composer/input handling, and event subscription all live in `coder-ui.ts` (1,572 ln), supported by `coder-markdown.ts` (490 ln), `coder-plain.ts` (190 ln), and the `onChange`/`snapshot()` emitter in `coder-session.ts`. This report compares against what actually exists.
6
7
## Component / Subsystem Breakdown
8
9
| Concern | Claude Code | OpenAgents Coder |
10
|---|---|---|
11
| Renderer | vendored Ink fork: `ink/ink.tsx` (1,723 ln), `reconciler.ts`, `renderer.ts`, `frame.ts`, `screen.ts`, `optimizer.ts` | none — direct ANSI writes in `coder-ui.ts` |
12
| Layout | Yoga WASM via `ink/layout/{engine,yoga,node}.ts` | fixed-row template (constants for fleet/composer/spacer rows) |
13
| Components | `components/` — 144 entries; design-system, messages/, PromptInput/, Spinner/, StructuredDiff/ | inline string builders (`justify`, `hints`, `fleetColor`, `childScreenLines`) |
14
| Screens | `screens/REPL.tsx` (5,005 ln), `Doctor.tsx` (574), `ResumeConversation.tsx` (398) | one `runCoderUi()` closure with three modes: `chat`, `skills`, `child` |
15
| Keybindings | `keybindings/` — 15 modules, 9,136 ln total | raw escape-sequence comparisons inline in `onData` |
16
| Theme | `utils/theme.ts`: 6 themes × ~70 semantic tokens; `ThemeProvider` with preview/save | 11 hardcoded SGR constants (`DIM`, `BOLD`, `CYAN`, …) |
17
| Input parsing | `ink/parse-keypress.ts` (801 ln) incremental state machine | chunk walker + 40 ms lone-ESC timer |
18
19
## Claude Code Implementation Details
20
21
**The Ink fork.** CC does not consume upstream Ink; it vendors and heavily modifies a React-based TUI. `reconciler.ts` mounts `react-reconciler` over a DOM of `Box`/`Text` nodes (`dom.ts`), lays out with Yoga (`layout/yoga.ts`), and renders through a double-buffered frame pipeline: `frame.ts` holds front/back `Screen`s built from interned pools (`CharPool`, `StylePool`, `HyperlinkPool` in `screen.ts`), `renderer.ts` diffs back→front and refuses to blit when `prevFrameContaminated` (post-render selection overlays, alt-screen enter, SIGCONT, forceRedraw), and `optimizer.ts` collapses the patch stream in one pass (merge cursor moves, cancel hide/show pairs, dedupe hyperlinks). Render scheduling is throttled at `FRAME_INTERVAL_MS = 16`. Alt-screen gets DECSTBM scroll-region hints (`scrollHint`), park patches on resize, and ScrollBox drain frames. Beyond core rendering: `hit-test.ts` dispatches clicks, `selection.ts` (917 ln) implements native-style text selection, `terminal-querier.ts` round-trips DECRPM/keyboard-protocol queries, and `searchHighlight.ts` overlays match positions.
22
23
**Component library.** `components/design-system/` provides `ThemedText`/`ThemedBox`/`Dialog`/`Tabs`/`FuzzyPicker`/`ProgressBar`/`StatusIcon` on top of the theme context. `components/messages/` maps every message type to a renderer; `VirtualMessageList` virtualizes long transcripts with scroll chrome; `PromptInput/` composes `TextInput` or `VimTextInput` (vim modal editing), history search, paste normalization, suggestions footer, and a `ShimmeredInput`. `Spinner/` is a 1,268-ln subsystem of glyph/shimmer/glimmer animations including teammate trees. `StructuredDiff/` renders syntax-aware diffs with word-level highlighting driven by theme tokens `diffAddedWord`/`diffRemovedWord`.
24
25
**Keybindings** form a real subsystem: `schema.ts` (zod, generates JSON schema), `parser.ts` (keystroke strings → normalized `ParsedKeystroke` with ctrl/alt/shift/meta/super), `match.ts` (Ink `Key` → name matching), `resolver.ts` (pure resolve returning `match | none | unbound | chord_started | chord_cancelled`), 18 named contexts (`Global`, `Chat`, `Confirmation`, `HistorySearch`, `DiffDialog`, …) so bindings scope by focus, `defaultBindings.ts` (340 ln, 20 context blocks, ~79 distinct action IDs, platform-aware: `shift+tab` falls back to `meta+m` on Windows without VT mode), `loadUserBindings.ts` (watches `~/.claude/keybindings.json`, hot reload), `reservedShortcuts.ts` (`ctrl+c`/`ctrl+d`/`ctrl+m` non-rebindable, terminal-reserved warnings), and `useShortcutDisplay`/`shortcutFormat.ts` so help text reflects actual user bindings.
26
27
**Theme.** `utils/theme.ts` defines `THEME_NAMES` = dark/light × {default, daltonized, ansi}; each theme is ~70 named semantic tokens (success/error/warning, diff quadrants, subagent palette `*_FOR_SUBAGENTS_ONLY`, shimmer variants of several tokens, rate-limit gauge fills). `ThemeProvider` resolves `'auto'` through OS detection (`systemTheme.ts`), supports live preview with save/cancel, persists to global config, and feeds a `ThemePicker` that renders live sample diffs.
28
29
## OpenAgents Coder Implementation State
30
31
`coder-ui.ts` is a deliberate zero-dependency ANSI painter — the header documents why: OpenTUI's FFI is Bun-only while the CLI must run on Node. It enters the alternate screen (`?1049h`), enables alt-scroll (`?1007h`), the kitty keyboard-disambiguation protocol (`>1u`), and bracketed paste (`?2004h`), then runs a closed-loop redraw cycle:
32
33
- **Differential painting.** `render()` builds all rows as strings; `paint()` compares against the previous `painted[]` array and emits `\x1b[N;1H` + `ERASE_LINE` + content only for changed rows, plus erases rows the last frame had. Nothing clears the screen or scrolls, preserving the terminal's own scrollback discipline. Resize drops `painted[]` and repaints everything.
34
- **Input.** `onData` treats a chunk as *many* keypresses, walking bytes: paste bodies are lifted out whole (held in `pendingPaste` until the terminator arrives), incomplete escapes held in `pendingEscape`, and a bare `\x1b` waits a 40 ms `ESCAPE_WINDOW_MS` before meaning Escape — so a single press interrupts. `controlFromKeyboardProtocol` maps kitty-style sequences back to control codes.
35
- **Modal screens.** `chat` / `skills` / `child` each own the keyboard; a stray letter never falls through into an invisible composer. The child screen pages tool output (`CHILD_OUTPUT_ROWS = 12`); skills screen is arrows + space-toggle.
36
- **Layout.** Fixed constants (`STATUS_ROWS`, `COMPOSER_ROWS = 3`, `SPACER_ROWS`, `FLEET_ROWS_MAX = 8`, `PREVIEW_ROWS = 3`, sidebar 34 cols gated at ≥100 terminal columns) replace a layout engine. Below threshold the fleet renders inline — a graceful-degradation rule stated in comments.
37
- **Scrolling.** An absolute-line `anchor` keeps a scrolled-up reader parked while content streams; snapping to follow happens at the bottom.
38
- **Markdown.** `coder-markdown.ts` provides `visibleWidth` (ANSI-aware), `wrapStyled`, and `renderMarkdown` for transcript entries; `coder-plain.ts` renders the identical snapshot line-oriented for non-TTY, both fed by one snapshot from `coder-session.ts`'s `onChange` emitter.
39
40
Testability is strong: `test/coder-ui.test.ts` (1,259 ln, 13 describe blocks) drives fake stdin/stdout and asserts on emitted strings — something the React tree makes far harder.
41
42
## Detailed Gap Analysis
43
44
1. **Theming (largest gap).** No palette abstraction: colour is 4-bit SGR baked into logic. No light/dark, no accessibility variants (CC ships daltonized pairs), no `NO_COLOR`/`COLORTERM`/truecolor negotiation, no user preference, no `auto`.
45
2. **Keybindings.** Keys are byte literals scattered through `onData` branches (`"\x1b[A" || "\x1bOA"` appears in four places). No rebindable actions, no contexts, no chords, no user config file, no display-string source — `/help` hints are hand-written literals that can drift from behaviour.
46
3. **Component reuse.** Every widget is a local function; a second consumer of the fleet block or status line would duplicate it. CC's design-system primitives (select lists, dialogs, progress bars, tabs) have no equivalents.
47
4. **Rich interaction.** No mouse (click-to-focus a child in the sidebar is the obvious win; OA already owns absolute row math), no text-selection assistance, no transcript search/highlight (CC: `searchHighlight` + ctrl+o transcript toggle), no history search (ctrl+r), no vim mode, no rewind/message-selector, no context-window visualization, no diff dialog with file list/detail drill-down, no Doctor diagnostics screen.
48
5. **Animation/feedback.** A boolean `pulse` vs CC's shimmer/glimmer/spinner grammar with stalled-intensity and token counters; status line lacks cost/token/context facts (CC tracks these centrally).
49
6. **Layout generality.** Fixed-row templates cannot express nested panes or bottom-anchored overlays (dialogs, pickers) without ad-hoc math; CC's Yoga layout generalizes, at heavy cost.
50
51
Architecturally the trade is explicit and mostly sound: OA trades Ink's ecosystem (mouse, selection, hyperlinks, devtools, arbitrary layouts) for Node-portability, zero deps, string-level testability, and a codebase one person can hold in their head. The gaps that hurt users are theming and rebinding; the gaps that hurt *development velocity* are the missing input-abstraction and widget layers.
52
53
## Concrete Actionable Porting Recommendations
54
55
1. **Port a token table first** (`coder-theme.ts`): copy `utils/theme.ts`'s semantic shape (subset: text, subtle, success/error/warning, diffAdded/diffRemoved, accent) mapped to SGR sequences, with a `NO_COLOR` and 4-bit fallback per palette. Mechanically replace the 11 constants. Unblocks every other item.
56
2. **Extract input parsing** (`coder-keys.ts`): normalize each walked chunk item into `{ name, ctrl, alt, shift }` (port `parser.ts`/`match.ts` concepts, not the Ink coupling). Handlers switch on logical names; delete duplicated byte literals. Keep the kitty protocol + 40 ms ESC fallback exactly as is — it is better tested than it looks.
57
3. **Add a binding table**: `{ action → sequences[] }` with CC-style reserved-shortcut validation (`ctrl+c/d/m`). Ship defaults matching today's behaviour, then read optional overrides from the existing persisted-configuration store — no watcher needed initially.
58
4. **Formalize the painter** into a tiny `Screen` class (row buffer + `diff()` + flush + cursor placement) extracted from `paint()`. Zero new deps; gives every future widget a testable contract.
59
5. **Promote widgets**: move `justify`, `hints`, `fleetColor`, status-line assembly, and child screen into `coder-widgets.ts`; then port CC's `ProgressBar`, `Tabs`, and select-list as pure string functions (patterns only — the React code will not transplant).
60
6. **Mouse**: send `?1006h` alongside the existing `?1007h`; decode SGR clicks; hit-test is trivial given owned row math — wire click-on-sidebar-row to the existing child screen.
61
7. **Transcript search**: scan rendered `lines[]` for a query, jump `anchor` to matches, highlight via reverse video; mirrors CC's ctrl+o/search pairing at 10% the surface.
62
8. **Diff presentation**: extend `summarizeToolCall` output with word-level added/removed colouring using the new theme tokens (parity with `StructuredDiff/Fallback.tsx`, not the full syntax highlighter).
docs/teardowns/cc/05-context-compaction-memory-skills.md added +62

@@ -0,0 +1,62 @@

1
# Teardown 05: Context Window Management, Compaction, Memory & Skills
2
3
**Claude Code** (`~/work/projects/repos/cc`) vs **OpenAgents Coder** (`packages/openagents-cli/src/`).
4
Note: the two counterpart filenames given in the task brief (`coder-history.ts`, `coder-token-economy.ts`) do not exist; the actual nearest neighbors are `coder-transcript.ts`, `coder-resume.ts`, `coder-thread.ts`, `coder-tool-budget.ts`, and `coder-memory.ts`. This report analyzes what is really there.
5
6
## Component / Subsystem Breakdown
7
8
| Concern | Claude Code | OpenAgents Coder |
9
|---|---|---|
10
| Prompt history | `history.ts` (464 ln) → `~/.claude/history.jsonl` | **absent** (resume replays server events instead) |
11
| Compaction | `services/compact/*` (~3,700 ln, 8 modules) | **absent entirely** |
12
| Micro-compaction | `microCompact.ts`, `apiMicrocompact.ts`, snip tool | per-result caps only (`coder-tool-budget.ts`) |
13
| Session scratch memory | `services/SessionMemory/sessionMemory.ts` (495 ln) | **absent** |
14
| Long-term memory | `memdir/` (MEMORY.md + daily logs, ~1,700 ln) | `coder-memory.ts` + `memory/` (signed engram ledger, ~3,200 ln) |
15
| Memory recall | `findRelevantMemories.ts` (LLM selector) | knowledge rail attach (`coder-knowledge.ts`), no selector |
16
| Skills | `skills/loadSkillsDir.ts` (1,086 ln) + bundled | `coder-skills.ts` (383 ln) |
17
| Durable transcript | `utils/messages.ts` serialization | `coder-transcript.ts` (server-pushed event log) |
18
19
## Claude Code Implementation Details
20
21
**Compaction is a layered state machine**, not one mechanism:
22
23
1. **Threshold math** (`autoCompact.ts`): effective context window = advertised window minus `min(maxOutputTokens, 20_000)` reserved for the summary itself (calibrated on p99.99 summary output of 17,387 tokens). Warning/error buffers and a blocking limit (`window − MANUAL_COMPACT_BUFFER`) drive UI state via `calculateTokenWarningState`. Env (`DISABLE_AUTO_COMPACT`, `CLAUDE_CODE_AUTO_COMPACT_WINDOW`) and user config gate it; recursion guards refuse compaction when `querySource` is `session_memory` or `compact` (forked agents would deadlock); feature flags route around *reactive* compact (catching API 413s after the fact) and *context collapse* (90% commit / 95% blocking-spawn headroom management).
24
2. **Full compaction** (`compact.ts`, 1,705 ln): runs the summary through a *forked agent* with tools disabled (`NO_TOOLS_PREAMBLE`), `<analysis>`-tagged reasoning, then `<summary>` extraction (`formatCompactSummary`). Post-compact restoration re-injects bounded context: ≤5 files / 50k tokens total / 5k each; skills ≤25k / 5k each; plan-mode and async-agent attachments. Boundary annotation preserves `tool_use`/`tool_result` pairing invariants. A circuit breaker (`MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES`) stops doomed retry loops; `RecompactionInfo` detects compaction-of-a-compacted-chain.
25
3. **Partial compaction**: three prompt variants (recent-window, up-to-boundary, base), with index adjustment that survives orphaned `tool_use_id`s split across same-`message.id` blocks.
26
4. **Micro-compaction** (`microCompact.ts`): time-based clearing of old results for a fixed `COMPACTABLE_TOOLS` set (Read/Bash/Grep/Glob/WebSearch/WebFetch/Edit/Write), image downsampling (2,000-token cap), and a cache-safe variant that stages edits so the Anthropic prompt cache is not invalidated wholesale (`promptCacheBreakDetection.ts` notifies consumers). The **snip tool** injects `[id:...]` tags so the model can excise messages itself, feeding freed tokens back into threshold estimates (`snipTokensFreed`).
27
5. **Session memory**: a post-sampling hook forks a subagent every N tool calls to maintain a markdown notes file without interrupting the main loop, tracking extraction token spend and init/update thresholds separately.
28
29
**Prompt history** (`history.ts`): append-only JSONL under the config home (cap 100 entries), reverse-read via `readLinesReverse`, advisory lockfile, batched flush with cleanup-registry drain. Large pastes are content-hashed out to a paste store and referenced inline as `[Pasted text #N +X lines]`; `expandPastedTextRefs` rehydrates them on submit. `removeLastFromHistory` implements an undo protocol (pending-buffer pop, else timestamp skip-set) so Esc-rewind doesn't double-show text in Up-arrow history. Tmux-spawned verification sessions opt out via env.
30
31
**Memory (`memdir/`)**: `MEMORY.md` is a capped live index (200 lines **and** 25 KB — dual truncation with explicit warnings naming which cap fired), backed by typed memory files (user/project/how-to references with frontmatter, drift caveats, "what not to save" guidance) and date-named daily logs (`logs/YYYY/MM/DD.md`) distilled by a nightly `/dream` skill. Recall is a two-stage pipeline: `scanMemoryFiles` builds a header manifest, then a cheap Sonnet `sideQuery` picks ≤5 relevant files, excluding `MEMORY.md` (already loaded), filtering against recently used tools while keeping their gotcha-docs, and de-duplicating already-surfaced paths.
32
33
**Skills (`loadSkillsDir.ts`)**: directory (`name/SKILL.md`) and single-file formats; rich frontmatter (`allowed-tools`, `disable-model-invocation`, `context: inline|fork`, `agent`, `model`, `hooks`, `whenToUse`, `argumentHint`); memoized command cache with explicit invalidation (`clearSkillCaches`); dynamic directory registration mid-session (`addSkillDirectories`, `activateConditionalSkillsForPaths`, conditional-skill counting); frontmatter token estimation for context accounting; programmatic bundled skills whose auxiliary `files` extract to disk on first invocation so Read/Grep work identically to disk skills.
34
35
## OpenAgents Coder Implementation State
36
37
**There is no compaction.** Nothing in `src/` matches "compact" except display formatting. The architecture instead *prevents* bloat and *defers* survival:
38
39
- `coder-tool-budget.ts` (190 ln) caps each tool result per model family (exhaustive `Record<ToolFamily, FamilyBudget>`, so adding a family forces a budget decision), with an explicit `charactersPerToken` (3.6 default) and fail-closed truncation: a cut result states it was cut, by how much, against which budget — echoing INVARIANTS.md. This addresses the *cause* micro-compaction treats (oversized results) but never removes accumulated turns.
40
- `coder-transcript.ts` persists every entry to the server event log with ordered queueing, retries, and one-time persistent-failure notice; `coder-resume.ts` replays both UI entries and wire messages (paged `GET /threads/{id}/events`), marking standing context already-delivered so replay isn't paid for twice. Durability lives off-process rather than in a summary.
41
- Tier switching (`coder-tiers.ts`, `coder-session.ts`) hands the wire transcript to the next source, which recomposes its own system anchor — a lane-change escape hatch, not a shrink.
42
- `coder-thread.ts` meters turn usage against server-grant limits (`remaining`, `thread_quota_reached`) — this is *quota*, not context-window management; nothing tracks tokens against the active model's window.
43
- Round-cap backstop on tool loops ("was six") bounds runaway turns.
44
45
**Memory is cryptographically stronger but differently shaped.** `coder-memory.ts` + vendored `memory/` (2,645 ln) implement an append-only JSONL ledger of NIP-AE-shaped engrams at `~/.openagents/memory/engrams.jsonl`: secp256k1 Schnorr signatures (key reused from the HMAC era, migrated), verified event ids, supersession chains for corrections, redaction gate before signing, pure/idempotent projections rebuilt from the log (never stored), a sync queue that can never block or fail a turn, subagent outcome harvest into parent heuristics (#227), and a bounded redacted advisory block seeded into child prompts (#226). What's missing relative to `memdir/`: no session-scoped scratch notes, no capped entrypoint index, no daily-log/dream distillation loop, no LLM relevance selector over the ledger (projection/ranking exist, but nothing asks "which memories matter for *this* query"), and nothing injects ledger content into the main session's own standing context — only children receive it.
46
47
**Skills** share the format (SKILL.md, YAML frontmatter, description-as-selection-key) but implement the cheaper half of the contract: catalog rides in the `skill` tool description; bodies load on demand (progressive disclosure, same idea as CC's ToolSearch). Three directories, nearest-first, first-claim-wins with builtins shipped last so repos/users can shadow them; hand-rolled folded/literal block scalar parsing; `/skills` toggle; `auto:` field. Missing: `allowed-tools` scoping, hook attachment, `context: fork` execution, `agent` binding, `disable-model-invocation`, dynamic mid-session directories, conditional path activation, and frontmatter token estimation. Bundled skills ship as plain files beside compiled output, not extracted-with-files definitions.
48
49
**`device-authorization-store.ts`** (assigned counterpart) is unrelated to context: an Effect-gen file store of pending OAuth device-flow grants keyed by origin at `~/.config/openagents/device-authorizations.json`, Schema-validated v1, atomic rename writes, in-memory test layer. Solid; no gap worth porting from CC.
50
51
## Gap Analysis
52
53
The decisive architectural trade-off: **CC spends ~5,500 lines managing a window it owns; Coder owns no window** — context composition belongs to whichever lane answers (hosted proxy pins the model; local Ollama lane composes its own anchor). Consequences: (a) a long Coder thread simply degrades or hits the vendor wall with no recovery path; (b) Coder's signed-ledger memory is more durable and tamper-evident than anything in `memdir/`, yet reaches neither the main loop nor recall-at-query-time; (c) skills lack permission/fork semantics, so a skill cannot safely elevate tools or isolate context; (d) resume-via-replay substitutes for prompt history but offers no cross-*session* input reuse (no paste store equivalent).
54
55
## Actionable Porting Recommendations
56
57
1. **Port the autocompact skeleton first** (highest value): `getEffectiveContextWindowSize` (window − reserved-summary), threshold/warning state machine, and the consecutive-failure circuit breaker into a new `coder-compact.ts`. Trigger from `ThreadReplySource` between rounds using `turnUsage.promptTokens`.
58
2. Implement `compactConversation` as a **delegate child** (reuse `coder-delegate.ts`) with tools withheld and a `<summary>`-extracting prompt adapted from `services/compact/prompt.ts`; rebuild wire transcript as `[summarized]` + tail, preserving `tool_use`/`tool_result` pairing via `adjustIndexToPreserveAPIInvariants` logic.
59
3. Add **post-compact restore budgets** (files/skills/plans with per-item and total caps) — constants and structure port directly.
60
4. Port `removeLastFromHistory`'s undo semantics and the paste-ref store into a small `~/.openagents/coder-history.jsonl` (cap 100) for Up-arrow continuity across sessions.
61
5. Bridge memory into the main loop: render top-ranked projected heuristics as a bounded standing-context block (the child-facing `buildSubagentMemoryContext` already exists — generalize it), and add a `findRelevantMemories`-style selector; on the local tier it can run against the free Ollama lane.
62
6. Skill parity: add `allowed-tools` enforcement in `coder-tools.ts`, `disable-model-invocation`, and frontmatter token estimation; defer `context: fork` until delegation is cheap enough to fork per skill.
docs/teardowns/cc/06-permissions-cost-telemetry-bridge.md added +55

@@ -0,0 +1,55 @@

1
# Teardown 06 — Permissions, Cost Tracking, Telemetry & Remote Bridge
2
3
**Claude Code** (`~/work/projects/repos/cc`) vs **OpenAgents Coder** (`packages/openagents-cli/src`). Scope: `cost-tracker.ts`, `costHook.ts`, `services/analytics/`, `remote/`, `bridge/`, `upstreamproxy/` versus `credential-store.ts`, `computer-policy.ts`, `computer-channel.ts`, and the thread/transcript event surface. Note: the assigned comparison file `thread-events.ts` does not exist in the CLI package; its role is played by `coder-transcript.ts` (`ThreadTranscriptWriter`) plus the `sink.record(...)` event calls inside `coder-thread.ts`.
4
5
## Component Breakdown
6
7
| Subsystem | Claude Code | OpenAgents Coder |
8
|---|---|---|
9
| Session cost accounting | `cost-tracker.ts` (323 ln) + `bootstrap/state.js` counters | Server-metered: `cost_microusd` read from grant in `coder-thread.ts` |
10
| Pricing model | Client-side `calculateUSDCost(model, usage)` per model family | None client-side; dollars shown from microusd (`$X.YY`) |
11
| Telemetry egress | `services/analytics/` -> Datadog + 1P logger + GrowthBook | **None.** Local-only ATIF traces (`trace-store.ts`, `coder-export.ts`) |
12
| Permission engine | `utils/permissions/*` (~20 files): modes, allow/deny/ask rules, bash classifier, remote approval | `computer-policy.ts` (380 ln): static tier + allowlist engine + journal |
13
| Credential custody | Keychain via `secureStorage`, OAuth tokens, trusted-device JWTs | `credential-store.ts`: Effect service, OS keychain adapter, fail-closed |
14
| Remote sessions | `remote/`: WS subscribe + control-request permission loop | Absent. Nearest analog: `computer-channel.ts` pairing socket |
15
| Cloud bridge ("teleport") | `bridge/` (29 files; `bridgeMain.ts` 2,999 ln, `replBridge.ts` 2,406 ln) | Absent |
16
| Egress proxy | `upstreamproxy/`: CONNECT-to-WebSocket MITM relay | Absent |
17
18
## Claude Code Implementation Details
19
20
**Cost tracking.** State lives in module-level counters (`getTotalCostUSD`, `getTotalInputTokens`, cache read/creation splits, API/tool durations, lines added/removed). `addToTotalSessionCost(cost, usage, model)` fans into three destinations: (1) a per-model `ModelUsage` map carrying `costUSD`, `contextWindow`, `maxOutputTokens`; (2) OpenTelemetry-style histogram counters — `getCostCounter()?.add(cost, {model})` and `getTokenCounter()?.add(tokens, {model, type})` for input/output/cacheRead/cacheCreation, fast-mode tagged separately; (3) recursive advisor-usage costing, each sub-model emitting a `tengu_advisor_tool_token_usage` event with `cost_usd_micros`. Persistence is project config keyed by `lastSessionId`: `saveCurrentSessionCosts()` writes `lastCost`, `lastModelUsage`, even FPS metrics; `restoreCostStateForSession()` restores only when the ID matches, so `--resume` continues an accurate total. Unknown models set `hasUnknownModelCost` rather than guessing a price. `costHook.ts` registers a process-exit hook printing the summary for billing-access accounts.
21
22
**Telemetry.** A dependency-free queue drains into a sink at init (`sink.ts`), fanning out to Datadog and a first-party event logger with BigQuery proto columns. Two phantom types — `AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never` and a PII-tagged variant — force explicit casts proving strings carry no code or paths; `sink.ts` strips `_PROTO_*` keys before any general-access fanout. GrowthBook gates ride the same pipe; `sinkKillswitch.ts` can silence sinks.
23
24
**Permissions.** Mode state machine (`default -> acceptEdits -> plan -> bypassPermissions`, with a kill switch) plus persisted rule sets (allow/deny/ask, tool- and content-scoped). Bash commands pass a statistical classifier (`bashClassifier.ts`, `dangerousPatterns.ts`) before rule evaluation, and `denialTracking.ts` feeds rule suggestions. Remotely, tool approval becomes a control-plane message: `can_use_tool` requests carry a `request_id`; responses return `behavior: allow|deny` plus optional `updatedInput` and `updatedPermissions` (rule grants like "always allow this command"), with server-initiated cancellation.
25
26
**Remote sessions** (`remote/RemoteSessionManager.ts`, 343 ln). Connect = `SessionsWebSocket` to `/v1/sessions/ws/{id}/subscribe` (handshake `{type:'auth', credential:{type:'oauth',token}}`), then HTTP POST for user messages. The WS state machine is explicit: `connecting|connected|closed`; reconnect delay 2 s, max 5 attempts, 30 s ping; close code 4001 (session-not-found, transient during compaction) retried up to 3 times; close 4003 unauthorized treated as permanent. `pendingPermissionRequests: Map<request_id, request>` tracks prompts; unknown control subtypes get an error response so the server never hangs. A `viewerOnly` flag disables interrupts and title updates.
27
28
**Bridge.** `bridgeMain.ts` runs a multisession poller: 2 s intervals while below capacity, 10 min while full, both tunable via GrowthBook flag; `capacityWake.ts` merges outer-abort and capacity-freed signals so at-capacity sleeps wake early. Transports come in v1 (poll) and v2 (WebSocket `HybridTransport`) flavors; `workSecret.ts` decodes session secrets and builds SDK URLs; `trustedDevice.ts` mints a 90-day device token during login (server gate: account age under 10 min) sent as `X-Trusted-Device-Token` because bridge sessions are `SecurityTier=ELEVATED`; `FlushGate` sequences teardown writes; `inboundAttachments.ts` carries files from the web app into local context.
29
30
**upstreamproxy.** Container-side zero-trust egress: read session token from `/run/ccr/session_token`, call `prctl(PR_SET_DUMPABLE, 0)` via libc FFI to block same-UID ptrace of the heap, concatenate the proxy CA onto the system bundle, start a localhost relay that accepts HTTP CONNECT and tunnels bytes over WebSocket wrapped in hand-encoded one-field protobuf (`UpstreamProxyChunk`, tag 0x0a + varint length), then unlink the token file so it exists heap-only. Every step fails open; NO_PROXY exempts loopback/RFC1918/IMDS/package registries; chunk cap 512 KiB sized to Envoy buffers; pings every 30 s against a 50 s idle timeout. The relay's CONNECT state machine buffers bytes arriving before `ws.onopen` fires and refuses plaintext writes once the tunnel carries TLS.
31
## OpenAgents Coder Implementation State
32
33
**Cost tracking is inverted: metering moved server-side.** The client never prices tokens. The proxy response carries a spend/budget envelope parsed in `coder-thread.ts` (`remaining["cost_microusd"] ?? limits["max_cost_microusd"]`), surfaced in status lines and fleet rows (`Done (12 tool uses / 45k tokens / 3m)` in `coder-fleet.ts`). Budget refresh happens in the turn-loop `finally` — including interrupted turns — on the stated ground that the proxy "had already bought the call," so a status line keeping its opening figure would under-report by exactly the turns a reader cut short. Per-turn usage events (`turn.assistant` carrying `prompt_tokens/completion_tokens/calls/cache_read_input_tokens`) stream through `ThreadTranscriptWriter` to the user's own server, where ATIF exports make cost-per-outcome computable from the trajectory alone. There are no OTel counters, no multi-model USD tables, no resume-keyed local cost persistence (resume rehydrates from the server thread instead). The local Ollama lane records tokens but never money — there is nothing to spend.
34
35
**Permissions.** `computer-policy.ts` is a pure static engine: three ordered tiers (`probe < curated < shell`, compared by rank via `tierAllows`), declared roots, a hard deny set of privileged binaries (privilege escalation, raw disk writers, keychain and service-control tools, packet filters), denied path fragments (`.ssh`, `.aws`, `.gnupg`, `.kube`, `.netrc`, credential files), shell-metacharacter rejection, and per-command argument validators (git restricted to read-only subcommands and branch-read options, `node --version` only, docker limited to ps/images/version). Decisions form a closed union — `Allowed{needsConfirmation}` or `Refused{reason among 8 causes, detail}` — and every decision is journaled append-only (`computer-journal.ts`: argv, cwd, outcome, detail). The Computer lane pairs over `computer-channel.ts`, a `ws` socket with heartbeats, reconnect backoff, and retryable-reason classification (`heartbeat_timeout` counts as reconnectable). Delegated children inherit policy; `delegation-push.ts` scrubs secrets with typed regexes (`oa_pat_/oa_agent_/smct_`, Bearer headers) before pushing.
36
37
**Credentials.** `credential-store.ts` is an Effect `Context.Service` returning `Option<Redacted<string>>` for two kinds (`api`, `computer`). The production adapter shells to macOS `security find-generic-password/add/delete-generic-password`; a test layer does atomic tmp+rename file writes at mode 0600/0700. It fails *closed*: without an approved OS adapter every operation fails with "Set OPENAGENTS_TOKEN."
38
39
**Telemetry.** None leaves the machine by design. `trace-store.ts` produces redacted local ATIF summaries; foreign stores (Claude/Codex session directories) are scanned metadata-only via lstat, symlink-refusing, depth- and count-capped walks that never write into them.
40
41
## Gap Analysis
42
43
1. **No remote-control plane.** CC's `control_request`/`control_response` protocol lets a human approve tools from another device mid-session. Coder has nothing comparable; `computer-policy` decisions are local and autonomous, confirmation existing only as a policy flag. For an unattended-first product this is defensible, but there is no path to interactive escalation from a paired phone or web UI.
44
2. **No durable resume of cost state.** CC restores exact totals across `--resume`; Coder depends on the server thread holding usage. Fine while the server lane exists; the local Ollama lane records tokens but never money (nothing to spend), so cross-lane cost totals never unify.
45
3. **No egress governance.** Nothing matches upstreamproxy's audited-MITM design — relevant if Coder children ever run in containers that must leak nothing to arbitrary hosts.
46
4. **Classifier gap.** CC layers a bash-risk classifier and denial-driven suggestions over static rules; Coder's curated allowlist simply refuses anything unlisted. Safer, far less capable at `shell` tier: every non-listed binary needs a prompt path that does not exist.
47
5. **Observability asymmetry.** CC debugs production via gated telemetry with kill switches; Coder sees no field behavior beyond what users' own servers record. An intentional privacy stance, but it removes staged rollout machinery entirely — gating must ship as code.
48
49
## Porting Recommendations
50
51
1. **Port the control-request permission envelope** (roughly 200 lines) onto `computer-channel.ts`: reuse its frame shape for `permission.request` / `permission.response {behavior, updatedArgv?, updatedPolicy?}` with a pending-request map, peer-initiated cancel, and error replies for unknown subtypes. This converts the Computer lane from headless-only to remotely supervisable without weakening the static engine — remote approval would drive the existing `needsConfirmation` path, not bypass tiers.
52
2. **Adopt CC's cost-persistence contract**: on thread close write `{sessionId, costMicrousd, prompt/completion/cacheRead, duration}` to local state so resumed threads render correct totals before first budget refresh; add an explicit `hasUnknownCost` bit instead of blank display when the server quotes nothing.
53
3. **Copy the `_PROTO_*` / phantom-type discipline** into `ThreadTranscriptWriter` payloads now, before any egress lands — cheap today, impossible to retrofit after consumers exist.
54
4. **Add journal-driven rule promotion**: CC's `denialTracking` plus "always allow" suggestions map naturally onto the existing append-only computer journal; letting users promote repeated refusals into `preApproved` entries keeps the closed-decision model while closing the capability gap.
55
5. **Keep upstreamproxy documented on the shelf**: if delegated children gain container isolation, lift `relay.ts`'s CONNECT-to-WS state machine, protobuf framing, and fail-open invariant verbatim rather than reimplementing.
docs/teardowns/cc/README.md added +41

@@ -0,0 +1,41 @@

1
# Claude Code teardown series
2
3
Date: 2026-08-26. Status: reference analysis. Six reports comparing Claude
4
Code (the clone at `~/work/projects/repos/cc`) against the OpenAgents coder
5
(`packages/openagents-cli/src/`, and `crates/coder-lite` for the terminal
6
front).
7
8
These are a source of candidate work, not a plan. A gap named here becomes
9
real work only through the normal route: an issue, a lever with a named
10
suite oracle, and a measured delta (`docs/coder/autoimprove.md`,
11
`docs/coder/runbook.md`). "Claude Code has it" is not a reason to build
12
something.
13
14
| # | Report | Subject |
15
| --- | --- | --- |
16
| 01 | [Architecture, lifecycle, query loop, entrypoints](01-architecture-query-loop.md) | Bootstrap, app shell, the turn loop, tool execution |
17
| 02 | [Tool surface, schemas, sandboxing](02-tool-surface-sandboxing.md) | Tool contract, registry, built-ins, shell safety |
18
| 03 | [Subagents and fleet orchestration](03-subagents-fleet-orchestration.md) | Task state machines, spawn plumbing, execution backends |
19
| 04 | [Terminal UI, components, keybindings, theme](04-terminal-ui-components-theme.md) | Renderer, layout, composer, status |
20
| 05 | [Context window, compaction, memory, skills](05-context-compaction-memory-skills.md) | History, compaction, micro-compaction, memory |
21
| 06 | [Permissions, cost, telemetry, remote bridge](06-permissions-cost-telemetry-bridge.md) | Permission engine, cost accounting, telemetry egress |
22
23
## Reading notes
24
25
The largest gaps these reports establish, in the order they cost us, are
26
carried into `docs/coder/autoimprove.md` §2.3 as structural candidates:
27
compaction (absent here, ~3,700 lines there), client-side prompt history
28
(absent; resume replays server events instead), and shell safety (a static
29
regex refusal table here, a parser and sandbox runtime there).
30
31
Reports 04, 05, and 06 each open by correcting their own brief: the
32
counterpart filenames they were told to compare against do not exist, so
33
they compare what is actually in the tree. That correction is the useful
34
part of those three, and it is the behavior the review loop wants — a
35
report that had invented the missing files would have been worse than no
36
report.
37
38
Where a gap is deliberate rather than missing, these reports do not always
39
say so. The absence of telemetry egress is a design position, not a
40
backlog item; the thin permission engine is bounded by the Computer
41
policy tier it inherits. Read a gap as a question, not a verdict.

This page updates live while a promote is in flight · changelog