Planning bug: a story's "The gap, verified" section enumerated three true facts and drew a false conclusion — the capability it was written to build had shipped six days earlier, under a name the search never used
Repo: motir-meta (notes.html + prompts/plan-rules/). Found by motir run MOTIR-3000, at guard #4, before the first child's worktree. The run halted and submitted plan cmszfupxc009mi2phf6sd1uho (nine modify proposals). No code was written.
What happened
MOTIR-3000 — "An agent can put its deliverable ON the card" — carried a section headed "The gap, verified" with three bullets:
POST /api/upload/issue-attachmentresolves the workspace from the active-project cookie context, never the payload. True.- There is no attachment route anywhere under
/api/v1. True. - No MCP tool uploads a file. True.
And concluded: "The agent physically cannot put a file on a card." False, and false in the strongest possible way: POST /api/work-items/[key]/design-evidence/upload-token + …/design-evidence (MOTIR-2664, merged 2026-08-12, six days before the card was authored) authenticate through authenticateCiPublisher(req, DESIGN_PUBLISH_PERMISSION) — GitHub OIDC or a bearer PAT — address the item by identifier, and resolve the workspace from the token. DESIGN_PUBLISH_PERMISSION is 'work_item:edit' (lib/tokens/grant.ts), which is in CLI_TOKEN_GRANT. A dispatched agent already holds the permission that route asserts, and design images have been landing on cards through it for a week. The acceptance-evidence pair is the same shape one artifact class over.
Four further facts fell with it:
enum AttachmentSourcewas quoted aseditor | panel | acceptance_video. It has five members (AMENDED 2026-08-20, MOTIR-3079 close-out: five when this card was written; six today — MOTIR-3056 has since shippedapi, which was that card's own deliverable. The claim was correct as written.) —acceptance_trace(MOTIR-1674) anddesign_asset(MOTIR-2666) were both missing — and MOTIR-3055 Q2 and MOTIR-3056 were specified against the three-member version.- MOTIR-3062 was named "the CLAUDE.md sentence that is currently false". That sentence has been true since 2026-08-12.
- MOTIR-3061's journey — a design card's rendered asset on the work item, seen without opening the pull request — is
tests/e2e/design-result.spec.ts. - MOTIR-3055 Q1 weighed "both shapes" against two precedents. There are three, and
docs/decisions/design-result.mdhad already decided the third for this exact payload class. An ADR written to Q1 as posed would have re-decided a live ADR without naming it.
Why the precondition rule did not fire
plan-rules/core.md gate 2 requires preconditions verified against shipped reality, cited by path. The card did that. Every path it names is real, every quoted behaviour is accurate, and the section is headed "verified" — which is what a discharged gate looks like.
The gate has a positive limb (does this exist?) and an absence limb (grep the target before asserting the gap). This card's claim is an absence of a CAPABILITY, not of a symbol: "no way exists to do X." Both limbs are discharged by grepping a name, and the failure is that the searcher and the shipped feature used different names. Every bullet greps attachment / upload; the shipped capability is called design-evidence and acceptance-evidence, and its routes live under app/api/work-items/[id]/, not under anything an attachment-shaped grep reaches. Three true readings of the wrong region.
Two things then made it feel finished rather than partial:
- Enumerated negatives read as exhaustive. Three bullets, each independently verifiable, each verified, produce the sense of a closed search. Nothing in the card's shape asks "what would this capability be called if it were not called what I searched for?"
- The strongest disconfirming evidence was in the runbook, not the code.
run.md's design stopper says in terms that "THE CARD IS NOT FINISHED AT THE PR — the deliverable is the DESIGN RESULT ON THE CARD", and MOTIR-2981 is adoneplanning bug about that publish failing. A planning pass that readrun.mdwhile authoring a card about design deliverables reaching cards had the refutation in front of it.
The rule this suggests
Gate 2's absence limb should carry a capability limb: an absence asserted of a CAPABILITY ("nothing can do X", "there is no way to Y") is not discharged by grepping the noun you would have used. Name at least one ALTERNATIVE vocabulary the capability could ship under — the artifact it moves, the lifecycle that owns it, the actor that performs it — and grep that too. The concrete discharge here is cheap and general: for a claim about what a TOKEN can reach, enumerate the callers of the token-auth gate (authenticateApiToken / authenticateCiPublisher) rather than grepping the feature name; for a claim about what an AGENT can do, read CLI_TOKEN_GRANT and find every route that asserts a permission in it.
Acceptance criteria
- ✅
motir-meta/notes.htmlcarries the lesson — entry #320, count line bumped to 320, landed by the replan itself in motir-meta PR #253. Step 4 of THE REPLAN ACTION is mandatory and immediate, so this half is discharged; the remaining criteria are this card's live scope. - The entry states the discriminator: three true facts about the wrong region, and a conclusion quantified over a capability rather than a symbol.
- ✅
plan-rules/phase-deepen.md's asserted-absence limb (orcore.mdgate 2, whichever owns it) gains the capability clause above, with the two concrete discharges named. (RESOLVED 2026-08-20 — the home is the NEGATIVE limb, not the asserted-ABSENCE limb:core.mdgate 2 already routes a negative that EXPLAINS the card rather than scoping an edit there, and MOTIR-3000's negative justified the whole story rather than scoping an edit. Landed in motir-meta PR #292, plus a droppable three-line pointer on gate 2.) - The entry records that the refutation was already in
run.mdand in adoneplanning bug (MOTIR-2981), so the failure is not "the evidence was unavailable". - No product change is owed by this card. The plan correction is plan
cmszfupxc009mi2phf6sd1uhoand is reviewed there, not here.
Context refs
- MOTIR-3000 — the card (AMENDED 2026-08-20: it read "now Blocked"; the correction plan was approved and MOTIR-3000 and all eight children are
done); the run's finding is its top comment - Plan
cmszfupxc009mi2phf6sd1uho— the submitted correction, ninemodifyproposals app/api/work-items/[id]/design-evidence/route.ts·…/upload-token/route.ts·lib/designEvidence/publishAuth.ts·lib/publishAuth/ciPublishAuth.tslib/tokens/grant.ts—DESIGN_PUBLISH_PERMISSION = 'work_item:edit'·lib/mcp/toolPermissions.ts—CLI_TOKEN_GRANTprisma/schema.prisma—enum AttachmentSource, five membersdocs/decisions/design-result.md·tests/e2e/design-result.spec.ts·app/(authed)/items/[key]/_components/DesignResultPanel.tsx- MOTIR-2664 (merged 2026-08-12) · MOTIR-2981 (the
doneplanning bug whose subject was this publish path) prompts/plan-rules/core.mdgate 2 ·prompts/plan-rules/phase-deepen.md(the NEGATIVE limb) ·prompts/run.md(the design stopper)
Close-out verification (2026-08-20, motir run MOTIR-3079)
Every rung-2 citation on this card re-executed. All exact — no drift, which is worth saying plainly:
notes.html#320 is at #320 onorigin/main(no collision renumber), landedbbebb45via motir-meta PR #253, merged 2026-08-19. It carries the discriminator and it records that the refutation was already inrun.mdand indoneplanning bug MOTIR-2981 — so ACs 1, 2 and 4 were already discharged before this run, and AC 3 was the live scope.- The routes, the permission constant and the grant are all as cited (
app/api/work-items/[id]/{design,acceptance}-evidence/,DESIGN_PUBLISH_PERMISSION = 'work_item:edit'atlib/tokens/grant.ts:57,work_item:edit∈CLI_TOKEN_GRANT). - The nine-
modifycorrection (plancmszfupxc009mi2phf6sd1uho) was approved and HELD — MOTIR-3055 now reads "which of FOUR upload entrances", MOTIR-3062 "the CLAUDE.md pointer that is missing (not false)".
Promote verdict: SHARPEN, on the trigger test rather than a count. The NEGATIVE limb landed 4f0043c 2026-08-17 and this card's subject was authored 2026-08-18, so the limb was loaded and it RAN — the card carried three searches with their outputs, which is exactly what the discharge asks for. It under-delivered because every coordinate the corpus disciplines is where you looked (directory · file · ref) and none is what you called it. Same shape as the accepted <ref>:<path> widening (MOTIR-2611): a second coordinate the limb's own sentences never name. A rule that runs and under-delivers is a trigger problem. The positive limb already enumerates capability / permission / access path (step 1, 5613440, 2026-06-19); the negative direction had no such item, and that asymmetry is the warrant.
Measured gap, both homes, with populated controls (origin/main 75d550b, 2026-08-20): synonym · alternative name · could ship under · restate the capab · nothing can do · no way to all 0 in prompts/plan-rules/*.md + run.md AND in motir-ai/src/llm/planningRulePacks.ts; vocabular 7 / 3 and quantif 1 / 1, none in this sense. Controls in the same sweep: asserted 30, negative 17, origin/main 45, grep BOTH homes 3 (meta) · GREP 45, absence 8, NEGATIVE 7 (mirror).
Mirror: OWED, and shipped as this card's SECOND PULL REQUEST. (AMENDED 2026-08-20 — this paragraph originally read "OWED, and PROPOSED rather than written", and filed the mirror as a separate motir-ai card on plan cmt1z8c4m001zi2n8n9r2z9eo. That was wrong on two counts, corrected the same day: this card is kind: bug, so ONE SUBTASK = ONE REPO = ONE PR never bound it; and targetRepos has no kind gate — assertSingleTargetRepoInput (motir-core lib/services/workItemsService.ts:230) rejects only more than one input form, and no PINNABLE_KINDS check exists in the write path. run.md guard #5's own closing sentence governs: a card that pins ONE repository while genuinely owing a coordinated change in another should usually be re-pinned to carry the set instead.)
The card now carries targetRepos: ["motir-meta", "motir-ai"] and ships two pull requests on one branch name, both titles carrying the key: motir-meta #292 (phase-deepen.md + core.md) and motir-ai #258 (planningRulePacks.ts's VERIFY_EVERY_PRECONDITION, the same clause lifted and genericised to that constant's register). The card completes only when BOTH merge (work-item-repository-set.md, amended 2026-08-18 / MOTIR-3037); merge order is free. The mirror's home was read off the repo, not reasoned to: VERIFY_EVERY_PRECONDITION already carries the NEGATIVE limb ("CARRY THE SEARCH, NOT THE VERDICT") and the sibling coordinate widening has its own constant there (THE_REF_HALF_OF_A_GREP_TARGET). motir-ai checks: 1 034 tests across the eight corpus-touching files, format:check and tsc clean, and the refreshed conservation baseline diffs as one contiguous 2 085-char insertion and nothing else.
Plan cmt1z8c4m001zi2n8n9r2z9eo is SUPERSEDED and should be DECLINED — it proposes a card that should not exist. A closed plan cannot be withdrawn from the authoring side.
Co-located writes on one paragraph — named, not wired. motir-meta #291 (MOTIR-3109) and #290 (MOTIR-3087) both append after the NEGATIVE limb's last bullet; MOTIR-2987 (W9) and MOTIR-3013 (W10) each still owe a clause to it. This PR inserts inside the bullet list and stacks a warrant line, so no two cards own the same write — whichever merges second rebases and must not replace. No blocked_by is owed; any order is correct.
Corpus scripts (baselines pinned on a clean origin/main worktree first, all three red on main by design): measure.py --check core.md 802/819 → 805/823 PASS, headroom 17 → 18 (the pointer paid for itself); worst cell 2520/2415 → 2553/2443, i.e. +33 lines for +5 overage on an already-red cell. conserve.py --base 5b32d96 unchanged — still only kind-bug.md violates C; phase-deepen.md and core.md both PASS. verify.py unchanged in kind. Nothing was trimmed to chase a script.