📝 Headless forks: block 2.3 handoff note
This commit is contained in:
@@ -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 <head_at_launch>..<head_at_end>` 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.
|
||||
|
||||
Reference in New Issue
Block a user