Files
reliquary/docs/workpad/plans/2026-10-16-02-headless-forks-build.md

34 KiB
Raw Permalink Blame History

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 run or 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's lost rule (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 540 is safe only if the Athanor passes timeout: 600000 on 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, every wait gets killed at 2 min with no JSON. §4 C2, G-text check, P0.1(d).
  • A3. Windows file semantics break the registry design. os.replace raises PermissionError while another process has the target open. Here that means wait/list readers 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 to ledger.jsonl are 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 shows partial, not a miss. The same fact changes the helper's semantics: a fork of a cold base will usually show partial. Separately, P0.6 needs a timed probe that runs for 65 min. It cannot be driven by foreground sleep (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 with KILL_ON_JOB_CLOSE and without BREAKAWAY_OK, CREATE_BREAKAWAY_FROM_JOB fails with access denied. The --attach fallback 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. If sys.executable is a venv launcher (python.exe that 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 allows Bash(uv:*) (.claude/settings.json:32), and forks have the Skill tool, so a fork can load spire:forks and run uv run …/forks.py fork. The deny rule Bash(claude:*) does not cover that, and it does not cover "$CLAUDE_CODE_EXECPATH" -p either. 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), and spire resolves to ./plugins/spire inside 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. wait exit codes are ambiguous. It is unclear what wait --all returns 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 parallel mode unless the brief names its file set" (design §3.4) means parsing the brief. Amended: --paths is 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, --shortstat and the rebase thresholds call git, and those calls can take index.lock opportunistically while a fork commits. §4 C7: every helper git call uses git --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-10 the 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 write notes/<block>.md and 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. availableModels can live in user, project and local settings, and their merge is unknown. Reliquary lists the alias sonnet (.claude/settings.json:64), and nobody has tested whether that admits claude-sonnet-5-5. Amended P0.5, §4 C8.

Minor

  • A15. Cut $SCRIPTS_UV from the Athanor's calls. "$SCRIPTS_UV" run … does not match a Bash(uv:*) allow rule, so it would prompt or be denied behind the closed door, and it is unset on this host. Calls use plain uv. where reports SCRIPTS_UV when it is set, and SKILL.md mentions it only for hosts without uv on PATH.
  • A16. Add a .python-version case to P0.0. The Athanor will build in Python projects that carry one. Add a cwd with .python-version pinning 3.9 and check that the script's requires-python >= 3.10 wins.
  • A17. R4 can be partly tested before release. Precedent: an ! injection at agent preload already runs on this host. Igor preloads spire: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 is roster.sh:18's own output, so the injection ran. What is untested: an Agent-tool subagent's preload, uv, and whether allowed-tools pre-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 no claude process behind" by the recorded PIDs. Checking by name would find the host, which is claude.exe too.
    • Generate the stub claude wrapper at test runtime in a temp dir. .gitattributes has no rule for .cmd/.bat, so nothing of the kind gets committed.
  • A19. PIDs are reused on Windows. lost and 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 the 600000 rule and the SPIRE_FORKS_CHILD guard.
  • 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, project and zathras (README.md:13-21). This build adds only forks.
    • The agent and skill lists in .claude/spire/project.md:31-32 are stale (no planner/attacker/reviewer; forks is missing after this build). That file is project config. Both are follow-ups.
  • 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 ## Build template. The defaults go into the procedure text (pattern assert; parallel tests off). Adding the keys is a follow-up for spire: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_state and warm_until are computed with the configured TTL (default 3600 s), with no inference and no pings. rebase_due and retrain_due are false with reasons: []. Phase 2 replaces the computation, not the fields.
  • C2. The wait call. SKILL.md states: call wait from Bash with timeout: 600000; never omit it. wait --timeout keeps its default of 540 and its maximum of 570.
  • C3. State files on Windows.
    • (a) Every registry write is temp-then-os.replace, retried on PermissionError (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 in jobs/<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_flight on the base under the lock. Other tickers skip a base that is in flight.
  • C4. wait exit codes.
    • Exit 3 while the wait condition is unmet. done[] may already hold finished results, failed ones included.
    • Exit 0 or 4 only when the condition is met: 0 if every job in the condition ended ok, else 4.
    • Every result is returned by at least one wait. list always shows all jobs.
  • C5. Cache verdict and cold start.
    • The verdict is set from the first assistant event's cache_read against the base's context tokens: hit ≥ 0.9, partial > 0, miss = 0.
    • cold_start is set when the base was cold at launch, or when the verdict is not hit and the fingerprint is ok.
    • SKILL.md says that a cold base usually shows partial, because the shared system prefix stays warm (F4d).
  • C6. How the supervisor is spawned. The interpreter is sys._base_executable when it differs from sys.executable (a venv launcher), else sys.executable, with -I. Stdio goes to files under raw/, and no inherited handles remain. The creation method is whichever rung of P0.1 passed. It is one constant in forks.py, and where reports it.
  • C7. Git calls. Every git call from the helper is git --no-optional-locks …. where reports the resolved git.
  • C8. Models outside availableModels (D5).
    • init applies the rule P0.5 observed, against the availableModels lists it finds in ~/.claude/settings.json, <cwd>/.claude/settings.json and <cwd>/.claude/settings.local.json. Managed settings are out of scope.
    • If the id is refused, init exits 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 train ends as status failed, reason model_refused. No settings file is ever written.
  • N1. Orphan watchdog.
    • The helper records last_orchestrator_call_at on the run whenever wait, fork, continue, train, rebase or list is called.
    • A supervisor whose job is still running kills its claude tree when no such call has happened for orphan_after seconds (init default 1200, more than 2 × 570). The job's status becomes killed, with flag orphaned, and a ledger line is written.
    • Why: A1. The procedure keeps the Athanor in a wait loop whenever a fork runs, so a silence of 20 min means it is gone.
  • N2. Parallel forks need --paths. fork --mode parallel requires --paths <p>…. The paths are stored in the job and echoed as paths in the result. Sequential forks may pass --paths too.
  • N3. No forks inside forks. The child env gains SPIRE_FORKS_CHILD=1 (on the keep list). Every launching command (train, fork, rebase, continue, tick pings) exits 2 when that variable is set. where, list, usage and wait stay 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 Δ. Commit lists 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 TaskStop non-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_UV cut from the calls.
  • Worktree .claude/worktrees/headless-forks on branch headless-forks.
  • Live smokes use claude-opus-5-5 (it is in the clone's availableModels).
  • 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-By or 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 --local in 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

  1. Commit on main by 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.
  2. Run git worktree add .claude/worktrees/headless-forks -b headless-forks main.
  3. Dispatch the installed 0.2.2 Athanor (model omitted) with this build plan. The worktree path is its checkout, and the run log is docs/workpad/reports/<YYYY-MM-DD-hh>-headless-forks-run.md inside 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.executable vs sys._base_executable (C6), and the resolved git and claude.
  • P0.1 ladder (A5). Each rung is tried only if the one before it failed:

    1. Popen with DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB from inside uv run.
    2. The same without breakaway.
    3. Creation through WMI/CIM: powershell -NoProfile -Command "Invoke-CimMethod -ClassName Win32_Process -MethodName Create …", capturing the returned PID.
    4. --attach under Bash run_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-5 vs sonnet);
    • an unlisted id of an unlisted family if one exists, else skip.

    Record the rule.

  • 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. A partial on 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.md with allowed-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 default that dispatches forkprobe as an Agent-tool subagent. The subagent quotes the injected text, then runs uv 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 a Bash(uv run:*) allow rule (plan §6 default).

  • Spend trigger (new). Sum the per-call total_cost_usd deltas (client estimates). If the sum passes $25 before P0.2 is complete, stop probing and hand back the partial report as STOP (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 hit while 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:

  1. grep -n "fork of yourself\|That reading is your trunk\|read the fix's diff\|the code the phase touches" plugins/spire/agents/athanor.md returns nothing.
  2. spire:forks appears in athanor.md's frontmatter skills:.
  3. grep -n "a fork of the builder" plugins/spire/skills/implementation/SKILL.md returns nothing.
  4. forks/SKILL.md contains 600000, SPIRE_FORKS_CHILD and --paths.
  5. The version values in both manifests are equal and read 0.3.0.
  6. Every command name in SKILL.md is in forks.py's dispatch, and every name in the dispatch (except _supervise) is in SKILL.md.
  7. 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)

  1. Merge and push headless-forks into the remote's default branch (U3). Until then nobody can install 0.3.0 (A8).
  2. Phase 4, plan §7 unchanged, with these entry options (U1): (a) smoke 0.3.0 first with --plugin-dir <worktree>/plugins/spire in the scratch clone, with spire@reliquary disabled in the clone's settings.local.json (assumed workable), then update the marketplace; or (b) update the marketplace directly. Option (b) switches every project's Athanor before any smoke.
  3. Phase 5, the trial, plan §7 unchanged. Entry needs U2.
  4. 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-dir before 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-dir on the cached 0.2.2 (~/.claude/plugins/cache/reliquary/spire/0.2.2, still present) or a worktree at a7ce5db; or run arm C before installing 0.3.0.
  • U3. Merge and push. Who merges headless-forks and 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:66 fork wording; :112 TaskStop of an Athanor should be followed by forks list/kill on its run (N1 covers it after 20 min).
  • ## Build keys for the assertion pattern, safe parallel tests, and the default fork owner model (full id), in spire:init / spire:project (A23).
  • README skills table drift; .claude/spire/project.md:31-32 lists (A22).
  • Root AGENTS.md still points at g4b_ai (plan §5).