📝 Add headless-forks goal, design, plan and build plan; session-harness app report
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
# 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](./2026-10-16-01-headless-forks.md). Section refs "plan §N" point there.
|
||||
**Design**: [designs/2026-10-16-01-headless-forks.md](../designs/2026-10-16-01-headless-forks.md) incl. the 01:33 runtime amendment (Python under uv)
|
||||
**Goal**: [goals/headless-forks.md](../goals/headless-forks.md)
|
||||
**Evidence**: [research/2026-10-16-00-claude-headless-fork-sessions.md](../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 `TaskStop`s 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).
|
||||
Reference in New Issue
Block a user