Skip to content

Commit 55fd5ee

Browse files
os-elon-muskclaude
andauthored
fix(pm): dispatch-gates refuses an absent path unless --repo asserts whose tree it is (#18409)
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](https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ecf3e3b commit 55fd5ee

1 file changed

Lines changed: 230 additions & 10 deletions

File tree

0 commit comments

Comments
 (0)