Type: Bug (code) · write-side content corruption. Follow-on to the bare-key normalization shipped for bug MOTIR-1440 (normalizeWorkItemRefs / normalizeBodyRefs).
Parent: MOTIR-1464 — the open "Planner self-improvement · auto-reported quality bugs" epic, the de-facto home for agent-discovered quality bugs (Yue's call, 2026-08-03; filed at root first). NOT parented by where the defective code lives: that would be Story 5.8 (MOTIR-1399) under Epic 5 (MOTIR-263) — done — so per the bug-parent rule the bug is not re-attached into a sealed epic. Nothing open is blocked by it.
Discovered in: out-of-band manual dogfooding on 2026-08-03, while filing MOTIR-2039 through the MCP create_work_item / update_work_item path.
I deliberately wrapped a work-item key in backticks — `[MOTIR-2039](motir:cmsdw94cf000i04kvb0ncz5hs)` — precisely so it would stay literal text. What you are reading in that sentence IS the bug: the backticks were around a bare key, and the stored body came back with a motir: link injected inside the code span. See the comment on this card. The API response's stored descriptionMd looked like:
`[MOTIR-<n>](motir:cmsboa8hm004004la11a8m7v6)`
A code span renders its content verbatim, so the reader sees the raw [KEY](motir:cms…) token — a visible corruption, not a chip.
The same rewrite hit a key sitting in a link destination: the plain path /items/MOTIR-<n> became /items/[MOTIR-<n>](motir:cms…) — a nested-bracket mess that is not a valid link and does not render as one.
The critical part: this happens at WRITE time and is persisted. It is not a render-layer decoration that could be ignored — the author's source text is mutated in the DB, and the editor round-trip now shows the token. The original literal is gone.
origin/main @ c4ec51b1)normalizeWorkItemRefs (lib/mentions/workItemRefs.ts, the normalizeWorkItemRefs export, ~line 110-130) does ONE String.replace over the raw body with:
const tokenSrc = '\\[[^\\]]*\\]\\(motir:[A-Za-z0-9_-]+\\)';
const keySrc = `\\b${escapeRe(projectIdentifier)}-(\\d+)\\b`;
const re = new RegExp(`(${tokenSrc})|${keySrc}`, 'gi');
The alternation has exactly two branches — an already-explicit motir: token (left verbatim, which is what makes it idempotent) and a bare key (rewritten). There is no branch for Markdown structure, so the scan is blind to:
`KEY```` … ``` — and indented code blocks[text](/items/KEY) — and bare URLs / autolinks<pre>/HTML blockThe module's own doc comment claims the rewrite is "Idempotent + non-destructive", and its skip-list names only three cases: an existing token, an unresolved key, and a foreign project prefix. Code context was never in the list. tests/work-items/work-item-refs.test.ts:74-… (describe('normalizeWorkItemRefs (bug [MOTIR-1440](motir:cmqwzpoh9000004k0k7tqkxeg))')) covers exactly those three — there is no code-span, code-fence or link-destination case, which is why the gap shipped.
Where it bites (blast radius) — normalizeBodyRefs (lib/workItems/normalizeBodyRefs.ts) runs inside the caller's write transaction at four sites:
lib/services/workItemsService.ts:781 — work-item create (descriptionMd + explanationMd)lib/services/workItemsService.ts:1193 — work-item update (same two fields)lib/services/plansService.ts:562 and :796 — plan materialize (descriptionMd / explanationMd)So AI-generated plan bodies are corrupted the same way: any planner output that quotes a card key inside a fenced block or a path gets a link spliced into it. Comments are unaffected — commentsService does not call it.
This is an implementation gap in a shipped fix, not a design defect — the intent ("a bare key should both relate AND chip") is right; the executor is context-blind. No notes.html entry warranted.
/items/<key>.get_work_item, or reopen the editor).[KEY](motir:<id>) in the stored body; the code span and code block then render the raw token text, and the path becomes a malformed nested link.remark-parse + remark-gfm are already dependencies, see lib/markdown/render.tsx) and rewrite only inside text nodes, which by construction excludes inlineCode, code, link.url, image.url, html and definition. If a full AST round-trip is judged too invasive for a write-path helper, the minimum viable alternative is to extend the alternation with skip-branches matched before the key branch — fenced/indented code, inline code (n-backtick-delimited, honouring the run length), a link/image destination, and an autolink — mirroring how the existing token branch already earns its "left verbatim".tests/work-items/work-item-refs.test.ts — key in an inline code span (including a double-backtick span), key in a fenced block, key in a link destination, key in a bare URL, and a mixed body where a prose key in the SAME string still normalizes. Write the failing repro first.Resolution: (open — the close-out subtask fills this)