📝 Headless forks Phase 0: preflight report (GO), run log, trunk notes
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
# Run log — Trained implementer, forked per block (`spire:forks`)
|
||||||
|
|
||||||
|
- Plan: `docs/workpad/plans/2026-10-16-01-headless-forks.md` · Build plan: `docs/workpad/plans/2026-10-16-02-headless-forks-build.md`
|
||||||
|
- Branch: `headless-forks` (worktree `.claude/worktrees/headless-forks`) · Start commit: `d373689`
|
||||||
|
- Trunk notes: `docs/workpad/reports/2026-10-16-08-headless-forks-trunk-notes.md`
|
||||||
|
- Athanor: `claude-opus-5-5[1m]` (installed Spire 0.2.2 shape) · Session journal: Igor's, `docs/workpad/journals/2026-10-15-21-grotto-work.md` (not touched by the Athanor)
|
||||||
|
- Forks: none yet (the helper does not exist until Phase 1)
|
||||||
|
|
||||||
|
## Phase 0 — Preflight (STOP gate)
|
||||||
|
|
||||||
|
| Block | Owner (model id) | Commit | Tokens (if known) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 Preflight probes and report | the Athanor, `claude-opus-5-5[1m]`, own Bash | __COMMIT__ | probe calls: $5.68 over 45 calls (client estimate, sum of per-call deltas); the Athanor's own tokens unknown |
|
||||||
|
|
||||||
|
### Block 0 — Preflight probes and report
|
||||||
|
|
||||||
|
- Model: `claude-opus-5-5[1m]`
|
||||||
|
- Start / end: 2026-10-16 08:16:26 +0200 / 2026-10-16 12:16 +0200 (last probe 10:13:01; `date` jumped from 10:13 to 12:16 across two consecutive commands, cause unknown, likely host suspend)
|
||||||
|
- Done: Worktree `.claude/worktrees/headless-forks` created on new branch `headless-forks` from `d373689`. Throwaway probes written and run from the Athanor's own Bash in the session scratchpad (`…\scratchpad\headless-forks-p0\`, not in git): P0.0–P0.8 in the build plan's amended order (P0.0, P0.1, start P0.6 detached via the P0.1 rung, P0.2–P0.5, P0.7, P0.8 while it ran, then collect P0.6). Preflight report `docs/workpad/reports/2026-10-16-09-headless-forks-preflight.md` with verdict, per-item results and the Phase 1 defaults. Nothing in `plugins/` changed.
|
||||||
|
- Deviations from plan:
|
||||||
|
- P0.6 was started once P0.1's rung 3 had passed (a), (b) and (d), before (c)'s 15 minutes were up, so the TTL clock started earlier. (c) passed later at 08:39–08:45; had it failed, P0.6 would have died with it and been restarted.
|
||||||
|
- The scratch clone is `git clone --local --no-hardlinks` (hardlinks fail from `G:` to `C:`). Same content, same `.claude/settings.json`.
|
||||||
|
- P0.5's model cases ran first in a copy of the project settings (`$P0\p05-cwd`), which turned out to be an **untrusted workspace** (project `permissions.allow` ignored). To get a trusted `availableModels` reading, five tool-less "Reply OK" calls ran with cwd = `G:\dev\projects\reliquary` (trusted) and one in the worktree (trust check). They cannot edit anything (`--tools ""`), but they are live calls outside the scratch clone, against plan decision 4's letter. They left transcripts under the reliquary project slug in `~/.claude/projects/`. The unlisted-family case was run through `--settings '{"availableModels":[…]}'` instead, because reliquary lists all four family aliases.
|
||||||
|
- P0.5's `--agent` test and P0.8 ran in separate scratch dirs (`p05-cwd`, `p08-cwd`) instead of the clone, so adding an agent and a skill could not change P0.2's prefix while it ran.
|
||||||
|
- P0.1 (d): the Bash tool no longer kills a call at its timeout; it backgrounds it. (d) was produced with `TaskStop` on the backgrounded call, which kills the process tree. Recorded in the report.
|
||||||
|
- The P0.5 effort-cache case ran inside the P0.2 probe (a `--effort medium` fork of the P0.2 base) instead of a separate base.
|
||||||
|
- Flagged for review:
|
||||||
|
- The P0.1 verdict rests on the strict reading of (d) (launcher still alive when the Bash call is stopped). Rung 1 survives when the launcher has already exited (`r1-dprime`). Rung 3 is the ladder's answer; the reviewer may want to weigh rung 1 plus an exit-first launcher against rung 3's CIM env handling.
|
||||||
|
- `r3-main` and `r3-tkill` ran PATH `claude` 2.1.283 (CIM env lacked `CLAUDE_CODE_EXECPATH`); survival evidence is unaffected, the binary is.
|
||||||
|
- P0.8 ran in an untrusted workspace; the pre-approval "no" may differ in a trusted project.
|
||||||
|
- Effort: a different effort did not miss the cache; whether `--effort` took effect is unobservable.
|
||||||
|
- A fork that backgrounds a Bash command can leave an orphan process after `claude` exits (seen once on 2.1.283).
|
||||||
|
- P0.6: the 50-min ping read only 0.916 of A's context (re-created 7,463 tokens, about the size of the shared system/tool prefix). Still a hit by C5 and the later fork read 1.0004; cause not isolated.
|
||||||
|
- Visible windows (coordinator's message 09:52, user report): every rung-3 supervisor before the fix (P0.1 `r3-*`, the P0.2 and P0.6 probes) opened a visible console window (Windows Terminal) on the user's desktop. "No visible window" was added to P0.1's pass criteria and the surviving variant retested with `Win32_ProcessStartup.ShowWindow=0` for the supervisor and `CREATE_NO_WINDOW` for every child: no window in 26 checks (plus 3 on an informational Popen `CREATE_NO_WINDOW` variant), including during `claude` → `bash` → `PING`; (a)–(d) and the tree-kill pass again. A positive control (`r3-wincontrol`, 15 s) showed the detector does see the old window. Recorded in the preflight report §P0.1.
|
||||||
|
- `$P0\raw\p01\` was not cleaned between runs; `r3-kill2` finished unobserved during a 20-minute `git clone` (08:24–08:45).
|
||||||
|
- Left for later: the Phase 1 defaults listed at the top of the preflight report. Survival of a rung-3 supervisor across the Athanor's hand-back is untested (Phase 1 smoke).
|
||||||
|
- Expected broken: nothing (no product code yet).
|
||||||
|
- Commits: __COMMIT__
|
||||||
|
|
||||||
|
### Phase 0 close
|
||||||
|
|
||||||
|
- Gate: none named for Phase 0 (probes only, no code in the repo). Every STOP item checked against its criterion in the report.
|
||||||
|
- Tests: not applicable (no product code). Probe results and raw paths in the report.
|
||||||
|
- Flags for the reviewer:
|
||||||
|
- Block 0: every deviation and flag above, one by one.
|
||||||
|
- Block 0: check the report's numbers against `$P0\raw\` (summaries `*.summary.json`, `p03-usage.json`, `p02\state.json`, `p06\state.json`, `p07\p07.json`, `p08\p08.json`, `p01\<tag>\check-*.json`).
|
||||||
|
- Not run / untested: survival past the end of the Athanor's turn; what effort changes; a trusted-workspace P0.8.
|
||||||
|
- Spend: $5.68 client estimate over 45 calls (trigger $25 not hit), plus three killed calls without a result (cents).
|
||||||
|
- Leftover processes: none. Swept at 10:13–12:16 by command line (`headless-forks-p0`, `p01_sup`, `p01_sleeper`, `p06_ttl`, `p02_base`) and by name (`PING`, `sleep`); the only hits were the Athanor's own Bash. One orphan was killed by hand earlier (`sleep.exe` pid 31372, from `r3-kill`).
|
||||||
|
- Open questions (for Igor and the user; none blocks Phase 1):
|
||||||
|
- U4 is moot for reliquary: the `sonnet`/`haiku` aliases in `availableModels` admit their full ids. An unlisted model is substituted silently, not refused; C8's runtime `model_refused` must key on the `system/informational` warning or a `modelUsage` mismatch.
|
||||||
|
- The Bash tool backgrounds instead of killing at its timeout. C2's `timeout: 600000` rule stays, but A2's failure mode ("killed with no JSON") is now "backgrounded, notification to the subagent". Does the Phase 3 text want a line on it?
|
||||||
|
- Rung 3 adds a PowerShell dependency (about 0.3 s per launch) and an env hand-off file. Rung 1 with a launcher that exits first would avoid both but dies if its launcher is in a stopped tree (e.g. pings launched from `wait`). The plan's ladder picks rung 3; the reviewer may weigh it.
|
||||||
|
- P0.8: injection works in a subagent preload, pre-approval does not (untrusted dir). SKILL.md needs the allow-rule note; the trusted-project case is for Phase 4.
|
||||||
|
- Forks that background a Bash command leave orphans after `claude` exits. Suggested default: a preamble line forbidding `run_in_background` in forks; Phase 1 decides.
|
||||||
|
- Verdict: **GO** (see the preflight report header).
|
||||||
|
- Status: provisional until review, fix pass and re-check close. Igor makes the GO/STOP call.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Preflight report — headless forks, Phase 0
|
||||||
|
|
||||||
|
**GO** — every STOP item passed (P0.0 uv, P0.1 detach on rung 3 hidden, P0.2 large base, P0.3 usage); P0.4 and P0.6 passed too, so the parallel cap stays 2 and pings default on.
|
||||||
|
|
||||||
|
- **Date**: 2026-10-16, 08:16–12:16 +0200 (times from `date`; probing ended 10:13, the clock jumped from 10:13 to 12:16 between two consecutive commands, cause unknown)
|
||||||
|
- **Run by**: the Athanor, model id `claude-opus-5-5[1m]`, from its own Bash (Agent-spawned subagent of the Desktop session, Windows 10, Git Bash)
|
||||||
|
- **Build plan**: [plans/2026-10-16-02-headless-forks-build.md](../plans/2026-10-16-02-headless-forks-build.md) §6 Phase 0 (amends plan §7 Phase 0)
|
||||||
|
- **Branch / checkout**: `headless-forks` in `.claude/worktrees/headless-forks`, from `d373689`
|
||||||
|
- **Raw files** (not in git): `C:\Users\g4b\AppData\Local\Temp\claude\G--dev-projects-reliquary\42c87f82-5542-46aa-8d01-4166e3f0bcfe\scratchpad\headless-forks-p0\` — `$P0` below. Probe code in `$P0\probes\`, streams and summaries in `$P0\raw\<item>\`.
|
||||||
|
- **Binary under test**: `$CLAUDE_CODE_EXECPATH` = Desktop `claude.exe` 2.1.299 (PATH `claude` is 2.1.283; used once by accident, noted at P0.1).
|
||||||
|
- **Spend**: **$5.68** client estimate (`total_cost_usd`), summed as per-call deltas over 45 calls by `$P0\probes\spend.py`; plus three killed `claude` calls that wrote no result (`r3-kill3`, `r3-kill5`, `r5-kill`, a few cents). Limit was $25 before P0.2 completed; $4.87 had been spent by 08:55, after P0.2–P0.5, P0.7 and P0.8.
|
||||||
|
|
||||||
|
## Defaults Phase 1 must use
|
||||||
|
|
||||||
|
| Topic | Default | From |
|
||||||
|
|---|---|---|
|
||||||
|
| Spawn rung | **Rung 3, hidden**: the launcher creates the supervisor through CIM with a hidden window: `powershell -NoProfile -NonInteractive -Command "$si = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{ShowWindow=[uint16]0}; Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine=…; CurrentDirectory=…; ProcessStartupInformation=$si}"`, reading `ReturnValue` and `ProcessId`. Launch cost about 0.3 s. Without `ShowWindow=0` the supervisor opens a **visible console window** (Windows Terminal tab titled with the interpreter path; user report and positive control). | P0.1 |
|
||||||
|
| No visible windows | Every child the supervisor starts (`claude`, any helper process) is spawned with `CREATE_NO_WINDOW` (`0x08000000`), never `DETACHED_PROCESS` or `CREATE_NEW_CONSOLE` (a console-less parent makes each console child open its own window). `claude`'s Bash children inherit the hidden console: no window seen with a 10-process tree (`claude.exe` → `bash.exe` ×3 → `PING.EXE`, 4 `conhost.exe`). Phase 1 tests and the smoke check "no visible window of the tree" (EnumWindows + IsWindowVisible on the tree's pids, plus visible `ConsoleWindowClass`/`CASCADIA_HOSTING_WINDOW_CLASS` windows). | P0.1 retest |
|
||||||
|
| Supervisor env | CIM gives the child the registry default user env, not the caller's. The launcher writes the full scrubbed child env into the job's state (e.g. `jobs/<job>.json` or a sidecar) and the supervisor passes exactly that env to `claude`. The binary path is stored absolute (it is in the fingerprint anyway). | P0.1 |
|
||||||
|
| Interpreter | `sys._base_executable` (differs from `sys.executable` under `uv run`: venv launcher `…\archive-v0\<id>\Scripts\python.exe` vs `…\uv\python\cpython-3.13.1-…\python.exe`), with `-I`. `-I` drops the script dir from `sys.path`; the supervisor must not rely on it. | P0.0, C6 |
|
||||||
|
| Kill | `taskkill /T /F /PID <supervisor>`; verify by recorded pid + creation time. | P0.1 |
|
||||||
|
| Scrub additions (uv) | Drop `VIRTUAL_ENV`; drop PATH entries under `VIRTUAL_ENV` or `sys.prefix` (one entry: `<venv>\Scripts`, prepended). uv sets no `UV_*` vars here; dropping `UV_*` stays as a guard. Keep `PYTHONDONTWRITEBYTECODE` (host-set, not uv). | P0.0 |
|
||||||
|
| `where` must report (host `CLAUDE_CODE_*` and kin on neither list) | `CLAUDE_CODE_SESSION_ATTENDED`, `CLAUDE_CODE_DISABLE_CRON`, `CLAUDE_CODE_EAGER_FLUSH`, `CLAUDE_CODE_SIMPLE`, `CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES`, `CLAUDE_CODE_ORGANIZATION_UUID`, `CLAUDE_CODE_ACCOUNT_UUID`, `CLAUDE_CODE_USER_EMAIL`, `CLAUDE_CODE_OAUTH_SCOPES`, `CLAUDE_CODE_DISABLE_TERMINAL_TITLE`, `CLAUDE_CODE_STARTUP_FAILURE_RESULTS`, `CLAUDE_AGENT_SDK_MCP_NO_PREFIX`, `CLAUDE_PREVIEW_CLASSIFIER_FLOOR`, `DISABLE_MICROCOMPACT`. All were inherited (kept) in every probe call and auth worked. | P0.0 env listing |
|
||||||
|
| Usage rule | Per call: the four columns and the 1h/5m split come from the `result` event's top-level `usage` (`input_tokens`, `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens`, `cache_creation.ephemeral_1h_input_tokens` / `ephemeral_5m_input_tokens`). For a fork this equals the `modelUsage` delta against the base's cumulative `modelUsage`, field for field (7 of 7 forks). `models_used` = the `modelUsage` keys whose counts grew. Cost Δ = `total_cost_usd` − the session total before the call (= the `modelUsage[m].costUSD` delta, 7 of 7). Never sum stream assistant events for `output_tokens`: they carry interim counts (seq forks: 102 summed vs 1,448 real). | P0.3 |
|
||||||
|
| `usage_mismatch` | Compare `result.usage` with the `modelUsage` delta (they agreed exactly here). | P0.3 |
|
||||||
|
| Cache verdict input | First assistant event's `usage` (input and cache fields are final there). Base context = `input + cache_creation + cache_read` of the base's last assistant message; the stream and the transcript give the same number (182,204). | P0.2, P0.7 |
|
||||||
|
| Parallel cap | **2** (P0.4 passed). | P0.4 |
|
||||||
|
| Pings | **on** by default (`init --pings on`): a ping 50 min after the base's last hit kept it warm to 65 min (P0.6 pass). A ping can itself re-create part of the prefix (7,463 of 88,571 here) and still count as a hit. | P0.6 |
|
||||||
|
| Fingerprint fields | Keep all launch fields of design §3.3. **Effort** stays pinned but is not cache-defining: a `--effort medium` fork of a `high` base read 182,202 of 182,204 tokens from cache, same as the control. `init` has no effort field (`per_turn_effort_active: true` only). | P0.5 |
|
||||||
|
| Model ids | Full ids accepted; `[1m]` accepted (`init.model` and the `modelUsage` key both read `claude-opus-5-5[1m]`; `message.model` reads `claude-opus-5-5`). Compare `models_used` with the requested id including the suffix. | P0.5 |
|
||||||
|
| `availableModels` rule (C8, D5) | A requested id is **admitted** if a list contains the id itself or its family alias (`opus`, `sonnet`, `haiku`, `fable`); no list in user/project/local settings admits everything. An id that is not admitted is **not refused**: Claude Code silently runs a permitted model and emits `{"type":"system","subtype":"informational","level":"warning","content":"Model \"<id>\" is restricted by your organization's settings. Using <other> instead."}`. So `init` applies the admit rule (exit 2 per C8), and at runtime the supervisor treats that event, or a `modelUsage` key ≠ the requested id, as `failed`, reason `model_refused`. In reliquary all four family aliases are listed, so every full id is admitted (U4 is moot for Sonnet and Haiku). | P0.5 |
|
||||||
|
| Injection (R4) | `!` injection **ran** in an Agent-tool subagent's preload (probe `where` called 4.8 s after the headless launch). `allowed-tools: Bash(uv:*)` did **not** pre-approve the subagent's later `uv run …/probe.py ping` (denied: "This command requires approval"). SKILL.md must tell projects to add a `Bash(uv run:*)` (or `Bash(uv:*)`) allow rule; reliquary has `Bash(uv:*)`. Caveat: the probe dir was an untrusted workspace. | P0.8 |
|
||||||
|
| Fork session ids | `--session-id <uuid>` with `--resume <base> --fork-session` is honoured: the fork gets that id. The helper may pre-assign fork ids. | P0.7 |
|
||||||
|
|
||||||
|
## Results per item
|
||||||
|
|
||||||
|
| Item | Result | Variant / notes |
|
||||||
|
|---|---|---|
|
||||||
|
| P0.0 uv | **pass** | (a) repo cwd, (b) `pyproject.toml` cwd, (c) `.python-version = 3.9` cwd: all rc 0, stdout one JSON line, Python 3.13.1 in all three, no `.venv`/`uv.lock` created. |
|
||||||
|
| P0.1 detach | **pass, rung 3 hidden** | Rungs 1 and 2 failed (d). Rung 3 passed (a)–(d) and the tree-kill but showed a visible console window (fails the added "no visible window" criterion). Rung 3 with `ShowWindow=0` and `CREATE_NO_WINDOW` children passed (a), (b), (d), the tree-kill and "no visible window"; (c) passed (1,000 s, no window in 24 checks). |
|
||||||
|
| P0.2 large base | **pass** | Base 182,204 context tokens, three sequential forks each read 182,202 on the first request (0.99999), no compaction. |
|
||||||
|
| P0.3 usage | **pass** | Rule above; reproducible on 7 forks (1, 3 and 7 turns). |
|
||||||
|
| P0.4 parallel | **pass** | No `index.lock`, both edits intact, both first requests 0.99999; the Athanor gated and committed both. |
|
||||||
|
| P0.5 flags and models | **pass (all accepted)** | Table below. |
|
||||||
|
| P0.6 TTL | **pass** | A's fork at 65 min after a 50-min ping: 1.0004; B's fork at 65 min without ping: 0.080 (`partial`). |
|
||||||
|
| P0.7 transcripts | **recorded** | Message usage present on every assistant line; copied base messages keep `message.id` and `uuid`; R6 honoured. |
|
||||||
|
| P0.8 injection proxy | **injection yes, pre-approval no** | Informational; fallback text applies. |
|
||||||
|
| Spend trigger | **not hit** | $5.68 total; $4.87 by 08:55 (P0.2 long done) |
|
||||||
|
|
||||||
|
STOP criteria (plan decision 1, build plan §6): P0.0 pass, P0.1 pass (rung 3), P0.2 pass, P0.3 pass. No STOP. No fallback beyond the ladder was needed; rung 3 needed the hidden-window fix added after the coordinator's message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0.0 uv
|
||||||
|
|
||||||
|
- Probe: `$P0\probes\p00_probe.py`, the plan §2 PEP 723 header. Command: `uv run "C:/…/probes/p00_probe.py"` from the Athanor's Bash. Raw: `$P0\raw\p00\`.
|
||||||
|
- uv 0.5.13 (c456bae5e 2024-12-27). Python chosen: CPython 3.13.1, the cached archive venv `C:\Users\g4b\AppData\Local\uv\cache\archive-v0\1V7ITIfzQwCDPBZsZhb9M`. The same venv in all three cwds.
|
||||||
|
- (a) cwd = worktree; (b) cwd = `$P0\p00-pyproject` with `[project]` (deps `requests`, hatchling build): no sync, no `.venv`, no `uv.lock`; (c) cwd = `$P0\p00-pyver` with `.python-version` = `3.9` (3.9.16 is installed): 3.13.1 chosen, `requires-python >= 3.10` wins.
|
||||||
|
- stdout: exactly the probe's one line in every run. stderr: one line, `Reading inline script metadata from …`.
|
||||||
|
- Latency: warm 125–134 ms (5 runs); cold uv cache (fresh `UV_CACHE_DIR`) 532 ms. The first-ever run in the real cache was not timed (the first probe call warmed it).
|
||||||
|
- Env diff Bash → inside `uv run`: added `VIRTUAL_ENV` (= the archive venv); PATH gains `<venv>\Scripts` at the head; no `UV_*`; `PYTHONDONTWRITEBYTECODE=1` is host-set (present in Bash too).
|
||||||
|
- Resolved tools inside `uv run`: `git` = `C:\Program Files\Git\mingw64\bin\git.EXE` (2.34.1.windows.1), `claude` = `C:\Users\g4b\.local\bin\claude.EXE` (2.1.283), `uv` = `C:\Users\g4b\.local\bin\uv.EXE`, `python` = the venv's.
|
||||||
|
- `sys.executable` = `…\archive-v0\1V7ITIfzQwCDPBZsZhb9M\Scripts\python.exe`; `sys._base_executable` = `C:\Users\g4b\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\python.exe` (C6 applies).
|
||||||
|
|
||||||
|
## P0.1 detach
|
||||||
|
|
||||||
|
Probes: `p01_launch.py` (run under `uv run`, spawns the supervisor by rung), `p01_sup.py` (supervisor: ticks every 10 s, a python sleeper child, `claude -p` calls at given offsets), `p01_check.py` (pid + creation-time liveness, CIM descendant snapshot, `taskkill /T /F`). Raw: `$P0\raw\p01\<tag>\` (`launch.json`, `state.json`, `tick.log`, `sleeper.log`, `check-*.json`, `claude*.jsonl`).
|
||||||
|
|
||||||
|
Job objects: `IsProcessInJob` was **false** for the `uv run` launcher and for every supervisor, so `CREATE_BREAKAWAY_FROM_JOB` had nothing to break from (A5's premise does not hold on this host today).
|
||||||
|
|
||||||
|
What the Bash tool does: a call that exceeds its `timeout` (5 s and 8 s tried) is **not killed**; it is moved to the background ("Command did not complete within its 5s timeout and was moved to the background"), and its completion notification arrived in this subagent. To get the "killed Bash call" of (d), the backgrounded task was stopped with `TaskStop`, which kills the **process tree by parent links**.
|
||||||
|
|
||||||
|
| Rung | Tag | (a) uv exits | (b) Bash call ends | (c) ≥ 15 min, growing, completes | (d) Bash call killed (launcher still alive) | Tree-kill |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 `DETACHED_PROCESS\|CREATE_NEW_PROCESS_GROUP\|CREATE_BREAKAWAY_FROM_JOB` (`0x1000208`) | `r1-main`, `r1-tkill` | pass | pass | pass: 1,020 s, ticks 1→3→12→103 over checks at 08:21:04, 08:21:28, 08:23:02, 08:45:23; `claude` at +20 s and +935 s both `P01-OK` | **fail**: supervisor and sleeper dead at the first check after `TaskStop` (08:21:28) | — |
|
||||||
|
| 2 same without breakaway (`0x208`) | `r2-tkill` | — | — | — | **fail** (same) | — |
|
||||||
|
| 3 CIM `Win32_Process.Create` | `r3-main`, `r3-tkill`, `r3-kill5` | pass | pass | pass: 1,020 s, ticks 2→103, `claude` at +20 s and +933 s both `P01-OK` | **pass**: alive after `TaskStop`; parent is `WmiPrvSE.exe` (pid 34932) | **pass**: 8 processes (`claude.exe` → `bash.exe` ×3 → `PING.EXE`, `conhost.exe` ×2, sleeper) all dead by pid + creation time; no `PING.EXE` left |
|
||||||
|
|
||||||
|
**Retest for visible windows (coordinator's message, 09:52; user saw a Python console window on the desktop).** Criterion added to P0.1: no visible window from the supervisor or any descendant. Window check: `EnumWindows` + `IsWindowVisible` for the tree's pids, plus any visible `ConsoleWindowClass` / `CASCADIA_HOSTING_WINDOW_CLASS` window.
|
||||||
|
|
||||||
|
| Variant | Tag | Visible window | (a) | (b) | (c) | (d) | Tree-kill |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Rung 3 as first run (positive control, 15 s) | `r3-wincontrol` | **yes**: Windows Terminal window (`CASCADIA_HOSTING_WINDOW_CLASS`, pid 19872) titled `…\cpython-3.13.1-…\python.exe` | — | — | — | — | — |
|
||||||
|
| Rung 3 hidden: `Win32_ProcessStartup.ShowWindow=0`; supervisor children `CREATE_NO_WINDOW` | `r5-main`, `r5-tkill`, `r5-kill` | **none**, checked 26 times incl. during a `claude` call and during `claude` → `bash` → `PING` | pass | pass | pass: 1,000 s, ticks 1→101 over 24 checks 09:55:54–10:13:01, `claude` at +20 s and +935 s both `P01-OK`, done | pass (alive after `TaskStop` at 09:56:31, check 09:56:43) | pass: 11 processes dead by pid + creation time, no `PING.EXE` left |
|
||||||
|
| Popen `CREATE_NO_WINDOW\|CREATE_NEW_PROCESS_GROUP\|CREATE_BREAKAWAY_FROM_JOB`, children `CREATE_NO_WINDOW` (informational) | `r6-win` | none (3 checks incl. a `claude` call) | pass | pass | not run | not run; expected to fail like rungs 1/2 (same parentage) | not run |
|
||||||
|
|
||||||
|
So the earlier visible windows came from every rung-3 supervisor before the fix (`r3-*`, and the P0.2 and P0.6 probes, which were launched with the same unhidden CIM call), and presumably (not measured) from the console children of the rung-1/2 `DETACHED_PROCESS` supervisors (sleeper, `claude`), which get a new console each when the parent has none.
|
||||||
|
|
||||||
|
Also recorded:
|
||||||
|
|
||||||
|
- `r1-dprime` (informational): rung 1 with the launcher already exited, then the Bash call stopped while a later process ran → supervisor **survived**. Rung 1 dies only while its launcher is alive in the stopped tree. That matters for the helper because `wait` stays alive while it would launch pings; rung 3 avoids the question.
|
||||||
|
- CIM env: a CIM-created process gets the registry default user env. 58 of the Bash env's variables were missing, among them every `CLAUDE*` var, `CLAUDE_CODE_EXECPATH`, `HOME`, `MSYSTEM`, Git's PATH entries (`$P0\raw\p01\wmi-env.json`). Effect seen: `r3-main` and `r3-tkill` (spawned before the fix) ran PATH `claude` 2.1.283 instead of 2.1.299. Fixed in the probe by shipping `env.json`; `r3-kill2`…`r3-kill5` and P0.2/P0.6 ran 2.1.299.
|
||||||
|
- Kill-test retries: `r3-kill` (2.1.283) let the harness background `sleep 240` and exited; the backgrounded `sleep.exe` was left **orphaned** after `claude` exited (killed by hand, pid 31372). `r3-kill2` was missed (the scratch clone took 20 min, 08:24→08:45, and the target finished). `r3-kill3` killed before the Bash child existed. `r3-kill4`: Opus 5.5 declined a bare blocking `ping` as pointless. `r3-kill5` with an honest brief: pass (above).
|
||||||
|
- Spend in P0.1: about $0.20 (killed calls `r3-kill3`/`r3-kill5` wrote no result and are not counted).
|
||||||
|
|
||||||
|
## P0.2 large base
|
||||||
|
|
||||||
|
- Probe `p02_base_forks.py`, run detached via `spawn3.py` (rung 3, still unhidden then: it showed a console window while it ran). cwd = scratch clone `$P0\clone` (`git clone --local --no-hardlinks` of `d373689`; `--no-hardlinks` because `G:`→`C:` cannot hardlink). Raw: `$P0\raw\p02\`.
|
||||||
|
- Fingerprint: `--model claude-opus-5-5 --effort high --permission-mode dontAsk --tools Bash,Read,Edit,Write,Glob,Grep,Skill --allowedTools <same> --disallowedTools "Bash(git push:*),Bash(git rebase:*),Bash(git reset --hard:*),Bash(claude:*)" --strict-mcp-config --mcp-config $P0\mcp-empty.json --append-system-prompt-file $P0\preamble-probe.md`, `--output-format stream-json --verbose`, prompt on stdin, scrubbed env (design §4 + `VIRTUAL_ENV` + venv PATH), `CLAUDE_CODE_PROMPT_CACHE_TTL=1h`, `DISABLE_AUTOUPDATER=1`.
|
||||||
|
- Base `d7c7a3aa-dfd5-40df-87f2-ccc82e49a369`: read 42 files (all `plugins/spire/**/*.md` plus 7 workpad docs) with Read, 43 turns, 74.7 s, context **182,204** tokens (last message: 2 + 45,677 created + 136,525 read), all writes `ephemeral_1h`, cost $1.6298, no compaction event.
|
||||||
|
|
||||||
|
| Fork | Turns | First request: cache read / created | Ratio | Cost Δ | Commit (clone) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| seq 1 | 7 | 182,202 / 1,242 | 0.99999 | $0.3116 | `64dbefa` |
|
||||||
|
| seq 2 | 7 | 182,202 / 1,247 | 0.99999 | $0.3091 | `9bfefb7` |
|
||||||
|
| seq 3 | 7 | 182,202 / 1,259 | 0.99999 | $0.3029 | `bb5514d` |
|
||||||
|
|
||||||
|
Each fork read `plugins/spire/CLAUDE.md`, wrote `scratch/p0-fork-<k>.md`, ran `git diff --check`, `git add`, `git commit`, `git log`, and replied with its hash. No denials. No re-run needed.
|
||||||
|
|
||||||
|
## P0.3 usage
|
||||||
|
|
||||||
|
Analysis `p03_usage.py` → `$P0\raw\p03-usage.json`.
|
||||||
|
|
||||||
|
- The `result` event carries `usage`, `modelUsage`, `total_cost_usd`, `num_turns`, `is_error`, `permission_denials` (plus `subagent_stats`, `terminal_reason`, timing fields). `usage` also has `iterations[]` and `output_tokens_details.thinking_tokens`; `modelUsage[m]` has `costUSD`, `contextWindow` (1,000,000 for `claude-opus-5-5` here), `canonicalModel`, `costBasis`.
|
||||||
|
- On every fork (seq 1–3, par 1–2, two 1-turn forks) `result.usage` == `modelUsage` delta vs the base for all four columns, and `total_cost_usd` delta == `modelUsage.costUSD` delta. Example seq 1: input 14, output 1,448, created 3,113 (all 1h), read 1,288,193; cost Δ $0.311559.
|
||||||
|
- Summing stream assistant events (last event per message id) matches input and both cache columns but not output (102 vs 1,448): stream events carry interim `output_tokens`.
|
||||||
|
- `num_turns` ≠ assistant messages on the base (43 vs 7 message ids): turns count tool round trips, several Read calls share one message. The helper's `turns` should come from `num_turns`.
|
||||||
|
|
||||||
|
## P0.4 parallel
|
||||||
|
|
||||||
|
Two forks launched together from the base (`par1`, `par2`, threads in the same probe), each writing `scratch/p0-par-<k>.md` and running `git status`, no commits. No `index.lock` in either stream; both files intact (3 lines each); both first requests 182,202 read (0.99999). The Athanor then gated (`git add -N` + `git diff --check`, rc 0) and committed each file by explicit path: clone `0f72edb`, `9d3ddc1`.
|
||||||
|
|
||||||
|
## P0.5 flags and models
|
||||||
|
|
||||||
|
Probe `p05_flags.py` → `$P0\raw\p05\`. Tool-less calls (`--tools ""`) unless noted.
|
||||||
|
|
||||||
|
| Check | Outcome | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `--effort` with `CLAUDE_EFFORT` scrubbed | accepted; not visible in `init`; a `medium` fork of the `high` base hit 0.99999 like the `high` control | keep pinned; not cache-defining |
|
||||||
|
| `--strict-mcp-config` + empty config | `mcp_servers: []`, no `mcp__*` tools (the claude.ai Docs connector is gone) | keep |
|
||||||
|
| `Skill` in `--tools` | accepted; `init.tools` lists it | keep |
|
||||||
|
| `[1m]` in `--model` | accepted; `init.model` and `modelUsage` key `claude-opus-5-5[1m]` | keep |
|
||||||
|
| `apiKeySource` | present, `"none"` (OAuth) | check stays |
|
||||||
|
| `--agent p0agent` (project `.claude/agents/p0agent.md`) + `--append-system-prompt-file` | both markers quoted (`AGENT-MARKER-3KX`, `PREAMBLE-MARKER-7Q4`); control without `--agent` quoted none | keep |
|
||||||
|
| agent `tools: Read` vs `--tools Bash,Read` | `init.tools` = `["Read"]`: the intersection | the agent file can only narrow `--tools` |
|
||||||
|
| `claude-opus-5-5` (listed) | runs as asked | — |
|
||||||
|
| `claude-sonnet-5-5`, `claude-haiku-5-5` (unlisted full ids, aliases listed), trusted checkout | run as asked | admitted via family alias |
|
||||||
|
| `claude-opus-4-6` (bare; only `[1m]` form listed), trusted checkout | runs as asked | admitted via `opus` |
|
||||||
|
| unlisted family | none exists (all four aliases listed); **skipped** in project settings, tested via `--settings` instead ↓ | — |
|
||||||
|
| `--settings '{"availableModels":["claude-opus-5-5"]}'`, request `claude-sonnet-5-5` | **substituted**: ran `claude-opus-5-5` with the `system/informational` warning quoted above | `model_refused` |
|
||||||
|
| same list, request `claude-opus-5-5` | runs | — |
|
||||||
|
| `--settings '{"availableModels":["opus","sonnet"]}'`, request `claude-sonnet-5-5` | runs as asked | alias admits full id |
|
||||||
|
|
||||||
|
Trust: in untrusted dirs (scratch clone, `$P0\p05-cwd`, `$P0\p08-cwd`) stderr says "Ignoring 36 permissions.allow entries from .claude/settings.json: this workspace has not been trusted". `G:/dev/projects/reliquary` is trusted; a tool-less call in the worktree printed no warning (trust is inherited by subdirectories). Project agents still load in untrusted dirs. The model rows marked "trusted checkout" ran tool-less in `G:\dev\projects\reliquary` (deviation, logged in the run log).
|
||||||
|
|
||||||
|
## P0.6 TTL
|
||||||
|
|
||||||
|
Probe `p06_ttl.py`, detached via `spawn3.py` (rung 3, unhidden at the time) at 08:48:45. cwd = `$P0\ttl-cwd` (empty, not a git repo), the P0.2 fingerprint (same tools, mode, preamble, strict MCP, TTL pinned 1h). Training content was passed in the prompt (no tools used): A = five orbit/dirigent research and report files, B = six superpowers research and report files from the clone; "reply READY". Raw: `$P0\raw\p06\` (`state.json`, `train*.jsonl`, `ping*.jsonl`, `fork*.jsonl`).
|
||||||
|
|
||||||
|
| Step | Elapsed since base's first event | First request: cache read / created | Ratio to base context | Cost Δ |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| train A (`9c2fa6d6-…`) | — | 0 / 88,569 (all 1h) | context 88,571 | $0.7086 |
|
||||||
|
| train B (`79af9c73-…`) | — | 7,469 / 85,406 | context 92,877 | $0.6848 |
|
||||||
|
| ping A (`Reply with only: OK`, `--max-turns 1`) | 3,004.6 s (50.1 min) | 81,144 / 7,463 | 0.916 | $0.0760 |
|
||||||
|
| fork A | 3,904.9 s (65.1 min), 15 min after the ping | 88,607 / 0 | **1.0004** | $0.0178 |
|
||||||
|
| fork B (no ping) | 3,902.7 s (65.0 min) | 7,469 / 85,444 | **0.080** | $0.6851 |
|
||||||
|
|
||||||
|
- **Pass**: A's fork ≥ 0.9 × A's context after the 50-min ping; B's fork < 0.5 × B's context. B shows `partial` (the 7,469-token shared prefix stayed warm, as A4/C5 predicted).
|
||||||
|
- A's fork read more than A's context (88,607 > 88,571) because the ping's identical prompt was cached too.
|
||||||
|
- The ping itself re-created 7,463 tokens instead of reading them (0.916, still a hit by C5). That size matches the shared system/tool prefix (7,469). Cause not isolated; it did not stop the ping from refreshing the base prefix.
|
||||||
|
- Economics: a cold start (fork B) cost $0.685; ping + warm fork cost $0.094. One ping pays for itself many times over at this base size.
|
||||||
|
|
||||||
|
## P0.7 transcripts (informational)
|
||||||
|
|
||||||
|
`p07_transcripts.py` → `$P0\raw\p07\p07.json`.
|
||||||
|
|
||||||
|
- Transcript dir = `~/.claude/projects/<cwd with every non-alphanumeric → '-'>/` (exists for the clone).
|
||||||
|
- Base transcript: 128 lines, 46 assistant lines, **all 46 carry `message.usage`**; several lines per message id (one per content block). Base context from the transcript's last assistant line = 182,204 = the stream's number. `max_base_tokens` can be read from transcripts.
|
||||||
|
- Fork seq 1 transcript: all 7 base message ids and all 106 base `uuid`s are present (copied unchanged), under the fork's own `sessionId`; no `fork*` field. Dedupe by message id works for the trial.
|
||||||
|
- R6: `--session-id f2806537-… --resume <tiny base> --fork-session` → `init.session_id` and `result.session_id` both `f2806537-…`; the fork recalled the base's word.
|
||||||
|
|
||||||
|
## P0.8 injection and pre-approval proxy (informational)
|
||||||
|
|
||||||
|
`p08_inject.py` in `$P0\p08-cwd` (copy of the project settings with `Bash(uv:*)`/`Bash(uvx:*)` removed; skill `forkprobe` with `allowed-tools: Bash(uv:*)` and `` !`uv run "${CLAUDE_SKILL_DIR}/probe.py" where` ``; agent `forkprobe` preloading it). Headless `claude -p --tools Task,Bash --permission-mode default --allowedTools Task,Agent` dispatched `forkprobe`. Raw: `$P0\raw\p08\`.
|
||||||
|
|
||||||
|
- Injection in the subagent: **yes**. The reply quoted `{"probe": "forkprobe", "marker": "INJECT-MARKER-W9P", …}`; `probe-calls.log` shows one `where` call 4.8 s after launch.
|
||||||
|
- Pre-approval: **no**. `permission_denials` holds the subagent's `uv run "…\probe.py" ping` ("This command requires approval").
|
||||||
|
- Wall time: 13.7 s for the whole run; the injection completed within the first 4.8 s.
|
||||||
|
- Caveat: the dir is an untrusted workspace (its project allow list was ignored anyway, which is what the probe needed). Whether `allowed-tools` pre-approves in a trusted project is untested; Phase 4 checks it with the real plugin.
|
||||||
|
|
||||||
|
## Not tested / open
|
||||||
|
|
||||||
|
- Survival of a rung-3 supervisor past the **end of the Athanor's turn** (all checks were inside one turn). Parentage under `WmiPrvSE.exe` makes a tree-kill from the agent impossible, so it is expected to hold; Phase 1's smoke can confirm it across a hand-back.
|
||||||
|
- What effort actually changes (no observable in `init` or in a 1-turn reply).
|
||||||
|
- Why the 50-min ping re-created 7,463 prefix tokens.
|
||||||
|
- `rate_limit_event` lines in every stream carry plan utilisation (`five_hour` 0.02, `seven_day` 0.56 this morning). Not used by the plan; a cheap input for R11 if Phase 1 wants it.
|
||||||
|
- Whether the Desktop's orphaned background Bash tasks (fork backgrounding a command) can be prevented by flags; the default is a preamble line.
|
||||||
Reference in New Issue
Block a user