# Trunk notes — headless forks build (`spire:forks`) Written by the Athanor (`claude-opus-5-5[1m]`) at Phase 0, 2026-10-16. For fresh agents and a successor Athanor. A page, not a design. ## Where things are - Build plan (authoritative): `docs/workpad/plans/2026-10-16-02-headless-forks-build.md`. Plan: `plans/2026-10-16-01-headless-forks.md`. Design: `designs/2026-10-16-01-headless-forks.md` (runtime amendment at the end: Python under uv). Research: `research/2026-10-16-00-claude-headless-fork-sessions.md`. - Run log: `docs/workpad/reports/2026-10-16-08-headless-forks-run.md`. Preflight report (Phase 0 output, the Phase 1 defaults): `docs/workpad/reports/2026-10-16-09-headless-forks-preflight.md`. - Checkout: worktree `G:\dev\projects\reliquary\.claude\worktrees\headless-forks`, branch `headless-forks`, cut from `d373689`. - Phase 0 probes and raw streams (throwaway, not in git): `C:\Users\g4b\AppData\Local\Temp\claude\G--dev-projects-reliquary\42c87f82-5542-46aa-8d01-4166e3f0bcfe\scratchpad\headless-forks-p0\` — `probes/` (code Phase 1 may reuse: `p0lib.py` scrub, command line, stream parser, Windows pid+ctime liveness, job-object query; `spawn3.py` CIM spawn), `raw/` (streams, summaries), `clone/` (scratch clone, untrusted workspace). - New code goes to `plugins/spire/skills/forks/` only (`forks.py`, `preamble.md`, `templates/`, `tests/test_forks.py`). Nothing else in `plugins/` changes before Phase 3. ## Host facts that shape the helper (measured, see the preflight report) - `uv` 0.5.13 at `~/.local/bin/uv`; `uv run /script.py` picks CPython 3.13.1 from a cached archive venv. `sys.executable` is the venv `Scripts\python.exe`; `sys._base_executable` is the real interpreter. A `pyproject.toml` or a `.python-version = 3.9` in the cwd is ignored. uv writes one line to stderr, nothing to stdout. Inside `uv run` the env gains `VIRTUAL_ENV` and a PATH head entry `\Scripts`; no `UV_*` vars. - No job objects anywhere (Bash tool, uv, supervisors: `IsProcessInJob` false). The Bash tool's stop kills by **process tree** (parent links). A supervisor whose launcher is still alive dies with the Bash call; one spawned through CIM (`Win32_Process.Create`) is parented to `WmiPrvSE.exe` and survives. - **No visible windows.** An unhidden CIM spawn opens a Windows Terminal window on the user's desktop (user report). Spawn the supervisor with `Win32_ProcessStartup.ShowWindow=0` and every child of it with `CREATE_NO_WINDOW` (`0x08000000`); never `DETACHED_PROCESS`/`CREATE_NEW_CONSOLE`. Tests check it. - CIM-spawned processes get the **registry default user env**, not the caller's (no `CLAUDE_CODE_EXECPATH`, no Git Bash PATH). The env for `claude` must be shipped explicitly (file next to the job) and the binary path must be absolute. - A Bash call that exceeds its `timeout` is **moved to the background**, not killed; its completion notification reached this subagent. Keep the `timeout: 600000` rule anyway. - Two binaries: PATH `claude` 2.1.283, `$CLAUDE_CODE_EXECPATH` 2.1.299 (Desktop). Pin the latter. - Host `CLAUDE_EFFORT=high` today (the research saw `medium`); scrubbed anyway. - Only `G:/dev/projects/reliquary` is trusted in `~/.claude.json`; the worktree inherits trust (no warning); the scratch clone does not (project `permissions.allow` ignored, stderr says so). ## Invariants Phase 1 must keep - stdout = exactly one JSON object; diagnostics and uv chatter on stderr. - Usage: fork `result.usage` equals the `modelUsage` delta against the base, field for field; cost delta = `total_cost_usd` minus the base's cumulative total. Stream assistant events carry interim `output_tokens`; never sum them. - Cache verdict from the **first** assistant event; base context = input + cache creation + cache read of the base's last assistant message (stream and transcript agree). - Every git call `git --no-optional-locks`. ## Known traps - `git clone --local` from `G:` to `C:` cannot hardlink; use `--no-hardlinks`. - Opus 5.5 may refuse a bare "run this blocking command" brief as pointless; say why it is wanted. - A fork whose Bash tool backgrounds a command leaves that command running after `claude` exits (seen: `sleep 240`, orphaned). `kill` by tree cannot reach it once `claude` is gone. - Unlisted models are not refused: Claude Code substitutes a permitted model and emits a `system/informational` warning. Detect it; do not trust `init.model` alone. ## State for Phase 3 (added 2026-10-16 18:15 +0200, end of Phase 2) - Phases 0–2 are built, reviewed and fixed (Phase 2 fix pass `d2d5104`; its re-check is Igor's call). The helper is complete for Phase 3's purposes: every command in build plan §6 G-text item 6 is in the dispatch (`where init train fork continue rebase wait list usage expire kill tick`, internal `_supervise`). - Phase 3 changes only text (`forks/SKILL.md`, `agents/athanor.md`, `implementation/SKILL.md`, `references/*`, README) and the two manifests. The run log's "Successor boot" section lists every review item routed to the Phase 3 text; treat that list as part of the brief. - The real JSON and exit codes to write the text against: run `where`, and ` --help` (prints one JSON object). The contract digests describe result fields, reasons and flags as built. - Live smoke references for numbers and shapes: `…\headless-forks-p0\smoke1\` (Phase 1) and `…\smoke2\` (Phase 2: rebase, pings, `usage` md). ## Phase 3 (added 2026-10-16 18:45 +0200, Phase 3 Athanor `claude-opus-5-5[1m]`) - The brief for every Phase 3 block is the digest `docs/workpad/plans/2026-10-16-18-gremlins-phase3-contract.md`, **including its closing "Amendment after pre-build check" section, which wins over the sections above it**. The check that produced it: `docs/workpad/reports/2026-10-16-19-gremlins-phase3-digest-check.md`. - The skill is renamed in block 3.1: `plugins/spire/skills/forks/` → `plugins/spire/skills/gremlins/` (`gremlins.py`, `tests/test_gremlins.py`). From 3.1 on, the gates are G-unit `uv run plugins/spire/skills/gremlins/tests/test_gremlins.py [Class …]` and G-where `uv run plugins/spire/skills/gremlins/gremlins.py where`. A single class runs as `uv run …/test_gremlins.py ClassName` or `ClassName.test_name`. - Words: the trained session is the **base**; a headless worker spawned from it is a **gremlin**; the command is `spawn` (job kind `spawn`, `method: "fork"`); env marker `SPIRE_GREMLIN_CHILD`; state dir `-gremlins/`. - Code map of `forks.py` before the rename (line numbers at `7351c40`): constants `:40-180`; paths and cygpath `:270-345`; atomic files and `RegistryLock` `:380-570`; ledger `:593-623`; processes (ctypes, ctime, liveness, table, tree, kill) `:626-895`; `_run`/`git` `:898-918`; scrub `:920-991`; binary/uv/python `:994-1077`; C8 `:1080-1127`; stream, usage, cache, warmth `:1130-1372`; job files, templates, `spawn_supervisor` `:1374-1460`; `_conclude`/`_rebase_succeeded`/`finalize_job` `:1463-1670`; orchestrator stamp, `reap_lost`, `kill_job` `:1684-1786`; argparse `:1794-1990`; `where`/`init` `:1998-2235`; launches `:2238-2927`; ping/tick `:2930-3112`; `Supervisor` `:3115-3338`; `wait`/`list`/`kill`/`expire` `:3341-3496`; `usage` `:3499-3728`; dispatch `:3731-3787`. - Test classes: `PathTests ScrubTests LockTests AtomicTests PidTests ProcessTreeTests InitTests CliTests SubprocessTests RuleTests WarmthTests TickTests UsageTests LaunchTests` (the last uses the real CIM detach and the stream stub; slow). Full suite about 150 s; run it only in block 3.8. - Test cadence (user): py_compile plus the touched test classes between blocks; the full suite once at phase end. No live `claude` calls in Phase 3. - File rule (user): Read, Write and Edit for files; never `cat`/`sed`/heredocs to read or change files.