<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
- Adds src/pr-title.ts, which enforces the trailing (<linear_id>) resolution token on a drone's PR title from the task spec's frontmatter: appends it when absent, and corrects it (while reporting the disagreement) when the agent named a different ticket in that position.
- Adds upsertLinearIdBodyMarker to src/pr-body.ts, stamping a hidden <!-- drones:linear-id AI-NNN --> marker into the PR body as a second source, since a title can later be edited by a human or an address round.
- Adds reportPossibleTicketReopen to src/linear.ts: on a title mismatch, checks the wrongly-named ticket's live Linear state and — when a Linear API key is configured — posts a warning comment directly onto *that* ticket. renderLinearCommentBody also surfaces the correction/possible-reopen note on the run's own Linear comment.
- Wires all of this into runDrone (src/runner.ts) right after pr_opened, before the Linear comment post and before browser-verify / the reviewer auto-fire.
- Adds four new receipt fields (prTitleCorrected, prTitleMismatchedId, prTitleReopenSuspected, prTitleReopenCheckedState) to DroneRunRecord.
## Why It's Needed
On 2026-08-04, AI-299's drone opened PR #151 titled AI-160: close two Mercy findings in spec-author.ts (fence escape + bracket-led title). AI-160 was the *parent-context* ticket (its PR merged the day before and produced the Mercy findings this PR closed) — the actual work was AI-299's. That silently reopened the already-Done AI-160 in Linear, meant AI-299 would never have auto-closed (write-back follows the PR), and broke scripts/merge-pr.mjs's /\(([A-Z]+-\d+)\)\s*$/ title-extraction convention — a bare AI-160: prefix ending in prose has no trailing parenthetical to match, and the body carried no ticket id either as a fallback.
The spec's frontmatter (linear_id: AI-299) had the right answer the whole time. Prompting the agent harder is the weaker fix: AI-299's own spec legitimately cites AI-160 several times (spec-author only went live once AI-160 merged), so the agent latching onto a salient nearby id was not carelessness — a convention an agent can be talked out of is not a convention.
## Changes
- src/pr-title.ts (new) — stampLinearIdInPrTitle (pure, idempotent): only the trailing (...) token is inspected/rewritten; any earlier ticket mention (a leading AI-160: prefix, a parenthetical aside) survives byte-for-byte untouched — this is the pinned regression guard against an over-eager "title sanitiser." Canonicalises to uppercase so a lowercase frontmatter linear_id still round-trips idempotently and stays matchable by merge-pr.mjs's case-sensitive regex. applyPrTitleAttribution does the gh I/O (fetch title/body, write back whatever changed, run the reopen check on a mismatch) — every step is individually best-effort so a partial failure never aborts an otherwise-successful implementer run. stampPrTitleAttribution mirrors the existing stampInlineRoutingDegraded / stampUnadjudicatedPostedReview receipt-stamp shape.
- src/pr-body.ts — upsertLinearIdBodyMarker / extractLinearIdBodyMarker. isTrailingPrBodyWrapperLine now also recognises the new marker so it splices into the same trailing wrapper block as the existing Closes / _END / badge-<div> markers, instead of being silently absorbed as prose content into whatever ## section happens to precede it (verified against the AI-220 six-section content-key check).
- src/gh-util.ts — fetchPrTitleAndBody / updatePrTitle (gh pr view/gh pr edit --title, injectable exec seam matching the existing helpers in this file).
- src/linear.ts — reportPossibleTicketReopen + renderTicketReopenWarningComment; renderLinearCommentBody extended with a title-correction / possible-reopen note.
- src/telemetry.ts — four new optional DroneRunRecord fields (see Summary).
- src/runner.ts — wiring inside runDrone's pr_opened branch; console WARN lines on append/correction/mismatch.
- ARCHITECTURE.md — documents the new module (arch-drift coverage).
- docs/decisions/ — new entry recording the design (marker-as-wrapper-line, direct-comment-on-the-reopened-ticket, canonicalisation).
Deliberately does not touch scripts/merge-pr.mjs or src/post-merge.ts — this makes their title/body input trustworthy, not their resolution logic (explicit hard constraint from the spec).
## Breaking Changes
None. All new DroneRunRecord fields are optional and additive; legacy receipts without them remain valid. The new PR-body marker is an HTML comment (invisible in rendered Markdown) and is excluded from the six required PR-body sections by construction.
## Test Plan
1. pnpm typecheck — clean.
2. npx vitest run on touched modules (pr-title.test.ts, pr-body.test.ts, linear.test.ts, arch-drift.test.ts) — 71/71 pass, including a pinned test per acceptance criterion: missing-suffix append, mismatch corrected + reported, body second source, idempotent title + body (including a lowercase-linear_id regression case), the two-ticket cross-reference regression guard, and six-sections-survive.
3. pnpm test (full suite, vitest + Python) — 110/110 vitest files pass, 3324/3324 tests pass (net +38 tests over the clean-main baseline of 3286 tests / 109 files, zero regressions); Python suite reports OK (440 tests). Baseline for comparison was built from a git worktree checkout of origin/main at the same commit this branch is based on.
## Verification Artifact
$ pnpm typecheck> tsc --noEmit
(clean, exit 0)
$ npx vitest run
Test Files 110 passed (110)
Tests 3324 passed (3324)
$ pnpm test (vitest + Python)
Test Files 110 passed (110)
Tests 3324 passed (3324)
...
Ran 440 tests in 0.160s
OK
Baseline (clean origin/main, via a disposable git worktree): 109 files / 3286 vitest tests, all passing. This PR nets +38 tests, 0 regressions.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-6af639cf-6269-45b7-8747-fde7bfefc6bb?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-6af639cf-6269-45b7-8747-fde7bfefc6bb&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>