Planning bug: a bug card filed FROM a sibling's unmerged branch inherits that branch as its substrate and gets no `blocked_by` — the discovery link is written as `relates_to`
Found by · motir run MOTIR-4002 (2026-08-30), at run.md guard #4 — before any worktree.
About · MOTIR-4002, authored 2026-08-30T20:43 by a motir run on MOTIR-3974.
Fix changes · the PLAN's authoring rule, not product code → this home, per run.md's discriminator.
What happened
MOTIR-4002 was filed from inside a run that was working on parent/MOTIR-3940-one-engine. Its acceptance criteria were written against that branch's working tree, so they name substrate that exists nowhere else:
- "
PLANNING_OPERATIONS, five members since MOTIR-3974" — onorigin/mainit has four. - "delete the
revise_plan:validate_planrow fromKNOWN_GAPS" — onorigin/mainKNOWN_GAPSis[]; there is no row. - "the parity guard passes over all five operations" — the guard iterates four, and
capturePasshas norevise_planrunner.
The card wired three relates_to links (MOTIR-3940, MOTIR-3974, MOTIR-3978) and zero blocked_by. It therefore sealed ready: true, openBlockers: [], valid: true and was claimable — against a base that cannot satisfy four of its five criteria. The next run claimed it, and the halt cost a session.
Worse than the usual shape: the substrate is not on an open PR. parent/MOTIR-3940-one-engine is local and unpushed (gh pr list --head parent/MOTIR-3940-one-engine --state all → []), living in another session's dirty worktree. A card was made claimable against a tree that exists only inside one process's checkout.
Why the existing machinery did not stop it
Not a missing detector — a severity one, and this is the widening run.md guard #4 already anticipated.
validate_work_item DID fire, naming MOTIR-3974 (implemented) and MOTIR-3978 (in_progress). Both came back at plain advisory, not likely-missing-edge, because that severity answers where in the card the reference appears — the keys sit in the card's prose header and its "note on the target", not inside a criterion. The criteria consume the sibling's output while naming no key at all: they name PLANNING_OPERATIONS, KNOWN_GAPS, capturePass. A key-scanning detector is structurally blind to that.
This is a second instance of the shape MOTIR-4002 itself is about: a guard is only as wide as the population it iterates. There the population was a four-member enum; here it is "references that carry a MOTIR-<n>".
The authoring rule that was missing
plan-rules/core.md gate 4 reads an AC that names a not-done card is a missing blocked_by until proven otherwise — keyed on naming a card. The rule owed is the SUBSTRATE limb, which the global lesson already states and no gate enforces:
the edge is owed on the SUBSTRATE, not on the reference — list the symbols, files and fields the criteria name and check each on the merged trunk.
And the trigger that makes it cheap to apply: a card filed BY a run, FROM a branch, is authored against that branch's tree by default. Its author is the one actor who cannot see the difference, because their checkout is the divergence.
Acceptance criteria
plan-rules/core.mdgate 4 gains a SUBSTRATE limb: a criterion naming a symbol, file, constant or test that is not on the merged trunk owes ablocked_byto the container delivering it, whether or not anyMOTIR-<n>appears in the criterion.- The limb states the FILING trigger explicitly — a card authored mid-run from a feature branch is checked against
origin/main, not the working tree, before it is sealed — and names the one-command check (git ls-tree/git grep origin/mainfor each named symbol). log-bug.md's filing steps carry the same check as a step, so a bug filed from a branch is measured on the trunk at CREATE time rather than at some later run's guard #4.- Cross-reference: where the referenced work sits in another container, the edge goes BETWEEN the containers (gate 7), as it did here — MOTIR-4002 → MOTIR-3940, not → MOTIR-3974.
- Both rule homes agree (
planning-rule-has-two-homes): theplan-rules/pack and any mirrored corpus row carry the same limb.
Evidence
$ git -C motir-ai merge-base --is-ancestor c6cfbbf origin/main # NO
$ git show origin/main:src/jobs/plannerInputs.ts | grep PLANNING_OPERATIONS
export const PLANNING_OPERATIONS = ['generate_tree', 'expand_item', 'augment', 'replan'] as const;
$ git show origin/main:tests/toolSurfaceSinkParity.test.ts | grep -n KNOWN_GAPS
149:const KNOWN_GAPS: readonly string[] = [];
origin/main @ c116d13 (2026-08-30T22:23+02:00). The counter-measurement is on parent/MOTIR-3940-one-engine: plannerInputs.ts:88-93 (five members), toolSurfaceSinkParity.test.ts:165-180 (the KNOWN_GAPS row naming MOTIR-4002), :256 (the revise_plan runner).
Disposition of MOTIR-4002 itself
Not cancelled — the defect it describes is real and its fix direction is right. It is blocked_by MOTIR-3940 and moved to Blocked; it becomes buildable, or provably already-fixed, when that story lands. No re-plan was submitted: the correction was one missing edge between two existing cards, which is a DIRECT link_work_items, not plan shape.