34 KiB
Plan: Trained implementer, forked per block (spire:forks)
Date: 2026-10-16 01:38 +0200
Planner: model id claude-opus-5-5[1m] (stage 4; does not attack, build or review)
Goal: goals/headless-forks.md, including "Decided 2026-10-16"
Design: designs/2026-10-16-01-headless-forks.md, plus the coordinator's runtime override of 2026-10-16 (Python under uv run, not Node; a dated amendment goes into the design file separately)
Evidence: research/2026-10-16-00-claude-headless-fork-sessions.md (claim ids such as 3a, F4c), reports/2026-10-15-23-athanor-trunk-fork-provenance.md
Filename: the brief named -02-; the hour at writing was 01, so the workpad rule (plugins/spire/skills/workpad/SKILL.md:72) gives 2026-10-16-01-headless-forks.md.
Next: attack (stage 4b), which writes the build plan. This plan is not the build plan.
The design stands except for the runtime. Section numbers like "design §5" refer to the design file. Where this plan adds something the design does not have, it says Planner addition and gives the reason.
1. Conventions this plan relies on or could break
| Rule | Where | How the plan meets it |
|---|---|---|
Gitmoji prefix on every commit, format <emoji> <short description> |
CLAUDE.md:7, docs/workpad/specs/commit-conventions.md:3-5 |
Per-step prefixes in §7. |
🤖 for agent files: CLAUDE.md, skills, AGENTS.md |
commit-conventions.md:91 |
Everything under plugins/spire/skills/ and agents/ is committed as 🤖, including the skill's script and tests (they live inside a skill). Workpad reports use 📝. |
| Each plugin is self-contained | CLAUDE.md:3 |
Helper, templates and tests all stay inside plugins/spire/skills/forks/. Nothing goes to the repo root. No dependency on plugins/project-uv/ at runtime. |
| Work on Spire uses Spire; workpad is authoritative for placement | CLAUDE.md:11, :15 |
Docs go to docs/workpad/. This build runs on the installed 0.2.2 Athanor shape, because the tool under test cannot build itself (design §10). |
Workpad naming YYYY-MM-DD-hh-<slug> for plans and reports; dated subfolder for a cascade |
plugins/spire/skills/workpad/SKILL.md:72, :80 |
Run log reports/<stamp>-headless-forks-run.md. Preflight report reports/<stamp>-headless-forks-preflight.md. The helper's state dir …-run-forks/ is a cascade folder (design §3.2). |
Spire README lists components; plugins/spire/CLAUDE.md points to it |
plugins/spire/CLAUDE.md:1, plugins/spire/README.md:11-21 |
Step 3.4 adds the forks row. |
| Version lives in two manifests, bumped together | plugins/spire/.claude-plugin/plugin.json:3, plugins/spire/.codex-plugin/plugin.json:3; precedent 7c1ed39, f40cbf5 |
Step 3.4 bumps both. Marketplace files (.claude-plugin/marketplace.json, .agents/plugins/marketplace.json) carry no version, so they stay as they are. |
| No CHANGELOG file exists. Release notes live in the release commit body | git ls-files (no changelog); 7c1ed39 body |
Step 3.4 writes the changelog as the body of the release commit. See open decision D2. |
LF line endings for *.md, *.py, *.json |
.gitattributes:1-5 |
forks.py, templates, registry and ledger are written with \n. The helper writes its files in binary or newline="\n" mode on Windows. |
**/*.log and **/*.tmp are gitignored repo-wide |
.gitignore:9-10 |
Planner addition: evidence and gate output under the state dir's logs/ uses .txt, so it is not hidden silently. Temp-then-rename files may use .tmp. |
uv conventions: PEP 723 scripts run as uv run path/to/script.py; SCRIPTS_UV names the uv binary; probes are stdlib-only |
plugins/project-uv/skills/project-uv/SKILL.md:21, :31; scripts/probe.sh:6-11; scripts/probe.py:4 |
forks.py is a single file with a PEP 723 block and stdlib only. It is invoked as uv run <abs>/forks.py …. $SCRIPTS_UV is honoured as described in §3.1. |
Skill shell injection plus a pre-approving allowed-tools, a pattern its own comment calls novel |
plugins/spire/skills/adepts/SKILL.md:4, :7-12 |
forks/SKILL.md follows it with Bash(uv:*). Phase 4 verifies it, and §6 marks it Assumed. |
Reliquary already allows Bash(uv:*); rm/mv/git push are ask; gh/ssh are denied |
.claude/settings.json:32, :53-54 |
Under dontAsk, forks get rm/mv denied. Denials surface as needs_attention (design §3.5). Briefs tell forks to use git rm/git mv. |
Project availableModels lists only Opus ids and aliases, no Sonnet or Haiku full id; effortLevel: medium; model: claude-opus-5-5[1m] |
.claude/settings.json:56-69 |
Headless runs load project settings (no --bare, 5a). P0.5 tests whether a full id outside the list is refused. See D5. |
| Athanor: goals are read-only; stage by explicit paths; no PRs; no force-push | plugins/spire/agents/athanor.md:53, :55 |
No step edits goals/. Igor ticks the goal's boxes. All commits stage explicit paths. |
| Every plan, handoff note and review stamps its exact model id | plugins/spire/skills/implementation/SKILL.md:68 |
Header above. The helper stamps the real model from modelUsage (design §6). |
| One phase per turn, cheap gate between blocks, tests once per phase, fresh phase review | implementation/SKILL.md:36-58, athanor.md:27-40 |
Phases in §7. |
| Spawning reference: status vocabulary, and "when a claim fails, fix it here with a new date" | implementation/references/spawning-claude-code.md:5-12 |
Step 3.3 updates rows with dated statuses. Old rows are replaced only where design §9 says so. |
| Model notes are added with a date, not silently dropped | implementation/references/models.md:3 |
Step 3.3 appends dated notes. |
No ## Build section in .claude/spire/project.md |
.claude/spire/project.md (absent) |
Defaults from the implementation skill apply. This plan names its own gate commands. |
Root AGENTS.md still points at g4b_ai |
AGENTS.md:9-15 |
Stale and Codex-facing. Out of scope (§5). |
2. Runtime change: Python under uv (coordinator override, 2026-10-16)
This replaces design §3.1. Everything else in design §3 stays.
-
Decision: the helper is
plugins/spire/skills/forks/forks.py, a single file, standard library only. Why: the user decided it.pythonis not on PATH here, but uv 0.5.13 is (~/.local/bin/uv), and it manages CPython 3.9 to 3.13 locally. -
Decision: the PEP 723 header has exactly this form. Its exact form is the decision.
# /// script # requires-python = ">=3.10" # dependencies = [] # ///Why: stdlib-only means
dependencies = []. The floor is kept low so that any uv-managed Python on this host (3.10 to 3.13) qualifies; 3.9 is past end of life. Nothing needstomllib. Raise the floor only with a stated reason. -
Invocation contract: always
uv run <absolute path to forks.py> <command> [args], from any cwd. With inline metadata, uv must ignore anypyproject.tomlin the cwd. P0.0 verifies this. -
Decision: the detached supervisor is spawned as
sys.executable <forks.py> _supervise <job>, not throughuv runagain. Why: it skips a second uv resolution and keeps uv's own wrapper process out from between the supervisor and its launcher, which is one fewer layer to detach from (R1). -
Planner addition, env scrub for uv:
uv runchanges the environment the helper sees. The childclaude(and every fork's Bash) must not inherit the helper's Python environment. On top of design §4, the scrub therefore dropsVIRTUAL_ENVand everyUV_*variable that uv injects, and removes the PATH entries that point insidesys.prefix. P0.0 measures the exact list. Why: otherwise a fork runningpythonoruv run pytestin a Python project would resolve to the helper's interpreter. -
wherereports the uv version, the Python version and path, the script path, the resolvedclaudebinary and its version, and the scrub check. Design §2 had node in this slot. -
$SCRIPTS_UV(project-uv contract) is honoured by the Athanor's calls: SKILL.md says to use$SCRIPTS_UVif it is set, elseuv. The injection line uses plainuv. Assumed, see §6. -
Design R14 ("node may be missing") becomes "uv may be missing, or may be unable to provide Python ≥ 3.10 (offline, no managed Python)".
wherefails loudly, and the Athanor falls back to the fresh-agent path and logs it.
3. Decisions the builder cannot make alone
Design §"Decisions this design made" 1–11 stand, with 1 amended by §2 above. On top of those:
- Phase 0 stop rule. If P0.0 (uv), P0.1 (R1), P0.2 (R2) or P0.3 (R5) fails, the plan stops. The Athanor hands back with
STOPand the preflight report, and Igor takes it to the user. Nothing inplugins/changes before Phase 0 passes. Why: design §10 and §11 "what would make this wrong". Adding uv as a stop item follows the coordinator's message. - Phase 0 runs from the Athanor's own Bash, as an Agent-spawned subagent, on this Windows host. Why: R1 is about exactly that configuration. A researcher run from the root would test the wrong process tree.
- Phase 0 probes are throwaway and live in the session scratchpad or a scratch clone, not in
plugins/. Their results get copied into the preflight report. The Phase 1 builder may reuse their code. - Scratch target for every live test (P0, the Phase 1/2 smokes, Phase 4) is a
git clone --localof this repo in the scratchpad. It is never the working checkout. Why: forks commit, and P0.2 needs ≥ 100k tokens of real reading, which this repo provides. The clone carries.claude/settings.json, so P0.5 tests the realavailableModelsbehaviour too. - Tests use a stub
claudethat replays cannedstream-json(init, assistant events, result) and can be told to hang, die, or emit denials. No unit test spends tokens. The builder chooses how the stub is made executable on Windows. The helper gains no test-only flags beyond what--binaryalready gives. Time is injected by monkeypatching one clock function in the imported module. - Tests live at
plugins/spire/skills/forks/tests/test_forks.py, a PEP 723 script usingunittest, run asuv run plugins/spire/skills/forks/tests/test_forks.py. Why: self-contained plugin, and it lifts with the skill into Dirigent. - Planner addition: supervisor liveness. The supervisor writes a heartbeat (
heartbeat_atin its job file) at least every 30 s. Ifwaitorlistsees a running job whose heartbeat is older than 120 s and whose supervisor pid is gone, it marks the joblostand appends a ledger line with whatever usage was captured. Why: the design has no answer for a supervisor killed by a job object or a reboot. Without one,waitreturns exit 3 forever. - Planner addition:
registry.jsoncarries"schema": 1. Why: the state format is the hand-off point to a later Dirigent or MCP tool. - The release is a minor bump, 0.2.2 → 0.3.0, in both manifests. Smoke fixes in Phase 4 bump the patch. Why: the Athanor's procedure changes incompatibly (no code reads, Agent-tool forks dropped), and the plugin cache is keyed by version. See D1.
- Owner models (defaults for the attacker to reshape). P0: the Athanor itself,
claude-opus-5-5[1m]. Phases 1–2 (process supervision, Windows, state machine): Opus 5.5. Phase 3 procedure text (athanor.md, implementation SKILL.md, forks SKILL.md): Opus 5.5. Phase 3 reference rows and README: Sonnet is acceptable.
4. Contracts and invariants
4.1 Command surface (design §3.4, unchanged except where)
where, init, train, fork, rebase, continue, wait, usage, list, expire, kill, internal _supervise and tick. Every command except where takes --run <run log .md>. Exit codes: 0 ok, 2 usage error or refused, 3 wait timed out with jobs still running, 4 a job failed.
Invariant: stdout carries exactly one JSON object, or for usage --format md exactly one markdown fragment. Diagnostics go to stderr. uv's own output goes to stderr too, and P0.0 confirms it.
Path contract (planner addition): --run, --brief and --cwd accept relative, Windows (G:\…, G:/…) and Git Bash (/g/…) forms. The helper normalises them before storing and hashing. Why: the Athanor's Bash is Git Bash, while forks.py runs on native Windows Python. The transcript slug is derived from the normalised Windows path (6d, 7).
4.2 Result object (what wait returns per finished job)
This is the contract between Phase 1/2 (helper) and Phase 3 (procedure text). Field names are fixed. Values follow design §6.
{"job":"j0007","kind":"train|fork|rebase|continue|ping","base":"B1","block":"1.3","phase":1,
"status":"ok|failed|timeout|killed|lost",
"flags":["stalled","needs_attention","usage_mismatch","ttl_downgrade","cold_start"],
"fingerprint":"ok|drift:<field>,<field>",
"session":"<uuid>","parent_session":"<uuid>|null",
"models_used":["claude-…"],"tokens":{"input":0,"output":0,"cache_creation":0,
"cache_creation_1h":0,"cache_creation_5m":0,"cache_read":0},
"cost_delta_usd":0.0,"cache":"hit|partial|miss","base_state":"warm|expiring|cold",
"wall_s":0,"turns":0,"denials":0,"note":"notes/1.3.md|null","raw":"raw/j0007.jsonl"}
fork additionally returns {job, base, base_state, warm_until, rebase_due, retrain_due, reasons[]} at launch (design §3.4). The ledger line is design §6's shape plus status and flags as above.
4.3 State machines
job: created ─► running ─► ok | failed | timeout | killed | lost
(flags may be set while running: stalled, needs_attention)
base: training ─► active ─► superseded (rebase / re-train) | retired (expire)
warmth (computed, never stored as truth): warm | expiring | cold — design §5
Invariants:
- Only
trainandinitaccept launch fields.fork,rebase,continueandpingread them from the base record. Overrides are refused with exit 2 (design §3.3). - A
forkfrom a base that is notactiveis refused without--force. A cold base is forkable. Coldness never triggers a re-train (design §5). - Registry writes happen under
registry.lock(create-exclusive, stale after 30 s), written to a temp file and then renamed. The ledger is append-only. Nothing else writes either file. - At most
parallel_capjobs of kindfork/continuerun per run (default 2). Pings and the tick do not count toward it. init.modelis never recorded as the model.models_usedcomes from themodelUsagedelta (design §6).total_cost_usdis never summed. Cost is always a delta against the session total before the call (3d,F4e).- Every launch re-checks binary version, size and mtime. A change sets a
binarydrift andrebase_due(design §5). - The env scrub is one constant (design §4 plus §2 above).
wherelists hostCLAUDE_CODE_*variables that are on neither the drop list nor the keep list.
4.4 Commit ownership (design §8.3, unchanged)
There is one committer at a time. Sequential forks commit their block by explicit paths after their gate passes. Parallel forks never commit; the Athanor commits each block afterwards by the paths its note lists. Planner addition: the Athanor commits the run log and the state dir (everything except raw/ and lock files) by explicit paths at phase close, as 📝.
5. Scope fence
Out of scope:
- An MCP server, a Dirigent harness, and token-count access beyond the helper's per-call returns (goal "Later").
--bareand API-key auth (goal decision: OAuth with a scrubbed env).- Agent-tool forks. They are removed from athanor.md, not kept beside the new path (goal decision).
- Editing
plugins/spire/skills/igor/SKILL.md. Line 66 ("forking yourself") is flagged by the design. Planner flag: line 112 has IgorTaskStopan Athanor, which would leave its detached forks running. Both go to Igor as follow-ups, see D4. - Codex support for
spire:forks. The skill says it is Claude Code only..codex-plugin/plugin.jsongets only the version bump. - Worktree-per-fork, cross-cwd forks, and auto-tuning of thresholds. The trial tunes them by hand (design §5).
- A CHANGELOG file (see D2), root
AGENTS.md, other plugins, and the planner, attacker and reviewer agent files. - Editing
goals/(Igor's) or the design file (the coordinator is amending it). - The other criteria of goal
lean-athanor-0.2.1, beyond what the trial answers.
6. Assumed
- Assumed: the preloaded skill's
!injection runs at agent start under Windows and resolves${CLAUDE_SKILL_DIR}, andallowed-tools: Bash(uv:*)pre-approves the Athanor's lateruv run …/forks.pycalls (design §2, R4). Default if not: the Athanor's brief says to rely on the project allow rule (reliquary has one at.claude/settings.json:32), and SKILL.md tells users of other projects to addBash(uv run:*). This is checked in Phase 4, the first point where the installed plugin exists. It is not a Phase 0 stop item. - Assumed:
$SCRIPTS_UVis honoured in SKILL.md's instructions, not in the injection line. - Assumed: evidence and gate logs under the state dir are committed, as design §3.2 gitignores only
raw/and locks, and they use.txt(because of.gitignore:9). - Assumed: Phase 0's real token spend is acceptable: a ≥ 100k base, about 8 forks, and the TTL test. It is notional under the subscription but draws on plan usage (R11).
- Assumed: if P0.6 (TTL refresh) fails, pings default to off (
init --pings off) and the keep-warm rule ships disabled. If P0.4 (parallel) fails,parallel_capdefaults to 1. - Assumed: the build runs on a branch named by Igor's brief, default
headless-forks, not onmain. - Assumed: after Phase 3 the user updates the installed plugin (marketplace update, reload) before Phase 4. The plan cannot do this itself.
- Design-level assumptions (
[1m]in--model,Skillin--tools,apiKeySource,--strict-mcp-configdropping claude.ai connectors,--agentresolution,DISABLE_AUTOUPDATER, transcript message usage, message ids kept in fork copies) are tested in P0.5/P0.7, exceptDISABLE_AUTOUPDATER, which stays assumed.
7. Phases
Gates used below:
- G-unit:
uv run plugins/spire/skills/forks/tests/test_forks.py(exit 0). - G-where:
uv run plugins/spire/skills/forks/forks.py where(exit 0, valid JSON). - G-text: the grep checks listed in Phase 3.
Phase 0: Preflight (stop gate)
Owner: the Athanor, from its own Bash. Target: the scratch clone (decision 4). Output: reports/<stamp>-headless-forks-preflight.md with, per check: command shape, raw numbers, pass or fail, and the Phase 1 default it sets. The raw stream files stay in the scratch dir, and their paths go into the report. Commit the report as 📝 (or ⚗️).
| Step | Check | Pass criterion | On fail |
|---|---|---|---|
| P0.0 | uv. A PEP 723 probe with the §2 header runs via uv run <abs path> from the Athanor's Bash. (a) cwd = repo; (b) cwd = a scratch dir holding a pyproject.toml with a [project] table: uv does not sync or build it. Record the uv version, the Python chosen, first-run and warm latency, whether stdout stays clean, and the env diff between Bash and inside uv run (VIRTUAL_ENV, UV_*, PATH prefix). |
Runs in both cwds, stdout holds only the probe's output, and the env diff is recorded. | STOP |
| P0.1 | R1 detach. From a uv run Python process, spawn a detached sys.executable supervisor that spawns a short claude -p (or a sleeper first, then claude). Windows: try the creation flags with and without breakaway from the job. The supervisor must outlive (a) the uv process and (b) the Bash call, and a later Bash call must see its output growing until it completes. Also test tree-kill. If that fails, run the --attach variant under Bash run_in_background: true and poll it from later foreground Bash calls for ≥ 15 min. |
Either variant survives for ≥ 15 min and completes, and a kill removes the whole tree. Record which variant and which flags. | STOP if both fail |
| P0.2 | R2 large base. Train a base with real tool use (Read/Grep over the clone) to ≥ 100k context tokens, using the design's default fingerprint: full model id, --effort, dontAsk, the tool list, strict MCP, --append-system-prompt-file, scrubbed env, pinned TTL. Then three sequential forks, each reading, editing, gating and committing one small file. |
Each fork's first assistant event shows cache_read ≥ 0.9 × the base's context tokens. Record whether any compaction happened. One re-run is allowed if a probe mistake in the fingerprint is found and named. |
STOP |
| P0.3 | R5 usage. For a fork with ≥ 5 turns: the stream-json result event has usage, modelUsage, total_cost_usd, num_turns, is_error, permission_denials. Compare the modelUsage delta (vs. the base's cumulative), the top-level usage, and the sum of per-assistant-event usage. |
A reproducible rule gives the four per-call columns (plus the 1h/5m split) and the cost delta. Record the rule; Phase 1 implements exactly it. | STOP if the fields are missing or no consistent rule exists |
| P0.4 | R3 parallel. Two tool-using forks in parallel on disjoint files, no commits, then the Athanor gates and commits both. | No index.lock error, both edits intact, both first requests hit. |
parallel_cap default 1 |
| P0.5 | Flags (R8, R9, R15) and models. Each must be accepted and visible in init or behaviour: --effort with CLAUDE_EFFORT scrubbed (and whether a different effort misses the cache); --strict-mcp-config with an empty config drops mcp__claude_ai_* tools; Skill in --tools; [1m] in --model; apiKeySource present (record its value); --agent <name> resolves a project .claude/agents/<name>.md together with --append-system-prompt-file (a fork quotes one marker string from each); agent tools: vs --tools; a full Sonnet id not in the clone's availableModels (.claude/settings.json:56-67). |
Each item recorded as accepted, refused or ignored, with the default it sets. | Per item: drop the flag or field from the fingerprint and record it. The availableModels result feeds D5. |
| P0.6 | R7 TTL. Base A: ping at ~50 min, fork at ~65 min. Base B: no ping, fork at ~65 min. Start it first and run it beside P0.2–P0.5. The Athanor keeps polling until it finishes. | A's fork hits and B's misses. | Pings default off |
| P0.7 | Transcript facts (informational). Message-level usage present in ~/.claude/projects/<slug>/<id>.jsonl lines (for max_base_tokens); copied base messages keep their ids in fork transcripts (for trial dedupe); --session-id combined with --resume --fork-session (R6). |
Recorded. | max_base_tokens falls back to the last result's context estimate; the trial dedupes by content hash |
Phase 0 complete when: the report exists with every row filled and a one-line verdict, GO or STOP, at the top, and it lists the defaults Phase 1 must use (scrub additions, detach variant and flags, usage rule, parallel cap, pings, fingerprint fields kept or dropped). Phase review: a fresh reviewer checks the report's numbers against the raw files, and Igor makes the GO/STOP call.
Phase 1: Helper core (lifecycle, fingerprint, usage)
Owner: Opus 5.5. Files: plugins/spire/skills/forks/forks.py, preamble.md, templates/train.md, templates/fork.md, tests/test_forks.py (plus the stub). Commits: 🤖.
- 1.1 Skeleton and state. PEP 723 header, command dispatch, exit codes, the stdout invariant, path normalisation (§4.1), state-dir derivation from
--run,registry.json(schema: 1) with lock and atomic write, the state dir's own.gitignore(raw/,*.lock),mcp-empty.json, and thewhereandinitcommands. Done when: G-where passes on this host, and the unit tests cover path forms, lock contention and stale-lock recovery, init idempotency, and exit 2 on bad input. - 1.2 Launch and supervise. Env scrub (design §4 plus §2 plus P0.0's list), the fingerprint (build, hash, launch-time binary re-check, runtime compare against
init→drift),train,fork(delta header: commits sincebase_commitplus--shortstat, and the refusals in design §3.4),continue,_supervise(the detach variant P0.1 chose, stream parsing, heartbeat, watchdogstimeout/stalled/needs_attention,lostper decision 7), results and ledger with the usage rule from P0.3,wait(without the tick),list,expire,kill(tree).preamble.mdand the two templates, per design §2 (stale-snapshot sentence infork.md, R13). Done when: the unit tests against the stub cover every job status and flag, the cost delta for fork and continue, the cache verdict, a refusal for each override, the parallel-cap refusal,lostdetection, andwaitexit codes 0, 3 and 4. - Phase 1 tests and smoke. G-unit green. Live smoke in the scratch clone:
init→traina small base → two sequential forks that commit → onecontinue→ onekillof a started fork →waitloops →list. Phase 1 complete when: both forks reportcache: hitandfingerprint: ok,models_usedequals the requested id, ledger lines match the result objects,killleaves noclaudeprocess behind, and the smoke's results and ledger are appended to the run log.
Phase 2: Bookkeeping (warmth, rebase, usage rendering)
Owner: Opus 5.5. Same files plus templates/rebase.md. Commits: 🤖.
- 2.1 Warmth and keep-warm. TTL inference,
last_hit,warm_until, margin, state (design §5); the tick inside each supervisor (60 s) and insidewait(30 s); the ping rule with its activity condition and break-even cap;ttl_downgrade;cold_start;init --pings on|offdefaulting per P0.6. Done when: clock-injected unit tests cover the warm/expiring/cold boundaries at 1 h and 5 min, ping eligibility (including no pings without activity in the last 10 min and the cap at 19 and 11), and TTL inference from 1h-only, 5m-only and read-only calls. - 2.2 Rebase and re-train flags. The
rebasecommand (fork → new base withparent, old basesuperseded),rebase_due(phase boundary via--phasechange, commit and line thresholds, delta-size estimate, binary drift),retrain_due(fingerprint-defining field differs,max_base_tokensper P0.7, four rebases in a lineage), thresholds frominit. Done when: unit tests trigger each reason separately, andfork's launch return lists the rightreasons[]. - 2.3 Usage rendering.
usage --block|--phase --format json|md. The md block row has exactly the columns of the new run-log block table (step 3.3); the phase table gives per-kind totals, cold starts, cache-hit ratio and a priced total. Done when: a unit test checks the md against a fixed ledger fixture. - Phase 2 tests and smoke. G-unit green. Live: in the scratch clone, train → fork (phase 1) → fork with
--phase 2returnsrebase_due→rebase --as B2→ fork from B2 hits B2's prefix; let one base sit intoexpiringand observe a ping when pings are on;usage --phase 1 --format mdrenders. Phase 2 complete when: all of that is in the run log with the helper's JSON.
Phase 3: Skill and procedure text, release
Commits: 🤖. The release commit carries the changelog body (step 3.4).
-
3.1
forks/SKILL.md. Frontmatter per design §2 withallowed-tools: Bash(uv:*)and the injection!`uv run "${CLAUDE_SKILL_DIR}/forks.py" where`. Body: when to use (the Athanor, complex blocks), the command contract (§4.1), the result object (§4.2), the rules the Athanor obeys (flags, not judgment; nothing running at hand-back; never resume or fork a headless session by hand;$SCRIPTS_UV), the fallback whenwherefails, and a Claude-Code-only note. Short: the design says "Short". Done when: every command and result field named matchesforks.py --helpand §4.2. -
3.2
agents/athanor.md. All of design §8.4's athanor bullets:skills:gainsspire:forks; intake no longer reads code and trains the base instead (the base writes the trunk notes); step 2 routes complex blocks to headless forks and mechanical or distant ones to fresh agents; no inline building; keepers gain "you do not read code", "nothing running at hand-back" and the commit table (§4.4); "read the fix's diff for loosened assertions" (athanor.md:34) becomes the mechanical check of design §8.2 step 5; the deliverable gains usage totals and base state. The Agent-tool fork path is removed. Done when: G-text passes (below). -
3.3 Implementation skill and references.
implementation/SKILL.md:45-49rewritten as "trained implementer and forks" (design §8.4), and line 65ff gains "headless forks take a full model id". Line 12's inline rule stays for non-Athanor use.references/spawning-claude-code.md: Fork-table rows and the new "Headless sessions (claude -p)" section exactly per design §9, each row with research id and date 2026-10-16, plus the models and rules-of-thumb lines, plus two rows from Phase 0 (detach behaviour on Windows from P0.1, uv env injection from P0.0) markedobserved 2026-10-16 (preflight report).references/run-log.md: theForks:header line, the new block tableBlock | Owner (model id from modelUsage) | Session | Commit | in | out | cache write | cache read | cost Δ, the phase-close usage table, and "Trunk notes (written by the base at training)".references/models.md: dated notes per design §8.4. Done when: G-text passes, and every new spawning row has a status and a date. -
3.4 README, version, release.
plugins/spire/README.mdskills table gainsforks. Both manifests go to0.3.0(D1). Release commit🤖 Spire 0.3.0: trained implementer forked per block (spire:forks), whose body is the changelog, as in7c1ed39: new skill, Athanor procedure change, Agent-tool fork dropped, references updated, preflight report path. Done when: both manifests show the same version and the README row exists. -
G-text (run at phase end):
grep -n "fork of yourself\|That reading is your trunk\|read the fix's diff" plugins/spire/agents/athanor.mdreturns nothing;grep -n "spire:forks" plugins/spire/agents/athanor.mdhits in the frontmatter;grep -n "a fork of the builder" plugins/spire/skills/implementation/SKILL.mdreturns nothing;- the
versionvalues in both manifests are equal; - every command name in
forks/SKILL.mdappears inforks.py; - G-unit and G-where still pass.
Phase 3 complete when: G-text is green and a fresh reviewer has checked the procedure text against the helper's actual JSON and exit codes. The review explicitly covers contract drift between Phases 1–2 and Phase 3.
Phase 4: Install and smoke (needs the user to update the plugin)
Entry: the user has updated and reloaded Spire 0.3.0. Owner: Igor dispatches a fresh 0.3.0 Athanor on a toy plan in the scratch clone: 2 phases, 3 complex blocks in one area plus 1 mechanical block.
- 4.1 The Athanor's start shows the
whereinjection output, and its firstforkscalls run without a permission prompt (the R4 Assumed above). Record the outcome in the spawning reference only if it differs from the assumption. - 4.2 The procedure is followed: the base writes the trunk notes; complex blocks go to forks with delta briefs; the mechanical block goes to a fresh agent; phase 2 starts from a rebased base; the run log carries
usagerows;listshows nothing running at hand-back. The Athanor's transcript shows noReadof source files under the clone'splugins/. - Phase 4 complete when: 4.1 and 4.2 are recorded in
reports/<stamp>-headless-forks-smoke.md. Fixes go in as patch versions (0.3.1 and up), both manifests,🤖/🐛.
Phase 5: Trial (goal "Proven, not assumed"), Igor-owned
Per design §10 "The trial": arms F and C, sealed inputs, a transcript audit with a positive control, a blind judge, four token columns priced for every agent including the root, wall time from date and git, and the arm F mechanism metrics. Output: reports/<stamp>-headless-forks-trial.md. Thresholds in design §5 are retuned from it, as a patch release. Entry needs D3. Complete when: the report exists and Igor has put the result in front of the user. Whether the goal box gets ticked is the user's call.
8. Risks the attacker should test this plan against
- R1 under uv. On Windows,
uv runmay hold its child in a job object, and so may the Bash tool. Breakaway may be refused. P0.1 is designed around this, but a variant that survives the Bash call and then dies at the end of the subagent's turn would be fatal only for hand-backs with running jobs, which the procedure forbids anyway. - The env bleed from
uv run(§2) also reaches the Athanor's gate commands if they are run through the helper. They are not: the Athanor runs gates in its own Bash. - The Phase 3 text and the Phase 1–2 JSON have different owners. §4.2 is the fixed seam. The Phase 3 review checks it.
availableModels(.claude/settings.json:56-67) may refuse the owner model a plan names for forks, in this repo and others. D5.- The arm C contamination in the trial (D3).
9. Open decisions (not settled by the goal, the design or the coordinator)
- D1. Version number. Default
0.3.0(decision 9). Alternative0.2.3. The goal says "version TBD". - D2. Changelog form. No CHANGELOG file exists. The default follows precedent: the release commit body (
7c1ed39). The alternative is a newplugins/spire/CHANGELOG.mdstarting at 0.3.0. - D3. Trial set-up. (a) Which real multi-block plan; the design says the user picks. (b) How arm C gets the 0.2.2 shape once 0.3.0 is installed, because the preloaded skills would be 0.3.0. Default: run arm C before the user installs 0.3.0 (between Phase 3 and Phase 4), and arm F after Phase 4. That means the trial plan must be picked before Phase 4.
- D4. Igor follow-ups.
igor/SKILL.md:66(fork wording) and:112(TaskStopof an Athanor orphans detached forks; it would need "then runforks list/killon its run"). Default: out of this plan, a one-line follow-up for Igor after Phase 4. - D5. Models outside
availableModels. If P0.5 shows that a full id not in the project's list is refused, should the helper refuse atinitwith a clear message (default), and should the user add full Sonnet or Haiku ids to.claude/settings.json? The plan does not touch user or project settings.