diff --git a/docs/workpad/reports/2026-10-16-08-headless-forks-run.md b/docs/workpad/reports/2026-10-16-08-headless-forks-run.md index da9ad68..a2adc10 100644 --- a/docs/workpad/reports/2026-10-16-08-headless-forks-run.md +++ b/docs/workpad/reports/2026-10-16-08-headless-forks-run.md @@ -364,3 +364,30 @@ Gate after 2.2 (Athanor, 17:25): G-unit exit 0, `Ran 112 tests in 120.036s`, OK; - Left for later: `usage` (2.3). - Expected broken: `usage` exits 2 "not implemented" (2.3). - Commits: `5fda2e2` (code, template and tests), then this note's commit. + +### Block 2.3 — Usage rendering + +- Model: `claude-sonnet-5-5` +- Start / end: Fri Oct 16 17:27:48 WEDT 2026 / Fri Oct 16 17:35:17 WEDT 2026 (code commit; this note follows) +- Done: `usage --run R [--block ID | --phase N] [--format json|md]` in `forks.py` (the "not implemented" stub is gone; `--block` and `--phase` are mutually exclusive; `PHASE2_COMMANDS` is now empty, so `usage` no longer tolerates unknown arguments). + - **R1**: `reconcile_ledger` runs first in every `usage`. It lists `results/*.json`; when every one has a ledger line it returns without taking the lock. Otherwise, under the registry lock, it re-reads the ledger and appends a line for each result without one (`ledger_append` dedupes on job id). The line is rebuilt from the result plus its registry entry (`started`, `ended`, `binary`, `cost_cumulative_usd`; `None` when there is no entry) and carries `"reconciled": true`. Corrupt result files are skipped with a stderr note. The json output lists the job ids appended in `reconciled`. + - json: `kinds` (all five, zeros when unused: `calls`, `input`, `output`, `cache_creation`, `cache_creation_1h`, `cache_creation_5m`, `cache_read`, `cost_usd`), `total`, `cold_starts`, `cache_hits`, `cache_verdicts`, `cache_hit_ratio` (hits / fork+continue+rebase calls with a verdict; `null` when none), `rows` = the ledger lines in scope in job order, `scope`, `reconciled`. + - md `--block`: one table with the fixed header, one row per call of the block (fork, its continues); thousands separators, `$0.0000`, session first 8 chars, null usage renders `—`. md `--phase` (and no scope): the per-kind table (kinds with calls only) plus a `total` row, then three bullets: cold starts, cache-hit ratio `h/n (p%)`, priced total. + - **Commit column**: the short hashes (7) of `git rev-list --reverse ..` in the job's cwd, comma-joined; `—` for a parallel fork, for no commits, or when either HEAD or git is unavailable. + - **`head_at_end`**: new registry job field. The supervisor passes `_git_head(cwd)` to `finalize_job(head_at_end=...)` after claude exits (all kinds); `finalize_job` reads HEAD itself for a fork/continue that is killed or lost. + - `--format md` stdout is written as UTF-8 bytes on `sys.stdout.buffer` (the header's `Δ` is not in cp1252); errors stay one JSON object, exit 2. `usage` is allowed under `SPIRE_FORKS_CHILD`. +- Tests (new `UsageTests`, 18; one added to `LaunchTests`): md `--block` and `--phase` against a fixed ledger fixture (exact text), parallel Commit `—`, null usage `—`, unknown block (header only), json totals per kind and overall and by scope, errors stay JSON, N3, UTF-8 under `PYTHONIOENCODING=ascii:strict`, R1 (one line for a result without one, none for a result that has one, none on a second `usage`; no registry entry; torn ledger tail; waits for a held lock; no lock when nothing is missing; corrupt result), and `head_at_end` end to end with the stub (a commit made during a sequential fork shows in the Commit cell). +- Existing tests changed (none loosened): `CliTests.test_phase2_not_implemented` removed (its only case was `usage`, now implemented); `test_commands_need_init` gains three `usage` cases (it asserts exit 2, one JSON line, and no state dir created). +- Gates: G-unit `uv run plugins/spire/skills/forks/tests/test_forks.py` — exit 0, 130 tests, 131 s; G-where `forks.py where` — exit 0, one JSON line; leftover-process check by command line (`forks-test-` prefix, `_supervise`) — none. +- Deviations from plan: none against the build plan or the digest. The Commit column lists hashes (build plan: "lists the commits"); the digest says "commits between", which I read the same way. +- Flagged for review (semantics I chose): + - A whole-run `usage --format md` (no scope) renders the phase layout (table + three bullets). + - Call counts include calls with null usage (killed, lost): they add to `calls` and nothing to the sums. The cache-hit denominator counts only calls that have a verdict. + - The md phase table omits kinds with zero calls; the json `kinds` always has all five. + - `ledger_from_result` for a reconciled line takes `ended` from the registry entry, else from the result file's mtime; `cost_cumulative_usd` is `None` when the entry has no `cost_cum_usd`. R1 also reconciles a result whose registry entry still reads `running` (kill between the result write and the registry write); the later finaliser/`reap_lost` then finds its ledger line present and appends none. + - `usage` stamps `last_orchestrator_call_at` like `list` (via `touch_orchestrator`; a fork's call does not). + - `head_at_end` is HEAD of the job's cwd when the call ended, so with several forks on one tree it can include another fork's commit; only a sequential fork's Commit column is shown for that reason. +- Not tested: `usage` over a ledger with `rebase` calls (same code path as the other kinds); a real `claude` run; POSIX. +- Left for later: none for 2.3. +- Expected broken: none. +- Commits: `4b92b51` (code and tests), then this note's commit.