Planning bug: a re-plan reason that admitted two OPPOSITE readings was acted on without confirmation — an approved plan archived the wrong surface and wrote a false lesson into the corpus
A RECORD of two planning defects, filed by the re-plan that corrected them. No code ships from this work item; its close-out is the disposition below.
⚠️ THIS RECORD WAS ITSELF WRONG ONCE AND IS CORRECTED HERE. Its first version reported "a story planned a per-run DETAIL page for facts a sibling had already put on the item page" — a confident, well-argued account of a defect that did not happen. That account came from misreading the instruction that prompted the re-plan. The real Defect 1 is the misreading, and it is the more useful of the two because it cost an archived work item, an approved wrong plan, and a global corpus row.
Defect 1 — a re-plan reason that admitted two OPPOSITE readings, acted on without confirmation
The reason, in full: "I don't the the detail view of each run, we have the detail logs print in the item detail page."
The missing verb makes it two sentences:
| reading | what it instructs |
|---|---|
| I do not want the detail view | delete /runs/[id] |
| I do not see the detail view [having the logs], whereas the item page does | /runs/[id] is INCOMPLETE — add the logs |
The first was taken. The second was meant. Yue, on reading the result: "I thought you are going to add the log view. I told you the log view is missing then you just remove the run view too? run view is the place the user can see every agent run. run in the item detail view is just a nice-to-have."
What it cost
A five-part re-plan ran on the wrong reading and produced a plan that was internally consistent at every point: it archived the run view, split its surviving substance onto a new sibling, re-scoped six work items, filed a product-copy bug, wrote two GLOBAL lesson rows, and returned valid: true from validate_work_item with an empty advisory array. It was reviewed and approved. One of the two lesson rows argued at length that the archived surface had been a redundant second home — a generic, confident row in the shared corpus, retrievable against unrelated work, sourced from a misread sentence. It has been retracted (motir-ai #335, 43a6467).
The evidence that would have settled it, and how cheap it was
One command, run only afterwards:
grep -oic 'console' design/runs/run-section.mock.html → 26
grep -oic 'console' design/runs/run-view.mock.html → 0 (CSS copied in the prelude only)
The surface being deleted was the one lacking the thing being complained about. The reason contrasted two surfaces on an element; counting that element in both refutes one reading outright. This is a measurement, not a judgement, and it was available before the first archive.
Why no gate fired
- The reason gate exists and is exactly right.
plan-procedure.md: "Unclear reason — it's vague / ambiguous and you cannot tell from it what is actually wrong … → STOP and ask the user." It did not fire because the sentence did not READ as ambiguous — one reading was picked up fluently and the other never surfaced. The failure is not a missing rule; it is a rule whose trigger is a feeling of clarity. - The resulting plan validates. Every mechanical check is about internal consistency, and a plan built on the wrong half of an instruction is perfectly consistent with itself.
- The archive is a one-way door for the agent.
unarchive_work_itemrequireswork_item:archive, which the MCP token does not carry; the approval that archived the item ran with the reviewer's permissions. So the correction needed the person who had been misunderstood.
The remedy, as a procedure rather than as care
Restate the reason as a POSITIVE instruction and read it back against the artefact. "You want me to delete X" and "you want me to add Y to X" — if BOTH survive contact with what is in front of you, that is two intents and the choice is not the agent's. Then measure before choosing, because this class is usually settled by counting one element in two files. Then order the work so the one-way door is last: re-scopes, new work items and corpus rows are all reversible by the agent; an archive is not.
The corpus half is the part that outlives the session, and it is the one to sweep first when a misread is discovered: a lesson authored from it is stated generically, is retrieved against unrelated work, and no longer carries the sentence it came from.
Defect 2 — the corpus's AUTHORING SHORTHAND shipped as user-facing COPY
This half was never in dispute and is unchanged. plan-rules/core.md's terminology block says it plainly:
"card" is authoring-voice shorthand only; never emit the word "card" into a work item's own description/copy (the product noun there is work item).
The rule protects the wrong artefact. A work item's body is read by people who already share the shorthand. The artefact that reaches a user is the design mock, and a mock is not a sketch — it is the copy, verbatim, in its shipping register. So the shorthand travelled: corpus → mock → locale catalog → screen.
What shipped, measured at origin/main 2fc5d6016:
| where | what a user reads |
|---|---|
runs-index.mock.html | a table column headed Cards; Took no cards as an empty state |
run-view.mock.html | a Card column head, four times; This run took no cards. |
run-section.mock.html | This card is 4 of 11 in a run of …; Nothing has been dispatched for this card. |
messages/en.json + zh.json | runs.oneOfN — the same sentence, built from the mock, in both locales |
grep -oic 'card' over the folder: 55 · 87 · 81 · 58. And it is not confined to this area: the same noun names a work item in ~14 other en.json keys plus their zh twins, across plan review, project access, issue views, settings, public projects, design result, platform admin and work-item to-dos — its own bug.
A THIRD finding, which is a product defect rather than a planning one
The log stream shipped with no producer. DispatchEventKind.log exists; dispatchRunReporter strips its body centrally unless --report-log is passed; the flag is on all three commands; the sweep deletes bodies after 30 days; and motir help promises the whole behaviour to the operator. Nothing in packages/cli/src ever emits a log event — the emitted vocabulary is run_opened · scope_claimed · snapshot_frozen · card_claimed · card_skipped · checkout_ready · prompt_issued · agent_started · agent_exited · leg_verdict · session_pr · plan_approved · ci_verdict · card_settled · run_closed, with no log. The only calls are in packages/cli/test/dispatchRunReporter.test.ts.
So the product ships a privacy control for a stream that does not exist, and its help text describes behaviour that does not happen. It is now its own subtask on the story, because the log pane depends on it — but the SHAPE is worth recording here: a green unit test proves a method works when called; it never proves that anything calls it. Every unit around this passed for the whole time it was inert.
What was DONE
- Corrected: the previous plan (
cmtf2fix0007yhvph4ucqpufa, approved) is superseded bycmtf443yz001ghvn8zppk7msv, which brings the run view back as a full-screen MODAL over/runs— the reusedProjectRoadmapCanvason the left, the log pane on the right — and adds the CLI log producer. - Retracted: the false global lesson, replaced with the misread itself (motir-ai #335,
43a6467). - Kept: the noun correction, and the product-wide copy bug.
⚠️ STILL OPEN
- MOTIR-3895 is ARCHIVED and must be restored by hand before the correcting plan's
modifycan land on it. The MCP token lackswork_item:archive, so an agent cannot undo an archive its own plan caused — which is the mechanical half of Defect 1 and is arguably a permission gap worth its own decision: a door an agent may walk through and not back out of. - MOTIR-1796 is
implementedand its correction is unbuilt. Amodifypatch cannot carry a status, so approving a corrected plan leaves it green with criteria nothing has satisfied. This is a general hole: a re-plan that re-scopes an already-implemented work item has no mechanism at all for re-opening it. - Does the terminology rule get WIDENED, and is that two cards?
core.md's block covers a work item's own body and says nothing about a design mock's rendered strings. Widening it is a corpus change with TWO HOMES (plan-rules/andSHARED_PLANNING_RULES), so two work items in two repositories. This pass routed the takeaway to the lesson store instead; the open question is whether a rule that must fire on EVERY mock belongs in a retrieval-by-relevance tier. - Is a "confirm before the one-way door" check mechanizable? The reason gate is prose and fires on a judgement about clarity. A candidate — the destructive half of a re-plan requires an explicit confirmation when the reason contains a negation with no object — has not been measured against the corpus, and
CORPUS-MAINTENANCE.md's standard is that a candidate is measured before it is adopted.
A MECHANISM DEFECT hit while executing the first re-plan
add_plan_items accepted patch.blockedByRemove: ["MOTIR-3895"] — a work-item KEY, which the tool's own schema documents as legal — and the final: true close then refused the whole plan: "names no work item in this workspace." The same value validated at append and failed at close, so the error surfaced on an unrelated call, named a proposal id rather than the work item, and asserted that an item which plainly exists does not. The workaround was to re-send every patch with the raw id. Whether the fix is to resolve keys at close or refuse them at append is a real choice — but the two doors must agree.
A FOURTH finding — approve 500s on a dependency CYCLE, and nothing upstream sees it
POST /api/plans/cmtf443yz001ghvn8zppk7msv/approve returned a bare 500, three times.
The cause was a 2-cycle the plan itself wrote, between two work items that already exist, expressed as two modify patches in the same batch:
MOTIR-3895 (the run modal) blockedByAdd MOTIR-3923 (the runs index)
MOTIR-3923 (the runs index) blockedByAdd MOTIR-3895 (the run modal)
The first is right — a UI surface that renders INSIDE a host is blocked_by the host's build item (phase-skeleton.md's UI-composition axis). The second was written on the reasoning "the index's row opens the modal, so the index isn't click-testable without it", which inverts the same axis: the host ships first and the thing rendering inside it is the consumer. An authoring error, not a platform one.
Three things made it cost more than it should have
1 · validate_plan returned VALID on the cycle, before and after. Its own description promises to reject "a ref cycle" — but that check is over the plan's planItem: REF graph. A cycle between two COMMITTED work items, expressed through patch.blockedByAdd, is not a ref cycle and it passed. validate_work_item --planId returned valid: true as well. So the pre-commit check that exists precisely so a plan does not fail at the button did not fire, which is the same shape as this record's Defect 1: the check ran, answered, and answered a narrower question than the one being asked.
2 · The route has no arm for it, so it reads as a platform outage. The database trigger raises WI_LINK_CYCLE: linking % is_blocked_by % would create a dependency cycle (enforce_work_item_link_no_cycle, BEFORE ROW); Prisma wraps it; app/api/plans/[id]/approve/route.ts maps PlanItemTargetMissingError → 422, a bad repo pin → 422, PlanApproveTimedOutError → 503 and a PrismaClientValidationError → 422, and this falls through the rethrow to a bare 500 with an empty body. The route's own comments predict this exact outcome twice — "without this arm it fell through to the rethrow below and the caller got a bare 500 with an empty body — which reads as 'something is broken' and led to Approve being pressed three more times" (MOTIR-3396), and again for MOTIR-3654. It happened a third time, for a third cause, and the trigger message already names both ids — so the arm is cheap and the payload is already there.
3 · The response says nothing. Three identical 500s with no body is indistinguishable from an outage, so the reviewer's only available move was to report it — and the plan is fine apart from one edge.
Fixed in place with update_plan_proposal (legal on a planned plan): the reverse edge is dropped, validate_plan still returns VALID, and the plan is approvable.
⚠️ ITS HOME IS MOTIR-3936, WHICH ALREADY EXISTED — this is a SECOND OCCURRENCE, not a new bug. That card's thesis is "an agent must not be able to hand a person a plan they cannot approve", and its plan-internal refusal list already names a cycle. The occurrence is recorded on it, with the three things this instance adds that its first fixture could not show: validate_plan answering VALID rather than no check running, a refusal that arrives as a bare 500 rather than as a classifiable rejection, and a modify patch as the carrier. Two of its acceptance criteria are sharpened and one is added there. Do not file a duplicate.
⚠️ And note what would have caught it: the DEPENDENCY-ARROW AUDIT, run over the edges the plan WRITES rather than over the tree it projects. Two modify patches in one batch are two halves of one graph edit, and reading them one proposal at a time — which is how they were written and how every check reads them — cannot see a cycle between them.
Acceptance criteria
- This record states Defect 1 as the MISREADING, with both readings of the sentence written out, the measurement that would have settled it, and the reason no gate fired.
- The false global lesson is retracted in the lesson store and the retraction is named here by commit.
- Defect 2 and the third (producerless) finding are recorded with the reading that established each — the measured
grepcounts and the emitted-vocabulary enumeration — so a later reader can check them without re-deriving. - The corrected shape is named: the superseded plan, the correcting plan, and what each open item above needs from a person.
- The still-open decisions are each either settled with a written answer or carried forward as their own work item; this record may not be closed while one is unanswered and unowned.
- The mechanism defect is either filed as its own bug against the plan-proposal surface or explicitly declined, with the reason.
- The approve-500-on-a-cycle finding carries all three of its parts: the authoring error and its correct arrow direction, the gap in
validate_plan's cycle check, and the missing route arm whose own comments predicted a third occurrence — and it is recorded as a SECOND OCCURRENCE on MOTIR-3936 rather than filed again.
Context refs
- MOTIR-1789 — the story, and its § Re-planned a THIRD time section.
- The run view / run modal — archived in error, restored and re-shaped; the run section — the surface Yue calls the nice-to-have.
- The design — re-scoped to draw the modal and purge the noun; the product-wide copy bug.
- MOTIR-3914 — the previous planning bug on this story, whose defect Yue also caught by reading a design.
motir-meta/prompts/plan-procedure.md§ THE REASON IS MANDATORY, CLEAR, AND CONSISTENT — the gate that should have fired;plan-rules/core.md— the terminology block and gate 19 (THE TWO HOMES).- Plans
cmtf2fix0007yhvph4ucqpufa(approved, wrong) andcmtf443yz001ghvn8zppk7msv(the correction, awaiting review).