34 KiB
Build plan: Trained implementer, forked per block (spire:forks)
Date: 2026-10-16 01:50 +0200
Attacker: model id claude-opus-5-5[1m] (stage 4b; did not write the plan, will not build or review it)
Plan (as written, unchanged): plans/2026-10-16-01-headless-forks.md. Section refs "plan §N" point there.
Design: designs/2026-10-16-01-headless-forks.md incl. the 01:33 runtime amendment (Python under uv)
Goal: goals/headless-forks.md
Evidence: research/2026-10-16-00-claude-headless-fork-sessions.md (claim ids 1a…F5b)
Filename: -02- as the coordinator named it (workpad rule 1, explicit instruction).
Build scope: Phases 0–3. Phase 4 (install and smoke) and Phase 5 (trial) are post-build steps; they need the user.
The plan stands except where this document says Amended or New. Where both say something, this document wins.
STOP-level findings
None. Nothing found blocks the build before it starts. Phase 0 is the stop gate the plan already has, and the findings below go into it or into the contracts.
These came closest:
- A5 (uv and job objects). On Windows,
uv runor the Bash tool may hold the helper in a kill-on-close job object that does not allow breakaway. Both of the plan's P0.1 variants would then fail, and the plan would STOP for a reason that has a known workaround. This plan adds that workaround as a rung before STOP. - A9 (untracked inputs). The goal, design and plan are untracked on
main, so a build branch would start without them. Igor commits them first (pre-build step B0).
1. What the attack found
Severity: Important changes a contract or the phase structure. Medium changes one block. Minor is a one-line fix. Each finding names where this plan resolves it.
Important
- A1. Forks are orphaned when the Athanor dies. Supervisors are detached by design, so they outlive the orchestrator. The orchestrator can die in several ways: Igor
TaskStops it when a new plan invalidates the run (plugins/spire/skills/igor/SKILL.md:112), it crashes, it runs out of context, or the user interrupts it. The forks then keep editing and committing on the branch for up to--max-minutes(90). The plan'slostrule (plan §3.7) covers a dead supervisor, not a dead orchestrator. D4 defers Igor's text, so this has to be fixed in the helper. New invariant N1 (§4): an orphan watchdog. - A2. The Bash tool's default timeout is 120 s. Its maximum is 600 s.
wait --timeout 540is safe only if the Athanor passestimeout: 600000on that Bash call. Neither the design nor the plan says so: design §7 calls 600 s the "ceiling", but it is the maximum, not the default. Without the parameter, everywaitgets killed at 2 min with no JSON. §4 C2, G-text check, P0.1(d). - A3. Windows file semantics break the registry design.
os.replaceraisesPermissionErrorwhile another process has the target open. Here that meanswait/listreaders and the supervisors that tick every 60 s. The plan also puts a 30 s heartbeat per job into the registry (plan §3.7), which multiplies writes under one lock. Appends from parallel supervisors toledger.jsonlare not atomic on Windows. Two tickers can each decide to ping the same base. §4 C3. - A4. P0.6's pass criterion cannot pass as written. Bases A and B share cwd, tools and system prompt, and P0.2–P0.5 run in the same clone. So the shared system and tool prefix stays warm (
F4d), and B's fork at 65 min reads that part from cache. It showspartial, not a miss. The same fact changes the helper's semantics: a fork of a cold base will usually showpartial. Separately, P0.6 needs a timed probe that runs for 65 min. It cannot be driven by foregroundsleep(the harness blocks it), so it depends on P0.1's detach mechanism, yet the plan starts P0.6 first. Amended P0.6 and the ordering in Phase 0, §4 C5. - A5. P0.1's escape route is too narrow. If
uv(or the Bash tool) puts its child in a job object withKILL_ON_JOB_CLOSEand withoutBREAKAWAY_OK,CREATE_BREAKAWAY_FROM_JOBfails with access denied. The--attachfallback then routes its notification to the session root (spawning-claude-code.md:29). There is a standard way out of any job: create the process through WMI/CIM (Win32_Process.Create), which starts it outside the caller's job. There is a second problem. Ifsys.executableis a venv launcher (python.exethat re-spawns the base interpreter), the supervisor gets an extra layer. Amended P0.1 ladder, §4 C6. - A6. Forks can launch forks. Headless runs load project settings (
5a). Reliquary allowsBash(uv:*)(.claude/settings.json:32), and forks have theSkilltool, so a fork can loadspire:forksand runuv run …/forks.py fork. The deny ruleBash(claude:*)does not cover that, and it does not cover"$CLAUDE_CODE_EXECPATH" -peither. New invariant N3 (§4). - A7. The successor Athanor would re-train. When an Athanor's context is spent, Igor starts a fresh one "from the run log and trunk notes" (
igor/SKILL.md:112). The new intake says "first phase:forks init,train" (design §8.2 step 1). It has no rule for an existing state dir, so a successor would init a second time and train a second base. Block 3.1 adds an adoption rule. - A8. The release path is missing a step. The marketplace is a git source (
.claude/settings.json:2-8), andspireresolves to./plugins/spireinside it (.claude-plugin/marketplace.json). So 0.3.0 reaches anyone's plugin cache only after the branch is merged and pushed to the remote's default branch. Installing it also switches the Athanor procedure in every project at once, before the smoke and the trial. Post-build steps, decisions U1 and U3. - A9. The build inputs are untracked.
goals/headless-forks.md, the design and the plan are??in git status, and the session journal is modified. A worktree or branch made now starts without them. Pre-build step B0.
Medium
- A10.
waitexit codes are ambiguous. It is unclear whatwait --allreturns when one job failed and others are still running. The Athanor loops "until exit 0 or 4" (design §7) and could stop looping with jobs running. §4 C4. - A11. Parallel mode would need a markdown parser. "Refuses
parallelmode unless the brief names its file set" (design §3.4) means parsing the brief. Amended:--pathsis required for parallel. The helper stores it, the result echoes it, and the Athanor commits exactly those paths (plan §4.4 becomes mechanical). - A12. The helper's git calls can collide with a fork's commit. The delta header,
--shortstatand the rebase thresholds call git, and those calls can takeindex.lockopportunistically while a fork commits. §4 C7: every helper git call usesgit --no-optional-locks. - A13. Consumers the plan's 3.2/3.3 lists miss:
athanor.md:23"otherwise orient via direct file reads". It must say plans and docs, not code.athanor.md:60"a fail-first test inside a block, a look one layer up before fixing" are code-level actions by the Athanor. They become things it writes into briefs.athanor.md:7-10the frontmatter description ("blocks in the foreground") is what Igor reads to route.implementation/SKILL.md:40"Each block commits and adds a short handoff note to the run log". Forks writenotes/<block>.mdand the Athanor appends them.models.md:49"a trunk read at intake of about 100k–250k". That read is now the base's: add a dated note, keep the old line.- Block 3.1 / 3.2.
- A14. D5 ("refuse at init") needs a rule the plan does not give.
availableModelscan live in user, project and local settings, and their merge is unknown. Reliquary lists the aliassonnet(.claude/settings.json:64), and nobody has tested whether that admitsclaude-sonnet-5-5. Amended P0.5, §4 C8.
Minor
- A15. Cut
$SCRIPTS_UVfrom the Athanor's calls."$SCRIPTS_UV" run …does not match aBash(uv:*)allow rule, so it would prompt or be denied behind the closed door, and it is unset on this host. Calls use plainuv.wherereportsSCRIPTS_UVwhen it is set, and SKILL.md mentions it only for hosts withoutuvon PATH. - A16. Add a
.python-versioncase to P0.0. The Athanor will build in Python projects that carry one. Add a cwd with.python-versionpinning 3.9 and check that the script'srequires-python >= 3.10wins. - A17. R4 can be partly tested before release. Precedent: an
!injection at agent preload already runs on this host. Igor preloadsspire:adepts(agents/igor.md:15), and the journals record its output, "Roster empty at boot" (2026-09-21, -09-29, -10-05, -10-15). That line isroster.sh:18's own output, so the injection ran. What is untested: an Agent-tool subagent's preload,uv, and whetherallowed-toolspre-approves the call. New P0.8, a headless proxy in the scratch clone. It is informational, not a stop item. - A18. Smoke and test details.
- Check that
kill"leaves noclaudeprocess behind" by the recorded PIDs. Checking by name would find the host, which isclaude.exetoo. - Generate the stub
claudewrapper at test runtime in a temp dir..gitattributeshas no rule for.cmd/.bat, so nothing of the kind gets committed.
- Check that
- A19. PIDs are reused on Windows.
lostand orphan detection compare PID plus process start time. - A20. G-text checks only one direction. Add the reverse (every command in
forks.py's dispatch is named in SKILL.md), plus checks for the600000rule and theSPIRE_FORKS_CHILDguard. - A21. Fix-pass commits land after the release commit. The release commit is the last block of Phase 3, but the Phase 3 review and fix pass come after it. That is fine: 0.3.0 is unpublished until it is pushed, and fixes before the push stay 0.3.0 with no version change.
- A22. Pre-existing drift, not in scope.
- The README skills table already lacks
adepts,curator,init,projectandzathras(README.md:13-21). This build adds onlyforks. - The agent and skill lists in
.claude/spire/project.md:31-32are stale (no planner/attacker/reviewer;forksis missing after this build). That file is project config. Both are follow-ups.
- The README skills table already lacks
- A23. The design cites project config keys that do not exist. It refers to an assertion pattern "from project config" (design §8.2 step 5) and "unless project config says parallel test runs are safe" (§8.3). Neither key exists in the
## Buildtemplate. The defaults go into the procedure text (patternassert; parallel tests off). Adding the keys is a follow-up forspire:init/spire:project.
2. Conventions pass
| Rule | Governing source | Plan §1 | Finding / action |
|---|---|---|---|
Gitmoji <emoji> <short description> |
CLAUDE.md:7, specs/commit-conventions.md:3-5 |
yes | ok |
🤖 for agent files incl. skills |
commit-conventions.md:91 |
yes | ok. Precedent is split for a new skill with a script: 1c4ec76 used ✨ (adepts + roster.sh), the newer f1de89e used 🤖 (orbit scripts). Keep 🤖; ✨ is acceptable for 1.1's first commit. Either way it is not a finding. |
No Co-Authored-By or other attribution lines |
user policy (coordinator, 2026-10-16); .claude/settings.json:70 includeCoAuthoredBy: false; precedent 7c1ed39 body has none |
missing | Added to every block below. It applies to commits made by forks and by fresh agents too. |
| Plugin self-contained | CLAUDE.md:3 |
yes | ok |
| Workpad placement and naming; cascade subfolder | plugins/spire/skills/workpad/SKILL.md:72, :80 |
yes | ok |
| README lists components | plugins/spire/CLAUDE.md:1 |
yes | Add the forks row only (A22). |
| Two manifests carry the version; marketplace files carry none | .claude-plugin/plugin.json:3, .codex-plugin/plugin.json:3; checked .claude-plugin/marketplace.json, .agents/plugins/marketplace.json |
yes | ok |
Codex loads ./skills/ wholesale |
.codex-plugin/plugin.json:10 |
partly | forks reaches Codex automatically and there is no per-skill opt-out. The "Claude Code only" line in SKILL.md is required, not optional. |
| LF endings | .gitattributes:1-7 |
yes | No rule for .cmd/.bat, so stubs are generated at test runtime (A18). |
**/*.log, **/*.tmp ignored |
.gitignore:9-10 |
yes | .txt for logs (plan). |
__pycache__/ ignored |
.gitignore:13 |
no | Covers the test import of forks.py. No action. |
Worktrees under .claude/worktrees/ (ignored) |
.gitignore:7; Igor: worktree for code runs, igor/SKILL.md:179 |
no | The build runs in .claude/worktrees/headless-forks, branch headless-forks (B0). |
| Push branches, never open PRs; push only if instructed | igor/SKILL.md:177, athanor.md:55 |
partly | The build does not push. Merge and push are post-build (U3). |
| Goals read-only for the Athanor; stage explicit paths | athanor.md:53, :55 |
yes | ok |
| Stamp exact model id | implementation/SKILL.md:68 |
yes | ok |
| Spawning statuses: verified / observed / assumed / unknown, dated | spawning-claude-code.md:5-12 |
yes | New rows use only these four. The current :61 "unverified" is outside that vocabulary, and the design's replacement row fixes it. |
| Model notes dated, not dropped | models.md:3 |
yes | ok (A13: :49 gets a dated note). |
uv: PEP 723, uv run path/to/script.py, SCRIPTS_UV |
project-uv/SKILL.md:21, :31 |
yes | A15 cuts $SCRIPTS_UV from the calls. |
Skill ! injection + allowed-tools |
adepts/SKILL.md:4, :7-12 |
yes | Precedent at preload (A17); P0.8. |
| Separation: builder ≠ reviewer; fresh review per phase | implementation/SKILL.md:22, :53 |
yes | ok |
3. Precedent search on the risks
Dropped (the repo or this host already does the risky thing):
| Risk | Precedent |
|---|---|
${CLAUDE_SKILL_DIR} (Windows path) inside an ! injection under Git Bash |
adepts/SKILL.md:12 with roster.sh; it ran at Igor's preload in four journals (A17). |
Nested claude -p from a subagent's Bash works at all, including cwd in the session scratchpad |
research T1–T7, D1–D3, E1, V1 (all from a subagent in this scratchpad). |
Running the Desktop-bundled binary via $CLAUDE_CODE_EXECPATH from outside its package |
research F5a, V1. |
| uv-managed Python ≥ 3.10 available offline | uv python list --only-installed: 3.10.16, 3.11.11, 3.12.x and 3.13.1 under %APPDATA%\uv\python (checked 2026-10-16). |
__pycache__ or .log files leaking into or hiding from git |
.gitignore:13, :9. |
| Absolute user paths in committed workpad files | the research doc already carries them. |
Reduced:
| Risk | Precedent | What remains |
|---|---|---|
R4 injection + allowed-tools |
main-thread preload works (A17); reliquary has a project allow rule for uv (settings.json:32), so pre-approval matters only in other projects |
Subagent preload and pre-approval: P0.8 proxy, then Phase 4. |
| D3(b) arm C once 0.3.0 is installed | the plugin cache keeps old versions: ~/.claude/plugins/cache/reliquary/spire/ holds 0.1.0, 0.1.1, 0.2.0 and 0.2.2 |
Whether --plugin-dir on the 0.2.2 cache dir (or a worktree at a7ce5db) runs cleanly beside an installed 0.3.0 is assumed (U2). |
| R3 parallel tool-using forks | index-lock races between parallel children happened in the 2026-10-15 experiment (design §8.3, M5), so the risk is real | The mitigation (parallel forks never commit) stays; P0.4 tests it. |
Kept (no precedent): R1, R2, R5, R7, the Windows file semantics (A3), contract drift at the Phase 1–2/3 seam, orphans (A1), R11 plan usage, R12 binary drift. See §8.
4. Contract amendments (on top of plan §4)
The exact form matters for these, so they are stated as invariants. Everything else in plan §4 stands.
- C1. Phase 1 values for Phase 2 fields. In Phase 1,
base_stateandwarm_untilare computed with the configured TTL (default 3600 s), with no inference and no pings.rebase_dueandretrain_duearefalsewithreasons: []. Phase 2 replaces the computation, not the fields. - C2. The
waitcall. SKILL.md states: callwaitfrom Bash withtimeout: 600000; never omit it.wait --timeoutkeeps its default of 540 and its maximum of 570. - C3. State files on Windows.
- (a) Every registry write is temp-then-
os.replace, retried onPermissionError(at least 20 tries, 50 ms apart). Readers retry the same way. - (b) Per-job runtime state (supervisor PID plus start time, claude PID plus start time,
heartbeat_at,turns,last_event_at) lives injobs/<job>.json. Its only writer is that job's supervisor. The registry holds config, bases and the job index. - (c) Ledger appends happen under
registry.lock. - (d) A ping is decided and marked
ping_in_flighton the base under the lock. Other tickers skip a base that is in flight.
- (a) Every registry write is temp-then-
- C4.
waitexit codes.- Exit
3while the wait condition is unmet.done[]may already hold finished results, failed ones included. - Exit
0or4only when the condition is met:0if every job in the condition endedok, else4. - Every result is returned by at least one
wait.listalways shows all jobs.
- Exit
- C5. Cache verdict and cold start.
- The verdict is set from the first assistant event's
cache_readagainst the base's context tokens:hit≥ 0.9,partial> 0,miss= 0. cold_startis set when the base wascoldat launch, or when the verdict is nothitand the fingerprint isok.- SKILL.md says that a cold base usually shows
partial, because the shared system prefix stays warm (F4d).
- The verdict is set from the first assistant event's
- C6. How the supervisor is spawned. The interpreter is
sys._base_executablewhen it differs fromsys.executable(a venv launcher), elsesys.executable, with-I. Stdio goes to files underraw/, and no inherited handles remain. The creation method is whichever rung of P0.1 passed. It is one constant inforks.py, andwherereports it. - C7. Git calls. Every git call from the helper is
git --no-optional-locks ….wherereports the resolvedgit. - C8. Models outside
availableModels(D5).initapplies the rule P0.5 observed, against theavailableModelslists it finds in~/.claude/settings.json,<cwd>/.claude/settings.jsonand<cwd>/.claude/settings.local.json. Managed settings are out of scope.- If the id is refused,
initexits 2. The message names the id and the files that hold lists, and says that the user must add the id. - At runtime, a refusal in
trainends as statusfailed, reasonmodel_refused. No settings file is ever written.
- N1. Orphan watchdog.
- The helper records
last_orchestrator_call_aton the run wheneverwait,fork,continue,train,rebaseorlistis called. - A supervisor whose job is still running kills its
claudetree when no such call has happened fororphan_afterseconds (initdefault 1200, more than 2 × 570). The job's status becomeskilled, with flagorphaned, and a ledger line is written. - Why: A1. The procedure keeps the Athanor in a
waitloop whenever a fork runs, so a silence of 20 min means it is gone.
- The helper records
- N2. Parallel forks need
--paths.fork --mode parallelrequires--paths <p>…. The paths are stored in the job and echoed aspathsin the result. Sequential forks may pass--pathstoo. - N3. No forks inside forks. The child env gains
SPIRE_FORKS_CHILD=1(on the keep list). Every launching command (train,fork,rebase,continue,tickpings) exits 2 when that variable is set.where,list,usageandwaitstay allowed. - Usage md columns (2.3 and
run-log.md, fixed here):Block | Owner (model id from modelUsage) | Session | Commit | in | out | cache write | cache read | cost Δ.Commitlists the commits between the fork's launch HEAD and its end, made by a sequential fork. Parallel forks show—, and the Athanor fills in the commit.
5. Decisions applied
The coordinator's defaults:
- D1 = 0.3.0, both manifests.
- D2 = the release commit body, no CHANGELOG file.
- D3 = trial deferred; it needs the user; Phase 5 is out of this build.
- D4 = the Igor follow-ups are out of this plan and recorded below. N1 makes Igor's
TaskStopnon-fatal in the meantime. - D5 = refuse at init with a clear message, no settings changes (C8).
The attacker's defaults, which the build takes unless the user overrides them:
- N1 with a 20 min threshold.
- N2, N3.
- A15:
$SCRIPTS_UVcut from the calls. - Worktree
.claude/worktrees/headless-forkson branchheadless-forks. - Live smokes use
claude-opus-5-5(it is in the clone'savailableModels). - The Phase 0 spend trigger in §6.
6. Build
Gates (run from the worktree root):
- 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. Stdout is exactly one JSON object; uv's own output goes to stderr. - G-text: the list in Phase 3.
Standing rules for every block:
- Commits use gitmoji, explicit paths, and no
Co-Authored-Byor other attribution lines. - No push, no PR.
goals/is read-only.- Each handoff note stamps its model id.
- Every live test targets a
git clone --localin the session scratchpad, never the worktree (plan §3.4). - Owners are fresh Agent-tool agents dispatched by the 0.2.2 Athanor. Unpinned agents get an alias, and each stamps its id (
spawning-claude-code.md:47-52).
Pre-build (Igor, before dispatch): B0
- Commit on
mainby explicit paths: the goal, the design, the plan, this build plan, and the journal. Message:📝 Plan headless forks (spire:forks): goal, design, plan, build plan. The other untracked report (reports/2026-10-16-01-session-harness-app-forms.md) is Igor's call. - Run
git worktree add .claude/worktrees/headless-forks -b headless-forks main. - Dispatch the installed 0.2.2 Athanor (
modelomitted) with this build plan. The worktree path is its checkout, and the run log isdocs/workpad/reports/<YYYY-MM-DD-hh>-headless-forks-run.mdinside it.
Phase 0: Preflight (STOP gate)
Owner: the Athanor itself, from its own Bash (claude-opus-5-5[1m]). This is one block, because the process tree is what is under test (plan §3.2). Probes are throwaway and live in the scratchpad; nothing in plugins/ changes.
Order (amended, A4): P0.0, then P0.1, then start P0.6 as a detached timed probe using P0.1's mechanism. Then P0.2–P0.5, P0.7 and P0.8 while it runs, then collect P0.6.
Changes to plan §7 Phase 0 (rows not named here stand as written):
-
P0.0 adds:
- (c) A cwd with
.python-version=3.9(A16). - Record
sys.executablevssys._base_executable(C6), and the resolvedgitandclaude.
- (c) A cwd with
-
P0.1 ladder (A5). Each rung is tried only if the one before it failed:
PopenwithDETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOBfrom insideuv run.- The same without breakaway.
- Creation through WMI/CIM:
powershell -NoProfile -Command "Invoke-CimMethod -ClassName Win32_Process -MethodName Create …", capturing the returned PID. --attachunder Bashrun_in_background: true, polled from later foreground calls.
Each rung must survive four things: (a) the uv process exiting; (b) the Bash call ending; (c) ≥ 15 min, with output growing in later calls; and (d) a Bash call that the tool kills on timeout (call with
timeout: 5000). A tree-kill must also leave none of the recorded PIDs alive. Record the rung and its flags. STOP only if all four rungs fail. -
P0.5 adds three model cases for C8:
- a listed full id (
claude-opus-5-5); - an unlisted full id whose alias is listed (
claude-sonnet-5-5vssonnet); - an unlisted id of an unlisted family if one exists, else skip.
Record the rule.
- a listed full id (
-
P0.6 (amended, A4). Bases A and B are trained with different training content and the same fingerprint, and run as a detached timed probe. Pass when A's fork after its 50 min ping shows first-event
cache_read≥ 0.9 × A's context, and B's fork at 65 min shows < 0.5 × B's context. Apartialon B is the expected result. On fail: pings default off (plan §6). -
P0.8 (new, informational, A17). In the scratch clone:
- Remove
Bash(uv:*)from the clone's.claude/settings.json(scratch only). - Add
.claude/skills/forkprobe/SKILL.mdwithallowed-tools: Bash(uv:*)and!`uv run "${CLAUDE_SKILL_DIR}/probe.py" where`. - Add
.claude/agents/forkprobe.md, which preloads it. - Run a headless
claude -p --tools "Task,Bash" --permission-mode defaultthat dispatchesforkprobeas an Agent-tool subagent. The subagent quotes the injected text, then runsuv run …/probe.py ping.
Record three things: whether the injection ran in the subagent, whether the later call was pre-approved (no entry in
permission_denials), and the wall time of the injection. On fail, SKILL.md says that projects need aBash(uv run:*)allow rule (plan §6 default). - Remove
-
Spend trigger (new). Sum the per-call
total_cost_usddeltas (client estimates). If the sum passes $25 before P0.2 is complete, stop probing and hand back the partial report asSTOP (budget)for the user.
Phase 0 done when: reports/<stamp>-headless-forks-preflight.md exists with every row filled. A one-line GO or STOP heads it, and it lists the defaults Phase 1 must use: scrub additions, the P0.1 rung and flags, the usage rule, the parallel cap, pings, the fingerprint fields kept or dropped, the availableModels rule, the injection outcome. Commit it as 📝. The Athanor hands back.
Review and decision: a fresh reviewer checks the report's numbers against the raw files in the scratchpad. Igor makes the GO/STOP call. GO continues without the user; STOP goes to the user.
Phase 1: Helper core
| Block | Owner | Scope | Gate | May stay broken |
|---|---|---|---|---|
1.1 Skeleton, state, where, init |
fresh, opus |
plan §7 1.1 plus: C3(a); jobs/ dir; C7; C8 in init; the orphan_after config (N1). where reports uv, Python (both executables), the script, claude, git, the spawn rung, SCRIPTS_UV if set, and unknown CLAUDE_CODE_* vars. |
tests the block owns pass via uv run …/tests/test_forks.py; G-where |
Launching commands exit 2 not implemented. |
| 1.2 Launch and supervise | fresh, opus |
plan §7 1.2 plus C1, C3(b–d), C4, C5, C6, N1, N2, N3, A19. The stub claude is generated at test runtime (A18). It must answer --version, replay init/assistant/result, hang, die, emit denials, and refuse a model. Tests also cover: replace while a reader holds the file open (C3a); exit codes in the mixed case (C4); the orphan kill under an injected clock (N1); refusal under SPIRE_FORKS_CHILD (N3); parallel without --paths refused (N2). |
G-unit | Warmth inference, pings, rebase, usage --format md (Phase 2). |
| 1.3 Phase tests, smoke, fallout | the Athanor runs the smoke from its own Bash; any fallout goes to a fresh opus agent |
G-unit, then the live smoke of plan §7 ("Phase 1 tests and smoke") in the scratch clone with claude-opus-5-5. Every wait is called with Bash timeout: 600000. The kill check uses recorded PIDs (A18). |
G-unit + the smoke criteria of plan §7 | none |
Commits: 🤖 (1.1 may use ✨). Fallbacks:
- If G-unit is still red after 1.3's fallout agent plus one more fresh fix agent, hand back red and do not start Phase 2.
- If a smoke fork shows a verdict other than
hitwhile P0.2 passed, it is a fingerprint bug. One fix, then re-smoke. If it still misses, hand back red with the raw stream paths. - If the Athanor judges its context has passed about 600k (
models.md:19, a guesstimate), hand back for a successor started from the run log.
Phase 2: Bookkeeping
| Block | Owner | Scope | Gate | May stay broken |
|---|---|---|---|---|
| 2.1 Warmth and keep-warm | fresh, opus |
plan §7 2.1, ping dedupe per C3(d); the cold-start rule per C5 | G-unit | rebase, usage md |
| 2.2 Rebase and re-train flags | fresh, opus |
plan §7 2.2. A missing binary (the Desktop path is versioned, F5a) gives retrain_due with reason binary_missing, and launches refuse. |
G-unit | usage md |
| 2.3 Usage rendering | fresh, sonnet |
plan §7 2.3 with the fixed column contract (§4) | G-unit | none |
| 2.4 Phase tests, smoke, fallout | the Athanor, plus a fresh opus for fallout |
the plan §7 "Phase 2 tests and smoke", same Bash rules as 1.3 | G-unit + the smoke criteria | none |
Commits 🤖. The fallbacks are the same as Phase 1.
Phase 3: Skill text, procedure text, release
| Block | Owner | Scope | Gate | May stay broken |
|---|---|---|---|---|
| 3.1 Procedure text | fresh, opus |
forks/SKILL.md per plan §7 3.1 (C2 rule, C4 and C5 semantics, N1–N3, the A15 cut, the Claude-Code-only line); agents/athanor.md per plan §7 3.2 plus A7 (a Forks: line in the run log → list and adopt, never re-init), A13 (:7-10, :23, :60), A23 defaults; implementation/SKILL.md per plan §7 3.3 plus A13 (:40); references/run-log.md (with the fixed columns) |
G-text items 1–4, 6, 7 | Versions still at 0.2.2. |
| 3.2 Reference rows | fresh, sonnet |
spawning-claude-code.md per design §9 and plan §7 3.3, including the P0.0 and P0.1 rows marked observed 2026-10-16 (preflight report), and the P0.8 row; models.md dated notes, including the A13 note on :49 |
every new row has one of the four statuses and a date | Versions still at 0.2.2. |
| 3.3 README, version, release | fresh, sonnet |
the README forks row; both manifests at 0.3.0; G-text in full; release commit 🤖 Spire 0.3.0: trained implementer forked per block (spire:forks) with the changelog as its body (precedent 7c1ed39): new skill, Athanor procedure change, Agent-tool fork path dropped, references updated, preflight report path, the known follow-ups. No attribution lines. |
G-text in full, G-unit, G-where | none |
G-text:
grep -n "fork of yourself\|That reading is your trunk\|read the fix's diff\|the code the phase touches" plugins/spire/agents/athanor.mdreturns nothing.spire:forksappears in athanor.md's frontmatterskills:.grep -n "a fork of the builder" plugins/spire/skills/implementation/SKILL.mdreturns nothing.forks/SKILL.mdcontains600000,SPIRE_FORKS_CHILDand--paths.- The
versionvalues in both manifests are equal and read0.3.0. - Every command name in SKILL.md is in
forks.py's dispatch, and every name in the dispatch (except_supervise) is in SKILL.md. - G-unit and G-where still pass.
Phase 3 review: a fresh reviewer checks the procedure text against the helper's real JSON and exit codes (run G-where and a stubbed wait). This is the Phase 1–2/3 seam. Fix-pass commits that land after the release commit stay 0.3.0 (A21).
7. Post-build (outside this build; the user is needed)
- Merge and push
headless-forksinto the remote's default branch (U3). Until then nobody can install 0.3.0 (A8). - Phase 4, plan §7 unchanged, with these entry options (U1): (a) smoke 0.3.0 first with
--plugin-dir <worktree>/plugins/spirein the scratch clone, withspire@reliquarydisabled in the clone'ssettings.local.json(assumed workable), then update the marketplace; or (b) update the marketplace directly. Option (b) switches every project's Athanor before any smoke. - Phase 5, the trial, plan §7 unchanged. Entry needs U2.
- Igor ticks goal boxes; the user decides on the "Proven" box.
8. Risks for the build
These shape the build. They are not the phase reviewer's scope.
| Risk | Where it bites | Handling |
|---|---|---|
| R1 detach under the uv and Bash job objects | P0.1, then every launch | Four-rung ladder; STOP only if all fail. |
| R2 large-base cache | P0.2 | STOP rule as planned. |
| R5 usage-field rule | P0.3 → 1.2 | 1.2 implements exactly the recorded rule. |
| R7 TTL refresh | P0.6 | Amended criterion; pings off on fail. |
| Windows file semantics | 1.1, 1.2, 2.1 | C3 plus its unit tests. |
| Contract drift between the Phase 1–2 JSON and the Phase 3 text | 3.1 | §4 fixes the seam; G-text 6; the Phase 3 review. |
| Orphaned forks | after the build, wherever 0.3.0 runs | N1; D4 follow-up. |
| R11 plan usage | Phase 0, smokes | Spend trigger; smokes on small bases. |
| R12 binary drift or disappearance (Desktop update) | any run | Version/size/mtime check; binary_missing → retrain_due. |
| Shipping the procedure change before the trial | post-build install | U1. |
| R4 injection and pre-approval in subagents | 0.3.0 in other projects | P0.8 proxy; fallback text in SKILL.md. |
9. Open decisions for the user
None of these blocks the build (Phases 0–3). U1–U3 must be settled before Phase 4.
- U1. How 0.3.0 reaches installs. Smoke via
--plugin-dirbefore the marketplace update (default), or update directly. Updating changes the Athanor in every project before the smoke and the trial. - U2. The trial (D3). (a) Which real multi-block plan. (b) Arm C after install:
--plugin-diron the cached 0.2.2 (~/.claude/plugins/cache/reliquary/spire/0.2.2, still present) or a worktree ata7ce5db; or run arm C before installing 0.3.0. - U3. Merge and push. Who merges
headless-forksand pushes, and when. - U4.
availableModels. If P0.5 shows that unlisted full ids are refused, do you want full Sonnet or Haiku ids added to.claude/settings.json? Without them, reliquary's forks are Opus-only. This build does not touch settings. - U5. Defaults you may override: the orphan threshold of 20 min (N1), the Phase 0 spend trigger of $25, the worktree and branch name.
10. Follow-ups (recorded, not built)
- D4:
igor/SKILL.md:66fork wording;:112TaskStopof an Athanor should be followed byforks list/killon its run (N1 covers it after 20 min). ## Buildkeys for the assertion pattern, safe parallel tests, and the default fork owner model (full id), inspire:init/spire:project(A23).- README skills table drift;
.claude/spire/project.md:31-32lists (A22). - Root
AGENTS.mdstill points atg4b_ai(plan §5).