fix(pm): every --pair run states the inputs it judged from - #18681
Merged
Merged
Conversation
`check-clause2-carriers --pair` answered 0 / 4 / 0 on one pair with an identical script blob, and nothing either run printed could settle which of them had read what. The judging half is already deterministic given a fixed document (`--pair-json` proves it); what was unpinned is what document the LIVE path builds. Every run past the board resolution now closes with a fenced `clause2 input record` block on stderr — the board and which of the three sources answered, the read path and every request it issued with its row count, the pair and the evidence it was derived from, the comments read by count/ids/newest, the claim comment SELECTED as the carrier with the rule that selected it and every candidate it rejected with a reason, each pooled claim's body fingerprint, the PR-body line, and this file's own blob hash and path. The field roster is the same on exit 0, exit 4 and every refusal, so two runs that disagree are diffed rather than re-run. The `--json` sweep carries the same record under `inputs`. No verdict, row, count or exit code reads any of it. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 17, 2026
os-justin
marked this pull request as ready for review
September 17, 2026 14:41
This was referenced Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18456
Clause-②: no
scripts/pm/sits outside every workspace package, and the root package is private, so nofiles[]can ship this diff —skip-changeset.The defect
check-clause2-carriers --pairis the landing pre-check every seat runs, and on one pair(PR #17917 / card #17425) it answered 0 at 02:57Z, 4 at 03:04:09Z and 0 at 03:58:33Z on
2026-09-13 with an identical script blob. Two explanations were ruled out with controls
(no comment on that thread was ever edited; the board is resolved from the environment, never
from the working directory), so the cause is still UNKNOWN — and the three runs could not be
compared, because not one of them had SAID what it read. The judging half is already
deterministic given a fixed document (
--pair-jsonproves that); what was unpinned is whatdocument the live path builds. This states it: every
--pairrun now closes with a fencedclause2 input recordblock on stderr, with the same field roster on every exit, so two runsthat disagree are settled by diffing their two blocks — never by re-running until one side
wins. ⛔ No guess at the cause is dressed as a fix here: no predicate, no state, no row, no
count and no exit code reads one character of the record, and the judging half is untouched.
The record's field roster
Rendered from
INPUT_RECORD_RUN_FIELDSandINPUT_RECORD_PAIR_FIELDSand from nowhere else,so a field cannot silently disappear: a declared field this run could not fill renders with an
explicit token rather than vanishing, and a field the builder fills that the roster does not
declare is NAMED in the block (
record.undeclared). Values too long for one line continue onindented lines under their key.
record.version·run.utc·run.mode·run.script.path·run.script.blob·run.script.bytes·run.node·board.repo·board.source·read.plan·read.api·read.token·read.served·read.pair-json·run.requests·pairs.derivedpair.N.)pr·card·derivation·head-sha·card-comments·card-comment-ids·card-comment-newest·pr-comments·pr-comment-ids·pr-comment-newest·claim.rule·claim.selected·claim.rejected·claim.clause2-line·pr-body.clause2-lineFour of them are worth naming for WHY they are there:
run.requests— every read the run issued, in order, with its channel, its exact pathand its row count. A page asked for with
per_page=100that answers with exactly 100rows is the one shape a truncated read and a complete one share, and nothing printed it.
claim.rule+claim.selected+claim.rejected— the carrier, the rule that picked itand every candidate it did not pick, each with its reason. That separates "the two runs
selected different comments" from "the two runs applied different rules".
claim.selected's body fingerprint (bytes +sha256:) — the field the measured 0/4/0actually needs. A
misplacedverdict on that thread requires the governing claim to havecarried no readable declaration while a superseded one did; same ids with a different verdict
is only possible if the BYTES differed, and the ids were all anybody could see.
run.script.blob— git's blob hash of this file, beside the path it ran from. "The blobwas identical on both sides" was a claim in the incident; it is now a printed fact any seat
checks with
git hash-object. On this PR's head it reads25d204236aa8296644813109fa77541d6efe1644,which is exactly
git rev-parse HEAD:scripts/pm/check-clause2-carriers.mjs.The
--jsonsweep carries the same record underinputs— the same record, ⛔ never a secondformat.
The two live blocks the card names
--pair 17917— the pair from the card. Both it and #18654 have since merged, so--pairanswers exit 2 on each today (the pair cannot be formed from a closed PR). ⭐ That is
precisely the class of exit the old code said the least about, and the block is now complete
on it:
--pair 18654— the pair this seat landed today, which answered 0 at 12:32Z and is likewisemerged now (exit 2):
⭐
diffof those two blocks is four lines:run.utcandrun.mode, twice. Same roster,same order, same shape — which is the property the card asked for.
A live block on exit 0
--pair 18659(open at the time of writing) — exit 0, the full pair half:Pins
Battery #18456: the
--pairinput record — the same block on every exit, so two runs thatdisagree can be diffed, registered in
SELF_TEST_BATTERIESwith a floor of 38; 41cases register.
SELF_TEST_BATTERY_FLOORraised 26 → 27 by exactly the one battery this adds.What is pinned, in the card's own terms:
a refusal that formed no pair — all three key lists asserted equal;
(with
INPUT_RECORD_UNSET), an empty record still carries every declared key, and a keyoutside the roster is named rather than printed in silence;
claimCarrierSelectionapplies — so the printed rule cannot drift from the applied one;
--pair-jsonrun names that read path as such and names the document;the measured shape, asserted directly;
gitBlobSha1is pinned against two valuesgit hash-objectprints.⛔ CONTROLS in the same battery: the block carries no verdict, no exit code and no finding row;
building it changes no reading; and the selection the block prints IS the pool
cardDeclarationjudged (ONE derivation —
cardDeclarationnow callsclaimCarrierSelectioninstead of derivingthe pool inline, so the record and the verdict cannot describe two different comments).
--self-teston this head: 786 cases pass, exit 0 (745 before; +41).Ablation
From the committed tree, blob
25d204236aa8296644813109fa77541d6efe1644(= this PR's headblob), the pair half of the record removed on disk, mutation proved before the run, restore by
blob hash under a
trap:Direction predicted before the run and observed: turns red. The module is run directly from
source by
node scripts/pm/…— no build and nodist/between the edit and the run, so theon-disk proof is the whole preflight.
cannot see
main's emission. An ablation that deleted the two lines inmain'sfinallywould come back green. What covers emission is the three live blocks quoted above, taken on this
head across three different exits.
Candidate cause, unproven — ⛔ not fixed here
Two readings taken while wiring the record. Neither is acted on in this PR.
1. On the blob all three 2026-09-13 runs ran, exit 4 was the DETERMINISTIC answer for that
pair — so what is unexplained is the two 0s, not the 4.
a5ed18ced(2026-09-12T06:05:25Z, "a key-INITIALclause-② line that QUOTES the spelling is not a declaration"); its next change was
4e3a496baat 2026-09-13T17:19:18Z, after all three runs.a5ed18ced's blob isaecbb2d86683eb908468fdacaac2ff53753f06ef— the same blob PR fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card #18448's body independently citesas "the exact blob the 2026-09-13 readings were taken from".
ObjectPermissionSchema's retiredallowRestore/allowPurge: only literalfalseparses (not a truthy/falsy split), and no post-parse guard can ever see either key #17425 at that moment was comment5650083758(2026-09-13T01:57:23Z). Its line 3 opens
Clause-②: no —and then quotes the spelling againinside the same line. Run first-hand against that historical blob's own
readClause2Line:{"kind":"near-miss","reason":"describing"}, andcardDeclarationon aone-claim thread reads
missing— ⛔ not a declaration. Today's copy reads it identically.--pair-jsondocument assembled from the REAL thread as it stood at 03:04:09Z (its 9comments, both carriers' real label event streams) answers exit 4, MISPLACED on this PR's
head, quoting the superseded
Clause-②: yesand naming5650083758as the correction target —which is what the 03:04Z reviewer and the 02:53Z dev round both reported.
the record's
claim.selectedfingerprint andclaim.clause2-linewould have shown, had the0-runs printed one.
readClause2Line(selfcontained) and
cardDeclaration(which imports today's sibling modules); the commit orderingis read from a shallow checkout, corroborated by fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card #18448's independent citation of the same blob.
2. The comment read — the one the declaration limb depends on — is the only read here with no
page discipline.
readCardCommentsissues ONE request,/issues/N/comments?per_page=100, withno
page=ladder and no short-read check.readCarrierEventsandreadPullFilesboth page toexhaustion and answer
null(UNJUDGED, never clean) when their cap is hit, for the reason theirown docblocks state. A card thread past 100 comments therefore loses its tail silently, and the
claim pool is built from whatever came back. Not the cause on #17425 (7 comments at 02:53Z, 16
today), but it is a live fail-open in this reading. The record makes it visible for the first
time: request
#3prints its row count, so a(100 row(s))on aper_page=100request is nowreadable. ⛔ Not fixed here; the seat files or re-scopes.
Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom the worktree with no hand-fed path list; re-derived after rebasing onto current
main(the derivation was STALE-TREE by 4 commits) — identical command list. All 34 run at head
f5773ce08, exit codes captured redirect-then-$?:Reconciled:
dispatch-gates --ran⇒ 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.Repo-wide
pnpm lint(eslint . --no-inline-config) atf5773ce08: exit 0.grep -naPfor control bytes over the changed file: no hits.⛔ Outside these 34, as the derivation itself prints: 53 artifact-roster families, 11
wide-population families, 7 pending-changeset families, 1 path-scheduled CI job and the
always-runs tail. Their absence here is not a clearance.
Acceptance notes
Out of scope, noted and ⛔ not filed:
--pair-jsonusagerefusals (a missing file, a non-JSON document, a board conflict), because everything past the
board resolution moved inside one
try/finally. One extra stderr line on those paths, in thedirection the file's own header argues for. Carrier: whoever next edits
main.main's--pairvalue is parsed in two places now (once forrun.mode, once for the pairitself). Both read the same argv through
flagIndex; a reader may prefer one. Carrier: whoevernext edits
main.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Generated by Claude Code