Planning bug: the dispatched agent's re-plan door was planned as a CLI shell-out, while every other instruction in the SAME protocol is an MCP tool call
Three cards in a row chose the wrong SURFACE for an agent-facing capability, and no gate could see it, because every one of them was internally consistent. Found in conversation on 2026-09-02 while reviewing MOTIR-4172, which had just been corrected once already for the same underlying reason.
What was planned
MOTIR-4083 makes a dispatched agent compose a six-field SettledRequirement; MOTIR-4172 carries it to the job envelope. The carrier was authored three times, and the first two are the bug:
- Six string flags —
--outcome,--behaviour, … onmotir plan. - One
--requirement <path|->JSON option onmotir plan, correcting (1) becauseparsePlanArgshas no flag grammar by design and the fields are multi-paragraph prose. - An optional
requirementobject on thesubmit_plan_sessionMCP tool — the correction this bug records.
(1) and (2) fought the same problem and neither questioned the premise. Getting a structured, multi-paragraph value through argv is awkward — and the awkwardness was read as a design constraint to route around (a file, then a - for stdin) rather than as evidence that the door was wrong.
The fact that settles it, and it was one grep away
lib/dispatch/promptTemplate.ts — the agent's entire contract, server-assembled — instructs five Motir MCP tools: publish_design_result (:274), mark_integrated (:879, :991), link_pull_request (:944, :1117), transition_status (:1121, :1193), create_work_item (:1261). At :1200, and nowhere else, it tells the agent to shell out to a binary — and that line is step 5 of a branch whose step 4 is a transition_status call, four lines above it.
The permission was already granted: submit_plan_session asserts ai:plan (lib/mcp/toolPermissions.ts:235), which CLI_TOKEN_GRANT carries. The sandboxed agent could always have called it. And runSubmitPlanSession (lib/mcp/tools/planSession.ts:257) is planChangeSessionsService.submit(…) — hop 6 of the seven-hop chain 4172 was written around, so five of those hops existed only to serve the wrong door.
Why nothing caught it
- The premise was inherited, not decided.
docs/decisions/run-findings-protocol.mdnamesmotir plan --detach <KEY>twice (:126,:182) and never argues for it — it argues, at length and correctly, that approval must NOT be an MCP tool (Q2: "MCP is the AGENT's surface"). The one sentence in the corpus about this boundary is about the case that goes the other way, so a reader checking the ADR finds the split named and confirmed, pointing away from the question. ONE SUBTASK = ONE REPOpasses. Both doors aremotir-core, so the repo gate is silent.- Precondition verification passes. Every symbol the CLI-door card named exists; the chain was traced hop by hop on
origin/mainand every hop was real. The card was wrong about which chain it should be tracing, and tracing a chain correctly cannot detect that. - The self-correction reinforced it. Correction (2) reads as diligence — it quotes
parsePlanArgs's own header, gives three reasons, and pins the fix. A card that has already visibly corrected itself once is the card nobody re-opens.
The RULE this is a case of
A capability whose ACTOR is a dispatched agent belongs on the agent's surface. If the instruction lands in
promptTemplate.ts, the door is an MCP tool — not a CLI command the agent is told to shell out to.The CLI is a client for a person or their CI; a dispatched agent is an MCP client that happens to have a shell. The tell is mechanical and needs no judgement: read the branch of the prompt the new capability lands in and count the tool calls against the shell-outs. A lone shell-out among tool calls is an actor mismatch, and the awkwardness of the argument surface — a value that will not fit through
argv, a temp file invented to carry it, a-for stdin — is the SYMPTOM, not the design problem.
And the corollary that would have caught it two corrections earlier: when a card is being corrected for the SECOND time on the same axis, the axis itself is the thing to question. Both wrong answers were about how to get a struct through a command line; neither asked whether it should be going through one.
Acceptance criteria
- The rule above is written into the planning corpus — both homes: a
prompts/plan-rules/pack andSHARED_PLANNING_RULESinmotir-ai, or an explicit note on this card saying which home it is narrowed to and why. - The rule is stated as a mechanical check on
promptTemplate.ts(count tool calls against shell-outs in the branch the capability lands in), not as a principle a reader has to apply. - The second-correction corollary is recorded with it, since it is what makes the rule fire earlier than the surface audit does.
- The cards this bug corrects — MOTIR-4172, MOTIR-4083, MOTIR-4085 — are already re-authored; this card owns only the RULE.
Advisory dispositions (validate_work_item, 2026-09-02)
valid: true, no blockers. Three likely-missing-edge advisories, all from the same acceptance criterion, and none is an edge this card owes:
- MOTIR-4172 · MOTIR-4083 · MOTIR-4085 (all
todo) — the criterion names them to say the corrections have already been made (their bodies were re-authored on 2026-09-02, in the same pass that filed this card); it does not wait on them. The severity is right — an AC naming a not-donecard is a missing edge until proven otherwise — and this is the proof: what this card consumes from those three is their re-authored TEXT, which exists now; their remainingtodois about building the code, which this card never reads. Wiringblocked_bywould also be cross-parent three times over (this card sits underMOTIR-1465, they sit underMOTIR-3942), satisfying gate 4 by breaking gate 7. - The reverse edge is the one that would be wrong. This card's deliverable is a RULE in the planning corpus; making three code cards wait on a corpus edit would gate shipped work on a lesson, which is the inversion
MOTIR-1465exists to avoid.