📝 Headless forks run log: Phase 2 fix pass note

This commit is contained in:
2026-10-16 18:13:26 +02:00
parent d2d51049d2
commit ba463fcd0b
@@ -441,3 +441,38 @@ Gate after 2.3 (Athanor, 17:36): G-unit exit 0, `Ran 130 tests in 130.584s`, OK;
- Spend (Phase 2): live smoke $0.4685 client estimate (7 calls: train, 3 forks, rebase, 2 pings). Unit tests spend nothing. Subagent tokens: 2.1 320,462; 2.2 350,639; 2.3 187,248 (harness totals); the Athanor's own unknown.
- Processes at hand-back: none.
- Status: provisional until review, fix pass and re-check close.
### Phase 2 fix pass
- Model: `claude-opus-5-5[1m]` (fixer, own tools, no agents dispatched)
- Start / end: Fri Oct 16 17:58:54 WEDT 2026 / Fri Oct 16 18:10:44 WEDT 2026 (`date`; the end is the code commit, this note followed)
- Review: `docs/workpad/reports/2026-10-16-18-headless-forks-phase2-review.md`. Fixed: I1, I2, Minors 1, 2, 8, 9, 10, 11. Not touched: Minors 3–7, 12–14.
- Done, per finding (every new or changed test fails on the unfixed `9564c22` helper: the 11 touched tests run against it in a scratch copy with the new test file give 9 failures and 3 errors; the one that passes there, `test_tick_runs_in_wait_and_supervisor`, prints the `ResourceWarning`):
- **I1** `delta_tokens` — `_NOTE_REF`/`listed_notes` (the "every `.md` path in the brief" scan) are gone. New rule (Athanor decision): chars/4 of (a) the delta header, (b) the notes attached with the new repeatable `fork --delta-note PATH` (also on `rebase`) whose mtime is after the base's `trained_at`, (c) the body of a `## Changes since base` section of the brief (case-insensitive heading, up to the next level-1/2 heading). Files a brief only names and the rest of the brief never count. All attached notes go into the prompt after the delta header, each under `#### \`<path>\`` (rendered into the existing `${delta}` of `templates/fork.md` and `templates/rebase.md`; the template files are unchanged). An unreadable note is refused (exit 2). Entries record `delta_notes`. `list` still counts the delta header alone. Test: `LaunchTests.test_delta_tokens_counts_only_what_the_base_does_not_know` (a brief naming two 80k-char docs in the cwd and an 80k `notes/1.1.md`, one also as an absolute path: nothing due; a 60k attached note: `delta_tokens` and the note with its path in the prompt's delta section; a small note: nothing; a missing note: exit 2; the same note against a base with `trained_at` after its mtime: nothing; a 60k `## Changes since base` section: `delta_tokens`, the same text under `## Background`: nothing; `rebase --delta-note` carries the note in its prompt, and the new base's forks no longer count it).
- **I2** ping phase — `ping_phase(reg, base)`: the phase of the latest (by job number) train/fork/continue/rebase job on the base that has one, else the base's own phase, else null; `tick_pass` records it on the ping entry, so result, ledger and `usage --phase N` carry it. Test: `TickTests.test_ping_carries_the_phase_and_counts_in_usage_phase` (fallbacks; a phase-2 fork after a phase-less train → ping phase 2; finalised `ok` with cost Δ $0.0123; `usage --phase 2` json lists it with the cost in `kinds.ping` and `total`, md has the `ping` row, the total row and `Priced total: $0.0123`; `--phase 1` has no rows).
- **Minor 1** — `train --supersedes BASE` (repeatable). Refused at launch (exit 2, before and again under the lock): an unknown name (`unknown base`, `bases`, `option`), the new base's own name. The names are stored on the training base (`supersedes`); when the training ends `ok`, each named base that is `active` becomes `superseded` with `superseded_by`, `superseded_at`. A failed training changes nothing. Test: `LaunchTests.test_train_supersedes` (refusals; a failed training leaves B1 active; an ok one supersedes B1 and B0, leaves the failed B2 alone; B1's ping decision reads `status:superseded`; a fork of B1 needs `--force`).
- **Minor 2** — `init` refuses an effective `warm_margin_s` ≥ the effective `ttl_s` (exit 2, nothing written; also a re-run that lowers `ttl_s` under the current margin). `base_warmth`: a configured margin is capped at the base's TTL / 2 (the formula is unchanged). Tests: `InitTests.test_warm_margin_must_be_below_ttl`, `WarmthTests.test_margin_capped_at_half_the_ttl` (TTL 300, margin 600: `warm`, `margin_s` 150 one second after the hit; `expiring` and a ping at 150 s).
- **Minor 8** — every `init` flag defaults to None. First init: the defaults as before (`--model` missing → exit 2, `error: usage`, as argparse said). Re-run: an omitted flag keeps its current value, launch fields (model, effort, permission mode, tools, allow/deny lists, agent name, cwd, binary path with its `binary_resolved_by`) and run-wide settings (`parallel_cap`, `orphan_after`, `pings`, `ttl_s`, `warm_margin_s`, `max_minutes`, `stall_after_s`, each threshold). `init` reads the config before validating and refuses (exit 2) if it changed before the locked write. Test: `InitTests.test_rerun_keeps_omitted_settings` (a re-run with only `--warm-margin-s` keeps `pings off`, `parallel_cap` 3, `orphan_after` 1500, `rebase_commits` 9, the model, effort and tools; `changed_fields == ["warm_margin_s"]`; a later `--pings on` alone lists `["pings"]`; a first init without `--model` writes nothing).
- **Minor 9** — `UsageTests.test_r1_waits_for_the_registry_lock` calls `reconcile_ledger` directly in a thread and also checks that no line was appended while it is blocked, and exactly one after. Confirmed it can fail: a scratch runner (`m9_nolock.py`) patched `forks.RegistryLock` to a no-op only when called from `reconcile_ledger` (the test's own lock stays real): the test fails (`[['j0010']] != []`).
- **Minor 10** — `Supervisor.run` closes `p.stdout` after `reader.join` when the reader thread has ended. `test_tick_runs_in_wait_and_supervisor` no longer prints the `ResourceWarning` (run alone and in the full suite; the full log has no warning line).
- **Minor 11** — the ledger stays append-only. `usage` takes `status`, `flags` and `reason` from `results/<job>.json` when any differs from the ledger line (tokens and cost stay the ledger's) and lists those jobs in the json's new `status_from_result` (always present). The aggregates use the result's flags (`cold_starts`). Test: `UsageTests.test_status_from_result_when_it_changed_after_the_ledger_line`.
- Existing tests changed (none loosened; each is a specified change):
- `WarmthTests.test_boundaries_1h_and_5m`: margin 900 at a base TTL of 300 was `expiring` from the hit on; it is now capped at 150 (`warm` at 0 and 149 s, `expiring` at 150 and 299 s, `cold` at 300 s, `margin_s` 150). The 1 h cases are unchanged.
- `WarmthTests.test_ping_rule_states_status_and_pings_off`: the margin-3600 case pinged at T0+100; with the cap (1800 at 1 h) it pings at T0+1800 and reads `state:warm` at T0+1799.
- `UsageTests.test_r1_waits_for_the_registry_lock`: see Minor 9 (stronger).
- `LaunchTests.test_ping_end_to_end_through_wait`: `--warm-margin-s 3600` is now refused; uses 1800 with the last hit set 2000 s back instead of 100 s. Its assertions are unchanged.
- `LaunchTests.test_due_reasons_each_alone`: the 60k note trips `delta_tokens` when attached with `--delta-note`; the brief that only names it now asserts nothing due (I1). Its `delta_tokens` assertions are unchanged.
- Gates:
- G-unit `uv run plugins/spire/skills/forks/tests/test_forks.py`, run once (18:08:03–18:10:23): exit 0, `Ran 137 tests in 139.525s`, `OK`, no skip, no warning line (130 → 137: 7 new tests).
- G-where `uv run plugins/spire/skills/forks/forks.py where`: exit 0, one line, one JSON object, `ok: true`, `spawn: cim-hidden`, binary 2.1.299 via `CLAUDE_CODE_EXECPATH`, `warnings: []`.
- Leftover processes (18:10, `Get-CimInstance Win32_Process` by command line: `forks-test-`, `_supervise`, `poscontrol`): none.
- No live `claude` calls (stub only).
- Flagged for the re-check:
- I1: every attached note goes into the prompt; only those newer than `trained_at` count. `trained_at` has whole seconds, so a note written in the same second as the training or rebase end counts as new (the safe side).
- I1: the `## Changes since base` section does not depend on the base; a brief reused after a rebase with a large section still trips the flag. The Athanor writes that section per base.
- I2: the rebase job belongs to the old base, so a rebased base's pings take its forks' phase or its own phase (`rebase --phase`). Pings recorded before this fix keep `phase: null`.
- Minor 2: the smoke's `init --warm-margin-s 3500` would now be refused; at a 1 h TTL the effective margin is at most 1800. The Phase 3 text and any smoke recipe must use that.
- Minor 8: on a re-run, `--tools` alone keeps the current `allowed_tools` (it does not re-derive it, like `train`, Minor 12). What `init` reads from disk (binary version, size and mtime; preamble and agent hashes) is re-read on every run as before, so it can still show in `changed_fields` when those files changed. A set `warm_margin_s` or agent cannot be cleared back to none by `init`.
- Minor 10: the pipe is closed only when the reader ended within `READER_JOIN_S`; otherwise it is left as before.
- Minor 11: a reconciled ledger line is not rewritten; only `usage`'s view changes. The md block table does not show status, so only the json `rows` and the aggregates' flags show it.
- Commits: `d2d5104` (code and tests), then this note's commit.