fix(pm): check-governed-merges refuses a flag its mode does not read, and a bare --pr beside a PM_SWEEP_REPO naming another repository - #21690
Merged
objectstack-fleet[bot] merged 2 commits intoOct 4, 2026
Conversation
… and a bare --pr beside a PM_SWEEP_REPO naming another repository Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk Co-authored-by: Claude <noreply@anthropic.com>
…ow per refusal, end to end against a recording fake API Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk Co-authored-by: Claude <noreply@anthropic.com>
objectstack-fleet
Bot
deleted the
claude/issue-21675-governed-merges-flag-refusal
branch
October 4, 2026 06:53
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 #21675
Clause-②: no
scripts/pm/check-governed-merges.mjsnow has a closed argument set for each mode. If a run passes a flag its mode does not read, the tool refuses with a usage error (exit 1) and names the flag. It does this before any git read, network read or child process. A bare--pr Nis refused whenPM_SWEEP_REPOnames a repository other than the one a bare number answers. That refusal prescribes the documented--pr OWNER/REPO#Nspelling. No new--repomeaning was added:--repois refused like any other flag the tool does not read, and the refusal spells out the qualified number. The exit code table is unchanged, because every new refusal uses the existing code 1.--test,--branch, the sweep and--self-testanswer exactly as before for every argument list they accepted before.One file changed:
scripts/pm/check-governed-merges.mjs(+383 / -7).The ruling this implements (triage comment on the card, quoted)
Reading 1: the card's four invocations, before and after
Before: base
7d0781482d(origin/mainat worktree creation). After: head6854ff6062. Each--prrow made real read-onlyGETs against the GitHub API. Angle-bracket placeholders in the tool's usage text are written below asN,OWNER/REPOandPATH, because the body sanitizer removes angle-bracket fragments.7d0781482d)6854ff6062)--pr 11590 --repo objectstack-ai/objectuiobjectstack-ai/objectstack/pulls/11590(7 files), ✅ NOT governed--pr objectstack-ai/objectui#11590PM_SWEEP_REPO=objectstack-ai/objectui+--pr 11590--pr 11590 --bogus-flag x--pr objectstack-ai/objectui#11590objectstack-ai/objectui/pulls/11590(18 files), ✅ NOT governeddiffempty); also identical withPM_SWEEP_REPO=objectstack-ai/objectuisetBefore, rows 1 to 3 (all three printed the same thing):
After, row 1:
After, row 2:
After, row 3:
Two more invocations show the same problem through a flag that belongs to another mode. Both were measured on the base and are closed by the per-mode sets:
--pr 11590 --repos objectui: before, exit 0, read objectstack's PR 11590. After, exit 1,--repos` is not a flag --pr reads.--test src/x.ts --since 7d: before, exit 0 with0 of 2 path(s), because7dhad joined the path list. After, exit 1,--since` is not a flag --test reads.Reading 2: self-test cases and battery floors, before and after
7d0781482d6854ff6062node scripts/pm/check-governed-merges.mjs --self-test✓ … 454 assertions, exit 0✓ … 476 assertions, exit 0SELF_TEST_BATTERY_FLOOR⭐ the argv is CLOSED: …How the batteries and floors were handled: the rows pin a different predicate from the ones in the existing
#17003battery. That battery is about how the file list is derived. These rows are about which arguments a run may carry at all. So they get a battery of their own instead of raising another battery's count. As AGENTS.md requires ("pin battery NAMES, never one total"), the roster floor moves from 31 to 32 in the same edit. If the new battery were deleted from the roster, it would fail at the floor instead of disappearing without a trace. The battery floor equals the cases the battery registers (22): 454 + 22 = 476.Each ruled pin has its own row, and the API is a fake on 127.0.0.1 (
GITHUB_API_URL) that records every request. The child process gets both token variables blanked,PM_SWEEP_REPOcleared andNO_PROXYset for loopback, so the self-test still makes no external connection:⭐ e2e-pr-N-with-a-bogus-flag-exits-1-names-it-prints-no-verdict-and-READS-NOTHING, plus pure rows for the walker, flags from another mode,--since=7d(the refusal prescribes--since 7d), and--pr --bogus(a flag is never taken as a value)--pr N --repo objectstack-ai/objectui:⭐ e2e-pr-N---repo-objectui-exits-1-prescribes---pr-objectui#N-and-READS-NOTHING, plus pure rows. A governed repo id after--repois written out as its slug.--reponaming objectstack itself is still refused, so no new meaning exists.PM_SWEEP_REPO=objectstack-ai/objectui --pr N:⭐ e2e-PM_SWEEP_REPO-objectui-with-a-bare---pr-N-exits-1-prescribes---pr-objectui#N-and-READS-NOTHING, plus pure rows. If the variable names this checkout's own repository (in any letter case), or is blank or unset, nothing is refused.--pr objectstack-ai/objectui#Nanswers as today:⭐ e2e---pr-objectui#N-answers-as-today-NOT-governed-on-exit-0-every-read-from-objectuis-PR-N(exactly 3 reads, all under/repos/objectstack-ai/objectui/), andand-PM_SWEEP_REPO-beside-the-qualified-spelling-changes-not-one-byte-of-the-answer.PM_SWEEP_REPOnaming objectstack with a bare number still answers from objectstack.--additions/--deletionsbeside--branch, so that mode's own "two readings" refusal still fires.Ablations: each refusal's pin turns red, and the file is restored to the HEAD blob
Each ablation ran through
node scripts/ablation-replace.mjs --file ABS_PATH --anchor … --replacement … -- node ABS_PATH --self-test. The fix was committed first, each anchor count went from 1 to 0, and each restore reportedok restored: blob == HEAD (89af2f9355e2) and git diff HEAD is empty, which was checked again independently after each leg.if (unreadRefusal !== null)changed toif (false && …)inmain()--repoand e2e--test … --since: eachstatus=0, and the first two read/repos/objectstack-ai/objectstack/pulls/11590…, which is the card's measured wrong answer--repo'--repo': 'value'added to the--prtable (--reposilently accepted)--repoprescription row, the "no new spelling" row, and e2e--repo(status=0, read objectstack's PR)PM_SWEEP_REPOif (ambiguous !== null)changed toif (false && …)inrunPullModePM_SWEEP_REPO(status=0, read objectstack's PR)bareTargetRefusalalso refuses a qualified--pr(over-refusal)status=1,reads=[]), and the byte-identical rowGates (run at head
6854ff6062)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no path argument) derived 32 commands from the change set: 1 path against merge base7d0781482, +383 / -7. That list is identical to the one in the dispatch. All 31 commands outside the lock exited 0, and each printed its own pass line. Among them:pnpm check:pm-governed-merges(476 assertions),check-scripts-symbol-anchors(3760 anchors resolve),check:entry-guard(221 export bindings, all inert on import),check-self-test-wiredandcheck-self-test-workflow-commands,check-comment-mask-corpus(8140 files, 0 disagree) andcheck:nul-bytes(OK).pnpm check:pm-dispatch-gatesran under the shared verify lock and passed:VERDICT command-exit 0,✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.and✓ dispatch-gates self-test: 1976 cases pass.It held the lock for 1224s (20m24s). The box was shared, and unlocked sibling work ran alongside it. The first attempt was still queued behind another seat's run of the same gate when its budget ran out (exit 99, slot kept). The resumed slot acquired after 476s more.Reconciliation:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list(oneCOMMAND :: exit CODEline per command, recorded as each one ran) printed✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of them is 3).Lint, narrowed and stated as such:
eslint --no-inline-config --format json scripts/pm/check-governed-merges.mjschecked 1 file and found 0 errors and 0 warnings. The resolved config for the file (--print-config) applies 2 rules (no-restricted-imports,comment-swallow/no-code-inside-block-comment) withparserOptionslimited toecmaVersion/sourceType. Type-aware linting is off (noproject/projectServiceanywhere ineslint.config.mjs), so this diff cannot change the lint result for any file it does not touch. The repo-widepnpm lintis left to CI.Acceptance notes
PM_SWEEP_REPOcomparison is made against the repository a bare number actually answers, which is the slug this checkout'soriginparses to. That isobjectstack-ai/objectstackin every run from this repo, so the behaviour is exactly the ruling's. It differs only under--rootpointing at another checkout, where it also refuses the reverse mismatch. The comparison ignores letter case. A malformedPM_SWEEP_REPOvalue also refuses, and the prescription then uses anOWNER/REPOplaceholder.--pr 11590 --repos objectuiwould still answer about objectstack's PR, which is the same class of wrong answer. Both cases were measured on the base (Reading 1).--additions/--deletionsstay readable to--pr/--branchonly so that those modes' existing, more specific refusal keeps its own words.main(), before the proxy re-exec, so a refused run starts no child process. ThePM_SWEEP_REPOrefusal happens inrunPullModebeside the existing--prargument errors. In a proxied container that means after the one re-exec, but still before any API read.GITHUB_REPOSITORYis not read: the ruling names two spellings. The board tools'resolveSweepRepofalls back to it, but nothing here adopts that.--self-testkeeps its own arguments. Its exit 1 means "a finding about this file", and it takes the--fixture-childmarker.--pr 42 43), and a repeated single-value flag (--pr 1 --pr 2reads the first), are still accepted. The ruling covers flags, and these were read from the code but not measured. Recorded here, not filed.scripts/pm/**belongs to no published package (skip-changeset).Generated by Claude Code