`update_plan_proposal` does not resolve a `MOTIR-<n>` key in `patch.blockedByAdd/Remove` — it stores the raw string, and the dangling ref surfaces at APPROVE, which is the failure the correction door's own gate promised to prevent
⚠️ THIS CARD'S FIRST DIAGNOSIS WAS WRONG AND IS CORRECTED HERE. It was filed 2026-08-29 as "the plan PROJECTION ignores patch.blockedByRemove". That is false. The projection applies the removal correctly the moment the ref is resolvable; it could not match a raw "MOTIR-3884" string against a committed edge, and reported the edge as surviving — which was the honest answer to a dangling ref. The defect is one layer earlier, on the WRITE side. The wrong first reading is kept here because it is the reading the symptom invites, and the next person will reach for it too.
The defect
update_plan_proposal accepts a MOTIR-<n> key in patch.blockedByAdd / patch.blockedByRemove, stores it verbatim, and neither resolves nor refuses it. add_plan_items resolves the same input to a work-item id.
Measured 2026-08-29 on plan cmtetu5630015hwph1qdl39zv, four modify proposals in one plan, three appended and one corrected:
| proposal | door | input | stored |
|---|---|---|---|
| MOTIR-3880 | add_plan_items | ["MOTIR-3879"] | ["cmteaay7i0045hwphi8hqc2yy"] ✅ resolved |
| MOTIR-3884 | add_plan_items | ["MOTIR-3881"] | ["cmteaay9t0047hwphhk88ri6f"] ✅ resolved |
| MOTIR-3886 | add_plan_items | ["MOTIR-3881"] | ["cmteaay9t0047hwphhk88ri6f"] ✅ resolved |
| MOTIR-3885 | update_plan_proposal | ["MOTIR-3884"] | ["MOTIR-3884"] ❌ raw |
Two symptoms, one cause. validate_work_item(…, planId) reported valid: false naming an edge the plan removes; and approve refused outright:
INVALID_PLAN_REF_GRAPH · reason: dangling
Proposal …'s patch.blockedByRemove "MOTIR-3884" names no work item in this workspace.
Re-issuing the identical correction with ["cmteaayd0004ahwphr7rhtxe7"] — the same card, as an id — stored it resolved and flipped the projection to valid: true. So the projection and the approve check are both correct; the stored value was garbage.
Why this escaped the gate that was built for it
MOTIR-3533's story is "…and an unresolvable ref refused at append", and MOTIR-3542 is the vitest gate over "the append-time ref refusal". Both shipped. The refusal covers planItem: refs, and a MOTIR-<n> key is not one — the correction door says so in its own success message: "Structural fields (patch) were re-validated against this plan's own proposals, so every planItem: ref you sent resolves." An intra-plan ref is checked; a workspace key is passed through untouched.
So the tool's schema advertises three accepted forms — "work-item keys (ACME-7), real work-item ids, or planItem:<id> refs" — and one door honours all three while the other honours two and silently corrupts the third.
Why the failure lands where it hurts
It only fires on a re-plan that moves a card between parents. Most plans add edges and never remove one; the corrupt value is written by the correction door, which is reached when an author has already found a mistake and is fixing it. The error then arrives at APPROVE — in front of the reviewer, on a plan that has been read — which is precisely the moment the append-time refusal exists to move it away from.
And the wrong fix is attractive. The symptom presents as a projection bug (this card's own first diagnosis). "Fixing" the projection to subtract a raw key would make valid: true and leave a dangling ref that still fails at approve — or, worse, would be paired with a lenient approve that drops the removal silently.
Acceptance criteria
update_plan_proposalresolvespatch.blockedByAdd/patch.blockedByRemoveexactly asadd_plan_itemsdoes — aMOTIR-<n>key, a work-item id and aplanItem:ref all end up stored in the same normalised form, asserted by a test that writes the same edge through both doors and compares the stored value.- A ref that names nothing is refused at the correction call, with the same typed error
add_plan_itemsraises — not accepted and discovered at approve. - The same normalisation and refusal apply to
parentRefandblockedByRefson the correction door; the test covers each field rather than only the one this bug was found on. - A regression test approves a plan whose correction used a
MOTIR-<n>key — the end-to-end shape this bug is, which a unit test on the resolver alone would pass while approve still failed. - A removal naming an edge that does not exist stays a no-op rather than an error, so a defensive sweep is not punished.
- ≥90% coverage on the files this PR touches.
Context refs
motir-core—update_plan_proposal/correctProposal, and the ref resolutionadd_plan_itemsuses- MOTIR-3533 — the story promising an unresolvable ref refused at append
- MOTIR-3542 — its vitest gate, which covers
planItem:refs only - MOTIR-3867 — the closest sibling, merged the same day: the projection reading
patch.parentRef. Same family, adjacent field motir-core/docs/decisions/agent-authored-plans.mdAMENDMENT 8 — the correction doors' contract- Plan
cmtetu5630015hwph1qdl39zv— the fixture, with three resolved rows and one raw one in the same plan