Repository navigation
check-governed-queue-guard: an authorised APPROVED review satisfies the merge-queue leg regardless of commit_id — maintainer 2026-09-04: a human approval record suffices, never pinned to the latest commit #15187
Description
Activity
Claim: PM loop round 5 — maintainer instruction (2026-09-04 chat, verbatim on the card)
Session:session_019RfFHiRCSs3JXLK4cwcfox
Branch:claude/issue-15187-queue-guard-unpinned-approval
Worktree:objectstack-issue-15187
Domain:domain:skills
File surface:scripts/pm/check-governed-queue-guard.mjsonly — themerge_grouppredicate, its header paragraph and refusal text, and the self-test cases that pin it (stop on breach; explain in the report). ⛔ No workflow file, noGOVERNED_APPROVERSchange, no change to the path test, the early-warning leg, the exit contract or the check-run name.
Container & model: S,mode:subagent,model: opus(a governance predicate whose every sentence must keep the rule's force; opus)
Clause-②: no (the gate's accept/reject behaviour DOES change — by the maintainer's explicit instruction, which is the ruling this card executes; the seat reviews at the contract tier)
Serial constraints cleared: no open PR touchesscripts/pm/check-governed-queue-guard.mjs; the guard's own workflow runs it inmerge_groupon every queue build, so the PR proves itself on its own queue build. Not a governed surface (scripts/**; the dev confirms with the governed--test) ⇒ draft PR, in-seat contract-tier review, ready + auto-merge by the seat.Decision re-read (05:31Z): the maintainer's instruction 「你的门禁有问题,只需要有人工批准记录就行,不需要卡最新的提交。」 is executed as written on the card — an authorised latest-decisive APPROVED review satisfies the queue leg regardless of
commit_id; dismissed, superseded, unauthorised and unreadable still refuse; the ruling quoted verbatim and dated in the header with the accepted cost stated once; the self-test pins the new predicate in both directions.
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 4, 2026 os-dev-report
{ "issue": 15187, "status": "blocked", "branch": "claude/issue-15187-queue-guard-unpinned-approval", "pr": "https://github.com/objectstack-ai/objectstack/pull/15203", "premise_still_valid": true, "summary": "The ruling is implemented in scripts/pm/check-governed-queue-guard.mjs and pushed as draft PR #15203 (head 76c93cb1, skip-changeset applied and read back). The merge_group leg now passes a governed PR iff an account in GOVERNED_APPROVERS holds a latest-decisive APPROVED review, on ANY commit; dismissed, superseded, unauthorised and empty still refuse, and an unreadable review list still fails closed (exit 4). staleApprovers is KEPT as a printed reading that never refuses - chosen by reading its two consumers, the queue log and the seat-side hook, both of which render it; approvals on an earlier commit are now listed there AND counted in approvers, so a cleared queue log names the commit each approval was given on and the accepted cost is visible where it is paid. The PR head is still read for that line but decides nothing, so an unreadable head is no longer a refusal (a read that decides nothing may not block a landing); it is printed as a missing reading. Header rewritten with the 2026-09-04 quote verbatim and dated, the supersedes-the-sha-pin-half sentence, and the accepted cost stated once against the 2026-08-12 #8161 precedent. BLOCKED, and this is the run's main finding: the ruling necessarily reddens a GOVERNED file. .github/workflows/lint.yml discovers and runs every .claude/hooks/*.selftest.sh as the job 'Claude hook guard self-tests', and .claude/hooks/guard-governed-enqueue.selftest.sh pins the retired sha pin in two cases - measured 49/0 on the pre-ruling guard vs 47/2 on this branch, by ablation, not inference. There is no way to keep them green without keeping the pin, so PR #15203 CANNOT LAND ALONE. The dispatch pins the file surface to the guard only and the PM claim says stop on breach, so I did not touch .claude/**; the companion work (hook self-test, hook prose + the deferred pinnedApprovalVerdict rename, AGENTS.md:311-313, .claude/skills/pm-dispatch/SKILL.md:729 - all governed paths) is filed as #15209 and linked as a sub-issue of #15187. TWO DELIBERATE DEVIATIONS FROM THE DISPATCH, both flagged rather than taken silently: (1) the function is NOT renamed - .claude/hooks/guard-governed-enqueue.sh imports pinnedApprovalVerdict and reads staleApprovers off its result, so renaming inside my file surface would break a seat-side governed guard into silent fail-open; the reason is written into the function's docblock and the rename travels with the hook's own edit in #15209; (2) the PR body opens 'Part of #15187' and deliberately carries NO closing keyword anywhere near a card number, because merging it does not complete the card and GitHub's parser ignores every negation you wrap around one. PM claim state was as described (assignee os-steve, pm:dispatched, claim comment 5536163108); I set no assignee and touched no label other than skip-changeset on my own PR.", "tests": "ALL exit codes captured by redirecting first, then reading $?; every verdict quoted is the gate's own printed line, never a bare $?. Union run on the FINAL head 76c93cb1 (after the origin/main merge; my file is byte-identical across that merge - git diff --stat a1b2f067 HEAD on the path is empty). GATE DERIVATION: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) - first run warned STALE TREE (5 commits behind, dispatch-gates.mjs itself changed), so I merged origin/main, reinstalled and re-derived; the second derivation is clean: '1 path(s) vs merge base 919beca43', 21 commands. ALL 21 DERIVED + 3 NAMED = 24 commands, every one exit 0: check-ci-filter-parity, check-closing-keyword-parity (+ --self-test), check-comment-mask-corpus, check-self-test-wired (+ --self-test), check-whole-set-label-write (+ --self-test), bare-root-worklist --self-test, check-governed-queue-guard --self-test, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:nul-bytes, check:parse-guard, check:pm-dispatch-gates, check:pnpm-filter-targets, check:refd-timer-probe, check:watch-hint-literal, check:pm-governed-merges, check:required-contexts, check-governed-merges --test on the changed path. VERDICT LINES: '✓ check-governed-queue-guard self-test: 133 cases pass' | '✓ dispatch-gates self-test: 1353 cases pass.' | '✓ check-governed-merges --self-test: 245 assertions' | 'check-nul-bytes: OK (scanned 8261 text file(s) ... no raw ASCII control bytes)' | '✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces'. check-governed-merges --test scripts/pm/check-governed-queue-guard.mjs: '0 of 1 path(s) hit the register ... NOT governed' - as the seat expected. pnpm lint: WHOLE REPO, no narrowing, through the shared verify lock (OS_VERIFY_LOCK_SLOT=issue-15187-lint) - 'os-verify-lock: VERDICT command-exit 0 · held the lock 77s · waited 0s'. I also ran the 7 roster families the derivation flags as silent-but-not-clearance because their roster sits under scripts/ or scripts/pm/: check:pm-governed-prose, check-published-list-mirrors (+ --self-test), check:console-injection, check:engine-double-contract, check:i18n-stale-fill, check:pm-label-desc-cap, check:pm-skill-ratchet - all exit 0. NOT MEASURED (not green, not red): pnpm check:published-readme-exports exits 1 in this worktree, every line reading 'type entry ... does not exist. Build first' - an unmet prerequisite over READMEs and dist/ this diff does not touch; CI builds before it. GENUINELY RED (the blocker): bash .claude/hooks/guard-governed-enqueue.selftest.sh - 47 passed, 2 failed. ABLATION / REVERSE VERIFICATION, run AFTER committing so a restore point existed, with an absolute-path trap and both legs proved on disk: expected direction was 'the two hook cases turn red only because of my change'. Mutation leg: git checkout origin/main -- ABSOLUTE_PATH put the pre-ruling guard back; confirmed on disk by hash - git hash-object gave 62b86b1cd3d8a59a28f1d4a7398769ab7fd7b8d4, equal to git rev-parse origin/main:PATH and NOT equal to the HEAD blob 5d5029f3d059487f940bf816306a5651d36a49a5 (a non-empty hash was required; an empty one would have been read as failure, not as 'nothing to compare'). Against that mutated tree the hook self-test printed '49 passed, 0 failed'. Restore leg: git checkout HEAD -- ABSOLUTE_PATH (never the bare form, which reads from the polluted index), proved by hash back to 5d5029f3 AND by git diff HEAD being empty. NO REBUILD LEG APPLIES AND NONE IS CLAIMED: this guard is a dependency-free .mjs that node runs directly from source - it has no dist/, the workflow invokes 'node scripts/pm/...' and the self-test imports the sibling by relative path, so there is no build output that could serve a stale copy. EDIT-LANDING PROOF (not the editor's exit code): the 31 hunks were applied by a script that asserted exactly one occurrence of each anchor before replacing, refused to double-apply, then re-read the file from disk and checked 5 retired strings were gone ('pinned to each governed PR', 'STALE, never counts', the old battery key, 'an-unparsable-head-sha-pins-NOTHING-fail-closed', 'const pinnedPass =') and 6 new ones present; git diff --numstat then showed 259/120 on the one path. CONSUMER PROOF for the design decision to keep the exported name: I ran the .claude/hooks/guard-governed-enqueue.sh node block verbatim against the edited module - authorised approval on the head ALLOW(0), authorised approval on an OLDER commit ALLOW(0) (the ruling, inherited for free), unauthorised BLOCK(2), no reviews BLOCK(2), superseded BLOCK(2); no TypeError, and the hook's now-unreachable STALE line never renders because it is only printed on the blocking path where the bucket is necessarily empty. SELF-TEST COUNTS: 133 cases (up from the pre-change run), no battery deleted, so SELF_TEST_BATTERY_FLOOR stays 17; two battery keys renamed to match the ruling and both stayed above their floors.", "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (curl) and git. Channel probe: gh ABSENT; GET /repos/objectstack-ai/objectstack 200 and GET /user 200, so REST was used for the PR create (201), the additive label write (200) and the issue create (201). CHANNEL SWITCH DECLARED: GET /search/issues is 403 in this session ('sessions are bound to their configured repositories'), so the dedup search used the repo-scoped list endpoint (state=open, sort=created desc, per_page=100, ONE page covering #15041-#15202 — the whole window this hours-old ruling could have produced a duplicate in) plus a local regex grep, not a broad paginated scan and not MCP. Control: the regex matched #15187 itself, so the empty result is a reading and not a dead pattern; the only hit was #15187.", "open_questions": [ { "question": "PR #15203 cannot land alone: the ruling necessarily reddens the CI job 'Claude hook guard self-tests', whose two failing cases live in the GOVERNED file .claude/hooks/guard-governed-enqueue.selftest.sh. How should the companion (#15209 — hook self-test, hook prose + the pinnedApprovalVerdict rename, AGENTS.md, the PM skill) land?", "options": [ "A. Extend PR #15203's file surface into the four governed paths and land it as ONE PR. It then becomes a governed-surface PR: no seat flips it ready, enqueues it or arms auto-merge; it needs the maintainer's own merge, or one authorised approval — which, under the very predicate the PR carries, no longer expires on a later push. One atomic landing, no window in which main is red, and the rename lands with its only importer.", "B. Keep them separate and land #15209 FIRST (governed, maintainer-merged), then #15203 through the ordinary seat path. Preserves the dispatch's file surface exactly, but #15209 alone makes the hook self-test assert a behaviour the guard on main does not yet have, so it is red between the two landings — the same outage, moved.", "C. Land #15203 alone and accept 'Claude hook guard self-tests' red on main until #15209 follows. Rejected outright: a permanently red required-adjacent check is the exact poison the 2026-08-18 ruling retired, one level down." ], "recommendation": "A. On 实际业务需求: the measured need is that governed landings stop being blocked, and B leaves a red window on main in between while A has none — the only option with no interval in which a gate lies. On 项目长远合理性 (contract-first): the hook IMPORTS this predicate, so producer and consumer are one contract; splitting a contract change across two PRs is exactly the shape rule 5 names, and it is also what forces the lying function name to survive on main for a while. On 防 AI 写代码犯错: the retained name pinnedApprovalVerdict and a staleApprovers bucket that no longer refuses are both traps a later author can 'restore' a pin from; A closes them in one sitting, B leaves them standing for a window, and every extra hour is one in which a dispatch reads the stale AGENTS.md and SKILL.md rule and holds a PR that needs no holding. On 创业阶段不扩散 / 立即退休 (「短期不考虑渐进」): no staged window and no dual spelling — the pin and its whole vocabulary retire in one move. The cost of A is real and is the PM's to weigh, not mine: it converts a seat-landable PR into a maintainer-merge or single-approval PR. That is precisely why I stopped instead of taking it." } ], "out_of_scope_findings": [ "filed as #15209 (linked as a sub-issue of #15187, no assignee, no labels — left for PM triage): propagate the 2026-09-04 unpinned-approval ruling into the four governed surfaces that still state the retired sha pin — .claude/hooks/guard-governed-enqueue.selftest.sh (2 cases RED in CI today, measured by ablation), .claude/hooks/guard-governed-enqueue.sh (its blocking message still tells seats 'A push after the approval unpins it', plus the deferred pinnedApprovalVerdict rename), AGENTS.md:311-313 ('any later push expires it'), and .claude/skills/pm-dispatch/SKILL.md:729 (「批准后再推提交即过期转红」 — the text every later dispatch reads, so a stale rule there propagates into work nobody has started yet)." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15187, "status": "done", "branch": "claude/issue-15187-queue-guard-unpinned-approval", "pr": "https://github.com/objectstack-ai/objectstack/pull/15203", "premise_still_valid": true, "summary": "Seat decision A executed: PR #15203 now carries the ruling whole, across five files, and the blocker from the previous round is gone. The guard's merge_group leg passes a governed PR iff an account in GOVERNED_APPROVERS holds a latest-decisive APPROVED review on ANY commit; dismissed, superseded, unauthorised and empty still refuse, and an unreadable review list still fails closed (exit 4). The PR head is still read so the queue log can name the commit each approval was given on - that is where the accepted cost is visible - but it decides nothing, so an unreadable head is now a printed missing reading rather than a refusal. Both renames landed WITH their only importer in one commit: pinnedApprovalVerdict became authorizedApprovalVerdict, and staleApprovers became approvalsOnEarlierCommits, because a bucket still named for a refusal is one a later reader re-derives a refusal from. .claude/hooks/guard-governed-enqueue.selftest.sh: the two cases that pinned the sha pin are FLIPPED, not deleted - the fixture governed-stale became governed-approved-on-an-earlier-commit and now asserts ALLOW, so reverting the guard would turn it red again; the case asserting the refusal says 'STALE approval' is replaced by two, one asserting the refusal states the ruled predicate and one asserting the retired sentence is ABSENT via a new expect_lacks helper (a retired rule leaves its sentence behind in the text a seat reads long after the predicate stopped enforcing it, and only an absence assertion catches that). The hook's blocking message no longer tells seats a push unpins the approval, and its approvalsOnEarlierCommits detail line is dropped because that bucket can only be non-empty on the ALLOW path while the detail string only ever prints on the blocking path. AGENTS.md and .claude/skills/pm-dispatch/SKILL.md state the ruled predicate with the ruling quoted verbatim; both rewrites are wrapped inside the 120-byte line budget with the quotation kept whole on one line, because a soft break inside the CJK quote marks renders as a space and a quotation with a space in it is a quotation that was edited. PR #15203 is GOVERNED now (4 of 5 paths hit the register) and STAYS A DRAFT: I did not flip ready, did not arm auto-merge, did not request reviewers - the seat requests os-zhuang and hotlong. skip-changeset is still on it (read back). #15209 is folded in — the PR body's first two lines are closing references to this card and to that one, in that order — and it carries a comment saying so. No closing keyword sits near any other card number in the PR body, in that comment, or here.", "tests": "Every exit code captured by redirecting first, then reading $?; every verdict quoted is the gate's own printed line. Final tree e2678a7f then 0a8763f6 after a second origin/main merge; the union below ran on head 0a8763f6, and my five files are byte-identical across both merges (git diff --stat on those paths between the pre-merge commit and HEAD is empty). THE BLOCKER, CLOSED - the hook self-test measured in three states: pre-ruling guard 49 passed / 0 failed (by ablation, previous round: origin/main's copy of the one file restored on disk, hash-confirmed 62b86b1c against git rev-parse, then restored from HEAD with git checkout HEAD -- ABSOLUTE_PATH and proved by hash plus an empty git diff HEAD); guard changed with the hook untouched 47 passed / 2 failed; THIS PR 50 passed, 0 failed (the count rose by one because expect_lacks added a case - the harness pins no total, only 'N passed, 0 failed'). GATE DERIVATION: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths). Ran it three times because origin/main kept moving and twice it warned STALE TREE naming files it derives from (once dispatch-gates.mjs itself, once lint.yml and package.json among 11); merged, reinstalled and re-derived each time rather than trusting a well-formed answer about a tree nobody is on. Final derivation is clean: '5 path(s) vs merge base 9c1bcda38', 31 commands. ALL 31 DERIVED FAMILIES GREEN on the final tree, plus the hook self-test. Verdict lines: 'bash .claude/hooks/guard-governed-enqueue.selftest.sh' -> '50 passed, 0 failed' | '✓ check-governed-queue-guard self-test: 133 cases pass' | check:pm-skill-ratchet -> '✓ AGENTS.md is 1161 lines (ceiling 1162; headroom 1)' and '✓ .claude/skills/pm-dispatch/SKILL.md is 971 lines (ceiling 1005; headroom 34)' | check:pm-governed-prose -> '✓ 2 instruction surface(s) name all 5 registered governed surfaces … and claim no others' | check:skill-frame-sync -> 'binding sentence present in all 2; 5 count mention(s) agree' | check:pm-skill-id-lint -> '✓ 23 file(s) clean' | check:pm-dispatch-gates -> '✓ dispatch-gates self-test: 1353 cases pass.' | check:pm-governed-merges -> '✓ check-governed-merges --self-test: 245 assertions' | check:doc-authoring -> '✓ 14730 customer-facing string(s) … clean' | check:nul-bytes -> 'check-nul-bytes: OK (… no raw ASCII control bytes)'. pnpm lint: WHOLE REPO, no narrowing, through the shared verify lock (OS_VERIFY_LOCK_SLOT=issue-15187-verify) - 'os-verify-lock: VERDICT command-exit 0 · held the lock 73s · waited 0s'. check-governed-merges --test on the final five paths exits 3: '4 of 5 path(s) hit the register … ⛔ GOVERNED — a human merge is the review record for this PR' - the expected answer now, and the reason the PR stays draft. TWO GATES THAT FIRST CAME BACK NON-ZERO AND WHY, both resolved rather than waved through: (1) check:pm-skill-ratchet exited 1 and it was MINE - 'AGENTS.md has 1 line(s) over the 120-byte budget: L313 (136B)', then after the AGENTS.md fix 'SKILL.md … L728 (121B), L730 (164B)'. Re-wrapped at legal break points, content otherwise unchanged, and re-run to green; AGENTS.md is 1161 against a 1162 ceiling and the skill 971 against 1005. (2) check:doc-formula-expressions exited 3 = PREREQUISITE NOT MET, twice, first for an unbuilt @objectstack/formula and then for an unbuilt @objectstack/lint - an environment reading over files this diff does not touch, NOT a finding. Rather than report it NOT MEASURED I built the closure through the lock (pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' build, VERDICT command-exit 0, 149s) and re-ran it to a real exit 0. check-required-contexts --verify-required-set exits 2 (NOT VERIFIED, HTTP 401) unless given NODE_OPTIONS=--use-env-proxy, which the gate's own failure text names; with it, exit 0 - and it prints a real pre-existing finding, see out_of_scope_findings. EDIT-LANDING PROOF, never an editor's exit code: the renames were applied by a script that counted occurrences before and after per file and then re-read each file FROM DISK to assert the old spellings were absent (22+9 in the guard, 3+2 in the hook, 2 in the hook self-test, and git grep confirms no other file in the repo referenced either name). The 13 prose hunks were applied by a script asserting exactly one occurrence of each anchor, refusing to double-apply, then re-reading from disk to check 6 retired strings gone (including 'any later push expires it' in AGENTS.md and 「批准后再推提交即过期转红」 in the skill) and 5 new ones present. PR BODY: written, PATCHed, and read back in full both times. First read-back: my 143 lines byte-identical, but the platform APPENDED a second, bare attribution footer beside my session-URL one - the platform reading the seat named, confirmed. One corrective PATCH moved durable attribution into body prose and dropped my own footer block; read-back now shows exactly one footer block and lines 1-142 byte-identical. ⛔ No third attempt - the contract forbids the re-paste loop, not the single correction a read-back justifies.", "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (curl) and git, in both rounds. gh is ABSENT in this container. GET /search/issues stays 403 ('sessions are bound to their configured repositories'), so the dedup read for this round used the repo-scoped list endpoint (state=open, sort=created desc, per_page=100, ONE page covering #15092-#15234) plus a local regex grep — declared as a channel switch, not a broad paginated scan and not MCP. Control: the regex matched a known-present phrase in the same page, so the result is a reading rather than a dead pattern. That read is what stopped me filing a duplicate: both findings I was about to file already exist as #15233 and #15234, filed by a sibling seat within the hour.", "open_questions": [], "out_of_scope_findings": [ "NOT filed by me — already open as #15233, found independently by a sibling seat and confirmed by my own dedup read, so I did not duplicate it: `node scripts/check-required-contexts.mjs --verify-required-set` (with NODE_OPTIONS=--use-env-proxy so the API read succeeds) reports 'direction B — required in the live set, pinned by NO registry row (1): Governed Surface Queue Guard'. The very check this PR changes is already a required context on main with no REQUIRED_CONTEXTS row pinning it, so renaming its job would detach it silently. Surfaced in the PR body too, because it changes how this PR lands: a red merge_group leg on a required context blocks everything behind it.", "NOT filed by me — already open as #15234, same sibling seat, same dedup read: two gates in the derived family need different NODE_OPTIONS values, so no single invocation satisfies both. I hit exactly that: check-required-contexts --verify-required-set needs --use-env-proxy to reach the API, while the heavy builds want --max-old-space-size." ] }
Generated by Claude Code
ACCEPT — governed terminal. Contract-tier review of PR #15203, head
0a8763f6(skills seat, 2026-09-04T07:12Z). The verdict covers #15209 as well: it is folded into this PR, and the PR body's first two lines close both cards.Implemented-by:
claude/issue-15187-queue-guard-unpinned-approval(os-dev subagent, taskaf9269aa4a82714a9)
Reviewed-by:session_019RfFHiRCSs3JXLK4cwcfox(skills seat)Verified in a detached worktree at
0a8763f6, not from the report:- The ruling is carried exactly, and nothing wider.
authorizedApprovalVerdictpasses themerge_groupleg iff an account inGOVERNED_APPROVERSholds a latest-decisive APPROVED review;commit_idis not consulted to reach that verdict (one statement at the predicate). The approver set is unchanged and still pinned toos-zhuang,hotlong; DISMISSED, superseded, unauthorized and empty review lists still refuse (exit 3); an unreadable review list still refuses (exit 4); an unreadable PR head is now a printed note and the reviews still judge. The render names an approval on an earlier commit "COUNTS ANYWAY" with the ruling quoted beside it, so the accepted cost is visible where it is paid. - Both renames landed with their only importer.
.claude/hooks/guard-governed-enqueue.shimportsauthorizedApprovalVerdictand no longer renders the retired bucket;git grepfindspinnedApprovalVerdict/staleApproversonly in the guard's own history docblock, which says they were retired. - Gates, all on
0a8763f6: hook self-test50 passed, 0 failed(the sha-pin fixture flipped to ALLOW rather than deleted; the retired "unpins it" sentence asserted absent through the newexpect_lacks);check-governed-queue-guard --self-test: 133 cases pass;check-skill-line-ratchet✓ (AGENTS.md 1161 of 1162, SKILL.md 971 of 1005);check-skill-frame-sync✓ (72 markdown files scanned);check-governed-prose✓;check-skill-id-lint✓ 23 files;check-governed-merges --teston the five paths → exit 3, 4 of 5 on the register, the expected answer;git merge-treeagainstorigin/main1bc3c092clean. - Prose: AGENTS.md and
.claude/skills/pm-dispatch/SKILL.mdstate the predicate with the maintainer's sentence quoted verbatim on one unbroken line each; the 2026-08-27 approver half is kept and the sha-pin half is marked retired 2026-09-04.
Governed terminal, four pieces: ① this verdict; ② the PR stays a DRAFT, visibly pending for the maintainer — ⛔ the seat never flips it ready, never enqueues it, never arms auto-merge; ③ review requested from
os-zhuangandhotlongon the PR; ④ seat post refreshed. Landing is a human merge, or an authorized approval and then the queue.⚠️ Until this PR itself is onmain, the guard that judges it is the current one there, which still pins the approval to the head — so an approval must sit on0a8763f6(or the maintainer merges by hand); once merged, the unpinned predicate applies to every later governed PR.
Generated by Claude Code
- The ruling is carried exactly, and nothing wider.
Filed by the
domain:skillsseat (sessionsession_019RfFHiRCSs3JXLK4cwcfox, os-steve) on a maintainer instruction given in the live PM chat on 2026-09-04 at ~05:1xZ, verbatim and untranslated: 「你的门禁有问题,只需要有人工批准记录就行,不需要卡最新的提交。」 — said while objectstack PR #15057 (approved three times by an authorised approver, enqueued three times) kept falling out of the merge queue. The seat's reading of that PR's failure is a different check (check-skill-frame-sync, root cause on the PR); but the instruction names a real property of THIS guard, measured: themerge_groupleg ofscripts/pm/check-governed-queue-guard.mjs(pinnedApprovalVerdict) counts an authorised APPROVED review only when itscommit_idequals the pull request's CURRENT head sha, so any push after the approval — a merge ofmain, a patch round, a rebase — turns the approval STALE and the queue refuses until the approver approves again. The maintainer rules that a human approval record on the pull request suffices and must not be pinned to the latest commit.What changes
pinnedApprovalVerdict: an authorised (GOVERNED_APPROVERS) reviewer's latest-decisive review that is APPROVED satisfies the queue leg regardless ofcommit_id; DISMISSED and superseded reviews still never count; thestaleApproversbucket is retired or kept as a printed reading only (never a refusal); an unreadable review list still fails closed;GOVERNED_APPROVERSitself is unchanged (the instruction says a HUMAN approval record — the authorised set is the technical control that it is one).pull_requestearly-warning leg, the exit contract, the check-run name, the generated-surface register.Refs
objectstack PR #15057 · objectui PR #7473 (approved repeatedly by os-zhuang while its head moved) · #11704 (the guard's regime) · #8161 (the 2026-08-12 ruling on the retired gate's approval predicate).
Generated by Claude Code