Files
reliquary/docs/workpad/reports/2026-10-16-09-headless-forks-preflight.md

25 KiB
Raw Permalink Blame History

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 §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 uuids 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.