From a7ce5db3a0aa8adec40e0fdea1f66da7d4b3400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Fri, 16 Oct 2026 01:16:50 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20Athanor=20trunk/fork=20pro?= =?UTF-8?q?venance=20and=20headless=20fork-session=20research?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .../journals/2026-10-15-21-grotto-work.md | 52 ++++++ ...-10-15-23-athanor-trunk-fork-provenance.md | 150 ++++++++++++++++++ ...-10-16-00-claude-headless-fork-sessions.md | 111 +++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 docs/workpad/reports/2026-10-15-23-athanor-trunk-fork-provenance.md create mode 100644 docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md diff --git a/docs/workpad/journals/2026-10-15-21-grotto-work.md b/docs/workpad/journals/2026-10-15-21-grotto-work.md index e200405..6435295 100644 --- a/docs/workpad/journals/2026-10-15-21-grotto-work.md +++ b/docs/workpad/journals/2026-10-15-21-grotto-work.md @@ -20,6 +20,48 @@ - Re-dispatched the orientation research (researcher, model: opus) over the new files → reports/2026-10-15-21-grotto-orientation-baseline.md. - Report back: reports/2026-10-15-21-grotto-orientation-baseline.md (stamped claude-opus-5-5). Grotto exists only as decisions in goals/drop-superpowers-0.2.2.md, with no code yet. No planning role reads project docs systematically. Hypothesis: nested CLAUDE.md files load lazily (only when a path under them is touched), so the planner sees constraints only along the paths it happens to open. That would make the superpowers "luck" structural. Superpowers itself has one checklist line for this. The report lists 7 placement options and gives no verdict. Unverified: the details of the Python project, superpowers v7, and whether nested CLAUDE.md files load in subagents. - User: the one superpowers sentence ("Explore project context — check files, docs, recent commits") may be load-bearing. The 0.2.2 session found that single sentences steer Opus 5.5 more than expected. There's precedent: a misread sentence contributed to the chatgpt-importer blow-up (counted under "even 0.2.0 athanor had issues"). An experiment is running in the other project, comparing plan + implementation from Spire 0.2.2 against superpowers. Igor's observation, a hypothesis not a decision: the planner's one orientation line says "Read the code the plan touches", which names code and not docs. That may actively narrow attention, so the gap could be the wording rather than a missing stage. +- Committed the journal + report (ec7c299, Haiku 5.5 subagent) and pushed main: 8028411..ec7c299, fast-forward, includes the rebased b881162. In sync with origin. Waiting for the user's experiment results (Spire 0.2.2 vs superpowers, plan + implementation). +- Pulled bbda8fc: reports/2026-10-15-22-spire-vs-superpowers-plan-and-build-experiment.md. Igor read it for synthesis. + - Plans: B (superpowers) beat raw A; C (attacked A) beat B narrowly. + - Build: the blind judge chose to merge Spire. Its edge came from route-level count pins, the attacker's fallback clause and a fresh reviewer; two of those three come from the attacker. + - "Code in plan adds nothing" is supported, n=1, with confounds. + - Rule discovery: confirms both of this session's hypotheses (wording: "read the code" vs "docs they might need to check"; and Bash reads bypassing on-demand nested CLAUDE.md loading). Plus luck: a single `cat` right after an `ls`. + - Proposals for planner, attacker, athanor, reviewer and Igor, including: Conventions section in the plan, attacker conventions pass, Skill tool for planner/attacker/reviewer. + - Igor note: this session's own system prompt (auto mode) carries the same "read files with cat … instead of Read" nudge, so the harness factor isn't limited to bypass mode. +- Talked through the probe's shape. **The user endorsed Igor's recommendation as the direction**; it's still at document-preparation stage, so no goal or plan has been written yet: + - Both artifacts. A separate **orientation probe** runs *after design, before plan*, by an agent with no stake in the plan, cheap tier (mechanical sweep: Haiku/Sonnet). It explicitly opens CLAUDE.md/AGENTS.md along the target paths and the layers called into, the ADRs those cite, sibling areas (reader↔writer), and 1–2 similar implementations with paths. It stamps the commit and writes **separate notes** at a fixed path. + - Plan carries an inline **Conventions section** (rules relied on or at risk, file:line), which is what the plan binds to. + - The attacker's conventions pass **diffs notes vs. section**: a rule in the notes that the plan neither follows nor explicitly waives is a finding. This makes a miss detectable; a section alone can't show absence. + - Notes are revisited by the planner when stuck, by the attacker (mandatory diff) and by the reviewer (convention-deviation check). + - Caveats: the notes are a floor, not a fence (the planner's wording must still say docs + conventions); cap similar implementations; drift → notes are per-change, not a knowledge base. + - Open (user's call): whether the probe notes feed adept folders, which would tension with grotto standing alone. +- User raised the token angle from the experiment report; discussed below. +- Token angle discussed. Spire 78.05 M all / 2.69 M excl. cache; superpowers 54.57 M / 4.12 M. The Athanor's 1M-context cache reads dominate the Spire total. Model mix and unpriced columns block a cost verdict. +- User: was the superpowers orchestrator (the SDD controller) measured, given that the Athanor was? The report doesn't say. Its "10 agents without reviewers" fits controller + 9 implementer dispatches, but that isn't stated. The general bias is real either way: orchestration living in a measured subagent gets counted, orchestration in the root doesn't. On Spire's side the root's work (Igor dispatching 4 reviewers, writing the addendum) is explicitly uncounted. Open thread: check the other project's audit to see whether the controller transcript was summed. +- User: Spire looks more cost-efficient, since cache reads cost 10 %. Igor's rough check at 10 %: Spire 75.4 M reads → 7.5 M + 2.69 M ≈ 10.2 M input-equivalent; superpowers 50.5 M → 5.0 M + 4.12 M ≈ 9.2 M. That's roughly even, not clearly Spire. The "excl. cache read" column mixes input, cache writes and output (all priced differently), and the model mix differs (superpowers is more Sonnet). The controller question is still open. A real answer needs per-column, per-model pricing. +- User asked how forks work and what the Athanor did. Explained from athanor.md:32, implementation SKILL.md:49 and spawning-claude-code.md:56-64. The Athanor built B1.1, B1.4 and B2.1 inline because no fork type was offered. **Igor correction:** the earlier claim that working forks "would cut most of the 7.5 M" was overstated. A fork is assumed to start with the caller's full transcript, so it would re-read the same context; the saving is only that the Athanor's own context grows less for later turns. Net effect unmeasured. +- User: their original Athanor intent was different. The Athanor learns plans + design (not code), delegates on that basis and reads implementer reports. For a hard task it trains up ONE implementer early with background info, then forks THAT implementer per subsection. The current docs have the Athanor read the code (trunk) and fork itself. Either this wasn't communicated, or evidence overruled it. Dispatched a provenance research (researcher, opus) → reports/2026-10-15-23-athanor-trunk-fork-provenance.md. Igor note: the spawning reference says a fork copies the *caller*, so "fork the trained implementer" may only be expressible if the implementer forks itself. Possible harness constraint. +- Provenance report back: reports/2026-10-15-23-athanor-trunk-fork-provenance.md (claude-opus-5-5, uncommitted). Finding (c), close to (a): + - On 10-05 the user's idea was journaled as a "trunk implementer, forks per task, updated by orchestrator", i.e. separate from the Athanor. + - The Opus redesign subagent moved the trunk into the Athanor on the reasoning "fork copies the caller". It took that from Igor-skill wording, not from a test. + - Igor proposed "intake=trunk"; the journal says "User: fully agreed". The actual words aren't preserved. + - "Athanor learns plans, not code" was never written down, and was never blocked by the harness either; it was lost as a side effect. + - Harness (official docs, fetched today): a fork copies the caller only; forks can't fork; fork mode makes every subagent background (clashes with foreground-sequential). Whether a subagent can fork itself is undocumented. + - Overlooked route: the trained implementer forks itself per subsection (depth limit, sub-orchestrator). +- (Past midnight, now 10-16.) User: the fork idea isn't good via the Agent tool, except perhaps by starting such an agent with `claude -p`. Igor: headless sessions can be resumed and forked by session id (`--resume --fork-session`, unverified in the current CLI), which would make "train one implementer, fork it per subsection" expressible from outside the caller. Caveats: outside the Agent-tool notification and accounting machinery; permissions, plugins and model must be passed explicitly; transcripts must be counted. +- 00:44 — Dispatched a headless-fork verification (researcher, opus). It may run a minimal local test with haiku in the scratchpad, ~10 invocations. Report → research/2026-10-16-00-claude-headless-fork-sessions.md. +- Headless-fork report back: research/2026-10-16-00-claude-headless-fork-sessions.md (claude-opus-5-5). Local test: 7 runs, ~$0.13, CLI 2.1.283. + - The pattern works: `claude -p --resume --fork-session` once per subsection. Forks see the base and not each other, the base is unaffected, parallel forks work, forks of forks work. + - The cache is shared only with identical flags (14.6 k read per fork vs 0 when flags differed). The cache lifetime is 1 h, so forks spaced further apart pay full price again. + - Launch flags do not carry over: forks need permission mode and tools re-passed. A headless permission prompt is denied, not answered. The system prompt and model carry over. The spire plugin loads without `--bare`. + - Running from a subagent's Bash is unguarded and untracked: the orchestrator must block on Bash or poll the JSON output. Transcripts are per fork under ~/.claude/projects/. + - Oddities: `--model haiku --permission-mode plan` ran on sonnet-5. The nested run inherited Desktop tools and the messaging channel. The `claude` on PATH is older than the host's. +- Follow-up verification appended to the headless-fork research (8 runs + 1 nested, ~$0.35). + - Depth: the count is per process. A nested `-p` can spawn subagents and further `-p` runs. The limit is silent: the Agent tool just disappears. + - Model oddity: plan mode upgrades Haiku to Sonnet. The init event still reports haiku; only modelUsage shows the real model. + - Desktop env: removing 12 vars with `env -u` drops the Desktop tools. `--bare` needs an API key, since it refuses OAuth. The messaging socket is the nested run's own pipe; whether it can reach the host is unverified. + - Cache TTL (docs): 1 h for main conversations, incl. `-p` and `--fork-session`, on a subscription within plan usage; otherwise 5 min, which also covers Agent-tool forks and subagents. Configurable via CLAUDE_CODE_PROMPT_CACHE_TTL / CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL. A hit resets the timer. Forks must run from the same cwd. + - Version: two installs (~/.local/bin 2.1.283 vs the Desktop's $CLAUDE_CODE_EXECPATH 2.1.299). Use one binary and a full model id for the base and all forks. ## Adepts @@ -28,6 +70,16 @@ ## Artifacts - docs/workpad/reports/2026-10-15-21-grotto-orientation-baseline.md — orientation baseline for grotto (what exists, how roles orient, placement options) +- docs/workpad/reports/2026-10-15-23-athanor-trunk-fork-provenance.md — how the trunk moved from a separate implementer into the Athanor +- docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md — headless `claude -p --resume --fork-session` mechanics, local tests + follow-up +- Input (user's): docs/workpad/reports/2026-10-15-22-spire-vs-superpowers-plan-and-build-experiment.md + +## Open threads + +- Was the superpowers SDD controller's transcript summed in the experiment's token totals? Check the other project's audit. +- Probe design (user-endorsed direction) → goal or grotto design document when document prep is done. +- The user's original Athanor model (learns plans, not code; train one implementer, fork it per subsection) is now expressible via headless forks. Not decided. +- The spawning reference (spawning-claude-code.md:56-64) has rows that today's research answers; not edited yet. ## Open threads diff --git a/docs/workpad/reports/2026-10-15-23-athanor-trunk-fork-provenance.md b/docs/workpad/reports/2026-10-15-23-athanor-trunk-fork-provenance.md new file mode 100644 index 0000000..c72d06e --- /dev/null +++ b/docs/workpad/reports/2026-10-15-23-athanor-trunk-fork-provenance.md @@ -0,0 +1,150 @@ +# Provenance: how the Athanor became "trunk and fork of itself" + +**Date**: 2026-10-15 +**Author**: researcher subagent, model id `claude-opus-5-5` (from system prompt). Read-only research. Plugin files not changed. +**Session journal**: [2026-10-15-21-grotto-work](../journals/2026-10-15-21-grotto-work.md) +**Question**: The current docs say the Athanor reads the build plan and the code (the trunk), forks itself for complex blocks, and sends mechanical or distant blocks to fresh agents that read trunk notes (`plugins/spire/agents/athanor.md:31-32`, `plugins/spire/skills/implementation/SKILL.md:45-49`, `references/spawning-claude-code.md:56-64`). The user remembers a different intent. Which of these happened: (a) the user's model was recorded and later changed, (b) it was never written down, or (c) something else? + +--- + +## 1. Answer + +**(c), and closest to (a).** The user's idea was written down once, but only as Igor's one-line paraphrase in a journal entry. It was then reshaped in a single step by an Opus 5.5 subagent (the redesign report). That agent gave a harness reason for the change: "fork copies the caller." The claim had not been tested; it came from reading the schema and from the Igor skill's wording. Igor turned the reshaped version into a proposal in chat. The journal records the user as "fully agreed." Igor then wrote it into the goal, and an implementer agent wrote it into the plugin files. No verbatim user text exists in the repo for any of these steps. + +Point by point: + +- **Recorded?** Partly. On 2026-10-05 at 21:20 the journal paraphrases the user's direction as "adept-like trunk implementer, forks per task, updated by orchestrator, may live a whole phase." That separates the trunk (an implementer) from the orchestrator that updates it. It does **not** record "the Athanor learns plans and design, not code" or "train ONE implementer early, then fork it per subsection." I found those two points nowhere before today's grotto journal (2026-10-15). +- **Changed by whom?** By the redesign subagent (general-purpose, `claude-opus-5-5`, dispatched by Igor at 21:20, report back at 21:30). It made the Athanor the trunk, and said so as its own proposal. +- **Stated reasoning?** "`fork` copies the *caller*, so the athanor itself must be the trunk." It also called the result "the closest match to the user's idea." It labelled the copies-the-caller claim "Verified in substance," based on the Igor skill's phrase "forking yourself" and on the schema having no fork-source parameter. It was not tested at runtime. In the later spawning reference the same row is marked "assumed." +- **Did the user approve?** The journal (Igor-authored) says Igor proposed the restructure at 21:55, including "intake=trunk", and the user "fully agreed." The user's actual words are not stored. The 2026-10-05 session was remote (paths under `/home/g4b/...`), and no transcript exists in the local `~/.claude/projects` folder. +- **Was the harness constraint real?** Mostly yes, according to the current official docs. Claude Code documents no way to fork an agent other than the current conversation, and "A fork can't spawn further forks." So "fork the implementer the Athanor trained" cannot be expressed directly. The only remaining route is for the trained implementer to fork itself. That route has its own problems (§5). + +--- + +## 2. Timeline with sources + +| When | Who | What | Source | +|---|---|---|---| +| 2026-09-14 (`e054ffd`) | Spire MVP | The Igor skill says: "Forking yourself is a last resort. The main valid use: synthesizing boot data from an expert's knowledge to brief a new expert. For actual work, dispatch a subagent or adept — don't fork yourself into doing the work." | `plugins/spire/skills/igor/SKILL.md:66` (blame: `e054ffd2`, unchanged since) | +| up to 2026-09-22 (`2a27c10`) | — | The Athanor is a decomposer and synthesizer. It loads superpowers SDD, fans out by model tier and reads its children's reports. It has **no** instruction to read the code it builds. | `git show 2a27c10:plugins/spire/agents/athanor.md`, lines 23-43 | +| 2026-10-01 | dirigent (user + agents) | Settled rule: "longer implementation before review" does not mean one implementer. "Never carry an implementer from one block to the next." | `docs/workpad/reports/dirigent/2026-10-01-lean-process-report.md:45`, `:410-415` | +| 2026-10-05 21:20 | user, paraphrased by Igor | "trunk-and-forks idea (adept-like trunk implementer, forks per task, updated by orchestrator, may live a whole phase)" | `docs/workpad/journals/2026-10-05-21-athanor.md:17` (commit `b0b3151`) | +| 2026-10-05 21:20–21:30 | redesign subagent (`claude-opus-5-5`) | Makes the Athanor the trunk because fork copies the caller. Proposes variants A, B and C. Calls B the closest match to the user's idea. | `docs/workpad/reports/2026-10-05-21-implementation-skill-redesign.md:16`, `:67-108` | +| 2026-10-05 21:30 | Igor (journal) | Summarises the report: "`fork` copies the caller → athanor itself must be the trunk; variant A … baseline, B … as trial". Igor flags the report's size and the superpowers drop, but not the trunk relocation. | journal `:19` | +| 2026-10-05 21:40 | user, paraphrased by Igor | "Igor must own the design direction it delegates, not relay agent output." | journal `:20` | +| 2026-10-05 21:55 | Igor proposes; user agrees (as recorded by Igor) | "Athanor phase turns: intake=trunk + trunk notes → foreground blocks, forks for complex trunk-area blocks, fresh for mechanical … User: fully agreed." | journal `:22` | +| 2026-10-05 22:00 | Igor | Writes the goal, which says: "intake (read build plan + touched code = the trunk …)" and "complex blocks in the trunk's area → fork of the Athanor". | `docs/workpad/goals/lean-athanor.md:30`, `:34` (commit `b0b3151`) | +| 2026-10-05 22:12 | implementer subagent (`claude-opus-5-5`) | Writes the current text: "That reading is your trunk" and "a fork of yourself (it inherits your context and model)". | commit `e515207`, athanor.md `+66-67`, SKILL.md `+91` | +| 2026-10-05 22:18 | fix pass after the reviewer's finding M5 | Changes "inherits your context" to "inherits your model and, assumed, your context". Adds a row marking fork availability as unverified. | commit `5909221`; `docs/workpad/reports/2026-10-05-22-lean-athanor-review.md:57` | +| 2026-10-05 22:55 | Igor | Opens goal 0.2.1: "Trunk and forks is tested, not assumed": a preflight plus a trial comparing trunk notes with forks. Still open. | `docs/workpad/goals/lean-athanor-0.2.1.md:12-14` | +| 2026-10-15 | experiment in another project | No fork type was offered, so the Athanor built 3 of 7 blocks inline "on a 1M-context Opus that had read the whole trunk". | `docs/workpad/reports/2026-10-15-22-spire-vs-superpowers-plan-and-build-experiment.md:364`, `:403` | +| 2026-10-15 | user, paraphrased by Igor | States the original intent (see §3.1). Provenance research dispatched. | `docs/workpad/journals/2026-10-15-21-grotto-work.md:43` (uncommitted) | + +The 0.2.2 release (`7c1ed39`) and the brainstorm file `docs/workpad/brainstorm/2026-11-15-implementation_thoughts.md` (committed in `8028411`) do not touch trunk or forks. The brainstorm ends at "Step 4: Attack the Plan" and says nothing about how the builder delegates. + +--- + +## 3. User-attributed statements + +All of these are Igor's paraphrases in journals. None is a verbatim quote. + +### 3.1 The user's model, as recorded + +- **2026-10-05 21:20** (`journals/2026-10-05-21-athanor.md:17`, commit `b0b3151`): + > "trunk-and-forks idea (adept-like trunk implementer, forks per task, updated by orchestrator, may live a whole phase); 5.5 fine to ~600k, both extremes (tiny implementers / keep-implementer) failed; skill must be lean and leave decision room" + + The trunk is called an *implementer* that is *adept-like*. Adepts are trained, persistent experts. An *orchestrator* updates it. That fits the user's account today: one trained implementer, separate from the Athanor. The entry says nothing about what the Athanor itself reads. + +- **2026-10-15** (`journals/2026-10-15-21-grotto-work.md:43`, working tree, not committed): + > "their original Athanor intent was different. The Athanor learns plans + design (not code), delegates on that basis and reads implementer reports. For a hard task it trains up ONE implementer early with background info, then forks THAT implementer per subsection." + +### 3.2 The user's agreement to the changed model, as recorded + +- **2026-10-05 21:55** (`journals/2026-10-05-21-athanor.md:22`): + > "Igor proposed Athanor restructure (… Athanor phase turns: intake=trunk + trunk notes → foreground blocks, forks for complex trunk-area blocks, fresh for mechanical → gate → …). User: fully agreed." + + There is no record of the proposal text, so it is unknown whether "intake=trunk" was spelled out as "the Athanor reads the code itself" when the user agreed. In the same session the user did correct other agent-originated points: the superpowers drop at 21:50, and "own the design direction, not relay agent output" at 21:40. No correction of the trunk location is recorded. + +### 3.3 Related user positions, as recorded + +- dirigent, 2026-10-01: "longer implementation before review" does not mean one implementer, because Sonnet implementers ran out of context at 500k–750k (`reports/dirigent/2026-10-01-lean-process-report.md:45`). The user's model avoids this by forking the trained implementer rather than carrying it. Variant C in the redesign carries one resident implementer instead, which is the shape this rule rejects. + +--- + +## 4. Agent-authored text that introduced the shape + +- **Redesign report** (`claude-opus-5-5`, `reports/2026-10-05-21-implementation-skill-redesign.md`): + - `:16`: "**Trunk and forks** is feasible in Claude Code with one catch: `fork` copies the *caller*, so the athanor itself must be the trunk." + - `:71`: defines the trunk as "an agent that has read the code relevant to the plan." The fork "can stay for a whole phase while its budget lasts." + - `:78`: "A fork copies **the caller**, not an arbitrary child | **Verified in substance.** The Igor skill describes it as 'forking yourself', and no parameter names a fork source. | Consequence: only the agent that holds the trunk context can fork it." + - `:93` (variant B): "At intake the athanor reads the build plan and the code the plan names, roughly 100–250k tokens. It then forks itself per complex block … This is the closest match to the user's idea, and the athanor's intake read also makes it a better judge of handoff flags." + - `:94` (variant C): "Phase-resident implementer … requires SendMessage resumes, which hit the routing hazard … Not recommended as the default. It is the shape that reached about 960k in spacecraft." + - It does not consider a fourth option: a trained implementer that forks *itself* per subsection while the Athanor stays plan-level. +- **Goal** (written by Igor, `goals/lean-athanor.md:30,34`): "intake (read build plan + touched code = the trunk; write short trunk notes)" and "complex blocks in the trunk's area → fork of the Athanor". The goal header says the redesign report is "input, not law — corrected by this goal where they differ" (`:4`). On the trunk question the goal does not differ from the report. +- **Implementer** (`e515207`) put the goal into the plugin files almost word for word. The reviewer (`reports/2026-10-05-22-lean-athanor-review.md:32,57`) only checked that the context inheritance was labelled as assumed. Neither checked the shape against the user's 21:20 idea, because the goal was the spec. + +--- + +## 5. Is "fork the implementer the Athanor trained" expressible in the harness? + +### What the repo says + +`references/spawning-claude-code.md`: + +- `:62`: fork copies the caller. Status: **assumed**. +- `:61`: fork availability is **unverified**. It was not offered on 2026-10-05, and again not offered on 2026-10-15 (`research/2026-10-15-11-spire-superpowers-coupling.md:171`; experiment report `:403`). +- `:64`: whether a fork can fork again is **unknown**. + +### What the official docs say + +From [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents), fetched 2026-10-15: + +- "A fork is a subagent that inherits the entire conversation so far … a fork sees the same system prompt, tools, model, and message history as the main session." A table compares fork and non-fork subagents. The fork column reads "Same as main session" for system prompt, tools and model. The docs describe forking only "the current conversation." There is no parameter for forking another agent. +- "**A fork can't spawn further forks.**" This answers one of the unknowns at `spawning-claude-code.md:64`. +- Fork mode is on by default in interactive sessions (v2.1.232+). It is off with `-p` and in the Agent SDK unless `CLAUDE_CODE_FORK_SUBAGENT=1` is set. When fork mode is on, "Claude Code runs the subagents Claude spawns in the background, forks and non-fork subagents alike … Claude Code also removes the Agent tool's `run_in_background` parameter, so Claude can't ask for the foreground." +- Depth: "a subagent can spawn subagents of its own, up to three layers below the main conversation." +- Resume: `SendMessage` resumes a specific agent "with full conversation history" and "in the background". That continues the agent; it does not branch it. + +The docs consistently say "main session" and "current conversation." They do not say whether a *subagent* such as the Athanor can fork itself at all. That makes the redesign's "Athanor forks itself" an assumption too. + +### What this means for each shape + +| Shape | Expressible as documented? | Notes | +|---|---|---| +| Athanor forks the implementer it trained | **No.** There is no fork-source parameter, and forks copy the current conversation. | This constraint is what pushed the redesign toward an Athanor trunk (report `:16`, `:78`). | +| Trained implementer forks itself per subsection (the implementer is the caller) | **Possibly.** It needs the implementer to hold the Agent tool, fork mode on, subagent-level forking allowed (not documented), and enough depth: Igor (root) → Athanor (1) → implementer (2) → forks (3), which is at the 3-layer limit. Each fork's result returns to the implementer, not to the Athanor. | The implementer becomes a sub-orchestrator. The Athanor then reads one report per subsection batch, not per subsection. Nobody has proposed or tested this. | +| Athanor resumes the trained implementer per subsection (`SendMessage`) | Expressible, but it is a resume, not a fork. Context accumulates (variant C). A resume from a subagent goes to the background and the parent stalls (`spawning-claude-code.md:41`; docs: resumes run in background). | This collides with dirigent's "never carry an implementer" rule. | +| Igor (root) trains the implementer and resumes it | Same as the row above. Resumes are safe from the root, but they still accumulate rather than branch. | Igor becomes the build orchestrator. | +| Training written to a file: the implementer writes trunk notes or a briefing, and fresh implementers read it | Expressible today. | This is variant A ("trunk notes"). The current docs already use it for mechanical blocks. The Athanor need not read code if a delegated implementer writes the notes; the redesign itself allows this ("The first block's implementer, or a short orientation agent, writes `trunk.md`", report `:92`). | +| Athanor reads the code and forks itself (current docs) | Partly. Fork mode on means no foreground. Fork mode off means no fork. Whether a subagent can fork is undocumented. | This conflicts with Spire's "foreground for sequential work" rule (`spawning-claude-code.md:16`) whenever forks are available. In practice the Athanor built blocks inline (experiment report `:364`, `:403`). | + +**Did the constraint push the design?** Yes, by the redesign's own account: "with one catch: `fork` copies the caller, so the athanor itself must be the trunk" (`:16`). The documented harness supports the premise that a fork copies the current conversation and nothing else. It does not support the conclusion that the Athanor is therefore the only possible trunk: the "implementer forks itself" row was never considered. The separate idea that the Athanor should not read code was never in tension with the harness. It was lost because the Athanor became the trunk, and the trunk is by definition the agent that has read the code. + +--- + +## 6. Other observations + +- **Inconsistency inside Spire**: the Igor skill (`igor/SKILL.md:66`, since `e054ffd`) says not to "fork yourself into doing the work" and keeps forking for "synthesizing boot data … to brief a new expert." That use is close to the user's "train up an implementer." The redesign cited the same line as evidence that forks copy the caller (`:78`), then used it to justify the Athanor forking itself to do the work. +- **The open 0.2.1 checkbox** "Trunk and forks is tested, not assumed" (`goals/lean-athanor-0.2.1.md:12-14`) compares only trunk notes against Athanor forks. Neither the user's shape nor "implementer forks itself" is among the arms. +- **What the evidence covered**: dirigent's and spacecraft's evidence (fresh context per block; 960k with a carried implementer) is about *carrying* one implementer. None of the material tests *forking* a trained implementer, which keeps a stable trained base and branches it per subsection. + +--- + +## 7. Options, without a verdict + +1. **Leave the docs as they are** and run the 0.2.1 trial as planned (trunk notes against Athanor forks). +2. **Add the user's shape as a trial arm**: the Athanor reads plan and design only; one trained implementer per hard task forks itself per subsection. Preflight first: can a depth-2 subagent fork, is the depth limit enough, does fork mode remove foreground dispatch, where do fork results land. +3. **Keep the Athanor plan-level and make the training a file**: a delegated implementer or orientation agent writes the trunk notes, and fresh implementers read them. This needs no new harness behaviour, but it replaces the user's in-context training with notes. +4. **Move build orchestration to the root**: Igor trains the implementer and resumes it. This is resume, not fork, so context accumulates. +5. **Update `spawning-claude-code.md`** with the documented facts either way: no fork of another agent; forks cannot fork; fork mode implies background-only; the fork-mode defaults and env var. Also answer the experiment report's request (`:403`) about what to do when forks are missing. + +--- + +## 8. Gaps + +- There is no verbatim user text from the 2026-10-05 session. The transcript is not local, and the session was remote. Igor's 21:55 proposal text is not stored, so what exactly the user agreed to is unknown. +- Igor's brief to the redesign agent is not stored. It is unknown whether the brief passed on "updated by orchestrator" or the separate-implementer reading. +- Whether a *subagent* can fork itself is not settled by the docs, and nobody has run it. +- Only one docs page was fetched (in two parts), plus one web search. The changelog was not checked against the claims about fork-mode version boundaries. + +Sources: [Claude Code docs — Subagents](https://code.claude.com/docs/en/sub-agents); search results also listed [startdebugging.net: Subtask vs Fork vs Background Agent](https://startdebugging.net/2026/08/subtask-vs-fork-vs-background-agent-in-claude-code/) and [labuladong: Fork Agent](https://labuladong.online/en/ai-coding/basics/fork-agent/) (not relied on). diff --git a/docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md b/docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md new file mode 100644 index 0000000..e3faed8 --- /dev/null +++ b/docs/workpad/research/2026-10-16-00-claude-headless-fork-sessions.md @@ -0,0 +1,111 @@ +# Claude Code headless: train one session, fork it per subsection + +Model: `claude-opus-5-5` (researcher). Date: 2026-10-16. CLI on PATH: `claude` 2.1.283 (`/c/Users/g4b/.local/bin/claude`); the host Desktop session runs 2.1.299 (`AI_AGENT=claude-code_2-1-299_agent`), so the nested CLI is older than the host. + +## Answer + +Yes, it works. `claude -p ... --output-format json` returns `session_id`. `claude -p --resume --fork-session` gives each run a new session id and a copy of the base history. The base stays as it was. Two forks ran in parallel without trouble, and each hit the prompt cache on the base prefix, provided its flags matched the base's. The catches: +- Permission, tool and model flags are not inherited. Pass them again, identically, on every fork, or the cache misses. +- The nested process is invisible to the Agent tool's tracking. The orchestrator has to block on Bash or poll for results. +- `--permission-mode plan` quietly changed the model. + +## Claims + +Sources: H = https://code.claude.com/docs/en/headless, C = https://code.claude.com/docs/en/cli-reference, S = https://code.claude.com/docs/en/sessions, P = https://code.claude.com/docs/en/permission-modes, E = https://code.claude.com/docs/en/env-vars, T = test log below. + +| # | Claim | Status | +|---|---|---| +| 1a | `--output-format json` prints a single result object with `session_id`, `result`, `usage` (incl. `cache_read_input_tokens`, `cache_creation_input_tokens`), `modelUsage`, `total_cost_usd`, `permission_denials`, `subagent_stats`. With `stream-json --verbose`, the first event is `system/init` with `session_id`, `model`, `tools`, `agents`, `skills`, `plugins`, `permissionMode`. | verified (docs H; local test T1, T6) | +| 1b | `--resume ` finds the session by id in any project on the machine. It also accepts a `.jsonl` path. Without `--fork-session` it appends to the same id. | verified (docs H, C, S; local T5: resumed base kept id `433df7b7…`) | +| 1c | `--continue` = most recent conversation in the cwd. `claude -p --continue` includes `-p`/SDK sessions; interactive `--continue` skips them. Do not use it for fan-out, because "most recent" is ambiguous. Pass explicit ids instead. | verified (docs C, S) | +| 1d | `--session-id ` lets the caller choose the base id up front, so no output parsing is needed. | verified (docs C, `--help`) | +| 2a | `--fork-session` exists in 2.1.283: "When resuming, create a new session ID instead of reusing the original". | verified (`--help`; docs C) | +| 2b | The fork sees the base context. Forks do not see each other. The base is unchanged on disk and in later resumes. | verified (docs S "The original is unchanged on disk"; local T2-T5) | +| 2c | N forks from one base, run concurrently, work, and each gets its own id and `.jsonl`. Resuming the *same* id in two processes without forking interleaves both into one transcript. | verified (local T2+T3 run in parallel; docs S for the interleave warning) | +| 2d | A fork can be forked again (fork of a fork). | verified (local T6 forked fork A) | +| 3a | Forks hit the cache on the shared prefix: T2/T3 each had `cache_read` 14650 and `cache_creation` ~345, against the base's 8661 creation. | verified (local test) | +| 3b | A cache hit needs identical tool set, model and system prompt. A fork with different flags (T6: no `--tools ""`, default mode, haiku) got `cache_read` 0 and creation 17474. A fork that changed only the model (T7) got 4293 read and 7213 created. | verified (local test) | +| 3c | Cache writes here were `ephemeral_1h` (1-hour TTL). Forks spaced more than about an hour after the base's last request pay full creation for the base prefix. Docs S: after about 1 h inactive "the session's prompt cache has expired". The 1 h TTL may be plan/account dependent. | verified (local usage field; docs S); TTL policy across accounts unverified | +| 3d | `total_cost_usd` on a resumed or forked run is the cumulative conversation total, base spend included. Per-fork cost = fork total minus base total (T2: 0.0403 - 0.0359 = ~$0.0044). | verified (docs H "reports the conversation's whole total"; local numbers consistent) | +| 4a | A `-p` run starts in `default` mode on accounts that fetch feature flags. It cannot prompt: without a host or `--permission-prompt-tool`, anything that would prompt is denied and the run continues (listed in `permission_denials`). `--permission-prompts none` also tells Claude not to retry. `dontAsk` denies anything not pre-allowed. `--dangerously-skip-permissions` = `bypassPermissions`. | verified (docs H, P) | +| 4b | A fork does not inherit the base's permission mode, `--allowedTools` or `--tools`. T6, forked without flags, came up in `default` with all 43 tools. Docs: `-p --resume` starts "in the permission mode a new `claude -p` run would start in" (the plan-mode exception is disabled by `--fork-session`), and session grants don't carry into a `--fork-session` process. Pass the flags on every fork. | verified (local T6; docs S) | +| 4c | Not restored on resume either: `--mcp-config`, `--settings`, `--plugin-dir`, `--fallback-model`, `--add-dir`. The system prompt (incl. `--append-system-prompt`) is recorded at the first request and reused on resume/fork until compaction. Later flag text is ignored. | verified (docs S, C) | +| 4d | The model is restored on resume. `--model` on the fork overrides it (T7 ran haiku on a sonnet-5 base). | verified (docs S; local T7) | +| 5a | Without `--bare`, `-p` loads installed plugins, skills, agents, hooks, MCP and CLAUDE.md like an interactive session. T6 init listed plugin `spire@reliquary` 0.2.2, `spire:*` agents (athanor, adept, …) and `spire:*` skills. | verified (docs H; local T6) | +| 5b | `--bare` skips all that discovery and requires `ANTHROPIC_API_KEY` (no OAuth). Context then has to come from `--append-system-prompt[-file]`, `--settings`, `--agents`, `--plugin-dir`, `--mcp-config`. | verified (docs H, `--help`) | +| 5c | `--model` (alias or full id), `--agents `, `--agent`, `--append-system-prompt[-file]`, `--settings`, `--max-budget-usd`, `--max-turns`, `--json-schema` all exist in 2.1.283. | verified (`--help`; docs C) | +| 5d | `--model haiku --permission-mode plan` ran on `claude-sonnet-5` (T1-T5). The same flags with `dontAsk` ran on `claude-haiku-4-5-20251001` (T7). User `settings.json` has no model key. Plan mode apparently swaps the model. The docs fetched don't explain it. | observed (local test); cause unverified | +| 6a | The nested `claude -p` inherits `CLAUDECODE=1` and `CLAUDE_CODE_CHILD_SESSION=1`. No recursion guard blocked it. `-p` sessions launched this way are still persisted (only nested *interactive* TUIs are excluded from `--resume`). | verified (docs E; local: `env` shows both, runs succeeded and persisted) | +| 6b | It is a separate process with its own Agent-depth budget (`subagent_stats.max_depth` 0 in its result; `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` default 3 applies per session). It does not count against the parent's Agent depth or concurrency caps. | depth separation inferred from per-process `subagent_stats` and docs E; not tested with a deep parent | +| 6c | Parent task notifications, `ListAgents` and `TaskStop` don't see it. The orchestrator must run it via Bash in the foreground (Bash timeout max 10 min) or background, then poll the output file or `--output-format json` result. `--bg` cannot combine with `-p`. | `--bg`/`-p` conflict verified (docs H); notification invisibility assumed (not a tracked Agent) | +| 6d | Transcripts land in `~/.claude/projects/ '-'>/.jsonl`, one file per fork. Each fork file holds the copied base history under its own `sessionId`. 30-day retention. | verified (docs S; local listing) | +| 6e | The nested process inherited the Desktop host's env (`CLAUDE_CODE_MESSAGING_SOCKET`, `CLAUDE_CODE_ENTRYPOINT=claude-desktop`, OAuth-refresh vars). T6's init showed Desktop-only tools (Artifact, ReportFindings, PushNotification) and a `messaging_socket_path`. Nested sessions may talk to the host app. Side effects unknown. | observed (local T6); implications unverified | +| 7 | Windows: worked from Git Bash with forward-slash paths. The project dir name is derived from the Windows path (`C--Users-g4b-…`). `CLAUDE_CODE_GIT_BASH_PATH` exists if bash isn't on PATH. There's no `python` on PATH here (use `node` to parse JSON). The CLI on PATH (2.1.283) lags the host (2.1.299). | verified (local; docs E) | + +## Test log + +cwd: `C:\Users\g4b\AppData\Local\Temp\claude\G--dev-projects-reliquary\42c87f82-…\scratchpad\forktest` (empty). Flags unless noted: `--model haiku --tools "" --permission-mode plan --output-format json`. 7 invocations, total ~$0.13 (cumulative-total arithmetic per 3d). + +| T | Command | session_id | Result | cache_create / cache_read | Model | +|---|---|---|---|---|---| +| 1 | base: "Memorize ZEBRA-7731, reply OK" | `433df7b7` | OK | 8661 / 5989 | sonnet-5 | +| 2 | `--resume base --fork-session`: recall + memorize FORK-A-111 (parallel with T3) | `c3fbf072` | ZEBRA-7731 | 343 / 14650 | sonnet-5 | +| 3 | same, FORK-B-222 (parallel with T2) | `7ccea6b5` | ZEBRA-7731 | 346 / 14650 | sonnet-5 | +| 4 | `--resume forkA` (no fork): list all tokens, include FORK-B if known | `c3fbf072` (same) | ZEBRA-7731, FORK-A-111 | 109 / 14993 | sonnet-5 | +| 5 | `--resume base` (no fork), same question, after T2-T4 | `433df7b7` (same) | ZEBRA-7731 | 102 / 14650 | sonnet-5 | +| 6 | `--resume forkA --fork-session --model haiku --output-format stream-json --verbose`, **no** `--tools`/mode | `20a04357` | DONE; init: mode `default`, 43 tools, spire loaded | 17474 / 0 | haiku-4-5 | +| 7 | `--resume base --fork-session`, `--permission-mode dontAsk` instead of plan | `b8e6b9cd` | ZEBRA-7731 | 7213 / 4293 | haiku-4-5 | + +What it shows: (a) forks see the base (T2, T3, T7); (b) forks are isolated from each other (T4 doesn't know FORK-B); (c) the base is unaffected (T5 knows only ZEBRA); (d) cache reuse depends on matching flags (T2/T3 vs T6/T7). + +## Caveats and gaps + +- Tiny contexts only. Behavior with a large trained base (100k+ tokens, compaction mid-fork) and with tool-using forks that write files was not tested. Parallel forks writing the same worktree would collide. Use `--worktree` or separate dirs. +- The plan-mode model swap (5d) is unexplained. Avoid `plan` for forks, or check `modelUsage` on each run. +- The 6b/6c claims about depth and notifications are inferred, not tested from inside an Agent-spawned subagent with a deep stack. +- The env inheritance from the Desktop host (6e) needs a look before relying on nested runs. Consider `--bare` + API key, or scrubbing `CLAUDE_CODE_MESSAGING_*` vars. +- Budget: 5 web pages fetched (headless, cli-reference, sessions, permission-modes, env-vars, the last two partially). Not read: Agent SDK sessions/cost-tracking pages and the prompt-caching page (exact TTL rules). + +## Follow-up 2026-10-16 + +Model: `claude-opus-5-5`. Coordinator follow-up on the open points above. The original text is kept; where it was wrong, this section corrects it (see F3c on 6e and F2 on 5d). Same scratch folder. 8 invocations plus 1 nested child, about $0.35 in client-side estimates. Extra sources: PC = https://code.claude.com/docs/en/prompt-caching, MC = https://code.claude.com/docs/en/model-config, CT = https://code.claude.com/docs/en/agent-sdk/cost-tracking, E = env-vars (as above). + +| # | Claim | Status | +|---|---|---| +| F1a | A `claude -p` launched from this subagent's Bash can spawn subagents with the Agent tool (`--tools "Task,Bash" --permission-mode dontAsk`). The subagent replied PONG. `subagent_stats`: spawned 1, `max_depth` 1, refused 0. | verified (local D1) | +| F1b | A second-level nested `claude -p` (launched by D1's Bash) works too: L2 returned `L2` with its own session id. No recursion guard, no error. | verified (local D1) | +| F1c | Depth is counted per process, starting from that process's main conversation. With the default limit, the nested run went two subagent layers down (spawned 2, `max_depth` 2, `spawned_by_subagents` 1, refused 0). That run was itself nested at least two levels under the Desktop host (host, then this subagent, then Bash). The host's depth is not passed down: there is no depth env var among the inherited ones. | verified (local D3; env listing) | +| F1d | Hitting the limit produces no error message. With `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1`, the layer-1 subagent simply had no Agent/Task tool. It answered "I don't have a Task/Agent tool available", and `refused.depth_limit` stayed 0. Docs: default 3, "set `1` to turn nesting off". | verified (local D2; docs E) | +| F2a | Reproduced. `--model haiku --permission-mode plan`: `system/init.model` reports the haiku model, but the assistant messages and `modelUsage` show Sonnet. On 2.1.283 it ran on `claude-sonnet-5` (init `claude-haiku-4-5-20251001`). On 2.1.299 it ran on `claude-sonnet-5-5` (init `claude-haiku-5-5`). With `dontAsk`, haiku runs as asked (T7). The debug log only shows `dispatching to firstParty model=claude-sonnet-5`, with no reason given. | verified (local M1, M2, T7) | +| F2b | Explanation: plan mode upgrades a Haiku session to Sonnet. MC, in the `availableModels` section: "A Haiku session that would normally upgrade to Sonnet in plan mode likewise uses the newest permitted Sonnet". The behavior is only implied there, not documented as a rule. It is not `opusplan`, and no env override is involved: there is no `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_*_MODEL`, `CLAUDE_CODE_SUBAGENT_MODEL` or `ANTHROPIC_SMALL_FAST_MODEL` in the inherited env, and no `model` in the user settings.json. Model-relevant inherited values: `CLAUDE_EFFORT=medium`, `CLAUDE_CODE_SIMPLE=0`. | docs (indirect, MC) + local env check; exact rule unverified | +| F2c | Practical point: `init.model` is not the model that ran. Read `modelUsage` or `assistant.message.model` instead. Aliases also depend on the CLI version: `haiku` is Haiku 4.5 on 2.1.283 and Haiku 5.5 on 2.1.299 (Haiku 5.5 needs v2.1.293 or later, Sonnet 5.5 needs v2.1.284 or later). | verified (local; docs MC) | +| F3a | Inherited env vars that point the nested run at the Desktop host (names only): `CLAUDE_CODE_MESSAGING_SOCKET`, `CLAUDE_CODE_MESSAGING_TOKEN`, `CLAUDE_CODE_HOST_SESSION_ID`, `CLAUDE_CODE_ENTRYPOINT`, `CLAUDE_CODE_DESKTOP_APP_VERSION`, `CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH`, `CLAUDE_CODE_TERMINAL_MCP_TOOLS`, `CLAUDE_CODE_REPORT_FINDINGS`, `CLAUDE_CODE_SESSION_ID`, `CLAUDE_AGENT_SDK_VERSION`, `CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL`, `CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING`, `CLAUDE_CODE_EXECPATH`, `CLAUDE_PID`, plus `CLAUDECODE`, `CLAUDE_CODE_CHILD_SESSION`, `AI_AGENT`. | verified (local env listing); each var's role unverified | +| F3b | Removing the first twelve of these with `env -u` drops the Artifact, ArtifactComments and ArtifactData tools (35 to 32 non-MCP tools). ReportFindings, PushNotification and the other built-ins stay. The 8 `mcp__claude_ai_Claude_Docs__*` tools also stay: they are a claude.ai account connector, not inherited. Auth (OAuth) still worked. Which single var gates the Artifact tools was not isolated. | verified (local E1 vs T6) | +| F3c | Correction to 6e: `messaging_socket_path` in `system/init` is the nested process's own pipe, not the host's. It was different in every run (`cc-msg-8e28...` in T6, `cc-msg-155d...` in E1) and never equal to the host's `CLAUDE_CODE_MESSAGING_SOCKET` (`cc-msg-f3c5...`). So there is no evidence that the nested run is wired to the host's channel, but there is also no proof that it isn't. Nothing was sent over the channel. | verified (local); "not wired" unverified | +| F3d | `--bare` removes all of it: 4 tools (Bash, Edit, PowerShell, Read), 0 MCP, no socket. It refuses OAuth, though (`Not logged in · Please run /login`, `is_error` true), so it needs `ANTHROPIC_API_KEY` (docs H). `plugins` was still listed in bare `init`, which looks odd. | verified (local E2; docs H) | +| F4a | The 1 h TTL is Claude Code's default only for the main-conversation bucket (interactive turns and `-p` runs) on a Claude subscription within plan usage. Usage credits, API key and cloud providers default to 5 m. Over the plan limit, the main conversation drops to 5 m. A `claude -p --resume --fork-session` run is a main conversation, which matches the 1 h writes we saw. In-process Agent-tool forks and subagents are in the "everything else" bucket and get 5 m by default. | verified (docs PC, CT; local `ephemeral_1h` in T1-T7) | +| F4b | It is configurable: `CLAUDE_CODE_PROMPT_CACHE_TTL` or the `promptCacheTtl` setting (main), `CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL` or `subagentPromptCacheTtl` (everything else), each `5m` or `1h`, v2.1.242 or later. `ENABLE_PROMPT_CACHING_1H=1` sets both; `FORCE_PROMPT_CACHING_5M=1` overrides everything. | verified (docs PC) | +| F4c | Cache hits refresh the TTL: "Each request that hits the cache resets the timer". So forks spaced under the TTL keep the base prefix warm for later forks. | verified (docs PC); not tested over time | +| F4d | Cache scope: "Sessions you run in parallel in the same directory build matching prefixes and read each other's cache". Different cwd or a different git-status snapshot changes the prefix. Run all forks from the base's cwd. | verified (docs PC); cross-cwd fork not tested | +| F4e | Cost accounting: on resume or fork, `total_cost_usd`/`modelUsage` include the restored session totals (v2.1.277 or later). Per-fork cost = fork result minus base total. Don't sum results. `--max-budget-usd` counts only the call's own spend. | verified (docs CT, C) | +| F5a | They are separate installs. PATH has `C:\Users\g4b\.local\bin\claude.exe` (native installer, 2.1.283, dated 2026-09-26). The Desktop host bundles its own `C:\Users\g4b\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude-code\2.1.299\f0584be0fbaf\claude.exe` (2.1.299, dated 2026-10-15), reachable as `$CLAUDE_CODE_EXECPATH`. | verified (local) | +| F5b | With 2.1.299 the results hold, except that the aliases resolve to newer models (F2c). A 2.1.299 fork of the 2.1.283 base worked (recalled ZEBRA-7731, new id). But it had `cache_read` 0 and 15020 created, because plan mode now ran on `claude-sonnet-5-5`, while the base was on `claude-sonnet-5`. A Claude Code upgrade also changes the prompt (PC, "Upgrading Claude Code"). Pin one binary and a full model id for the base and all its forks. | verified (local V1; docs PC) | + +### Follow-up test log + +| T | Command (cwd forktest) | Result | +|---|---|---| +| M1 | 2.1.283 `--model haiku --tools "" --permission-mode plan`, stream-json, `--debug-file` | init haiku-4-5, ran sonnet-5 | +| M2 | same on 2.1.299 | init haiku-5-5, ran sonnet-5-5 | +| D1 | `--tools "Task,Bash" --allowedTools "Task" "Bash(claude -p *)" --permission-mode dontAsk`: spawn subagent, then run nested `claude -p` | SUB=PONG; L2=L2; max_depth 1 | +| D2 | `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1`, subagent asked to spawn a sub-subagent | layer-1 has no Agent tool; refused 0 | +| D3 | same as D2 with the default depth | DEEP; spawned 2, max_depth 2 | +| E1 | `env -u` (12 Desktop vars), default tools, dontAsk | 32 built-ins + 8 MCP; Artifact tools gone; own socket | +| E2 | `--bare`, same | 4 tools, no socket; "Not logged in" | +| V1 | 2.1.299 `--resume base --fork-session`, T2 flags | ZEBRA-7731; cache 15020 created / 0 read (model changed) | + +### Remaining gaps + +- The single env var that gates the Desktop Artifact tools, and whether the nested pipe can reach the host, were not isolated. +- The plan-mode Haiku-to-Sonnet upgrade is documented only by implication. +- TTL refresh and the cross-cwd fork cache behavior come from docs only.