Skip to content

fix(pm): dispatch-gates refuses an absent path unless --repo asserts whose tree it is - #18409

Merged
os-elon-musk merged 3 commits into
mainfrom
claude/issue-18383-dispatch-gates-absent-path-refuses
Sep 16, 2026
Merged

os-elon-musk merged 3 commits into
mainfrom
claude/issue-18383-dispatch-gates-absent-path-refuses

Conversation

@os-elon-musk

@os-elon-musk os-elon-musk commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18383

A path named on argv that is not in this tree has two readings — a surface of THIS repo that is not written yet, and a path belonging to ANOTHER repo — and a repo-relative path cannot tell them apart. The tool resolved that ambiguity silently toward the harmless reading: exit 0, the absent path filed under "apply once this card's changeset exists", the gate list derived from THIS repo's workflows. A seat read that exit code and wrote "it refuses objectui paths by design" into five dispatch texts.

Unasserted, the ambiguity is now NOT MEASURED: the run ends at exit 3 (EXIT_PREREQUISITE_NOT_MET, this tool's existing NOT MEASURED code) with the absent paths listed and both resolving spellings printed verbatim, before any family derivation runs.

The three shapes, before and after

Measured on does/not/exist.mjs, exit codes captured before any pipe. Before = origin/main 7358c1c5b; after = this branch at ab0864186.

shape before after
(a) absent path, no --repo exit 0, derived 7 commands exit 3, NOT MEASURED, absent path named, stdout empty
(b) absent path, --repo objectstack-ai/objectstack exit 0 exit 0 — byte-identical stderr+stdout to before
(c) absent path, --repo objectstack-ai/objectui exit 2 exit 2 — byte-identical stderr+stdout to before, same text

The card's literal Reading, --commands skills/objectui/SKILL.md AGENTS.md with no --repo, went from exit 0 to exit 3 naming skills/objectui/SKILL.md.

Controls that must NOT move, all measured at ab0864186:

control exit
real path, no --repo (--commands scripts/pm/dispatch-gates.mjs) 0
real path, --tier packages/spec/src/index.ts 0
a glob (--commands 'packages/*/src/index.ts') — a pattern is not an absent file 0
--changed — a tree's own diff cannot be another repo's 0

The refusal text:

dispatch-gates: NOT MEASURED — an absent path may be another repo's — assert --repo to derive as a not-yet-written path of THIS repo, or name the other repo to be refused.
  1 of 1 named path(s) are absent, and no --repo says whose tree they are from: does/not/exist.mjs
  Two readings, and nothing in a repo-relative path tells them apart. Pick one — copy a line:
    --repo objectstack-ai/objectstack   — they are paths of THIS repo that are not written yet; derive as before.
    --repo owner/the-other-repo   — they are another repo's; be refused with both repos named, from a checkout of that repo.
  (Exit 3 = NOT MEASURED, distinct from the 2 a usage error and the wrong-repo refusal carry. Capture it BEFORE any pipe.)

Shape of the change

  • absentDeclaredPaths() extracted from bannerLines, so the banner's count and the refusal's list are ONE reading and can never name different sets.
  • absentPathVerdict() — the verdict, mirroring repoAssertionVerdict's shape. Returns ok when there is nothing absent, or when an assertion is present at all: the wrong-repo refusal runs first and keeps its own exit 2 and its own text, so --repo other is unreached by this branch.
  • One dispatch branch, placed after the assertion block and before every mode branch. Guarded by argvPaths.length > 0, so only paths NAMED on argv reach it.

Applied uniformly across modes, --tier included, and that is a deliberate call worth a reviewer's eye: --tier derives no family, but the wrong-repo refusal above it ALREADY ends a --tier run over another repo's slug (measured on origin/main: --tier does/not/exist.mjs --repo objectstack-ai/objectui exits 2), so the repo an answer is about is already load-bearing there. Exempting --tier would resolve the same ambiguity silently toward the harmless reading in the one mode a claim comment pastes from. Consequence, stated plainly: --tier PATH where PATH is not yet written now exits 3 until --repo is added.

The one in-tree caller this broke, and its repair

The self-test's pending-changeset probe hands the CLI a HYPOTHETICAL changeset path — the whole point of it — with no assertion. The new branch turned that case and the one below it red. The battery named both; this was measured, not predicted. Asserting the repo is the whole repair, and it is exactly the migration every dispatcher of a not-yet-written path owes. Both cases now assert a shape in EACH branch, so the no-readable-remote path pins the refusal rather than passing over empty output.

Caller census

git grep -n "dispatch-gates.mjs" -- scripts .github .claude = 284 hits across 108 files. Almost all are prose: other gate scripts' headers citing this file. Narrowed to real invocations:

  • CI / hooks: none passes a path at all. package.json wires check:pm-dispatch-gates = node scripts/pm/check-dispatch-gates.mjs --self-test && node scripts/pm/check-dispatch-gates.mjs, which reaches this tool only by spawnSync(..., '--self-test'). .github/workflows/lint.yml's eight hits are that one step plus comments. The three .claude/hooks/* hits are allowlist fixtures naming --self-test and --repo objectstack-ai/objectstack. So no CI or hook invocation passes a possibly-absent path without --repo, and none is changed here.
  • Two agent-facing prose lines instruct the shape that can now refuse — .claude/skills/pm-dispatch/SKILL.md:478 and .claude/skills/checklist-test/SKILL.md:98, both spelling --tier PATHS with no --repo. A PM claiming a card for a not-yet-written file gets exit 3 there until the flag is added. ⛔ Not changed in this PR (file surface is this script only) — listed as a finding for the seat.
  • scripts/git-merge-regen.mjs:119 already spells --commands --repo objectstack-ai/objectstack; unaffected.

Gates

Derived in-worktree; --commands PATH --repo objectstack-ai/objectstack and the no-path re-derivation off the real change set produced identical 30-command lists, and the list was re-derived unchanged at the final head. All 30 run, exit codes captured before any pipe, then reconciled with --ran:

✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED
  (a DERIVED zero — all 30 recorded an exit code and none of them is 3).
Run reconciliation — 30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN.

All 30 exit 0 at ab0864186, including pnpm check:pm-dispatch-gates — this tool's own battery, run exactly as lint.yml step 「PM dispatch-gates self-test」 runs it (pnpm check:pm-dispatch-gates, repo root, no extra env), detached per the file header's foreground-cap rule with the exit code captured to disk:

✓ dispatch-gates self-test: 1746 cases pass.
check:pm-dispatch-gates: the battery took 631.6s on this box.   exit 0

The patch round, stated plainly

The first pushed head 284855e7f went red on CI at exactly this step, and it reproduced locally at that same commit: ✗ dispatch-gates self-test: 7 of 1742 case(s) failed. All seven were the two blocks that drive the CLI over the pending-changeset probe path — hypothetical by design, passed with no assertion — so the new refusal fired inside them and the derivation they assert on never ran.

Fixed on the CASE side, which is what the card's contract prescribes: those probes now assert the repo, spelled once in a runCliHypothetical helper beside runCli so every probe of a not-yet-written path carries it and the next one is written through the same door. ⛔ No case was skipped, disabled or loosened; ⛔ no exemption was carved for the self-test, for a fixture directory or for a .changeset/ prefix; ⛔ the refusal itself was not weakened.

The battery went 1742 → 1746 cases: the value-bearing block's inner cases sit behind if (doc && vbRows.length >= 1), so while the outer CONTROL was red they never ran at all. Four more cases now execute — the diagnostics-increase direction, not a rewrite.

Acceptance notes

  • --json needed no new shape: a refusal prints nothing on stdout and exits non-zero, which is what --repo other already did (measured on origin/main: --json ABSENT --repo objectstack-ai/objectui = exit 2, 0 bytes of stdout). The new refusal matches it.
  • With this checkout's origin unreadable the refusal says the assertion cannot be CHECKED either, rather than printing a slug it does not have — noted, not filed: an unverifiable identity plus an absent path is NOT MEASURED in both directions, which is the honest answer.

Generated by Claude Code


Generated by Claude Code

…whose tree it is

A path named on argv that is not in this tree has two readings — a surface of
THIS repo that is not written yet, and a path belonging to ANOTHER repo — and a
repo-relative path cannot tell them apart. The tool resolved that ambiguity
silently toward the harmless reading: exit 0, the path filed under "apply once
this card's changeset exists", the gate list derived from THIS repo's workflows.
A seat read that exit code and wrote "it refuses objectui paths by design" into
five dispatch texts.

Unasserted, the ambiguity is now NOT MEASURED and ends the run at exit 3 (this
tool's existing EXIT_PREREQUISITE_NOT_MET), with the absent paths listed and
both resolving spellings printed verbatim. An assertion naming this checkout
restores the previous derivation byte-for-byte; an assertion naming another repo
keeps its own exit 2 and its own text, unreached by the new branch.

The absent-path predicate is extracted so the banner's count and the refusal's
list are one reading. Globs are not counted (a pattern is not an absent file)
and --changed paths are not covered (a tree's own diff cannot be another
repo's). Three end-to-end CLI cases plus the pure half in --self-test.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
…thetical path

The pending-changeset probe in the self-test hands the CLI a changeset path that
does not exist — the whole point of it — with no assertion, so the new
absent-path refusal turned it and the case below it red. Measured, not
predicted: the battery named both.

Asserting the repo is the whole repair, and it is the migration every dispatcher
of a not-yet-written path owes. Both cases now assert a shape in each branch, so
the no-readable-remote path pins the refusal rather than passing over empty
output.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
… that owes it

Five more probes name the pending-changeset path — hypothetical by design —
and the battery reddened all seven cases reading them. Rather than repeat the
flag five times, runCliHypothetical spells it once beside runCli, so every probe
of a not-yet-written path carries the assertion and any future one is written
through the same helper.

⛔ Not by weakening the refusal for the modes these probes use: the runs are
genuinely ambiguous and the assertion is the answer.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
@os-elon-musk
os-elon-musk marked this pull request as ready for review September 16, 2026 10:28
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 55fd5ee Sep 16, 2026
40 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18383-dispatch-gates-absent-path-refuses branch September 16, 2026 10:48
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…epo so a not-yet-written path derives a tier (objectstack-ai#18463)

Fixes objectstack-ai#18425

Clause-②: no

## 维护者速读(草稿)

**改了什么**:两处 agent 指令行里的命令拼写,从 `node scripts/pm/dispatch-gates.mjs --tier
PATHS` 改为 `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --tier PATHS`(占位符在文件里是尖括号拼写,本正文为避开 GitHub
sanitizer 写作 PATHS)。只动这两行 —— checklist-test 侧是同一条 bullet 内的两行重新折行 ——
两个文件的行数不变:pm-dispatch SKILL.md 812 / 812,checklist-test SKILL.md 234 /
238。

**为什么改**:`dispatch-gates.mjs` 对树上不存在的路径,在没有 `--repo` 申报时以 exit 3(NOT
MEASURED)拒绝 —— 这是 PR objectstack-ai#18409 的有意设计,不是本卡的缺陷。PM 认领一张文件面尚未写出的卡(新
reference、新脚本)时照抄这两行,得到 exit 3、拿不到档位。改后的拼写照抄即 exit 0(下方 A7 读数)。

**风险与代价(含回滚)**:纯文案,不发布任何东西,不改脚本。代价一处:pm-dispatch SKILL.md 该行 120 B 预算容不下
+34 B —— 命令 85 B + 「引当次 … 输出」15 B + 「⛔ 不凭记忆」16 B + bullet 前缀与两个空格 4 B =
恰 120 B,所以主语「Container & model 行的档位」与逗号被删;保留「档位」的最短写法是 125
B,超预算。派发裁决禁止动该文件其它任何一行,所以「别处删一行换一行」不在本 PR 的选项内。回滚 = revert 这一个 commit。

**席位意见**:(留空)

**你要做的(一个动作)**:接受去掉主语的写法则给一个 authorized APPROVED;若要保留「档位」,请裁定允许在
SKILL.md 别处删一行来换这条 bullet 的第二行,本 PR 再补一轮。

## What changed — each line before / after, in bytes

Byte counts are of the file's spelling, where the placeholder is written
in angle brackets (7 B); this body writes it `PATHS` because the GitHub
body sanitizer eats angle-bracket fragments even inside backticks.

`.claude/skills/pm-dispatch/SKILL.md` :477 — file 812 / 812 (ceiling),
`MAX_LINE_BYTES` 120, no line added:

- before, 120 B: 「- Container & model 行的档位引当次 `node
scripts/pm/dispatch-gates.mjs --tier PATHS` 输出,⛔ 不凭记忆。」
- after, 120 B: 「- 引当次 `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --tier PATHS` 输出⛔ 不凭记忆」

`.claude/skills/checklist-test/SKILL.md` :98–:99 — file 234 / 238, the
same bullet re-wrapped across its existing two lines, no line added, the
floor / default / ceiling parenthetical untouched:

- before :98, 88 B: 「 `node scripts/pm/dispatch-gates.mjs --tier PATHS`
的输出、⛔ 不凭记忆(floor」
- before :99, 90 B: 「 sonnet · default opus · ceiling fable);每个给:该项
JSON、RUNNER.md、dogfood 技」
- after :98, 100 B: 「 `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --tier PATHS` 的输出、」
- after :99, 113 B: 「 ⛔ 不凭记忆(floor sonnet · default opus · ceiling
fable);每个给:该项 JSON、RUNNER.md、dogfood 技」

## Judgment call on the SKILL.md line (for the reviewer)

The dispatch named three load-bearing pieces — the exact command
spelling with `--repo objectstack-ai/objectstack --tier PATHS`, 「引当次 …
输出」 (quote THIS run's output) and 「⛔ 不凭记忆」 — and a 120 B gate. Those
three plus the bullet prefix and two separating spaces measure exactly
120 B, so nothing else fits on the line: the subject prefix 「Container &
model 行的档位」 and the comma are gone. The dispatch's own shortest
candidate (「- 档位引当次 `…` 输出,⛔ 不凭记忆。」) measures 130 B; the shortest
wording that keeps 「档位」 at all (「- 档位引当次 `…` 输出⛔不凭记忆」) measures 125 B.
The bullet still sits in the claim-comment list and `--tier` names the
tier itself; the claim template line (:808, untouched) keeps 「档位,引当次
--tier 输出」 as the subject. Typography note: a CJK glyph directly before
⛔ (「输出⛔ 不凭记忆」) has zero precedent on `origin/main` SKILL.md; the exact
phrase 「⛔ 不凭记忆」 is preserved so a grep for it still hits this line.

## Self-verification of the instruction (dispatch A7)

The command was extracted verbatim from between the backticks of
SKILL.md :477 (`sed`, not retyped), the placeholder substituted with
`packages/this-repo-has-no-such-package/src/index.ts` (absent from the
tree, `ls` confirms), and run from the worktree; exit codes captured
before any pipe:

- new spelling → **exit 0**; output: 「Model tier — no path-derived
mandate: the surface hits none of the 3 declared glob(s), derived here,
not recalled. The tier stays the PM's per-card judgment call (floor
sonnet · default opus · ceiling fable).」
- control, bare spelling extracted the same way from `origin/main` :477
→ **exit 3** (NOT MEASURED); output names the two `--repo` readings.
- the spelling extracted the same way from checklist-test :98 is
byte-identical to the SKILL.md one.

## Gates

Derived from the merge base with `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack --commands` (17 commands, merge base
`e915c190`), each run in the foreground with the exit code redirected
before any pipe, then reconciled with `--ran` at `7a7f3c22`: 「Run
reconciliation — 17 derived, 17 run, 0 NOT-MEASURED, 0 UNRUN.」 (exit 0).

| command | exit |
| --- | --- |
| `pnpm check:pm-skill-ratchet` (rerun on the committed head `7a7f3c22`)
| 0 — 「pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom 0)」 ·
「checklist-test/SKILL.md is 234 lines (ceiling 238; headroom 4)」 |
| `pnpm check:pm-skill-id-lint` | 0 — 27 file(s) clean |
| `pnpm check:skill-frame-sync` | 0 |
| `pnpm check:pm-governed-prose` | 0 |
| `pnpm check:pm-half-states` | 0 — 4356 self-test cases |
| `pnpm check:nul-bytes` | 0 — 8733 text files, no raw control bytes |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 — first run exit 3 PREREQUISITE NOT MET (`@objectstack/formula` and
`@objectstack/lint` unbuilt); built both under the verify lock (`VERDICT
command-exit 0`, 171 s), rerun → 0 |
| `pnpm check:agent-test-spelling` · `pnpm check:doc-authoring` · `pnpm
check:driver-memory-census` · `pnpm check:pm-governed-merges` · `pnpm
check:refd-timer-probe` · `pnpm check:watch-hint-literal` | 0 each |
| `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`) ·
`node scripts/check-comment-mask-corpus.mjs` · `node
scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 each |

No package is touched, so there is no ① dependency-closure build and no
② package test; the control-character self-scan (`grep -naP` over the
two files) is clean.

## Changeset

None; label `skip-changeset`. AGENTS.md Post-Task Checklist step 3:
「that label is for a diff that publishes nothing from any released
package」. `.claude/**` is in no package's `files[]` and is os-dev's
declared fast lane for exactly this reason; nothing released moves.

## Acceptance notes

- Rules layer (`.claude/skills/**`): draft PR; the dev never flips
ready, never arms auto-merge, never requests review. Landing waits for
an authorized APPROVED and the owning seat.
- `scripts/pm/dispatch-gates.mjs` is deliberately untouched — its exit-3
refusal is the design landed in PR objectstack-ai#18409, not the defect.
- Out of scope, not edited (dispatch A3): pm-dispatch SKILL.md :637 (a
review heuristic) and :808 (the claim template, `引当次 --tier 输出` — not a
command spelling); `references/contract-review.md` :17;
`references/lanes/skills.md` :20; `references/platform-readings.md` :391
「`dispatch-gates.mjs --tier PATH` 逐路径现推」 — a mechanism reference without
`node scripts/pm/`, so not a verbatim-copyable command; noted, not
filed. Bearer: whichever skills-seat PR next edits
`platform-readings.md`.
- The dispatch's suggested route (drop 「Container & model 」, keep 「档位引当次
… 输出,⛔ 不凭记忆」) was measured at 130 B and falsified; the arithmetic above
is the replacement.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01HPfcjvF23QBoBj7P47DDxs)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants