Files
reliquary/docs/workpad/reports/2026-10-16-08-headless-forks-trunk-notes.md
T
2026-10-16 18:44:28 +02:00

7.6 KiB
Raw Blame History

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 <abs>/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 <venv>\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 <command> --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 <run log without .md>-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.