12 KiB
Phase 2 contract digest — spire:forks bookkeeping
Written by: the Athanor, claude-opus-5-5[1m], 2026-10-16 16:45 +0200, at Phase 2 intake.
What this is: the brief the Phase 2 block owners (2.1, 2.2, 2.3) build against. It pins points the build plan, the plan and the design leave open, and folds in the Phase 1 review items the coordinator assigned to Phase 2. Precedence: build plan plans/2026-10-16-02-headless-forks-build.md (§4, §6 Phase 2) > coordinator decisions (below) > plan plans/2026-10-16-01-headless-forks.md §7 Phase 2 > design designs/2026-10-16-01-headless-forks.md §5–§6 > this digest's own choices. Where this digest conflicts with a higher source, follow the higher source and say so in your handoff note.
Inputs to read besides those: the Phase 1 digest plans/2026-10-16-12-headless-forks-phase1-contract.md (still in force where not changed here), the Phase 1 review reports/2026-10-16-16-headless-forks-phase1-review.md (M6–M16, the P0.6 ping note), the re-check reports/2026-10-16-17-headless-forks-phase1-recheck.md (R1), the run log reports/2026-10-16-08-headless-forks-run.md (block notes and the fix pass), and the code plugins/spire/skills/forks/.
Coordinator decisions (2026-10-16, Phase 2 brief)
- R1: a kill between the registry write and the ledger append can leave a finished job with no ledger line.
usagereconciles the ledger againstresults/before aggregating: any result without a ledger line gets one appended, idempotently (dedupe on job id, under the lock). Cost is never under-counted. - M6: the runtime fingerprint also compares sorted
skills, and identifies plugins without aversionby the last segment of theirpath. Both become drift reasons. - M7: the fingerprint hash covers the mcp config content (
{"strict": true, "servers": {}}), not its per-run path. The path stays in the config, outside the hash. - M8: widen the unit visible-window check: diff the visible
ConsoleWindowClass/CASCADIA_HOSTING_WINDOW_CLASSwindows before and during the job as well as the tree-pid check, add a positive control (a deliberately visible spawn that the check must detect, closed at once), and skip (not fail) when no desktop is present. - M10: follow plan §4.3.
trainaccepts launch-field overrides (the same flags asinit); the base records the launch fields it was trained with and their fingerprint hash. Every later launch on that base (fork,continue,rebase, pings) builds its command line from the base's record, not from the run config. - M11: a result's
noteis set only when the note file's mtime is at or after this job's start. - M13: Git Bash rooted paths that are not drive forms (
/tmp/x,/usr/…): convert withcygpath -wwhen it is on PATH; if it is not, refuse with exit 2 and a message. Never silently map them onto the cwd's drive. - M16: rename the job-file progress counter
turnstomessages; the result and ledger keepturns=result.num_turns. - Keep a test for "an error after the requested model billed": the call billed the requested model, then the result is
is_error; it must readfailedwith an error reason (is_erroror the API-error reason), nevermodel_refused. The Phase 1 fix pass changed the stub'serrordirective to bill nothing, so add a directive for the billed case.
Blocks
| Block | Owner | Scope |
|---|---|---|
| 2.1 Warmth and keep-warm | fresh, opus |
Warmth inference, pings, tick, ttl_downgrade, cold_start rule (C5), ping dedupe (C3d); plus M8, M16 and the billed-error test. |
| 2.2 Rebase and re-train flags | fresh, opus |
rebase, templates/rebase.md, rebase_due, retrain_due and their reasons, binary_missing; plus M6, M7, M10, M11, M13. |
| 2.3 Usage rendering | fresh, sonnet |
`usage --block/--phase --format json |
| 2.4 Phase tests, smoke | the Athanor; fallout to a fresh opus |
G-unit, live smoke of plan §7 "Phase 2 tests and smoke". |
2.1 Warmth and keep-warm (design §5)
- TTL inference per base, from its calls (base and all its forks, continues and pings):
3600if the most recent call wroteephemeral_1htokens, or read cache and the previous known TTL was 3600;300if it wrote onlyephemeral_5m; else the configuredttl_s(3600 default). A call that wrote onlyephemeral_5msets flagttl_downgradeon its result. - last_hit: unchanged from the Phase 1 fix (I3): train end; a fork's first assistant event; a ping's first assistant event. Never a continue.
- warm_until =
last_hit + ttl; margin =max(120, ttl/10)unlessinit --warm-margin-ssets it (a tunable threshold like the others; default unset = the formula). State: warm / expiring / cold as design §5. - Ping =
--resume <base session> --fork-session --session-id <new>, promptReply OK,--max-turns 1, the base's launch record, kindping, its own job id, ledger line, result. Pings do not count towardparallel_capand never return throughwait'sdoneas blocks do: they go towait'spings: [...]list (each ping result returned once). They are N3-refused underSPIRE_FORKS_CHILDlike any launch. - Ping rule: base
activeand stateexpiringand the run had a running fork/continue job or awaitcall in the last 10 min and pings since the base's last real (non-ping) call <break_even(floor((write − read) / read)with ratios 1h write 2.0, 5m write 1.25, read 0.1 → 19 at 1 h, 11 at 5 min) andpingsisonin the config and no ping for this base is in flight. - Ping dedupe (C3d): the decision and
ping_in_flight(with the ping job id) are written on the base under the registry lock; other tickers skip a base that is in flight; the flag clears when the ping finishes (any status). - Ping verdict: a ping's cache verdict is computed like a fork's, but a
partialping still refresheslast_hit(P0.6: a 50-min ping re-created ~7.5k of the shared system prefix and still kept the base warm; on a small base that readspartial). Only amiss(cache_read 0) or a failed ping leaveslast_hitalone and adds flagping_missedto the ping's result. - Tick: one pass of the ping rule over all bases of the run. Runs inside every supervisor every 60 s and inside
waitevery 30 s;tick --run Rruns one pass by hand (N3-refused in a fork). A ping is launched through the same CIM hidden spawn as any job. - cold_start (C5, already in Phase 1): keep; verify against the inferred TTL now.
init --pings on|offdefaulton(P0.6 passed).- M16, M8, billed-error test: as above.
- Tests (clock-injected): warm/expiring/cold boundaries at 1 h and 5 min (and with
--warm-margin-s); ping eligibility incl. no ping without activity in the last 10 min, the cap at 19 and 11,pings off, in-flight dedupe across two tickers; TTL inference from 1h-only, 5m-only and read-only calls;ttl_downgrade; a stubbed ping end to end throughwait'spingslist; partial ping refreshes, missed ping does not.
2.2 Rebase and re-train flags (design §5, build plan 2.2)
rebase --run R --base B --as NEW --brief FILE [--phase N]: a fork of B whose prompt istemplates/rebase.md(read the listed notes and diffs, do not edit code, update the trunk notes if the brief names a path, reply READY) + delta header + brief. Onok, its session becomes baseNEW(status: active,parent: B,lineage_rebases= B's + 1,base_commit= HEAD at the end, context tokens from its last assistant message, its launch record = B's, unless a binary drift made the rebase run under the new binary: then the new binary is recorded); B becomessuperseded. On failure B staysactiveandNEWis not created.rebasecounts toward the cap and is N3-refused.- rebase_due reasons (on
fork's launch return, and inlistper base):phase_boundary(the fork's--phaseis greater than the highest phase of any fork from this base, or than the base's own phase),commits:<n>(commits sincebase_commit>rebase_commits),lines:<n>(git --no-optional-locks diff --shortstat <base_commit>insertions + deletions >rebase_lines),delta_tokens:<n>(chars / 4 of the delta header plus the notes listed in the brief >rebase_delta_tokens),binary_drift(stored binary version/size/mtime differ from the current file),runtime_drift:<fields>(the last call on this base reportedfingerprint: drift:<runtime fields>, e.g.skills,plugins— M6). - retrain_due reasons:
defining:<fields>— a fingerprint-defining field of the base (model, cwd, tools, permission mode, agent name or agent file sha, preamble sha) differs from what a launch would use now. "Now" means: the preamble and agent file re-hashed from disk; and for fields the base did not override attrain, the run config.initmay therefore change config values after bases exist (this replaces Phase 1's refusal): bases that did not override a changed field reportretrain_due. Alsomax_base_tokens:<n>(the base's context tokens >max_base_tokens; measured from the transcript's last assistant line when the file exists, else from the stored context — P0.7),rebases:<n>(lineage_rebases≥max_rebases),binary_missing(the stored binary path is gone; launches refuse with exit 2 and this reason). - The flags are advisory:
forkstill launches when they are true (exceptbinary_missing). The Athanor obeys them. - M6, M7, M10, M11, M13: as above.
- Tests: each reason triggered separately (and only it),
fork's launch return lists the rightreasons[];rebaseend to end with the stub (new base active, old superseded, fork from the new base getshitagainst the new context);trainwith overrides (recorded per base; a fork of it uses the base's record);initchanging the model after a base exists → that baseretrain_due: defining:model; M7 hash stable across two runs; M11; M13 with and withoutcygpath.
2.3 Usage rendering (design §6, build plan §4 "Usage md columns")
usage --run R [--block ID | --phase N] [--format json|md](default json). N3: allowed in a fork. Before aggregating: R1 reconciliation (under the lock).- json: totals (
input,output,cache_creationwith 1h/5m,cache_read,cost_usd= sum ofcost_delta_usd, calls) per kind (train,rebase,fork,continue,ping) and overall;cold_startscount;cache_hit_ratio= hits / (fork + continue + rebase calls with a verdict);rows= one per ledger line in scope. - md,
--block: exactly one markdown table with the fixed header| Block | Owner (model id from modelUsage) | Session | Commit | in | out | cache write | cache read | cost Δ |, one row per call of that block (fork and its continues). Owner =models_usedjoined; Session = short session id (first 8); Commit = commits between the job'shead_at_launchand its end HEAD (the supervisor must recordhead_at_end; add it if missing) for a sequential fork,—for a parallel one; numbers with thousands separators; cost Δ as$0.0000. Null usage (killed/lost) renders—. - md,
--phase: the per-kind totals table (| Kind | Calls | in | out | cache write | cache read | cost |plus a total row), then one line each for cold starts, cache-hit ratio and the priced total. - stdout for
--format mdis exactly one markdown fragment (the plan §4.1 exception to the JSON rule); errors still JSON with exit 2. - Tests: md against a fixed ledger fixture (exact text), json totals, R1 reconciliation (a result with no ledger line gets exactly one line, a second
usageadds none).
Not in Phase 2
SKILL.md, the Athanor and implementation text, references, version bump (Phase 3). M12, M14, M15 and the post-exit-sweep / continue-frame parts of M16 are Phase 3 text.