<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
Adds a bounded, one-time section identity repair picker to the Budget Bot add-on. When apply_section, chat_rewrite, add_section, or remove_section cannot resolve exactly one live section heading (ambiguous or missing), only that operation's card pauses; the user sees every exact live candidate heading with a non-secret location, picks one explicitly (Editor only), and the pick is revalidated and seeded as one named range before the original operation retries exactly once.
## Why It's Needed
Before this change, an ambiguous or missing section identity surfaced only as static error text with no in-sidebar recovery path — the user had to manually edit document headings and hope a later retry happened to resolve uniquely. That risked either a stuck operation or, if someone else silently disambiguated by heading order/position, a write landing on the wrong section. This closes that gap without ever guessing: the picker requires an explicit Editor choice, revalidates it live, and leaves everything else in the sidebar (review, chat, other cards) fully usable throughout.
## Changes
- DocumentPlanning.gs (pure, credential-free testable): sectionIdentityRepairCandidates_ (exact-title matches + a heading-ordinal location label, TITLE/SUBTITLE excluded), sectionRepairRoleFromDrivePermission_ (Drive Permission enum → editor/commenter/viewer/unknown), planSectionIdentityRepairOutcome_ (the typed, serializable read-only outcome), planSectionIdentityRepairSubmission_ (fail-closed re-validation of role + selected heading against a fresh scan).
- Code.gs: currentSectionRepairRole_ (reads DriveApp.getFileById(docId).getAccess(user) — the same Drive ACL the backend's own can_edit check is backed by, no new backend call), getSectionIdentityRepairState (read-only fetch for the picker), submitSectionIdentityRepair (Editor-only seed via the existing rebuildSectionNamedRange_ primitive; never mints or touches a ledger operation id).
- Sidebar.html: withSectionIdentityRepair_ wraps a card's existing apply runner so an eligible failure opens the picker in-card (releasing the write-queue lock so other cards keep working) instead of the plain error text; every other failure and every success is unchanged. Candidate rows render every exact heading + location with no default/preselected choice; submit is disabled until an explicit selection; a successful seed retries the *same* original runner exactly once through the existing single-writer apply queue. Viewer/commenter see concise guidance and no selection control. Zero candidates offer Restore-heading guidance + explicit Retry (never an auto-seed). Cancel is side-effect-free. One-shot guards block duplicate seed/retry from repeat clicks or rerenders. Wired into approveChat (chat_rewrite) and approveRemoveSection (remove_section) — see Out of scope below for why add_section/rename_section are not wired.
- Tests: tests/section-identity-repair-planning.test.js (pure candidate/role/submission shaping), tests/section-identity-repair.test.js (Code.gs host adapter over a DocumentApp/DriveApp fixture), tests/section-identity-repair-sidebar.test.js (full DOM interaction via happy-dom, new test-only devDependency). tests/production-vm.js gained loadSidebarSectionIdentityRepair, mirroring the existing loadSidebarRenameRecovery bounded-span pattern.
### Contract surface affected
- New Apps Script server functions callable from the sidebar: getSectionIdentityRepairState(sectionId, title, operation) (read-only) and submitSectionIdentityRepair(sectionId, title, operation, selectedIndex, selectedText) (the only new write path, gated fail-closed on a live role check + live candidate re-match). Both are purely additive; no existing function signature changed.
- renderChatProposal's remove_section card template gained one additive data-title attribute so approveRemoveSection can build a repair state; applyRemoveSection's own signature is unchanged.
- Section ID format (BBOT_SEC::<section_id>), the rename operation protocol, and the backend permission source (Drive can_edit, read here via Apps Script's own DriveApp.getAccess) are unchanged — no backend/API changes at all.
### Out of scope
- add_section's own ambiguous/not_found case targets an *anchor* section, whose title the sidebar never receives from the backend (only before_section_id/after_section_id) — there is no title to build exact-match candidates from, so it is not wired to the picker.
- rename_section's ambiguous/not_found path already compensates into the existing KLAIR-3231 ledger repair_required state, remediated by the existing renameRecoveryCardHtml_ / approveRenameRepair recovery cards. Layering this new picker over that would duplicate an existing (if less flexible) repair flow and touch operation-ledger semantics, both explicitly out of scope for this ticket; it is untouched.
- No backend/Board Doc API changes, new section schema, new role model, OAuth scope changes, automatic section creation, heading rename, fuzzy/prefix/position matching, candidate ranking/default choice, or redesign of the rename ledger, named-range format, or add-on renderer.
## Breaking Changes
None.
## Test Plan
All commands run from budget-bot-addon/:
- pnpm install — installed deps (added happy-dom as a devDependency, test-only).
- pnpm test (vitest run) — 258/258 tests passed, 0 skipped, 11 test files, including the 3 new files (14 + 14 + 22 = 50 new tests) and every pre-existing file unchanged and still green:
- tests/section-identity-repair-planning.test.js — 14 passed
- tests/section-identity-repair.test.js — 14 passed
- tests/section-identity-repair-sidebar.test.js — 22 passed
- tests/rename-operation-protocol.test.js — 71 passed (unchanged)
- tests/addon-response-validation.test.js — 64 passed (unchanged)
- tests/sidebar-diff.test.js — 11 passed (unchanged)
- tests/addon-response-consumption.test.js — 21 passed (unchanged)
- tests/section-targeting.test.js — 20 passed (unchanged)
- tests/renderer-planning-goldens.test.js — 10 passed (unchanged)
- tests/server-planning.test.js — 10 passed (unchanged)
- tests/oauth-transport.test.js — 1 passed (unchanged)
- Tests explicitly cover: duplicate exact titles → distinct unselected candidates with distinct locations; prefix/near-collision titles never auto-matched; one explicit Editor selection → exactly one BBOT_SEC::<id> range seeded + exactly one retry; duplicate clicks/rerenders → at most one seed/retry; stale selected-heading and stale/partial-cleanup range failures → actionable state, no wrong-range write; zero headings → Restore-heading/Retry with no automatic seed; viewer/commenter → guidance only, no selection control, no RPC call even if attempted; permission downgrade between display and submit → rejected fail-closed; cancel → no RPC call, no retry; retry-after-seed failure → bounded message, never a silent second retry; unrelated ineligible failures and successful unique-resolution operations → byte-for-byte unchanged behavior.
- Static/credential-free checks (repo has no lint/tsc for this JS/Apps-Script package): node --check on each .gs file and the extracted Sidebar.html <script> body — all pass. pnpm exec clasp --project .clasp.json.example status --json — reports exactly the expected deployed file set (appsscript.json, Code.gs, DocumentPlanning.gs, MarkdownPlanning.gs, Sidebar.html), confirming the deployed source shape is unchanged and clasp can still parse it. clasp push was never run (per README, it's an approved-deployment-only action).
- No klair-client/klair-api files changed, so their lint/tsc/pytest suites are not applicable to this diff.
## Verification Artifact
No documented browser-preview command exists for this add-on's Sidebar.html — it depends on Apps Script's injected google.script.run runtime and normally only boots inside an authenticated Google Doc with a real OAuth/Drive flow, which cannot be provisioned in this credential-free cloud VM. Per the task's fallback allowance, I built a local, uncommitted static HTML harness that serves the *real, unmodified* Sidebar.html production code with a mocked google.script.run bridge (fixture review/chat/repair responses, a role toggle for Editor/Viewer) and drove it end-to-end with a real browser:
- 
- 
- 
This is a real interactive walkthrough of the exact shipped code (not a toy/contrived duplicate), not an authenticated Google Docs add-on boot — the automated tests above are the primary evidence for the fail-closed/permission/idempotency contracts, since the harness's fixture backend can't exercise real Drive ACLs or the actual backend ledger.
## Impact Estimate
Business value: Lets users recover a single ambiguous or missing section identity without risking a write to the wrong section or losing access to the rest of Budget Bot.
Pre-AI estimate: 3 points -- a human would need roughly three days to trace the Apps Script identity and permission seams, build a bounded accessible repair flow, preserve operation idempotency, and verify adversarial role and document-drift cases.
Closes KLAIR-3233
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 10 m (implementer 0 m · reviewer 10 m · addresser 0 m)
Summed across phases. The 7 reviewer dimensions ran concurrently, so this exceeds elapsed wall-clock.
Efficiency vs. estimate: ~139.5× (3 points = 24 h of pre-AI effort)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 0 m (excludes reviewer) (implementer 0 m · reviewer not measured · addresser 0 m)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 7 m (implementer 0 m · reviewer 7 m · addresser 0 m)
Summed across phases. The 4 reviewer dimensions ran concurrently, so this exceeds elapsed wall-clock.
Efficiency vs. estimate: ~205.6× (3 points = 24 h of pre-AI effort)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 0 m (excludes reviewer) (implementer 0 m · reviewer not measured · addresser 0 m)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 10 m (implementer 0 m · reviewer 10 m · addresser 0 m)
Summed across phases. The 7 reviewer dimensions ran concurrently, so this exceeds elapsed wall-clock.
Efficiency vs. estimate: ~139.5× (3 points = 24 h of pre-AI effort)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 0 m (excludes reviewer) (implementer 0 m · reviewer not measured · addresser 0 m)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 7 m (implementer 0 m · reviewer 7 m · addresser 0 m)
Summed across phases. The 4 reviewer dimensions ran concurrently, so this exceeds elapsed wall-clock.
Efficiency vs. estimate: ~205.6× (3 points = 24 h of pre-AI effort)
<!-- drones:impact-actual:end -->
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>
<!-- drones:impact-actual:begin -->
Agent time: 0 m (excludes reviewer) (implementer 0 m · reviewer not measured · addresser 0 m)
<!-- drones:impact-actual:end -->
## Review Round Completeness
- outcome: indeterminate
- round: 1
- dispatched: 4
- reported: 3
- missing: security-review
- cause: dimension_shortfall
- head: b431ece2e100f5e54e94a9cef2a9de23cc7e24ec
- run: fanout-3701-2026-09-02T22-16-00-252Z
<!-- drones:round-completeness head=b431ece2e100f5e54e94a9cef2a9de23cc7e24ec run=fanout-3701-2026-09-02T22-16-00-252Z -->
An incomplete review round is not a clean round. Do not merge without re-firing review (drones review --pr <N> --post), which re-stamps this section, or an explicit operator override.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-2134ba22-98cd-44c4-af27-8e14e384f7b9?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-2134ba22-98cd-44c4-af27-8e14e384f7b9&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>