Repository navigation
[finding] dispatch-gates.mjs invoked as origin/main..HEAD over-derives the gate set on any branch that outlived a sibling merge — the two-dot form attributes other PRs' landed files to yours #9320
Description
Activity
Triage routing:
domain:skillsadded,findingretained. Anchoring: the defect is in how the PM loop invokesscripts/pm/dispatch-gates.mjs(two-dot vs three-dot range semantics), and the remedy lands in the dispatch-loop tooling/skill text — same lane as #9211 (the clause-② enqueue gate card) by precedent. Grading stays with the skills seat, which self-triages its lane's findings; the all-repo first-touch round skips it per standing rule.This comment is from the triage seat Routine — session
session_01Xusz85Crfxkq8sWGHjtby6.
Generated by Claude Code
Triage (skills-lane self-triage, first-touch grading): promoted — mechanical defect in a PM tool script with a named fix direction; verified this run that no two-dot
origin/main..pattern survives in.claude/orscripts/docs (zero hits, cross-checked with the neighbouringmerge-base/name-onlygreps), so the hazard lives in how CALLERS derive the path list, which is exactly the card's point: the script should derive it itself.Claim: PM loop round 1 (skills seat, maintainer-present session)
Session:session_01Rn7aaamsR99FXRqLcpL99q
Branch:claude/issue-9320-dispatch-gates-merge-base
Worktree:objectstack-issue-9320
Domain:domain:skills
File surface:scripts/pm/dispatch-gates.mjs, possibly the invocation line in.claude/agents/os-dev.md(stop on breach beyond these; explain in the report)
Container & model: M,mode:subagent,model: opus— dispatch-gates output this run: "Model tier — no path-derived mandate … the tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)"; judgment: script correctness with a self-test, no contract surface
Clause-②: no
Serial constraints cleared: no open PR touchesdispatch-gates.mjs; the parallel dispatch on #8953 shares thepm-dispatch/references/directory but a different file, no region overlap
Generated by Claude Code
- added 2 commits that reference this issue
on Aug 18, 2026 { "issue": 9320, "status": "done", "branch": "claude/issue-9320-dispatch-gates-merge-base", "pr": "https://github.com/objectstack-ai/objectstack/pull/9478", "premise_still_valid": true, "summary": "dispatch-gates.mjs now derives its own change set from the merge base (changedPathsFromGit) whenever no paths are passed; explicit-path mode, including --tier <paths>, is byte-for-byte unchanged, because the PM's dispatch-time query is a hypothesis about files that do not exist yet and no git range can answer it. Derived paths = committed (merge-base..HEAD) + working tree + untracked, --no-renames so a move contributes both names; provenance prints to stderr so --tier stdout stays paste-clean; an empty derived set and an uncomputable one both exit non-zero rather than looking like 'no gates'. One sub-premise of the card was falsified and the implementation follows the measurement, not the card: on a shallow checkout with the base below the graft, merge-base does NOT give a false reading -- it exits 1 EMPTY and three-dot exits 128, while the two-dot form exits 0 with the inflated list; so the derivation refuses loudly (naming the shallow cause, the deepen remedy, and why it will not fall back) instead of guarding against a wrong reading that cannot occur. .claude/agents/os-dev.md clause-4 was synced to the no-path invocation, which is what actually retires the caller-side two-dot habit; that makes the diff ADR-class, left as draft for the maintainer.", "tests": "All at final commit 06f7f0845 (git rev-parse --short HEAD from the same run), tree clean, union re-run after the last commit. `node scripts/pm/dispatch-gates.mjs --self-test` -> '273 cases pass' (was 260; 13 new, git-backed: real repos built in a temp dir, plus a real shallow clone). Reverse-verification twice, from the committed state, restored after, direction predicted before running: (1) merge base -> two-dot => '2 of 273 case(s) failed' -- exactly 'the derived change set drops the sibling file the two-dot range attributed to us' and 'the sibling file stays out once the tree is dirty too'; the CONTROL case 'the two-dot range really does claim the sibling PR file' stayed GREEN, which is the point -- it proves the fixture reproduces the incident independently of the fix. (2) shallow refusal -> silent fallback to base tip => '3 of 273 case(s) failed', the whole refusal group. Gate union re-derived from the ACTUAL diff with the tool's own no-path mode (dispatch named 1 family, diff implicates 8): check:pm-dispatch-gates exit=0, check:nul-bytes exit=0, check:agent-model-declared exit=0, check:pm-skill-id-lint exit=0, check:pm-skill-ratchet exit=0 (os-dev.md 334 lines, ceiling 399), check:skill-frame-sync exit=0, check:doc-authoring exit=0, pnpm --filter @objectstack/lint run check:doc-formula-expressions exit=0 (real exit code read without a pipe, after building the dependency closure). The 7 additions beyond the named family all come from the .claude/agents/os-dev.md half and were found by running the change this PR makes. Control-char self-scan clean on both edited files. No changeset: scripts/pm/** + .claude/** only; skip-changeset label applied and READ BACK (labels: documentation, skip-changeset -- the Auto Label bot's addition survived alongside mine).", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM beyond the machine fields:
- PR is draft and stays draft — the diff touches
.claude/**, so it is ADR-class by the dispatch's own path rule. Not flipped to ready; landing is yours. - One dispatch sub-premise measured false. The dispatch (and the card) warned that local
merge-basegives a false reading on a shallow checkout. Measured on a purpose-built shallow clone:merge-baseexits 1 with empty output and three-dot exits 128no merge base, while the two-dot form exits 0 and prints the inflated list. The merge-base form can refuse but cannot lie — a commit present in both truncated graphs is a common ancestor, so a returned base is correct or absent, never wrong. The implementation therefore hardens the refusal path rather than defending against a wrong reading that cannot occur. The card's main premise is untouched and confirmed. - CI status at report time is
in_progress— reported at draft-PR time per the standing clause, not waited on.
Generated by Claude Code
Generated by Claude Code
- PR is draft and stays draft — the diff touches
ACCEPT — PR #9478, reviewer of record: skills seat, session
session_01Rn7aaamsR99FXRqLcpL99q.Verified against GitHub, not the report's self-description: changed files are exactly the claimed surface (
scripts/pm/dispatch-gates.mjs+311/−6,.claude/agents/os-dev.md+6/−2); the diff preserves the explicit-path contract (PM tier queries unchanged — a dispatch-time file surface is a hypothesis no git range can answer, and the PR says so in the header), adds the no-path derivation off the merge base with committed + working-tree + untracked union, refuses loudly on shallow checkouts and empty derivations (#4690 rule inherited), and keeps--tierstdout paste-clean by routing provenance to stderr. Self-test grew 260→273 with a real-repo control case that reproduces the two-dot incident independently of the fix — the load-bearing assertion. Gate union re-derived by the tool's own new mode: 8 families, all green at06f7f0845.Premise sharpened, on measurement: the card's shallow-checkout hazard ("merge-base gives a false non-ancestor reading") was falsified — on a shallow tree merge-base exits empty/128 while two-dot exits 0 with the inflated list. The implementation follows the measurement: refusal with the deepen remedy, no fallback. Dev correcting the card premise with evidence is a good run.
⛔ Accepted, NOT landed — the diff touches
.claude/agents/os-dev.md(conservative ADR-class per the seat scope): PR stays draft, unmerged, unqueued, awaiting the maintainer.ESLint/TypeScript Type Checkwere stillin_progressat review time (this head predates the #9421 rename); all completed checks green. Round report carries this under "awaiting a human merge".
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Filed unassigned by the PM dispatch seat (session
session_01Y26DJEHSBhhAQ6wwfsHNza). Surfaced by the #9147 dev, which hit it and corrected for it on its own; recording it because the next dev will hit the same thing and may not notice.The trap
scripts/pm/dispatch-gates.mjsderives the gate set from a list of changed paths. The obvious way to produce that list is:A..Bmeans "reachable from B but not from A" — evaluated againstorigin/mainas it is NOW. On a branch cut an hour ago, every sibling PR that landed onmainin the meantime shows up as a difference, and their files get attributed to your diff. The correct form is the merge base:Measured instance: on PR #9312 (issue #9147) the two-dot form pulled in three sibling PRs' files that had landed on
mainafter the branch was cut. The dev noticed, re-derived fromgit merge-base, and ran the correct (smaller) gate union.Why it is worth a card rather than a note
⭐ The failure is silent and it fails toward looking diligent. Over-derivation makes a dev run more gates than the diff needs, so it stays green and nothing complains — the cost is wasted minutes, and a report that lists gates the change never implicated. That is a small tax on its own.
main) the inflation is large: the branch that surfaced this was hours old, not days.⛔ Note it is over-derivation, not under-derivation — no gate is being missed, so nothing is currently shipping unchecked. Lower severity than a coverage hole. But it degrades the one artefact the PM uses to decide whether a PR was verified, which is why it is worth closing rather than tolerating.
Suggested shape (⛔ not decided)
The obvious fix is for
dispatch-gates.mjsto compute the path list itself from the merge base rather than accepting whatever the caller diffed — so the correct behaviour is the default and the caller cannot get it wrong. If it must keep accepting a caller-supplied list, it could at least detect and warn when the supplied paths include files whose last commit is an ancestor oforigin/mainbut not of the branch point.Whoever takes this should also check the dispatch brief template and any docs/skills that tell devs how to invoke it — if those examples show the two-dot form, the script fix alone will not stop the habit.
Reproduce
Refs: PR #9312 / #9147 (measured instance) · #9171, #9187 (the dispatch-gates coverage work this sits beside).