Skip to content

fix(pm): make dispatch-gates name the repo it answers about, and refuse a contradicted --repo assertion - #11126

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10971-dispatch-gates-repo-guard
Aug 23, 2026
Merged

fix(pm): make dispatch-gates name the repo it answers about, and refuse a contradicted --repo assertion#11126
os-zhuang merged 2 commits into
mainfrom
claude/issue-10971-dispatch-gates-repo-guard

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #10971

The defect

scripts/pm/dispatch-gates.mjs derives gate families from the workflows and check
scripts 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:

$ node scripts/pm/dispatch-gates.mjs package.json pnpm-lock.yaml
dispatch-gates: 136 check famil(ies) discovered across 26 workflow file(s) ...
EXIT=0

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.json and the lockfile, which every repo
in 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.

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit 422f997d5 (...).
  Families are a property of THAT repo. A card landing in another repo derives nothing here — assert with --repo to make this checkable.

--repo owner/name, an assertion that refuses on mismatch. Checked against this
checkout's own origin remote; a contradiction ends the run, exit 2, naming both repos
and printing no answer at all:

$ node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectui package.json pnpm-lock.yaml
dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'.
  Repo-relative paths cannot tell the two apart: the same manifest and lockfile names exist in both, ...
EXIT=2, stdout 0 bytes

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 (--repo pointing at another checkout to derive from) is deliberately NOT
added
, 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 --repo value shaped like a filesystem path is refused with
this 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: splitArgv replaces the old filter(a => !a.startsWith('--')) argv parse. That
parse 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.md gains a 6-line caveat where it instructs the dev to re-derive
with this tool: the script lives only in objectstack, the banner names the tree, sister
repos hand-derive from their own manifest and workflows, and --repo is the mechanical
belt. Ratchet paid: 392 → 398 lines against ceiling 399 (headroom 7 → 1). No issue
numbers added (check:pm-skill-id-lint green).

.claude/skills/pm-dispatch/SKILL.md is where the PM-side clause belongs, and it is at
682/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 TERM restore 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-census while asserted to be about objectui. Self-test went red
(5 of 542). Restore proved byte-identical: marker count 0, span back, git diff clean
against HEAD.

Gates — all 14 families node scripts/pm/dispatch-gates.mjs derives for this diff,
run at final commit 6952e2187 with 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-config also 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 --test with the changed paths, at this
commit: ⛔ GOVERNED — .claude/** ×1 — .claude/agents/os-dev.md; one hit governs the
whole PR. Draft, assigned to os-zhuang, skip-changeset (publishes nothing). No seat
flips 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.

Maintainer ruling, 2026-08-23, on this PR (verbatim, of the three options put to them — A drop the +6 os-dev.md paragraph / B raise the ceiling / C pay by further deletion): 「B:提天花板 399→405」

Executed as commit bee7967ac6: scripts/pm/check-skill-line-ratchet.mjs os-dev.md ceiling 399→405, annotated with the ruling date and this PR, sized to exactly 405 so headroom returns to zero once #11137 and this PR both land. Gate + 19-case self-test green at that commit.


Generated by Claude Code


Generated by Claude Code

…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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Aug 22, 2026
@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 22, 2026 — with Claude
@os-zhuang os-zhuang self-assigned this Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

PM 验收 — ACCEPT(终态)

复核:席位 session_01MsbKEG4LtERSLaDrbehM3e 审毕 diff 主体(守卫段全读 + os-dev.md caveat 以门与自测佐证)。裁定吻合:候选 1 地板落地为「无条件 stderr 首行横幅(命名答案所出仓+commit)+ --repo 断言不符即拒(exit 2 零 stdout、两仓齐名)」;永不从路径形状猜仓(实测陷阱正是 package.json 两仓皆有);候选 2 按分诊输入不加且以机械拒绝防误读(路径形状值被拒);无路径模式不动。顺手修掉 argv 旧解析吞断言值为路径的次生坑并 pin。自测 506→542,消融复现原始失败形状(EXIT=0 + 40 行自信错答 + 零仓名提及),恢复三重证。

开放问题席位裁定(均在既有裁定框架内):① --repo 保持可选(A)——定级明言「零新能力」为界,强制断言是协议级破坏性变更,横幅已补上缺陷本体(无提示);B 路径保留可达。② SKILL.md 侧 PM caveat 维持 DEFER(A)——682/682 零余量,其 ~5 行记入 #11086 全文重构轮的折入清单(维护者已裁定该轮,重构中支付比单独抬顶更合适);若在此之前再次发生跨仓误导出,升级为抬顶裁定请求。

治理面终态:os-dev.md 被触碰 → 整 PR 治理面——保持 draft,仅维护者合并;已指派。合并后剥 #10971 状态标,并随即派发 #10542(同文件串行后半)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 22, 2026 19:48
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32594804017 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.93s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 92 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Copy link
Copy Markdown
Contributor Author

队列踢出签名收据(按平台读数纪律:无收据不重投,收据留在 PR 上):


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Aug 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32595589611 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.12s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 94 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Copy link
Copy Markdown
Contributor Author

第二次踢出判读(终态收据,按「同一 PR 第二次被踢 ⇒ 停止重投、四分支重判」纪律):

  • 签名:与第一次逐字同(Console Pin Gate / pinned objectui SPA 构建 / ~6s 失败)。
  • 根因实锤(本次拉了完整 job 日志,17 个 job 仅此一红):pinned objectui 的 packages/types/src/zod/theme.zod.ts:18@objectstack/spec/ui import 六个已退休 Schema(ColorPalette/Typography/BorderRadius/Shadow/ThemeMode/Theme)——6 个 MISSING_EXPORT,即 objectstack#10856 记录的缺陷本体,对 cache-miss 的合并组是确定性失败,与本 PR 的 diff 零交集。今日部分 PR 能过队列是 turbo 构建缓存命中所致,非门为绿。
  • 判定:⛔ 不再重投——重投只在缓存命中时侥幸通过。耐久解法已在飞:objectui#5710(六个悬空 import 移除,p0: main is RED — #10695 retired six spec/ui theme schemas that the PINNED objectui still imports, so Console Pin Gate cannot build the SPA #10856 A+C 裁定第一腿)已由 ui 席于 19:54Z 以 fable 档派发;其落地后 objectstack#10856 执行 pin bump,Console Pin Gate 恢复确定性绿,届时重投本 PR(维护者执行——治理面)。
  • 同队列的 fix(hooks): guard-main-checkout-bash reads shell comments as text, not commands #11129 同理,预计同签名踢出时按本判读处理,不另烧队列。

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
@os-zhuang
os-zhuang enabled auto-merge August 23, 2026 01:00
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit 584fe2e Aug 23, 2026
30 of 32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10971-dispatch-gates-repo-guard branch August 23, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: dispatch-gates.mjs lives only in objectstack, so a dispatch for a sister-repo card silently returns the WRONG repo's gate list

2 participants