Skip to content

fix(pm): dispatch-gates derives its own change set from the merge base - #9478

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-9320-dispatch-gates-merge-base
Aug 18, 2026
Merged

fix(pm): dispatch-gates derives its own change set from the merge base#9478
os-support-ai merged 3 commits into
mainfrom
claude/issue-9320-dispatch-gates-merge-base

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #9320

scripts/pm/dispatch-gates.mjs took its change set from whatever the caller diffed. The obvious way to produce that list is the two-dot range, which is evaluated against origin/main as it is now — so on a branch that outlived a sibling merge, the sibling's landed files are attributed to your diff. The list is now computed inside the tool, off the merge base, whenever no paths are passed.

What changed

scripts/pm/dispatch-gates.mjs — new changedPathsFromGit() plus a second input mode:

  • No paths (or --changed): the tool derives the change set itself from git merge-base — committed work, working-tree changes, and untracked files, unioned. Untracked and uncommitted are included deliberately: a dev re-deriving before committing would otherwise get a short list, and under-derivation is the one failure direction the original defect did not have.
  • Paths passed: untouched, including --tier followed by paths. This is the PM's form and it answers a different question — at dispatch time the card's file surface is a hypothesis about files that do not exist yet, which no git range can answer. Derivation is what the no-path invocation means, never an override of paths that were passed.
  • Provenance (base sha, per-source counts, the path list) goes to stderr, so --tier stdout stays paste-clean for a claim comment.
  • Rename detection is off, so a moved file contributes both names — a gate watching the old directory is exactly as implicated by the move as one watching the new.
  • An empty derived set exits non-zero rather than deriving over nothing (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690: an unreadable input must never look like an empty answer). "No gates" is the most expensive thing this tool could say wrongly.

.claude/agents/os-dev.md — the clause-④ re-derivation line named the invocation that required the dev to hand-derive a path list, which is the step that produced the two-dot range. It now names the no-path form and states what the two-dot form costs. The script fix alone would not have stopped the habit.

The shallow boundary — measured, and the opposite of the intuition

The card warned that local merge-base gives a false reading on a shallow checkout. Measured on a purpose-built shallow clone whose true base sits below the graft point, that is not what happens:

form exit output
git merge-base 1 empty
three-dot diff 128 fatal: no merge base
two-dot diff 0 the inflated list

The merge-base form cannot lie here — it can only refuse. The form that silently returns a wrong answer is the two-dot one. This is argued from the reachability of the graft: a commit present in both truncated graphs is a common ancestor, so a returned base can be correct or absent, never wrong.

So the derivation refuses on a shallow checkout with no reachable base, names the shallow cause and the deepen remedy, and says in the error text why it will not fall back to the two-dot form. No silent wrong list, in either direction.

Tests

node scripts/pm/dispatch-gates.mjs --self-test273 cases pass (was 260; 13 new), at 06f7f0845.

The new cases build real git repositories and run the real derivation over them. A fixture cannot stand in: the claim is about what a git range means when history moved underneath a branch, which is a property of git, not of a string this file could parse.

The load-bearing one is the control. Asserting only that the sibling's file is absent from the derived set would pass just as happily against a fixture that never reproduced the incident — so the same tree is diffed the wrong way in the same breath, and the sibling has to show up there:

✓ the CONTROL reproduces the incident: the two-dot range really does claim the sibling PR file
✓ the derived change set drops the sibling file the two-dot range attributed to us
✓ an untracked new file joins the change set
✓ a shallow checkout with no reachable merge base REFUSES instead of answering
✓ and it says why it will not fall back to the two-dot range

Reverse-verification (from the committed state, restored after; both directions were predicted before running):

  • merge base → two-dot: 2 red, exactly the two exclusion claims. The control stayed green, which is the point — it proves the fixture reproduces the incident independently of the fix.
  • shallow refusal → silent fallback to the base tip: 3 red, the whole refusal group.

Gates

Re-derived from the actual diff with the tool itself (its own no-path mode), at 06f7f0845 — the dispatch named one family, the diff implicates eight:

gate exit
check:pm-dispatch-gates (named) 0
check:nul-bytes 0
check:agent-model-declared 0
check:pm-skill-id-lint 0
check:pm-skill-ratchet 0 (os-dev.md 334 lines, ceiling 399)
check:skill-frame-sync 0
check:doc-authoring 0
check:doc-formula-expressions 0

The seven additions all come from the .claude/agents/os-dev.md half, and they were found by running the change this PR makes.

No changeset: scripts/pm/** and .claude/** only — nothing published.

⚠️ This diff touches .claude/**, so it is ADR-class: draft, left for the maintainer. Not flipped to ready.


Generated by Claude Code

claude added 2 commits August 18, 2026 03:45
Two-dot `origin/main..HEAD` is evaluated against origin/main as it is NOW, so
a branch that outlived a sibling merge attributed that sibling's landed files
to its own diff. The list is now computed inside the tool, off the merge base,
whenever no paths are passed; the explicit-path form is untouched.

Fixes #9320
…path mode

The dispatch brief told devs to hand a changed-path list to dispatch-gates,
which is the step that produced the two-dot range. Name the no-path invocation
instead, and say what the two-dot form costs.

Part of #9320
@os-support-ai
os-support-ai added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit af2a989 Aug 18, 2026
21 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-9320-dispatch-gates-merge-base branch August 18, 2026 05:22
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

3 participants