Skip to content

[finding] the review-of-record template says 「on the PR or its card」, but the Governed Surface Queue Guard's REFERENCES tier reads only the PR thread — a record posted where the template allows gets the PR dequeued #18701

Closed

Description

@os-justin

Class b — two tools' texts disagree about where the same artefact lives, and following the one that prints the template gets the other to refuse.

Reading (measured on PR #18689, 2026-09-17)

  • scripts/pm/check-clause2-carriers.mjs --template prints, on main f8eaf6704 :3159: 「ONE comment on the PR or its card; the NEWEST one naming this head governs.」 — the docblock at :326–:331 and :3359 says the same. The skills seat posted the ## Contract review for PR docs(pm-skills): adopt eight measured platform readings into the governed fact table, each paid in place #18689 (a fact-layer PR, one file under .claude/skills/pm-dispatch/references/) on the carrier card finding(platform-readings): three readings owed to the governed fact table — zod v4 parse-spying, a per-TOKEN (not per-channel) REST write identity, and a depth-wait command that is not repo-local #18426 as comment 5716694216 at 15:10:43Z; --pair 18689 read it at once as C6-RECORD 「review of record on this head: card thread」.
  • The Governed Surface Queue Guard's merge_group leg (scripts/pm/check-governed-queue-guard.mjs, run 35238999808, job 105262255304) then refused the merge group at 15:15:59Z and the queue dequeued the PR at 15:36:18Z with CI_FAILURE: 「⚖️ landing tier: REFERENCES … ⛔ NO review of record on this head either (0 comment(s) read on the PR thread)」. Its option 3 text spells the requirement as 「a ## Contract review comment on the PR thread naming this head」 (:1284), and its docblock at :288–:289 says 「the ## Contract review comment on the PR thread」.
  • Repair that worked: the same record re-posted on the PR thread as comment 5717108355 at 15:38:11Z, --pair now reads 「PR thread, comment 5717108355」, auto-merge re-armed and the PR re-entered the queue at 15:38:54Z. Cost: one dequeue, ~23 min of queue time, and a second copy of a 6 KB record.

Why it is a defect

The guard's docblock (:303–:309) says its head-sha span test, newest-of resolution, Reviewed-by: and Served-tier: readers 「all come from the two files that already own them」 — so the two tools share the READER but not the THREAD SET: check-clause2-carriers reads PR thread + card thread, the guard reads the PR thread alone. The template is the one thing a seat copies from, and it names the wrong place for the one tier where the record is load-bearing. A seat that does exactly what the printed template says gets a CI_FAILURE dequeue whose only cure is to post the record again somewhere else.

Remedy (the successor's; two shapes, one to pick)

  1. The guard reads the card thread as well, through the same helper check-clause2-carriers uses (one thread set, one reader), so 「on the PR or its card」 becomes true for the REFERENCES tier too; or
  2. The template, the --template docblock, references/contract-review.md and the guard's option-3 text all say 「on the PR thread」 for a fact-layer landing — and the lane text says why (the guard reads one thread).

Either way, one sentence in one place should decide it; today two say different things and both are read as authoritative.

Provenance

Skills seat session_01Gqi43smmqjJ5sUrhfoPeKu, from its own dequeue. Dedupe words: review of record, PR thread, or its card, check-governed-queue-guard, REFERENCES tier, contract review template. Checked against the 533 open issues at 15:10Z: #18536 (does a Clause-②: no PR owe a review at all) and #18469 (the classifier denies the landing command) are neighbours; neither is this.


Generated by Claude Code

Activity

  1. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    Claim: PM loop round 1
    Session: session_01Gqi43smmqjJ5sUrhfoPeKu
    Branch: claude/issue-18701-review-of-record-thread
    Worktree: objectstack-issue-18701
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: scripts/pm/check-governed-queue-guard.mjs + scripts/pm/check-clause2-carriers.mjs only (the --template text and docblocks say a review of record is 「ONE comment on the PR or its card」 while the Governed Surface Guard's REFERENCES tier reads the PR thread alone — measured this afternoon on PR #18689: record on the carrier card 5716694216 at 15:10Z, --pair read it as C6-RECORD, the merge_group leg dequeued the PR CI_FAILURE at 15:36Z with 「0 comment(s) read on the PR thread」, cured only by a second copy on the PR thread 5717108355; deliverable = ONE thread set for the two tools that already share the reader — either the guard reads the card thread too through the same helper check-clause2-carriers uses, or the template, its docblocks and the guard's option-3 text all say PR thread — chosen on the four axes and pinned so the two tools' thread sets cannot drift again (a self-test that asserts the template's stated location set equals the guard's read set); references/contract-review.md carries no such sentence and is ⛔ not on the surface — PR #18666 holds its :41); non-governed scripts/pm/** ⇒ draft, skip-changeset, the seat lands it by CCR after the in-seat contract-tier review
    Container & model: M(两工具线程集合一 + 漂移 pin), mode:subagent, model: claude-opus-5 — node scripts/pm/dispatch-gates.mjs --tier --repo objectstack-ai/objectstack scripts/pm/check-governed-queue-guard.mjs at 2026-09-17T17:02Z on the fast-forwarded tree (df1b275c7): 「Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)」; default tier taken; the seat's contract-tier review at delivery
    Clause-②: no
    Thread-read: 5717467602
    Serial constraints cleared: 27 open PRs' file lists read at 2026-09-17T17:02Z — none touches either script (PR #18681 on check-clause2-carriers.mjs LANDED 1bc22b3dc at 2026-09-17T15:03:30Z); os-verify-lock.sh --status at 2026-09-17T17:02Z: free; shared checkout at origin/main a7e9a6600, origin/main df1b275c7 (no harness path moved). Hot-file note: #18536's machine-side half and #18683 also name check-clause2-carriers.mjs — behind this card. Slot: the one freed by #18599's delivery (PR #18725); #18673 and #18543 in flight. Total order at 2026-09-17T17:02Z: p1 — this card · #18709 (serial behind #18543 on post-stamped.mjs); p2 by age — #18536 · #18601 (serial behind PR #18713) · #18661 (behind #18673) · #18680 · #18683 · #18693 · #18699 (behind PR #18725 on os-dev.md) · #18702; p3 — #18664 · #18527 · #18622 · #18662 · #18665 · #18686 · #18705.
    Ruling-ref: 5717467602


    Generated by Claude Code

  2. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 18701,
      "status": "done",
      "branch": "claude/issue-18701-review-of-record-thread",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18738",
      "head_sha": "d13df2636157f539187391287238f9af53f69b1b",
      "files": [
        "scripts/pm/check-governed-queue-guard.mjs",
        "scripts/pm/check-clause2-carriers.mjs"
      ],
      "session": "session_01Gqi43smmqjJ5sUrhfoPeKu — harness-stamped id of this run (mode:subagent, so the parent seat's)",
      "premise_still_valid": true,
      "canonical_sentence": "NOT silent — the governed text says it in four places on origin/main (080688b561). references/contract-review.md :27 「复核记录 = 一条评论落 PR 或卡,达档与默认档同形;散文、dev 自评、`os-dev-report` 恒不算。」; same file :40 「落地前检三条:① 席内条款②复核 PASS 在案,即 PR 或卡上同形的复核记录(档位按实测)。」; same file :59 「PASS、FAIL 与作废都落 PR 或卡;……」; SKILL.md 〈入队与落地〉 :640 「交付后复核归派发席:达档席内审,未达档循保险丝起子代理;记录 = 同形评论落 PR 或卡。」 The tool that disagrees is scripts/pm/check-governed-queue-guard.mjs. ⚠️ The claim comment's aside that references/contract-review.md carries no such sentence is the ONE sub-premise that was falsified: :27 is that sentence and it is on main. Everything else in the card held — the template text, the three docblocks, the guard's PR-thread-only read and the #18689 evidence all read exactly as filed.",
      "side_moved": "THE GUARD, all of it. No governed file touched. Four axes, all pointing one way: (1) 实际业务需求 — the card record is measured behaviour (the seat did it on 2026-09-17 following this repo's own --template), both carriers are live on every pair (declaration on both, --pair judges both, the label is stripped from both), and the population wanting the card to stop counting is empty; (2) 项目长远合理性 — contract-first: the governed text is the contract, the two tools are consumers, and a consumer reading narrower than the declaration is the defect; 基本裁决原则 puts 声明 above 实现, so narrowing the declaration to match an implementation gap is the forbidden direction; (3) 防 AI 写代码犯错 — this is a declared-but-unhonoured capability in its purest form (an instruction names a location a gate does not look at), and the structural repair is ONE constant both sides derive from plus a pin that reds if either is edited alone, not the same sentence written in more places; moving the template would leave three more hand-written location sentences free to drift one layer along; (4) 创业阶段不扩散需求 — moving the guard is the smaller change and adds no capability (helper already exists, card number rides the pull object the head read already fetches, at most one extra thread read and only for a population refused outright today), while moving the template would RETIRE a carrier the protocol uses everywhere else, across governed files — a maintainer's ruling, not a dev PR. No axis conflicts. Decisive fifth fact: the template side is unreachable from this card's file surface anyway (its sentences live in .claude/skills/pm-dispatch/**, governed).",
      "shared_constant": "REVIEW_OF_RECORD_THREADS (+ its derived REVIEW_OF_RECORD_LOCATION) in check-clause2-carriers.mjs, the file that owns the reader. Three consumers all derive from it and none spells a set of its own: (1) locateReviewOfRecord builds its searched rows, its gap sentences and its per-thread read counts from the list — --template and --pair output byte-identical to main; (2) contractReviewTemplateLines builds its printed sentence from REVIEW_OF_RECORD_LOCATION; (3) the guard fetches ONE thread per entry in the LOADED list, and its whole record reading is now the imported locateReviewOfRecord — the guard's own two-line filter is deleted, leaving only the tier gate (Served-tier stands + the #18060 model-identifier refusal), which is that leg's own fact. The card is derived, never configured: deliveredCardNumber(pull) enumerates candidate numbers and asks deliveryEvidence — the one relation derivePairs/H8/H31 pair by — so it can never accept a card that relation rejects; no card, or two at equal strength, searches NO card thread and says so (the refusing direction). Card number and head sha come from ONE GET /repos/{owner}/{repo}/pulls/{number}, the request the head read already made, so the guard keeps its zero-write, event-payload-only posture with no checkout. REVIEW_OF_RECORD_LOCATION is mirrored at the guard's module scope for the pure synchronous renderer only (a module-scope import is the measured exit-13 deadlock its own docblock records); the SET the leg acts on is the loaded list, never a mirror, and the mirror's words are pinned to the constant in the guard's battery.",
      "pin": "In check-clause2-carriers' self-test (the file that owns the reader), and a MEASUREMENT rather than two lists side by side. STATED = the locations read out of the text contractReviewTemplateLines() actually prints. READ = obtained by DRIVING check-governed-queue-guard.mjs's runGuard once per entry in REVIEW_OF_RECORD_THREADS, with a record built from this file's own contractReviewRecordLines() sitting on that thread ALONE, collecting the threads whose run answers exitCode === EXIT_CLEAR with record.state === 'stands'. Asserts stated === read === the constant's own set, with four controls: the set has two locations (not vacuous); each run located the record on the thread it was posted to; the guard fetched exactly one thread per entry, no more and no fewer; a run with the record on NEITHER thread is refused, so the pin can fail. selfTest() became async to take that lazy import; the dispatch uses .then(...), ⛔ never a top-level await (the guard's docblock records that as an exit-13 deadlock for both modules).",
      "counterfactual": "The #18689 dequeue reproduced offline, one fixture through runGuard, no network and no git: a references-tier pair whose record sits only on the card thread. The same script runs against either generation (it passes both the old fetchPullHead and the new fetchPull). BEFORE (080688b561): exitCode 3 REFUSED, conclusion 'refused', record.state 'absent', rendered 「⛔ NO review of record on this head either (1 comment(s) read on the PR thread)」 — the #18689 sentence. AFTER: exitCode 0, conclusion 'cleared', record.state 'stands', record.where 'card', rendered 「✅ review of record on this head: comment 5716694216 (2026-09-16T15:10:43Z) on the card thread is a `## Contract review` comment naming …」",
      "ablation": "Two legs, each with on-disk proof (grep -c of removed and injected text, plus git hash-object before/after), trap restore EXIT INT TERM on an absolute path, restore by git checkout HEAD -- path, and a byte-identity check against the HEAD blob. No build or dist is involved — both files are run directly by node and by package.json, so the on-disk proof is the whole preflight. LEG A, the loop that unifies the thread set (for (const thread of recognisers.threads) narrowed to the first entry): blob 9a111f6c -> b57227d0, removed-count 1->0, injected-count 0->1; cross-tool pin 4 of 802 failed, guard battery 25 of 253 failed — the loop is load-bearing for the whole references leg. LEG B, the line that supplies the card's number (card: card.card -> card: null), which reproduces the pre-fix thread set exactly and is the clean 'new red / old green' reading: blob 9a111f6c -> 9d935d92, counts 1->0 and 0->1; NEW PIN RED — check-clause2-carriers 3 of 802 failed, all three the new battery (stated=[PR,card] read=[PR], the card run locating nothing, fetch list [[4101],[4101]] instead of [[4101,4102],[4101,4102]]); NEW GUARD CASES RED — 5 of 253, all five in the #18701 battery; OLD CASES GREEN — 799 of 802 and 248 of 253, every pre-existing battery in both files including #18020. Restore on both legs: worktree blob equals HEAD blob 9a111f6c…, git diff HEAD empty, git status --porcelain empty, both self-tests back to exit 0.",
      "self_tests": "node scripts/pm/check-governed-queue-guard.mjs --self-test :: 239 cases / 21 declared batteries BEFORE -> 253 cases / 22 AFTER (new battery '⭐ #18701: the record lives on the PR or its card, and BOTH are read', 14 cases, floor 14; SELF_TEST_BATTERY_FLOOR 20 -> 21), exit 0. pnpm check:pm-clause2-carriers :: 786 cases / 28 declared batteries BEFORE -> 802 cases / 29 AFTER (new battery '#18701: ONE thread set -- what the template STATES is what the queue guard READS', 16 cases, floor 16; SELF_TEST_BATTERY_FLOOR 27 -> 28), exit 0. --template, --help and the no-event refusal path (exit 1 with its own message) all still answer.",
      "tests": "All exit codes captured redirect-then-$?, ⛔ never across a pipe. DERIVED GATES — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree, no hand-fed path list (change set: the two scripts; 2 committed / 0 working tree / 0 untracked; three-dot against merge base 080688b56): 36 derived, 36 run, EVERY ONE exit 0. Reconciled after a fresh git fetch origin main with --ran carrying each line as COMMAND, a double colon, then the word exit and its numeric CODE: '✓ dispatch-gates --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED (a DERIVED zero — all 36 recorded an exit code and none of them is 3)'. The 36: check-adr-0087-registration (--base origin/main, --self-test), check-changeset-no-major (--base, --self-test), check-ci-filter-parity, check-closing-keyword-parity (+--self-test), check-comment-mask-corpus, check-declaration-mirrors (+--self-test), check-scripts-symbol-anchors (+--self-test), check-self-test-wired (+--self-test), check-self-test-workflow-commands (+--self-test), check-whole-set-label-write (+--self-test), pm/bare-root-worklist --self-test, pm/check-governed-queue-guard --self-test, and pnpm check: agent-test-spelling, bash32-floor, changeset-gate-self-tests, cli-command-ids, closing-target-claim, cross-package-test-inputs, driver-memory-census, entry-guard, nul-bytes, parse-guard, pm-clause2-carriers, pm-dispatch-gates, pnpm-filter-targets, ratchet-remedy-authority, refd-timer-probe, watch-hint-literal. REPO-WIDE LINT — NODE_OPTIONS=--max-old-space-size=4096 pnpm lint (eslint . --no-inline-config, the whole tree, as PR #18654 did) :: exit 0, no output, run at final HEAD d13df26361. LANDING CHECK — node scripts/pm/check-clause2-carriers.mjs --pair 18738 :: exit 0 (pair 18738/18701 derived via closing keyword, head d13df2636, claim declares `Clause-②: no`, PR body carries the same line, 5 reads all HTTP 200). MAIN MOVED to 62bce5c297 during the run; its three commits touch packages/** and .changeset/** only — nothing under scripts/**, nothing this diff reads — so no merge was taken and the derivation is unaffected. CONTROL-CHARACTER SWEEP — grep -naP over both files: clean; pnpm check:nul-bytes exit 0.",
      "labels": "skip-changeset hung at PR creation via POST /repos/objectstack-ai/objectstack/issues/18738/labels (HTTP 200), comparative read-back: union(read, target) = {size/l, skip-changeset}, current = {size/l, skip-changeset}, nothing stripped. skip-changeset criterion measured, not assumed: the whole diff is scripts/pm/**, which is on the fast-track not-published list and appears in no package's files[]. ⛔ needs:contract-review is the seat's — not hung, not stripped, not waited on by this dev; the PR carries no such label right now and `--pair 18738` exits 0.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
      "api_writes": "3 REST proxy writes, plus 3 git pushes. (1) POST /repos/objectstack-ai/objectstack/pulls (draft, HTTP 201, PR #18738); (2) POST /repos/objectstack-ai/objectstack/issues/18738/labels (skip-changeset, HTTP 200); (3) POST /repos/objectstack-ai/objectstack/issues/18701/comments (this report). git pushes: the empty-branch routing probe, then two commits. Nothing else; ⛔ no PATCH of the PR body — it was written once at creation and read back byte-intact (the only delta is one trailing newline the platform strips; one footer, session-URL form, verified).",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: `deliveryEvidence` (check-half-states.mjs) applies a precedence between closing-keyword / part-of / part-of-inline / branch-name internally but exports no ordering, so DELIVERY_EVIDENCE_PRECEDENCE is declared in check-clause2-carriers.mjs as a MIRROR and measured against that function pair by pair in the self-test rather than trusted. Successor: none — that ranking has exactly one caller today, and moving it to check-half-states.mjs is a one-line move whenever a second one appears.",
        "noted, not filed: the guard now issues TWO identical GET /repos/{owner}/{repo}/pulls/{number} requests per governed pull request on a merge group — one from makePullReader (governed leg) and one from makeLabelReader (carrier leg) — and both already existed before this PR. An efficiency observation, not a defect: both legs are fail-closed, the answers cannot disagree, and the cost is one request per queued PR. Successor: whoever next touches the carrier leg's reader; ⛔ not folded in here because the two legs are deliberately independent predicates.",
        "reported, not a finding: the guard's card read is built on the PAGED reader, ⛔ not on the un-paged path #18683 names. makeCommentReader is 100-per-page / 10-page and THROWS rather than truncating, and GET /repos/{owner}/{repo}/issues/{number}/comments is the same route for a pull request and for a card, so one reader serves both threads. #18683 is untouched, exactly where it was, on check-clause2-carriers' own readCardComments. Said in the guard's header and in the PR body as the dispatch required."
      ]
    }

    Generated by Claude Code

  3. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    ACCEPT — in-seat contract-tier review of PR #18738 (head d13df2636, report 5718794634) by the skills seat, 2026-09-17T18:05Z. Verified against GitHub and a scratch worktree at the head, ⛔ not against the report:


    Generated by Claude Code

  4. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    LANDED — PR #18738 (#18701, the review of record lives on the PR or its card, and BOTH are read) merged through the queue at 2026-09-17T18:29:43Z (merged_at), squash a84f61a7c (single parent 7636cd9b8, an ancestor of origin/main; REVIEW_OF_RECORD_THREADS and deliveredCardNumber read in check-clause2-carriers.mjs on main, the guard imports locateReviewOfRecord, and the guard's --self-test on main answers 253 cases); armed by this seat through the CCR route after the in-seat ACCEPT 5719028655 — ready 2026-09-17T18:05:48Z, added_to_merge_queue 2026-09-17T18:05:49Z. The card auto-closed completed at 2026-09-17T18:29:44Z on the PR's Fixes; pm:dispatched is stripped in the same act. Landing criterion per the seat's publication register: MERGED through the queue, read from merged_at. The dequeue of PR #18689 this card came from cannot recur: a fact-layer record on the carrier card now satisfies the merge_group leg, and the pin reds if the template's stated set and the guard's read set are ever edited apart. Follow-ups on this file, now free of this serial: #18719 (p1) → #18683 → #18536's machine-side half.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    a84f61a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions