Skip to content

Commit da93a8b

Browse files
fix(pm): the queue guard's refusal remedies send a queued PR to the maintainer's manual removal, never claiming a DRAFT conversion dequeues it (#20894)
Fixes #20855 Clause-②: no ## What changed `scripts/pm/check-governed-queue-guard.mjs` prints two refusal remedies: the governed-surface leg's (`renderGuardVerdict`) and the SIZE leg's (`renderSizeVerdict`). Both refusals fire only on the `merge_group` leg (`guardVerdict` returns `warned` and `sizeGuardVerdict` returns `not-applicable` on `pull_request`), so the pull request they name IS in the queue. Both told a seat to "take the pull request out of the queue: convert it back to DRAFT (disarming auto-merge alone does NOT dequeue it) and park it there". No seat act is measured to dequeue a queued pull request, and that includes a draft conversion (`.claude/skills/pm-dispatch/references/platform-readings.md`, queue membership). Step one of both remedies now says what AGENTS.md Prime Directive #14 has said since `2d5fe76f`: - the pull request is in the queue, and no seat act is measured to take it out, disabling auto-merge and converting it to DRAFT included; - ask the maintainer AT ONCE to remove it from the queue by hand; - disable auto-merge AND convert it back to DRAFT, which disarms it so it does not re-enter once removed; - confirm from the remote that it is in neither the queue nor `origin/main`. The rest of each remedy is unchanged: the approval route, both landings, and the bypass-rules option. The quote 「四件套留 draft 等人批,⛔ 不翻正式不入队」 is kept, now as the place a governed PR waits once it is out of the queue. No dequeue act is named: no GraphQL mutation and no relay op. Only printed text and its self-test move. No verdict, exit code or read changes. ## The pins - **Ordering pin (moved).** The governed remedy's ordering pin now checks three positions in order: the pull-back (`ask the maintainer AT ONCE`), then `obtain an APPROVED review`, then `CLAIMING SEAT lands it from there`. It also refuses a missing phrase. The old pin compared raw `indexOf` results, and a missing phrase answers -1, which sorts first. - **New battery (floor 4).** `⛔ the queued pull-back: the remedy names no dequeue act a seat has`; `SELF_TEST_BATTERY_FLOOR` goes from 23 to 24. The battery collapses whitespace in each text before reading it, so a phrase wrapped across two array entries still counts as one. Its four cases: 1. every governed refusal kind (unapproved, unreadable, unattributed) carries the pull-back; 2. every size refusal kind (oversized, unreadable, no-pull) carries the same pull-back; 3. a control: the draft-dequeue detector fires on each of four fixtures, and every spelling it lists fires on at least one fixture; 4. the negative pin: no rendering (the six refusals and the `pull_request`-leg warning) matches any spelling of "a DRAFT conversion dequeues it". - Self-test: 292 → 296 cases, all passing. ## Ablation (one-time; no permanent test file) Both legs ran through `node scripts/ablation-replace.mjs` in WRAP mode, from the committed head `7f89016f9`. Each leg ran inside a script with an EXIT/INT/TERM restore trap on the absolute path. - **Leg A: the old sentence restored.** The size remedy's new step one was replaced by the sentence at `4edb61449`: anchor 1 → 0, blob `3f59569d5403` → `8bd07044a078`, on-disk count of `alone does NOT dequeue it` 1 → 2. The self-test printed `2 of 296 case(s) failed`: `every-size-refusal-kind-carries-the-SAME-pull-back`, and `NO-rendering-claims-a-DRAFT-conversion-dequeues-a-queued-PR` on the three size renderings. Restore: blob == HEAD `3f59569d5403`, `git diff HEAD` empty. - **Leg B: the negative pin alone.** The old parenthesis was appended to the governed remedy, and the new text was left intact: `1 of 296 case(s) failed`, the negative pin only, on the three governed renderings. Restore: blob == HEAD, `git diff HEAD` empty. - No build or `dist/` is involved, because the self-test runs this script from source. ## Gates, at head `7f89016f9` `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) derives the same 31 commands the dispatch named. All 31 exit 0, and `--ran` reconciles them: `31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED`. - The 16 direct-node commands: `check-ci-filter-parity`, `check-closing-keyword-parity` (+ `--self-test`), `check-comment-mask-corpus` (7658 files, 0 disagree), `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`, and `pm/check-governed-queue-guard --self-test` (296 cases pass). - `pnpm check:agent-test-spelling`, `bash32-floor`, `cli-command-ids`, `closing-target-claim`, `cross-package-test-inputs`, `driver-memory-census`, `entry-guard`, `gitlink-declared`, `nul-bytes`, `parse-guard`, `pnpm-filter-targets`, `ratchet-remedy-authority`, `refd-timer-probe` and `watch-hint-literal`. - `pnpm check:pm-dispatch-gates` ran detached: `dispatch-gates self-test: 1976 cases pass`, 882.7 s, exit 0. - NOT MEASURED locally, left to CI: the guard's own event-payload run (it is CI-measured only), 3 families that take a value from the workflow, and 1 CI job scheduled by these paths. - Lint, narrowed: `eslint scripts/pm/check-governed-queue-guard.mjs --no-inline-config --format json` read 1 file with 0 errors and 0 warnings. The effective config for this file has no `parserOptions.project`, and its two rules (`no-restricted-imports`, `comment-swallow/no-code-inside-block-comment`) are per-file, so this diff cannot move the verdict on any untouched file. - A control-byte scan of the file came back clean (`grep -naP` exit 1). ## Publishing `scripts/pm/**` ships in no package's `files[]`, so this PR has no changeset. ## Acceptance notes - `scripts/pm/check-governed-merges.mjs:773-774` is a header comment, not printed text, and it still says "Disarming alone does not dequeue: converting the PR back to draft is what removes it from the merge queue." That file is outside this card's file surface, so it is not edited here. Carrier: none named. --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 33b6e8b commit da93a8b

1 file changed

Lines changed: 102 additions & 16 deletions

File tree

‎scripts/pm/check-governed-queue-guard.mjs‎

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -546,14 +546,16 @@ const SELF_TEST_BATTERIES = Object.freeze({
546546
'⭐ #18701: the record lives on the PR or its card, and BOTH are read': 14,
547547
'⛔ #19036: the SIZE line at the queue — imported, per queued PR, fail-closed': 54,
548548
'⭐ #19344: the remedy names a path the ruleset actually offers': 5,
549+
'⛔ the queued pull-back: the remedy names no dequeue act a seat has': 4,
549550
'⭐ the 2026-09-20 ruling: a certified PURE REGENERATION carries the record to the queued head': 11,
550551
});
551552

552553
// DELETING an entry silences that battery's floor exactly as effectively as
553554
// zeroing it, so the roster's own size is pinned too. Lowered 24 → 23 when the
554555
// contract-review carrier battery left with the label it read (ruling record
555556
// 5770886272 on #19061, letter B): the ordinary direction, one battery, one row.
556-
const SELF_TEST_BATTERY_FLOOR = 23;
557+
// Raised 23 → 24 with the queued pull-back battery: one battery, one row.
558+
const SELF_TEST_BATTERY_FLOOR = 24;
557559

558560
// The key an assertion is filed under when no battery is open. It is not a
559561
// declared battery, so it reds by the same set difference rather than silently
@@ -1448,9 +1450,14 @@ export function renderGuardVerdict(verdict) {
14481450
lines.push(
14491451
'',
14501452
' What satisfies this check:',
1451-
' 1. ⭐ FIRST, whatever comes after it — take the pull request out of the queue: convert it back to',
1452-
' DRAFT (disarming auto-merge alone does NOT dequeue it) and park it there. 「四件套留 draft 等人',
1453-
' 批,⛔ 不翻正式不入队」 — parked outside the queue is the SAFE state, not a stalled one.',
1453+
' 1. ⭐ FIRST, whatever comes after it — the pull request is IN the queue, and no seat act is measured',
1454+
' to take it out of the queue, disabling auto-merge and converting it to DRAFT included',
1455+
' (`.claude/skills/pm-dispatch/references/platform-readings.md`, queue membership).',
1456+
' So ask the maintainer AT ONCE to remove it from the queue by hand; disable auto-merge AND convert',
1457+
' it back to DRAFT, which disarms it so it does not re-enter once removed; then confirm from the',
1458+
' remote that it is in neither the queue nor `origin/main` (AGENTS.md Prime Directive #14). Out of',
1459+
' the queue and in draft is where a governed PR waits — 「四件套留 draft 等人批,⛔ 不翻正式不入队」',
1460+
' — the SAFE state, not a stalled one.',
14541461
` 2. Then: obtain an APPROVED review by an authorized approver (GOVERNED_APPROVERS: ${GOVERNED_APPROVERS.join(', ')})`,
14551462
' on each governed PR, and the CLAIMING SEAT lands it from there — ruling C (#17971, maintainer',
14561463
' 2026-09-13, verbatim 「C. approve 后不管后续改动都由席位落地:」), 「席位落地 = 过落地前',
@@ -1908,8 +1915,13 @@ export function renderSizeVerdict(verdict) {
19081915
lines.push(
19091916
'',
19101917
' What satisfies this check:',
1911-
' 1. ⭐ Take the pull request out of the queue: convert it back to DRAFT (disarming auto-merge',
1912-
' alone does NOT dequeue it) and park it there — parked outside the queue is the SAFE state.',
1918+
' 1. ⭐ The pull request is IN the queue, and no seat act is measured to take it out of the queue,',
1919+
' disabling auto-merge and converting it to DRAFT included',
1920+
' (`.claude/skills/pm-dispatch/references/platform-readings.md`, queue membership).',
1921+
' So ask the maintainer AT ONCE to remove it from the queue by hand; disable auto-merge AND convert',
1922+
' it back to DRAFT, which disarms it so it does not re-enter once removed; then confirm from the',
1923+
' remote that it is in neither the queue nor `origin/main` — the pull-back of AGENTS.md Prime',
1924+
' Directive #14. Out of the queue and in draft is the SAFE state.',
19131925
' 2. Then ONE of the two landings — the same terminal a Tier H governed diff has: ACCEPT on the card,',
19141926
' `needs-user-decision` on the PR, a final 维护者速读, review requested from GOVERNED_APPROVERS',
19151927
` (${GOVERNED_APPROVERS.join(', ')}); and then EITHER`,
@@ -2819,15 +2831,18 @@ export async function selfTest() {
28192831
assert('a-refusal-names-the-pull-request', refusalText.includes('#9527'), refusalText);
28202832
assert('a-refusal-states-what-would-satisfy-it', /What satisfies this check/.test(refusalText) && /DRAFT/.test(refusalText) && /APPROVED review/.test(refusalText), refusalText);
28212833
// ⭐ The remedy is an ORDER, not a menu, and the order is the landed rule's:
2822-
// out of the queue first, then the authorized approval, then the claiming
2823-
// seat lands it (ruling C, #17971). The pin reads all three positions rather
2824-
// than the first two, because a remedy that stopped at the approval would
2825-
// leave a seat waiting for a merge nobody is going to perform.
2826-
assert(
2827-
'a-refusal-orders-the-remedy-DRAFT-then-the-authorized-APPROVAL-then-the-CLAIMING-SEAT-lands-it',
2828-
refusalText.indexOf('DRAFT') < refusalText.indexOf('obtain an APPROVED review') &&
2829-
refusalText.indexOf('obtain an APPROVED review') < refusalText.indexOf('CLAIMING SEAT lands it from there'),
2830-
refusalText,
2834+
// the queued pull request goes back to the maintainer's manual removal first
2835+
// (AGENTS.md Prime Directive #14 — no seat act is measured to dequeue it),
2836+
// then the authorized approval, then the claiming seat lands it (ruling C,
2837+
// #17971). The pin reads all three positions rather than the first two,
2838+
// because a remedy that stopped at the approval would leave a seat waiting
2839+
// for a merge nobody is going to perform; and each position must EXIST, since
2840+
// a phrase that is gone answers -1, which sorts before everything.
2841+
const remedyOrder = ['ask the maintainer AT ONCE', 'obtain an APPROVED review', 'CLAIMING SEAT lands it from there'].map((p) => refusalText.indexOf(p));
2842+
assert(
2843+
'a-refusal-orders-the-remedy-the-MAINTAINER-PULL-BACK-then-the-authorized-APPROVAL-then-the-CLAIMING-SEAT-lands-it',
2844+
remedyOrder.every((i) => i !== -1) && remedyOrder[0] < remedyOrder[1] && remedyOrder[1] < remedyOrder[2],
2845+
`${JSON.stringify(remedyOrder)} :: ${refusalText}`,
28312846
);
28322847
// ⛔ The pre-ruling-C remedy, pinned in the REFUSING direction. This file used
28332848
// to tell a seat to leave the merge to the maintainer and to call that merge
@@ -3681,6 +3696,74 @@ export async function selfTest() {
36813696
assert('⭐ one-configured-bypass-actor-makes-the-named-path-REACHABLE-and-the-pin-clears', judgeRemedy(sizeRemedy, { bypass_actors: [{ actor_type: 'RepositoryRole', bypass_mode: 'pull_request' }] }).ok === true);
36823697
assert('⛔ and-a-remedy-drifting-back-to-a-bare-maintainer-click-REDS-even-where-the-path-IS-offered', judgeRemedy("the maintainer's own click lands it (人工直合).", { bypass_actors: [{ actor_id: 5 }] }).ok === false);
36833698

3699+
// ── the queued pull-back: the remedy names no dequeue act a seat has ─────
3700+
//
3701+
// Both refusals print on the merge_group leg, so the pull request they name
3702+
// IS in the queue. No seat act is measured to take a queued pull request out
3703+
// of it — a DRAFT conversion and an auto-merge disable included
3704+
// (`.claude/skills/pm-dispatch/references/platform-readings.md`, queue
3705+
// membership) — so the remedy is AGENTS.md Prime Directive #14's pull-back:
3706+
// the maintainer's manual removal at once, the disarm so it does not
3707+
// re-enter, and a confirmation from the remote. A remedy that tells a seat
3708+
// the draft conversion dequeues sends it away believing the PR is parked
3709+
// while the queue can still merge it.
3710+
//
3711+
// The texts are read FLATTENED: a phrase wrapped across two array entries
3712+
// is still one phrase to the reader, so it is one phrase to the pin.
3713+
battery('⛔ the queued pull-back: the remedy names no dequeue act a seat has');
3714+
const flatText = (t) => t.replace(/\s+/g, ' ');
3715+
const QUEUED_PULL_BACK = Object.freeze([
3716+
/no seat act is measured to take it out of the queue, disabling auto-merge and converting it to DRAFT included/,
3717+
/ask the maintainer AT ONCE to remove it from the queue by hand/,
3718+
/disable auto-merge AND convert it back to DRAFT, which disarms it/,
3719+
/confirm from the remote that it is in neither the queue nor `origin\/main`/,
3720+
/AGENTS\.md Prime Directive #14/,
3721+
/platform-readings\.md`, queue membership/,
3722+
]);
3723+
// Every spelling a remedy or a rule has used to say that a DRAFT conversion
3724+
// takes a queued pull request out of the queue, plus the general shape: a
3725+
// draft followed, inside one clause, by an affirmative dequeue verb.
3726+
const DRAFT_DEQUEUE_CLAIMS = Object.freeze([
3727+
/out of the queue: convert it back to DRAFT/i,
3728+
/auto-merge alone does NOT dequeue/i,
3729+
/draft is what removes/i,
3730+
/\bdraft\b[^.;]{0,80}\b(?:dequeues|removes (?:it from the (?:merge )?queue|queue membership)|takes it out of the queue)\b/i,
3731+
]);
3732+
const missingPullBack = (t) => QUEUED_PULL_BACK.filter((re) => !re.test(flatText(t))).map(String);
3733+
const draftDequeueClaims = (t) => DRAFT_DEQUEUE_CLAIMS.filter((re) => re.test(flatText(t))).map(String);
3734+
const governedRefusals = [refusedV, unreadableV, unattrV].map((v) => renderGuardVerdict(v));
3735+
const sizeRefusals = [overOne, unreadOne, sizeNoPull].map((v) => renderSizeVerdict(v));
3736+
assert(
3737+
'⭐ every-governed-refusal-kind-sends-the-queued-PR-to-the-MAINTAINERS-manual-removal-disarms-it-and-confirms-from-the-remote',
3738+
governedRefusals.every((t) => /REFUSED/.test(t) && missingPullBack(t).length === 0),
3739+
JSON.stringify(governedRefusals.map(missingPullBack)),
3740+
);
3741+
assert(
3742+
'⭐ every-size-refusal-kind-carries-the-SAME-pull-back',
3743+
sizeRefusals.every((t) => /REFUSED/.test(t) && missingPullBack(t).length === 0),
3744+
JSON.stringify(sizeRefusals.map(missingPullBack)),
3745+
);
3746+
// The control that keeps the negative pin below able to fail: each spelling
3747+
// the detector lists fires on a fixture, and each fixture is caught. A
3748+
// detector that matches nothing would hold the pin green forever.
3749+
const DRAFT_DEQUEUE_FIXTURES = Object.freeze([
3750+
'take the pull request out of the queue: convert it back to\n DRAFT (disarming auto-merge alone does NOT dequeue it) and park it there.',
3751+
'Convert it back to draft AND disable auto-merge — draft is what removes queue membership, disabling alone drops only the arming.',
3752+
'converting the PR back to draft is what removes it from the merge queue.',
3753+
'convert it back to DRAFT, which dequeues it.',
3754+
]);
3755+
assert(
3756+
'⛔ the-detector-FIRES-on-every-fixture-and-every-listed-spelling-fires-on-some-fixture',
3757+
DRAFT_DEQUEUE_FIXTURES.every((t) => draftDequeueClaims(t).length > 0) &&
3758+
DRAFT_DEQUEUE_CLAIMS.every((re) => DRAFT_DEQUEUE_FIXTURES.some((t) => re.test(flatText(t)))),
3759+
JSON.stringify(DRAFT_DEQUEUE_FIXTURES.map(draftDequeueClaims)),
3760+
);
3761+
assert(
3762+
'⛔ NO-rendering-claims-a-DRAFT-conversion-dequeues-a-queued-PR',
3763+
[...governedRefusals, ...sizeRefusals, warnText].every((t) => draftDequeueClaims(t).length === 0),
3764+
JSON.stringify([...governedRefusals, ...sizeRefusals, warnText].map(draftDequeueClaims)),
3765+
);
3766+
36843767
// ── the WIRING pin: the workflow still spells this context name ──────────
36853768
//
36863769
// Without this, renaming the job detaches the required context silently —
@@ -4319,7 +4402,10 @@ export async function selfTest() {
43194402
'pull_request leg silent and read-free; and the two-leg exit precedence (governed, size) pinned on ' +
43204403
'every combination — and the #19344 remedy pin: every limb names the Merge button\'s bypass-rules option, ' +
43214404
'judged against the recorded ruleset reading, red on a present-and-empty `bypass_actors` and on a remedy ' +
4322-
'drifting back to a bare click, and passing with the reading PRINTED when the field is unreadable.',
4405+
'drifting back to a bare click, and passing with the reading PRINTED when the field is unreadable — and the ' +
4406+
'queued pull-back: every refusal kind on both legs sends the queued pull request to the maintainer\'s manual ' +
4407+
'removal, disarms it and confirms from the remote, and no rendering claims a DRAFT conversion dequeues it, ' +
4408+
'with a detector control that fires on every listed spelling.',
43234409
);
43244410

43254411
selfTestReachedVerdict = true;

0 commit comments

Comments
 (0)