fix(pm): make dispatch-gates name the repo it answers about, and refuse a contradicted --repo assertion - #11126
Conversation
…se a contradicted --repo assertion The tool derives gate families from the tree it runs in and never said so. Handed a sister repo's paths it returned a confident, well-formed, exit-0 answer about the wrong repo, with no tell — and four of the five repos in scope hold no copy of the script at all, so every sister-repo dispatch was exposed to it. Every derivation now opens with a banner naming the repo and commit the answer came from, and `--repo <owner>/<name>` turns a caller's expectation into a checked assertion that refuses, naming both repos, on mismatch. Paths are repo-relative, so a path's home repo is never guessed: the banner is the unconditional tell and the assertion is the mechanical one. The no-path merge-base mode is untouched apart from the banner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e
PM 验收 — ACCEPT(终态)复核:席位 session_01MsbKEG4LtERSLaDrbehM3e 审毕 diff 主体(守卫段全读 + os-dev.md caveat 以门与自测佐证)。裁定吻合:候选 1 地板落地为「无条件 stderr 首行横幅(命名答案所出仓+commit)+ 开放问题席位裁定(均在既有裁定框架内):① 治理面终态:os-dev.md 被触碰 → 整 PR 治理面——保持 draft,仅维护者合并;已指派。合并后剥 #10971 状态标,并随即派发 #10542(同文件串行后半)。 Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32594804017 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32595589611 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
第二次踢出判读(终态收据,按「同一 PR 第二次被踢 ⇒ 停止重投、四分支重判」纪律):
Generated by Claude Code |
…3 maintainer ruling The merge group for this PR went red on check-skill-line-ratchet: os-dev.md composes to 401 lines against main (which #11112 took to 395), and to 405 once the queued #11137 (395→399) lands ahead of it — each PR green alone, over the ceiling only in composition. Maintainer ruling, 2026-08-23, on this PR (option B of the three put to them, verbatim): 「B:提天花板 399→405」 — keep the +6-line cross-repo caveat and raise the ceiling, quoted in the PR body as the ratchet requires. Sized to exactly 405 so headroom returns to zero once both queued PRs land. Gate + 19-case self-test green at this commit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e
Fixes #10971
The defect
scripts/pm/dispatch-gates.mjsderives gate families from the workflows and checkscripts of the tree it runs in — that is the design and it is right — but the answer
never said so. Run from this checkout with a sister repo's paths, it returned a
confident, well-formed, exit-0 answer about the wrong repo. Reproduced on the tree
before this change, with the card's own invocation:
Nothing in either stream names the repo the answer is about. Four of the five repos in
scope hold no copy of this script at all, so every sister-repo dispatch was exposed.
The fix — two halves, because that is what can be detected honestly
Paths handed to the tool are repo-relative, and the explicit-path mode is documented
as a hypothesis about files that may not exist yet. So a path's home repo is not
recoverable from its shape, and existence-in-this-checkout is a weak signal in both
directions — the measured failure used
package.jsonand the lockfile, which every repoin the family has. This change therefore never guesses a path's home repo. It adds:
A banner, unconditional, first line of every derivation. Names the repo and commit
the answer came from, and says families are a property of that repo.
--repo owner/name, an assertion that refuses on mismatch. Checked against thischeckout's own
originremote; a contradiction ends the run, exit 2, naming both reposand printing no answer at all:
Refusal rather than a warning for the assertion half: a warning is the failure mode
already measured one level up — output that reads as an answer while being about the
wrong tree. The banner half is unconditional precisely because it cannot refuse: with
no assertion there is nothing to contradict, and refusing every explicit-path run would
break the mode the PM uses on every dispatch.
The banner goes to stderr, not to hide it but because stdout is contractually the
answer and nothing else — both modes' stdout is pasted verbatim (the tier verdict into a
claim comment, the gate list into a dispatch prompt). The derived-change-set provenance
next door already made this call for the same reason. It is still the first thing printed.
Candidate 2 (
--repopointing at another checkout to derive from) is deliberately NOTadded, matching the triage seat's read that it "adds capability that wants a real
second-repo consumer first". The sister repos hold no copy of this script and their seats
hand-derive, so no consumer exists. The misreading is blocked mechanically rather than by
prose: a
--repovalue shaped like a filesystem path is refused withthis flag ASSERTS ... it does not point the derivation at another checkout.The no-path merge-base mode is untouched apart from the banner — it is inherently
cwd-repo-correct and remains so.
Also:
splitArgvreplaces the oldfilter(a => !a.startsWith('--'))argv parse. Thatparse would have taken the assertion's value as a path — a new silent wrong answer
inside the fix for a silent wrong answer. Pinned in both spellings.
Operator-facing text
.claude/agents/os-dev.mdgains a 6-line caveat where it instructs the dev to re-derivewith this tool: the script lives only in objectstack, the banner names the tree, sister
repos hand-derive from their own manifest and workflows, and
--repois the mechanicalbelt. Ratchet paid: 392 → 398 lines against ceiling 399 (headroom 7 → 1). No issue
numbers added (
check:pm-skill-id-lintgreen)..claude/skills/pm-dispatch/SKILL.mdis where the PM-side clause belongs, and it is at682/682 — headroom 0, so that half is deferred: the equivalent caveat measures
~5 lines and cannot be paid without a maintainer ruling to raise the ceiling. Re-wrap is
not payment, so nothing was reflowed to make room.
Verification
Self-test extended by 36 cases (506 → 542), covering both directions of the guard: remote
parsing in both URL spellings, the argv split, a satisfied assertion, a contradicted one
(refusal naming both repos), an unverifiable one (refuses rather than passing), the
checkout-path misreading, the banner's wording and stream, and the deliberate absence of
an "all paths present" clearance line. Five of them drive the real CLI in a child process.
Reverse-verification, from the committed state, under an
EXIT INT TERMrestore trap:the two guard wiring sites were mutated (on-disk proof anchored at both spans — 2 injected
markers, 0 surviving originals), and the measured failure returned exactly:
EXIT=0,40 lines of confident answer, 0 mentions of the repo in either stream, naming
check:driver-memory-censuswhile asserted to be about objectui. Self-test went red(5 of 542). Restore proved byte-identical: marker count 0, span back,
git diffcleanagainst HEAD.
Gates — all 14 families
node scripts/pm/dispatch-gates.mjsderives for this diff,run at final commit
6952e2187with a clean worktree, all green:check:agent-model-declared,check:cross-package-test-inputs,check:doc-authoring,check:doc-formula-expressions,check:entry-guard,check:nul-bytes,check:parse-guard,check:pm-dispatch-gates,check:pm-governed-merges,check:pm-skill-id-lint,check:pm-skill-ratchet,check:skill-frame-sync,scripts/check-ci-filter-parity.mjs,scripts/check-cross-package-test-inputs.mjs.Repo-wide
eslint . --no-inline-configalso ran in full at that commit: 4911 files,0 errors, 0 warnings — not a narrowing.
Landing class
GOVERNED.
node scripts/pm/check-governed-merges.mjs --testwith the changed paths, at thiscommit:
⛔ GOVERNED — .claude/** ×1 — .claude/agents/os-dev.md; one hit governs thewhole PR. Draft, assigned to os-zhuang,
skip-changeset(publishes nothing). No seatflips it ready, enqueues it, or arms auto-merge.
Merge-group ratchet red — cleared by maintainer ruling (2026-08-23)
The third queue kick was NOT the Console Pin Gate (that chain closed when #11146 merged): it was
check-skill-line-ratchet— os-dev.md composes to 401 lines against main after #11112 (395), and to 405 once the queued #11137 (395→399) lands ahead of this PR, against a 399 ceiling. Each PR green alone; over only in composition.Generated by Claude Code
Generated by Claude Code