Planning bug: a card specified a state check by pointing at an EVENT check — the completion gate's default-branch comparison reads a live payload field the mirror never persists
Found at motir run MOTIR-2725 (parent run), on the child MOTIR-2729, by guard #4 — the "X doesn't exist" claim gate. The correction is already applied; this card is the telemetry.
The claim, and what is actually on origin/main
MOTIR-2729 specifies its satisfied side as:
for each expected repository, does the item have a linked change request that is merged onto that repository's own default branch? Reuse the default-branch comparison the first gate already makes against the mirrored
repo.defaultBranch.
Reuse the comparison is where the claim hides. The first gate makes it against cr.baseRef, a field of the LIVE delivery payload (changeRequestStatusSync.ts:213 — cr.baseRef !== repo.defaultBranch), and the mirror row it writes one screen earlier does not persist the base at all:
$ git grep -n "baseRef\|base_ref" -- lib/repositories/githubPullRequestRepository.ts
(no matches)
github_pull_request carries state, merged, merged_at, head_ref, title, changed_paths — and no base. baseRef is NormalizedChangeRequest's required field on both providers and on the historical backfill (lib/git/types.ts:80, providers/github.ts:221, providers/gitlab.ts:177, github/historicalPullRequests.ts:150), so it arrives on every path and is dropped at the write.
Not a deliberate omission — checked, because a comment that reads like one sits nearby (notes.html #229's shape). lib/git/provider.ts:147 and types.ts:70 both discuss baseRef as the field the completion decision reads, in the singular, per delivery; neither says the mirror should not keep it. It was sufficient for a per-delivery gate and nobody needed it historically until now.
Why it matters, and why the weaker rule is not good enough
A per-delivery gate only ever asks about the merge in front of it. A SET-based gate asks about merges that already happened, in other repositories, possibly weeks ago — and the only record of those is the mirror row. Without the base, the strongest question the gate can ask is "is there a merged linked PR in that repository?", which counts a stranded merge (MOTIR-1873's case: merged onto a sibling's branch that was itself deleted, merged: true forever, no path to the trunk) as satisfying the repository. That is the exact composition of the two shipped gates, and it re-opens half of MOTIR-1873 inside the fix for MOTIR-1604.
The tell, and the gate that should have caught it
The precondition-verification rule wants a card's premise checked against shipped reality at PLAN time. This card's premise passed every cheap check: changeRequestStatusSync.ts exists, repo.defaultBranch exists, the comparison exists, and the card cites all three by name. What does not exist is the JOIN between them at rest — a field on the row.
The generalizable shape: a card that says "reuse the comparison X already makes" inherits X's INPUTS, not just its logic — and an input that is a live payload field is not available to a consumer reading a row. A per-event check and a per-state check look identical when written in prose and are different programs. Nothing in the card's wording distinguishes them, and validate_work_item cannot: the reference is to a file, and the file is there.
What was done (already applied — nothing here is a proposal)
- MOTIR-2729 re-scoped IN PLACE, on the record: the mirror gains
base_ref(nullable — rows written before this change genuinely do not know theirs, and a null must read as UNKNOWN rather than as "the default branch"), the upsert persistscr.baseRefon both providers and the historical backfill, and the gate's satisfied side reads it. Re-estimated 5 → 8 points, 60 → 90 minutes. - The ADR amended.
docs/decisions/work-item-repository-set.md§4 stated the completion rule without noticing the base is not stored — the same claim, one altitude up. AMENDED 2026-08-18, MOTIR-2979 close-out: the section reference was wrong. This originally read "§4.2 now names the column and the null-base disposition"; §4.2 is The hold is VISIBLE and says nothing about either. The amendment is in §4's own testable-terms list, in the ⚠️ block under What a merge on each must satisfy — landed with the fix in64c6e38donparent/MOTIR-2725-repo-set, still unmerged tomain. Cite a section by its TITLE, not its number. notes.htmlcarries the lesson.
Acceptance criteria
- This record stands as the corpus entry; no product change is owed by THIS card — MOTIR-2729 carries the fix and its tests.
- SETTLED 2026-08-18 at the close-out, replacing this card's original conditional criterion. It read: "if the shape recurs … it is promoted from a LESSON to a RULE in
plan-rules/phase-deepen.md's precondition-verification section. One occurrence is a lesson." That is thenotes.html#221 format — a conditional cannot record whether it was later discharged, so the next reader finds a prescription, finds a recurrence, and re-litigates a question that was answered. The verdict is LESSON, no promotion, on the grounds in Close-out verification below. Do not restore the conditional form.
Close-out verification (2026-08-18)
The correction HELD, and for once every citation is EXACT — say so plainly rather than implying drift that was not found. changeRequestStatusSync.ts:213 is cr.baseRef !== repo.defaultBranch; lib/git/types.ts:80 is baseRef: string;; providers/github.ts:221, providers/gitlab.ts:177 and historicalPullRequests.ts:150 are each that producer's baseRef, line; provider.ts:147 sits inside the done-means-merged-not-shipped block. The negative re-runs clean on today's origin/main — baseRef|base_ref over githubPullRequestRepository.ts returns 0 — and the column list is right: the model carries state, merged, merged_at, head_ref, title, changed_paths and no base.
Two things this card got wrong, both fixed above and here. The ADR section reference (§4.2 → §4's testable-terms list). And MOTIR-2729's re-scope banner carried a hand-authored chip with a placeholder id, [MOTIR-2979](cmsyk0000planningbug), resolving to nothing — replaced with the bare key, which the server autolinks.
MOTIR-2729 itself checks out. The re-scope is in the DESCRIPTION and reached the ACCEPTANCE CRITERIA, not only a comment — three new criteria cover the write on both providers plus the backfill, the stranded-merge composition, and the NULL-base hold; sizes are 8 pts / 90 min as claimed. The fix is committed on parent/MOTIR-2725-repo-set (64c6e38d): baseRef: string | null on the mirror row, selected and mapped on the read.
The promote question, settled: LESSON
Three grounds, in order of strength.
- A governing clause covers it and predates the defect.
plan-rules/kind-leaf-deepen.md's data-seam rule — "A card that says it CONSUMES what another card shipped is claiming a DATA SEAM — verify the READ, not the WRITE" — reaches this card verbatim: "reuse the comparison the first gate already makes" is a claim to consume shipped work, and the rule's discharge (name the field the consumer actually reads) returns the right answer here. Its title landedfcddcbd2026-08-01 and its five-failure-mode list8b47f822026-08-04; MOTIR-2729 was authored 2026-08-12T00:10:25Z, andkind-leaf-deepen.mdis loaded by every leaf deepen. Eight to eleven days, same planner ⇒ a diligence miss, not a trigger gap. - The corpus already carried the discharge, on this same table.
notes.html#140 (2026-07-04) recorded MOTIR-1579 specifying a surface over the pr-row'stitle, which MOTIR-892's ingestion never persisted — and its prompt hint is this check already: "for each FIELD the card renders, name the column/derivation that produces it." Family by content is honestly TWO, and the earlier one predates every candidate rule, so the post-rule family is ONE. - Family of one, and no adjacent promotion to lean on. The W-series has no live claimant on this axis (
MOTIR-2929/W6 holdskind-leaf-deepen.md,MOTIR-2953/W7 holdsphase-deepen.md; neither touches the data-seam rule).
The tempting SHARPEN, refused explicitly so nobody re-derives it. The five enumerated data-seam failure modes are all about the READ side of a value that exists somewhere, and this case is the mirror of #179 — the value is on the EVENT and no write path stores it, which reads like an enumeration stopping one item short. Refused: the rule's governing sentence covers a consumption claim in either direction, and its discharge asks what the consumer reads, which is exactly the question that catches this. A rule whose general clause covers the case while its illustrations do not is a diligence miss. Promoting on it is the MOTIR-1917 over-reach.
And the 0-grep is NOT evidence of absence — record it so it is not re-run as a finding. Probing all three homes (prompts/plan-rules/*.md, prompts/run.md, motir-ai/src/llm/planningRulePacks.ts) for this check's own vocabulary returns 0 on every stem — reuse the check/comparison, inherits its INPUTS, per-event vs per-state, at rest / live payload, mirror row — against populated controls in the same sweep (ONE SUBTASK = ONE REPO 10, asserted-ABSENCE 4, claim-vs-pointer 3, A METHOD IS NOT A PATH 2, verify the READ 2). The gate is there under a different vocabulary; a stem probe that misses it is the notes.html #298 trap, not a totality gap.
What the close-out found that the record did not
The mechanical discharge, which is better than the record's own prompt hint. When a check reads through a MIRROR of a normalized type, the question is a diff of two lists and needs no judgement: NormalizedChangeRequest carries seven fields (providerRepoId, number, state, merged, headRef, baseRef, title); github_pull_request persisted six — and the single field in that difference is the one this card needed. Every field a persistence boundary discards is a future card until somebody needs it.
And #140's verb is what let it happen twice. That hint is scoped to fields a card renders; MOTIR-2729 renders nothing, it compares — so a reader auditing the rendered surface finds nothing to check. Both entries now say renders OR reads, and #140 carries a dated Recurred pointer so the family is visible from both ends.
Corpus state, and where this card's own edits ship
The lesson is authored and is not on origin/main — it rides motir-meta PR #240 (the MOTIR-2725 parent-run PR). Two things had to happen for that to be reachable.
The PR was CONFLICTING, twice, and both times for the same reason. notes.html has one append point and one count line, so any two open PRs collide there. First main and the branch had each numbered an entry #309; then, an hour later, PR #241 merged as #310, the number the branch's first entry had just taken. Both resolved the same way — main's entry is published and keeps its number, the branch's pair renumbers — leaving #311 (MOTIR-2664) and #312 (this lesson), count line 312. Each resolution verified gapless, no duplicate numbers, tag balance reproducing origin/main's {'p': -1} baseline exactly. PR #242 is also open and appends to notes.html, so whichever of #240/#242 merges second pays for one more renumber; #240 first is the cheaper order.
This card's own two findings ship separately, in motir-meta PR #243 (docs/MOTIR-2979-mirror-field-diff, +23 lines, one file) — the mirror-field-diff discharge and the #140 family pointer, plus #140's dated Recurred note. They were briefly on #240 and were split off: they are this card's deliverable, not the parent run's, and #240's own content is the conflict resolution and the two entries. #243 is STACKED — its base is parent/MOTIR-2725-repo-set, because the entry it edits exists nowhere else. Merge #240 to main first, then RETARGET #243 to main. Merging #243 into the parent branch would flip this card Done while its content is still off main. Its Recurred note names the sibling entry by its CARD rather than its number, since these numbers have now moved twice in an hour.
This card therefore rests at In Review, not Done — its close condition is the lesson and its correction reaching origin/main: Yue's merge of #240, then of the retargeted #243.
Context refs
lib/services/changeRequestStatusSync.ts:213— the live-payload comparison.lib/repositories/githubPullRequestRepository.ts— the upsert that drops it.lib/git/types.ts:80·lib/git/provider.ts:147— wherebaseRefis described, in the singular.- The card corrected: MOTIR-2729. The decision amended: MOTIR-2726. The precedent: MOTIR-1873.