fix(pm): dispatch-gates derives its own change set from the merge base - #9478
Merged
Merged
Conversation
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
This was referenced Aug 18, 2026
os-support-ai
marked this pull request as ready for review
August 18, 2026 05:04
os-support-ai
enabled auto-merge
August 18, 2026 05:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9320
scripts/pm/dispatch-gates.mjstook its change set from whatever the caller diffed. The obvious way to produce that list is the two-dot range, which is evaluated againstorigin/mainas 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— newchangedPathsFromGit()plus a second input mode:--changed): the tool derives the change set itself fromgit 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.--tierfollowed 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.--tierstdout stays paste-clean for a claim comment..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-basegives 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:git merge-basefatal: no merge baseThe 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-test— 273 cases pass (was 260; 13 new), at06f7f0845.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:
Reverse-verification (from the committed state, restored after; both directions were predicted before running):
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:check:pm-dispatch-gates(named)check:nul-bytescheck:agent-model-declaredcheck:pm-skill-id-lintcheck:pm-skill-ratchetcheck:skill-frame-synccheck:doc-authoringcheck:doc-formula-expressionsThe seven additions all come from the
.claude/agents/os-dev.mdhalf, and they were found by running the change this PR makes.No changeset:
scripts/pm/**and.claude/**only — nothing published..claude/**, so it is ADR-class: draft, left for the maintainer. Not flipped to ready.Generated by Claude Code