Repository navigation
fix(pm): dispatch-gates --ran never reads a killed run as RUN — a claim with a reason wins, a bare kill is UNRUN, a verdict-plus-claim contradiction prints (#18074) - #18108
Conversation
A recorded exit code that is a KILL rather than a verdict — 124 from a
`timeout` wrapper, or any code at or above the 128 signal floor — is a
number the gate never chose: the process was ended before it produced a
result. Counting such a family as `run` asserts a measurement that does
not exist, which is the false green the whole reconciliation is built to
refuse, and it is what the reconciler did.
The set is one named constant, `RUN_RECORD_KILL_EXITS`, judged in one
place (`runRecordKillLabel`), so no branch and no rendered line carries a
bare code of its own. A killed family lands:
- NOT-MEASURED when the record ALSO carries a claim with a stated
reason for that command — the claim WINS over the run line beside
it, because the runner who recorded the code and declared the family
has said everything there is to say. Its own source, its own block
(NOT-MEASURED · KILLED), and a row naming both the code and the
reason. It is counted once: the family is already inside `coded`, so
`killClaimed` is reported beside the other counts and added to none.
- UNRUN otherwise — the direction that costs a rerun.
A run line carrying a VERDICT code (0, 1, 2, and anything else below the
floor) plus a claim stays a genuine contradiction, run still wins, and
the contradiction line still prints on the default output. The conflicts
rendering now reads the class the reconciliation actually assigned
instead of re-deciding it, so it cannot report a reading the totals above
it do not share.
Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
The battery pins the card's four readings as four readings of ONE instrument — same command, same derivation, only the record's spelling differs — plus the controls that say the kill branch was inserted BESIDE the two existing channels rather than over either of them: exit 3 is still a DERIVED refusal, a lone reasoned claim is still CLAIMED, and a verdict code (0, 1, 2 and 127, which is unusual-looking but below the floor) still reads as run. Every member of the kill set is driven both ways round, bare and declared, so the floor is a floor rather than the four signals that have been met so far. The contradiction line is read off the RENDERED text and not out of `conflicts`: a non-empty array says the tool noticed, and the card's own reproduction filtered the output down to the count line, so it could not tell whether anything was ever printed. Measured before writing: the line does print, and it prints for a verdict-plus-claim record unchanged. The existing both-ways case is re-spelled, not weakened — same expectation, and the sentence now says which run lines it covers, since a bare line carries no code for a kill to be read out of. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
The first ablation of the kill classification did not report which cases the regression moved: the reverted reading leaves `unrun` empty, the pin read `unrun[0].why`, and the TypeError took the whole battery down at that line — so 1712 pins produced one named failure and a stack trace. A pin that crashes on its own subject is a pin that can only be graded by the author who already knows the answer. The row-content assertions now read through a helper that yields '' for an absent entry. The class assertions beside them need no guard: each one follows a `length === 1` conjunct that short-circuits. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…n-record-cap-kill-is-not-run
Contract reviewHead: ① derived judgments (seat-measured on the fetched head, ⛔ not taken from the report):
② semver: unchanged — PM tooling, nothing published. ③ boundary flags: none binding. Two readings for the file's next editor: self-test pins that index Implemented-by: Verdict: PASS — ready + auto-merge by this seat. Generated by Claude Code |
Fixes #18074
scripts/pm/dispatch-gates.mjs --ranhad two channels for declaring a gate familyunmeasured, and one silently ate the other. A recorded
exit 124— the number atimeoutwrapper reports when it had to signal the child — classified as run,and so did every code at or above the signal floor. A record that also carried an
explicit
NOT-MEASURED ... :: reasonfor the same command had that declarationdiscarded without a word. The most careful record produced the least truthful total.
A kill is not a verdict. The process was ended before it produced a result, so
counting the family as
runasserts a measurement that does not exist — the falsegreen the whole reconciliation exists to refuse.
What changed —
scripts/pm/dispatch-gates.mjsonlyOne named constant, judged in one place.
RUN_RECORD_KILL_EXITSholds thetimeout wrapper's
124and the128signal floor, with a docblock saying why eachmember is in it;
runRecordKillLabel()is the only expression that compares a codeagainst it, so no reconciliation branch and no rendered line carries a bare number of
its own.
124is the member the set exists for: it sits below the floor, so afloor test alone never catches it.
A killed family lands in one of two places, never
run:command. The claim wins over the run line beside it — the runner who recorded
the code and declared the family has said everything there is to say, and the two
lines do not actually disagree. It gets its own source, its own block
(
NOT-MEASURED · KILLED) and a row naming both the code and the reason.code and prescribes the spelling that would declare it.
It is counted once. A killed-and-declared family arrives through two doors and is
one family. It is already inside
coded, sokillClaimedis reported beside theother evidence counts and added to none of them;
accountedstays equal to the numberof families the record accounts for.
A verdict plus a claim is still a genuine contradiction —
0,1,2and everyother code below the floor — run still wins, unchanged, and the contradiction line
still prints. The conflicts rendering now reads the class the reconciliation actually
assigned instead of re-deciding it, so it can no longer report a reading the totals
above it do not share.
Premise readings, taken before writing
P1 — reading D reproduces. 2026-09-14T02:11Z, worktree at
a26a114d7, the card'sown reproduction (
--repo objectstack-ai/objectstack packages/spec/src/system/metrics.zod.ts,73 derived families on this tree), exits captured by redirect-then-capture:
The seat's open question, measured on the full output rather than the card's
reconciliation-only filter: the contradiction line does print. It is line 77 ofstdout, and it reads
So
conflictswas both computed and rendered; the defect was never that the toolfailed to notice, it was which bucket the family landed in. Since the line already
printed, it is pinned rather than repaired — by a self-test that reads the rendered
text, not the
conflictsarray.P2 — a bare
exit 124classified as run. Same run, reading A:73 derived, 1 run, 0 NOT-MEASURED, 72 UNRUN— identical to D. Reading B (exit 3)and reading C (the claim alone) both read
0 run, 1 NOT-MEASUREDon the same tree,which is what makes A and D readings of one instrument rather than four instruments.
P3 — the module argues the opposite way round. Its own
parseRunRecorddocblock:P4 — hold #14290, declared rider, nothing folded. 2026-09-14T02:20Z: #14290 is
open,
domain:devx,priority:p3; its hold comment5556473243carriesRestart-touch: scripts/pm/dispatch-gates.mjsandRestart-when: closed objectstack-ai/objectstack#16132, which has fired. Its subjectis the STAGE-THEN-RUN edge on the derivation side —
check:objectui-changesetinheriting no watch hints from
scripts/bump-objectui.sh, an edge neither followtraverses. This PR touches only the
--ranrecord reconciliation and itsrendering: it adds no follow, reads no program text and changes no hint extraction, so
it does not meet that edge and stops short of it. The hold's own re-pricing stays in
the
domain:devxlane. Riders #12797 and #12808 are closed.Before / after, same command, same derivation
CMD :: exit 12473 derived, 1 run, 0 NOT-MEASURED, 72 UNRUN73 derived, 0 run, 0 NOT-MEASURED, 73 UNRUNCMD :: exit 30 run, 1 NOT-MEASURED, 72 UNRUNNOT-MEASURED CMD :: reason0 run, 1 NOT-MEASURED, 72 UNRUN1 run, 0 NOT-MEASURED, 72 UNRUN0 run, 1 NOT-MEASURED, 72 UNRUNCMD :: exit 0+ a claim1 run, 0 NOT-MEASURED, 72 UNRUNReading D's family row and its conflict line, after:
Reading A's, after:
Tests
node scripts/pm/dispatch-gates.mjs --self-test— before:1682 cases pass,after: see the verification section below. Every existing case is kept. The one
re-spelling is the both-ways case: its expectation is byte-for-byte what it always
asserted, and the sentence now says which run lines it covers, because a bare run
line carries no code for a kill to be read out of — it is the control beside the new
branch, not a casualty of it.
New pins: reading A (bare kill → UNRUN, and the row names the code), reading D (kill +
reasoned claim → NOT-MEASURED, and the row carries both halves), the double-count
control on
accounted, every member of the kill set driven both ways round(
124,130,137,141,143,128,149), the verdict controls (0,1,2and
127— unusual-looking, below the floor, and still run), a kill code beside anunreasoned claim staying UNRUN, the rendered contradiction line for the
verdict-plus-claim case, and readings B and C unchanged.
Verification
All readings below are from real output on this branch head
dda4d903e(
origin/mainmerged in atca7886047), exits captured by redirect-then-capture.Self-test, before and after.
node scripts/pm/dispatch-gates.mjs --self-testFalsifiability, one-shot. With the classification reverted on disk — the single call
site
const killed = runRecordKillLabel(recorded.code);replaced byconst killed = null;,which restores the pre-fix fall-through exactly — 16 of the new cases fail BY NAME and
every control stays green:
Readings B and C, the verdict controls (
0,1,2,127) and the renderedverdict-plus-claim contradiction line all stay ✓ under the ablation, which is what says
the new branch sits beside the two existing channels rather than over either of them.
The mutation was proven on disk before the run (injected text present once, deleted text
absent, and the blob hash moved:
224b8a91→adf94009), and the restore was proven bybytes and not by an exit code —
git hash-objectback to224b8a91,git diff HEADempty,
git status --porcelainempty. The script carriedtrap restore EXIT INT TERMthroughout, since a cap kill landing mid-mutation is theexact failure this card is about.
Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 31 families for this diff on
dda4d903e— the tool deriving its own families,since the changed path is the tool. All 31 were run with each exit captured by
redirect-then-capture, recorded as
CMD :: exit CODE, and handed back through--ran,so the tool judged its own record on the same run that exercises the new classification:
Every family exited 0,
check:pm-dispatch-gatesincluded — the family that wascap-killed at the container ceiling on the round that surfaced this card, and the reason
that round's ledger read
81 runover a gate that never reached a verdict.npx eslint --no-inline-config scripts/pm/dispatch-gates.mjs— exit 0, no output. Therepo-level
pnpm lintscan is CI's run, not this PR's, and is not claimed here.The changed file is self-scanned for control bytes
(
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'): none, andcheck:nul-bytesisamong the 31 green families.
Acceptance notes
scripts/pm/dispatch-gates.mjsis that hold'sRestart-touch:file, so this PR names it; the change does not meet theSTAGE-THEN-RUN edge it describes and stops short of it. Its re-pricing belongs to
domain:devx.skip-changesetis the declaration. Thechangeset-checkjob in.github/workflows/pr-automation.ymlcounts added.changeset/*.mdfiles withgit diff --name-only --diff-filter=A MERGE_BASE HEAD -- '.changeset/*.md'and hasno path exemption — its only two exemptions are the live
skip-changesetlabeland the changesets release branch.
scripts/pm/dispatch-gates.mjssits at the reporoot, whose package is
private: true, and no released package'sfiles[]namesscripts/pm, so this diff publishes nothing.only from the ✓ line, so it can never be printed over a killed family; the comment
now says so, but the coupling between that sentence and the caller that reaches it
is implicit rather than asserted. Observation, not a defect. Carrier: the next PR to
touch
runRecordEvidenceLines/notMeasuredEvidenceTerm.Clause-②: no
🤖 Generated with Claude Code
https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Generated by Claude Code