Repository navigation
[finding] dispatch-gates --ran silently discards an explicit NOT-MEASURED claim when the same command also carries a run line — and a cap-kill exit (124/143) reads as RUN #18074
Description
Activity
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsTriage (skills-lane self-triage; session
session_01DAcomhvR9kKizeYgg89Vo8, the skills seat), 2026-09-14T00:07Z: class (a) — reproducible, four readings of one instrument on the card (same command, only the record's spelling differs). Lands inscripts/pm/dispatch-gates.mjs(the--ranrecord parser: an explicit NOT-MEASURED claim must survive a run line on the same command, and a cap-kill exit 124/143 must read as KILLED, never RUN);domain:skills; Bug;priority:p2— a killed run reading as RUN is a false green on the gate ledger every landing record cites.pm:queue;findingremoved — 定级即离标.⚠️ scripts/pm/dispatch-gates.mjsis aRestart-touch:trigger file of hold #14290 (patrol anchor #9857, H17 index): the dispatch order names that hold and its rider clause. Default tier.
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 14, 2026 claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsRelease — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-14T01:16Z: the claim pair written at 2026-09-14T01:12Z is returned topm:queue(assignee cleared) before any dispatch — the maintainer's word in this seat's chat at 2026-09-14T01:15Z, verbatim 「18085 优先」, takes the slot (cap 2, #18055 in flight). Readings taken for the dispatch order, kept here so the next claim does not redo them (onorigin/maind438b3a9, a full worktree): the card's reading D reproduces — a record carrying BOTH<cmd> :: exit 124andNOT-MEASURED <cmd> :: cap-killed…reconciles as71 derived, 1 run, 0 NOT-MEASURED, 70 UNRUN; reading A (exit 124 alone) the same; reading C (the claim alone)0 run, 1 NOT-MEASURED. The loop at :12035–:12061 still classifies every recorded code other thanEXIT_PREREQUISITE_NOT_METas run; main computesconflictsat :12022 and renders them at :12246 (self-test :23806 「claimed BOTH ways reads as run and the contradiction is reported」) — whether that contradiction line actually prints for reading D was not read (the card's own reproduction filters onreconciliationonly); the cap-kill-reads-as-RUN half stands regardless.scripts/pm/dispatch-gates.mjsis hold #14290'sRestart-touch:file (itsRestart-when: closed #16132has since fired — #16132 is closed — so that hold is due back in its own lane's queue; riders #12797 / #12808 are closed). Next claim: name #14290 in the order; no fold.
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsClaim: PM loop round 1 (second claim — the first, at 2026-09-14T01:12Z, was released at 5657694356 to give the slot to the maintainer's 「18085 优先」)
Session:session_01DAcomhvR9kKizeYgg89Vo8(GitHubos-project-manager, skills seat), claimed at 2026-09-14T02:06Z
Branch:claude/issue-18074-ran-record-cap-kill-is-not-run
Worktree:objectstack-issue-18074
Domain:domain:skills(non-governed PM tooling,scripts/pm/dispatch-gates.mjs; in-seat contract-tier review lands it). Graded p2pm:queueBug by this seat's lane self-triage (5657245831). The pair (pm:dispatched+ assignee) was written through the REST proxy at 2026-09-14T02:06Z and read back.
File surface:scripts/pm/dispatch-gates.mjsonly — the--ranreconciliation loop (the branch that classifies a recorded exit code other thanEXIT_PREREQUISITE_NOT_METas run, and the claim map it never reaches for a command on the run list) and its self-test battery.⚠️ Declared rider:scripts/pm/dispatch-gates.mjsis hold #14290'sRestart-touch:file (H17 index, anchor #9857) — that hold's ownRestart-when: closed #16132has fired and its re-pricing is thedomain:devxlane's dispatch; riders #12797 / #12808 are closed. ⇒ the order names #14290; the dev folds nothing and reports any STAGE-THEN-RUN interaction it meets.
Container & model:M,mode:subagent,opus; seat review at the contract-review tier.
Clause-②: no
Thread-read: body + grading 5657245831 + the release 5657694356 (its readings are the dispatch's premises) at 2026-09-14T01:15Z, tail re-read at 2026-09-14T02:06Z
Premise check at 2026-09-14T02:06Z onorigin/maina26a114d7:dispatch-gates.mjslast touched226970bb(#18033) — unchanged since the release's readings ond438b3a9, where reading D reproduced (<cmd> :: exit 124+NOT-MEASURED <cmd> :: …⇒1 run, 0 NOT-MEASURED) and reading C alone reads1 NOT-MEASURED; the loop still sends every recorded code exceptEXIT_PREREQUISITE_NOT_METtoran(:12048–:12058) and computesconflictsat :12022 (rendered at :12246, pinned at :23806 as 「reads as run and the contradiction is reported」) — whether that line prints on reading D is the dev's first measurement. Serial constraints: no in-flight branch names this file (#18085's branch: 0 hits); verify lock free; rate ≥14.7k/15k; devs in flight after this one: 2 of 2 (#18085 the other; PRs #18093 and #18094 are armed for the queue).
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 18074, "status": "done", "branch": "claude/issue-18074-ran-record-cap-kill-is-not-run", "pr": "https://github.com/objectstack-ai/objectstack/pull/18108", "premise_still_valid": true, "summary": "scripts/pm/dispatch-gates.mjs only. 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) no longer classifies as run. The set is one named constant, RUN_RECORD_KILL_EXITS, compared in exactly one place (runRecordKillLabel), so no branch and no rendered line carries a bare number. Such a family lands in NOT-MEASURED when the record also carries a claim with a stated reason for the same command - the claim WINS over the run line, gets its own source and its own NOT-MEASURED KILLED block, and the row names both the recorded code and the reason - and in UNRUN otherwise, with the row naming the code and prescribing the spelling that would declare it. It is counted once: the family is already inside `coded`, so the new `killClaimed` count is reported beside the other evidence counts and added to none of them. A run line with a VERDICT code (0/1/2 and everything else below the floor) plus a claim stays a genuine contradiction, 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. Premises P1-P4 all held. P1's open question is answered: the contradiction line DOES print on the full default output for reading D (stdout line 77) - the card's reproduction filtered on 'reconciliation' and could not see it - so it is pinned by a self-test that reads the rendered text rather than the conflicts array. Assignee was os-project-manager (the PM's dispatch pair) throughout; I never wrote it. No governed surface touched (register printed: docs/adr/** . .claude/** . skills/** . AGENTS.md . CLAUDE.md).", "tests": "SELF-TEST: `node scripts/pm/dispatch-gates.mjs --self-test` - before `1682 cases pass`, after `1712 cases pass` (exit 0). READINGS (worktree, --repo objectstack-ai/objectstack packages/spec/src/system/metrics.zod.ts, 73 derived, exits by redirect-then-capture): A `CMD :: exit 124` 1 run/0 NOT-MEASURED/72 UNRUN -> 0 run/0 NOT-MEASURED/73 UNRUN; D (exit 124 + claim) 1 run/0 NOT-MEASURED -> 0 run/1 NOT-MEASURED; B (exit 3) and C (claim alone) unchanged at 0 run/1 NOT-MEASURED; E (exit 0 + claim) unchanged at 1 run/0 NOT-MEASURED with the contradiction line still printed. ABLATION (one-shot, trap restore EXIT INT TERM, absolute paths): call site `const killed = runRecordKillLabel(recorded.code);` replaced by `const killed = null;`. On-disk proof BEFORE the run - injected text count 1, deleted text count 0, blob hash moved 224b8a91 -> adf94009. Ablated battery exit 1 with 16 NEW cases failing BY NAME (readings A and D, their row-content assertions, the double-count control, all seven kill codes 124/130/137/141/143/128/149 both ways round, the unreasoned-claim case, and the three rendered-text pins); readings B and C, the verdict controls 0/1/2/127 and the rendered verdict-plus-claim contradiction all stayed green. Restore proven by BYTES not by an exit code: git hash-object back to 224b8a91, `git diff HEAD` empty, `git status --porcelain` empty. Restored battery: 1712 cases pass, exit 0. GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final head dda4d903e (origin/main ca7886047 merged in) derives 31 families - the tool deriving its own families. All 31 run, each exit captured by redirect-then-capture, recorded as `CMD :: exit CODE` and handed back through --ran (the tool judging its own record): `Run reconciliation - 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.` / `OK dispatch-gates --ran: 31 derived famil(ies) accounted for - 31 run, 0 NOT-MEASURED (a DERIVED zero - all 31 recorded an exit code and none of them is 3).` exit 0. Every family exited 0, check:pm-dispatch-gates included. `npx eslint --no-inline-config scripts/pm/dispatch-gates.mjs` exit 0, no output; the repo-level pnpm lint scan is CI's and is not claimed here. Control-byte self-scan of the changed file: none. NOT OWED: no (1) dependency-closure build and no (2) package test/typecheck - the diff touches no workspace package (scripts/pm/ sits at the repo root, whose package is private). The script's own suite is its --self-test plus the derived family check:pm-dispatch-gates, both green; `git grep dispatch-gates -- '*.test.ts'` hits are prose mentions only, verified.", "mcp_calls": "0 - every GitHub read and write went through REST on the proxy; no mcp__github__* tool was called", "api_writes": "8 - 4 pushes (empty branch probe, the fix commit, the crash-safety commit, the merge commit), POST /repos/objectstack-ai/objectstack/pulls (draft #18108), PATCH /pulls/18108 (final body, read back: exactly one footer, +58 bytes, the platform's bare append), POST /issues/18108/labels (additive, skip-changeset; read back: size/m, skip-changeset), POST /issues/18074/comments (this report)", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the card's own reproduction pipes the run through `grep -i reconciliation`, which hides the contradiction line the tool already printed - the reproduction under-reports what the instrument says, and the card's 'the claim line is gone with no diagnostic' reads narrower than what the full output shows. Carrier: this PR's body, which states the full-output reading either way.", "noted, not filed: the evidence block's 'a DERIVED zero' sentence is reachable only from the OK 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.", "noted, not filed: self-test pins in this file that index into a result array (`x[0].field`) crash the whole battery when the regression they exist to catch empties that array - the first ablation produced one named failure plus a stack trace instead of sixteen named failures. Fixed for the new pins in this PR; the same shape exists elsewhere in the battery and was left alone. Carrier: the next PR to add pins to this file." ] }
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsACCEPT — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-14T03:55Z. Theos-dev-reporton this card is read in full; PR #18108 headdda4d903reviewed in-seat at the contract-review tier:## Contract reviewPASS on the PR. Seat-measured on the head: a killed run (124 / ≥128) never reads as run — a reasoned claim wins into a KILLED block that keeps the reason, a bare kill is UNRUN, a verdict-plus-claim contradiction stays run with the line printed; the card's four records reproduced on the head with the new readings; self-test 1712/1712; the ablation reds 16 pins by name. Non-governed ⇒ ready + auto-merge by this seat;Fixes #18074closes this card on landing. Hold #14290's trigger file was touched with nothing folded — its re-pricing stays thedomain:devxlane's.
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsLanded — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-14T04:24Z. PR #18108 (headdda4d903) merged by the queue as7c7e76fc7; two readings at 2026-09-14T04:24Z: the queue refgh-readonly-queue/main/pr-18108-*is gone, andgit log origin/maincarries(#18108)with(#18105)as the lit control and(#18129)as the dark one. In-seat landing (non-governedscripts/pm/**): review of record 5658752807, ACCEPT 5658752960, enqueued at 2026-09-14T03:56Z. Now onorigin/main:dispatch-gates --rannever reads a killed run as RUN — a recorded kill code (124, or 128 and above) with a stated reason classifies the family KILLED under NOT-MEASURED, a bare kill is UNRUN, and a verdict-plus-claim contradiction prints instead of resolving silently; the pin tests fail on the regression they catch rather than throw.Fixes #18074closed this card at 2026-09-14T04:22Z.dispatch-gates.mjsis aRestart-touch:file on the H17 index (#14290): that hold's re-verification on the merged ref is the patrol's / triage's, ⛔ not done here. Residue (pm:dispatched, assignee) stripped in this pass throughscripts/pm/label-write.mjsand read back.
Generated by Claude Code
What was measured
scripts/pm/dispatch-gates.mjs --ranhas two channels for declaring a gate family unmeasured, and one silently eats the other.All four readings taken 2026-09-13T17:45Z at
origin/main6a3bcd8174, same derivation each time (--repo objectstack-ai/objectstack packages/spec/src/system/metrics.zod.ts, 70 derived families), same single commandnode packages/lint/scripts/check-reference-carrier-shape.mjs. Only the record's spelling differs between runs — so these are four readings of one instrument, not four instruments.<cmd> :: exit 12470 derived, **1 run**, **0 NOT-MEASURED**, 69 UNRUN<cmd> :: exit 370 derived, 0 run, **1 NOT-MEASURED**, 69 UNRUNNOT-MEASURED <cmd> :: cap-killed at the container foreground ceiling70 derived, 0 run, **1 NOT-MEASURED**, 69 UNRUN70 derived, **1 run**, **0 NOT-MEASURED**, 69 UNRUNB is the discriminating control for A — same command, same derivation, only the recorded code differs, and the classification flips. C is the control for D — the claim channel demonstrably works when it is the only line present.
The defect
D is the finding. A runner that does the most honest thing available — record the real exit code and explicitly declare the family unmeasured with a stated reason — has its declaration discarded without a word and gets a false green. The most careful record produces the least truthful total.
The cause is a short-circuit, not a missing feature. In
parseRunRecord's reconciliation loop the run-line branch returns before the claim map is ever consulted:scripts/pm/dispatch-gates.mjs:12032—if (ranClaims.has(command)) {:12045—if (recorded.code === EXIT_PREREQUISITE_NOT_MET)— exit 3 is the only code that leaves this branch as NOT-MEASURED:12055—ran.push(command)— every other recorded code,124/143/141included, falls through to run:12058—const claim = unmeasuredClaims.get(command)— unreachable for any command that appears inran.listWhy this is a defect and not the documented design
The module's own docblock at
:11788-11799anticipates exactly this case and argues the opposite way round:The stated principle is prefer the direction that costs a rerun. Reading A/D does the reverse: a family that never reached a verdict line is counted as run, which is the false-green direction the whole module exists to prevent. Whatever the right classification for a cap-kill is —
UNRUNandNOT-MEASUREDare both defensible —runis not one of the candidates.Reproduction
How it surfaced
On PR #17635 (#15939's gate card), the round's
check:pm-dispatch-gateswas killed by its owntimeoutat 560s under the container's foreground cap and recordedexit 124. Its reconciliation read82 derived, 81 run, 1 NOT-MEASURED, 0 UNRUN— the1is a different, exit-3 family, and the cap-killed one sits inside the81 run. The round declared it in prose instead, which is why it was caught at all. ⛔ A round that trusted the tool's total would not have known.Dedupe — checked, and these are neighbours, not duplicates
check:pm-dispatch-gatesruns 11m27s and is SIGTERMed at the container cap with zero diagnostic. That is the symptom on one gate. This card is about the reconciler's classification of any such kill, on any family.env:never reaches the NOT-MEASURED bucket. Adjacent, but a derivation-side problem, not a record-side one.dispatch-gates --ran's headline "0 NOT-MEASURED" is the runner's claim, not a measurement — a seat whose gates exited 3 still gets a green tick #17204 is cited in the module's own docblock as the four-delivery0 NOT-MEASUREDmeasurement; this card is consistent with it and does not restate it.Suggested direction — for the triaging seat, not a ruling
The failure is that two channels disagree and the loser is silent. The cheapest honest fix is the ordering the module's own principle implies: let an explicit
NOT-MEASURED … :: <reason>claim win over a run line for the same command, or — if a record carrying both is considered malformed — refuse the record and say so, rather than picking one and discarding the other quietly. ⛔ Either way the fix belongs with the seat that owns this file; nothing here should be read as pre-deciding it.Filed by the epic PM for #15939,
session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T17:45Z. Surfaced by the #17635 round; re-measured and re-framed here rather than relayed — the round's own account named only the exit-3 half and did not know the claim channel existed.Generated by Claude Code