# threads

id: 019e3506-2416-7710-a326-95b34bda0a3c
source: exec
model_provider: openai
cwd: C:\Users\decid\Documents\projects\claude_skill_owl
sandbox_policy: {"type":"read-only"}
approval_mode: never
git_sha: bae72df01474f946ac4d46f6f051ecc01567a079
git_branch: main
git_origin_url: https://github.com/SaberMage/claude_skill_owl.git
cli_version: 0.130.0
memory_mode: enabled
model: gpt-5.5
reasoning_effort: high
rollout_path:
  C:\Users\decid\.codex\sessions\2026\05\17\rollout-2026-05-17T01-20-55-019e3506-2416-7710-a326-95b34bda0a3c.jsonl

title:
  # Cross-AI Plan Review Request

  You are reviewing implementation plans for software project phase 34.
  Provide structured feedback on plan quality, completeness, and risks.

  ## Project Context
  # Owl Skill (SPT — Spacetime / Sentience Pocket Transacter)

  ## What This Is

  A zero-dependency inter-agent messaging and live-agent lifecycle system for Claude Code, shipped as the `spt` plugin via the `cplugs` marketplace. Native Rust binary (`owl.exe`) uses TCP-primary delivery with SQLite spool fallback. Live agents run a detached Psyche wrapper (periodic `claude -p --resume` sessions) that absorbs `COMMUNE` updates from Self and reacts to scheduled pulses, file-drops, and SessionStart boundaries. Listener now streams `<EVENT>` envelopes via Claude Code's Monitor tool. Used by multiple people — Windows-native portability with no runtime dependencies is a hard constraint.

  ## Core Value

  Reliable, zero-dependency agent-to-agent messaging that just works on any machine, with a live-agent layer that survives `/clear`, `/compact`, binary handoffs, and orphan boundaries without losing context.

  ## Current Milestone: v1.7.1 Seamlessification II

  **Goal:** Polish round on live-agent friction — listing/picker correctness, skill hint surface, fresh-start commune prompt, auto-resume affordance, plugin-update changelog notification.

  **Target features:**
  - `list` fn overhaul (`--all` / `--offline` / `--here`; default = online-only) for `$OWL list` + `$LIVE list`
  - `argument-hint` frontmatter coverage on every `/spt:*` skill
  - Fresh-start live-agent commune flow — when agent identity is known-new (via pick-spec OR psyche-download empty), surface first-commune summary and prompt user before init
  - `$LIVE pick-spec` correctness — return live agents in repo even when all are online; calling flow enforces "new name or cancel"
  - `/spt:live --auto` flag — SessionStart auto-surfaces pick-spec JSON for non-live/non-listening agents; `--auto` picks most-recent + immediately resumes any clear next body of work; activates on casual prompts ("pick up where we left off", "keep going", "resume work")
  - Version-change changelog notification — sentinel + `%LOCALAPPDATA%` current-version file + SessionStart/PreToolUse hook prompts user with old→new diff

  ## Current State

  **Shipped: v1.7 Seamlessification** (2026-05-16; deployed v1.10.10)

  Five phases (26-30) focused on friction removal across the live-agent lifecycle:
  - **Phase 26** — `/spt:live` auto-pick + AskUserQuestion fork + free-text resolve
  - **Phase 27** — Listener poll migrated from Bash to Monitor tool (streaming `<EVENT>` envelopes)
  - **Phase 28** — SessionStart injects `<psyche-context>` on live perches (`/clear` & `/compact`)
  - **Phase 29** — Auto-fire echo-commune at boundaries + typed `<EVENT type="echo_commune">` envelope cutover
  - **Phase 30** — Commune/signoff file-drop protocol (`.claude/{id}-{commune,signoff}.md`) replaces stdin piping

  26 plans, 211 commits, 7-day cycle. Off-roadmap sub-iterations 18.x continued through v1.10.x defense-in-depth deploys (stale-signoff drain, init_signoff envelope-shape predicate, FileDropOutcome::BreakLoop).

  **In flight: v1.7.1 Seamlessification II** — Phase 31 complete (2026-05-17): `$LIVE pick-spec` now emits `kind:"all-live"` when every known agent in the repo is online (covers PICK-01/02/04/05/06; PICK-03 deferred per D-10/D-11). Phases 32 (list overhaul + skill hint audit), 33 (fresh-start commune + auto-resume, depends on 31), and 34 (version-change changelog) remain.

  **Next milestone:** v1.8 Psyche Restructure — Phases 23/24/25 (commune/signoff project-root + HEAD SHA stamping, tracked-dir per-agent layout + sessions audit log, psyche perch nesting + general/project context split).

  ## Requirements

  ### Validated

  - ✓ Harden owl.sh against race conditions and edge cases — v1.0
  - ✓ Unified output formatting: compact symbols, ANSI color codes, no emoji — v1.0
  - ✓ Auto-setup env vars and session cleanup hook — v1.0
  - ✓ Live Agent start + Psyche subagent + periodic pulse + COMMUNE format — v1.0
  - ✓ Native Rust binary with P2P TCP messaging + SQLite offline spool — v1.1
  - ✓ Golden test suite + cross-platform CI (4 targets) — v1.1
  - ✓ Timed pulses + Spine supervisor + single-binary drop-in — v1.1
  - ✓ Plugin migration to `/spt` namespace — v1.5
  - ✓ Spacetime filesystem (`%LOCALAPPDATA%\spt` / `~/.spt`) with `SPT_HOME` override — v1.5
  - ✓ Persistent offline perches + indefinite queueing + reconnection — v1.5
  - ✓ Memformat-driven Psyche brainstorming + INSIGHT messages — v1.5
  - ✓ PerchState enum + owl doctor + session detection + optional agent_id — v1.5
  - ✓ Hand-rolled MCP server (JSON-RPC over stdio) — v1.6
  - ✓ Spool unification (peek_all + mark_delivered) + Result variant refactor — v1.6
  - ✓ 13 MCP tools + 5 MCP resources + check-on-request notifications — v1.6
  - ✓ Seamless binary handoff for long-running spt processes — v1.6 (Phase 18.4/18.5 sub-iterations)
  - ✓ Echo-commune fresh-session jsonl excerpt + 15-min cadence gate — v1.6 (Phase 18.8 sub-iteration)
  - ✓ `/spt:live` auto-pick + AskUserQuestion fork + `$LIVE pick-spec`/`$LIVE fork` — v1.7
  - ✓ Listener poll: Monitor tool primary + `<EVENT>` envelopes + Bash `--once` fallback — v1.7
  - ✓ SessionStart `<psyche-context>` injection on live perches — v1.7
  - ✓ Auto-fire echo-commune on `/clear`+`/compact`+orphan; typed envelope cutover — v1.7
  - ✓ Commune/signoff file-drop protocol (`.claude/{id}-{commune,signoff}.md`) — v1.7

  ### Active

  - Phase 23 — Commune/signoff payload includes `project_root` + `HEAD` SHA (v1.8 candidate)
  - Phase 24 — `psyches/tracked/<self_id>/` per-agent layout + sessions audit log (v1.8)
  - Phase 25 — Psyche perch nesting + general.md / proj-{project}.md context split (v1.8)
  - Phase 19-22 — Capsule (psmux-based sendkeys pipeline, abrupt messages, GSD proceed via sendkeys) (v1.9)
  - Resolve `psyche-stale-after-clear` debug session (root cause known, fix pending)
  - 2 Phase 28 raw-stdout /clear-cycle Human-UAT tests (deferred, non-blocking)

  ### Out of Scope

  - macOS/Linux-specific features — cross-platform CI covers builds but not platform-specific optimizations
  - External dependencies beyond the single binary — copy-one-file install is core constraint
  - MCP transport prioritization — deprioritized post-v1.6 in favor of CLI-first model (see `feedback_mcp_performance.md`); MCP layer remains but Phase 18.1+ sub-iterations refactored away from MCP-centric flows

  ## Phase 34: Version-Change Changelog
  ### Roadmap Section
  ### Phase 34: Version-Change Changelog
  **Goal**: When a user's installed `spt` plugin version changes underneath them, the next SessionStart surfaces an old→new changelog prompt (via AskUserQuestion) sourced from compiled-in `CARGO_PKG_VERSION` against an `$SPT_HOME/last-seen-version.json` sentinel — without firing on first-install, without colliding with the SessionStart auto-pick (AUTO-03), and without racing the binary handoff (Phase 18.4/18.5)
  **Milestone**: v1.7.1 Seamlessification II
  **Depends on**: Nothing in v1.7.1 (independent at the Rust level); sequenced AFTER Phase 33 to keep the two SessionStart-hook extensions (AUTO-03 + VERS-04) in separate phases
  **Requirements**: VERS-01, VERS-02, VERS-03, VERS-04, VERS-05, VERS-06, VERS-07, VERS-08, VERS-09
  **Success Criteria** (what must be TRUE):
    1. On a fresh install (no `$SPT_HOME/last-seen-version.json` present), SessionStart silently writes the current `CARGO_PKG_VERSION` and emits NO changelog prompt to the user
    2. After the plugin version changes (compiled-in `CARGO_PKG_VERSION` differs from sentinel contents), the next SessionStart emits a single `SPT plugin updated v{old} → v{new}. Print changelog?` AskUserQuestion (yes/no/dismiss); the sentinel is atomically rewritten with the current version regardless of user response, so the prompt does not re-fire next session
    3. A multi-version skip (e.g. v1.10.5 → v1.12.0) surfaces a version-list summary plus a pointer to `CHANGELOG.md` — never full prose for each interim version
    4. Repo-root `CHANGELOG.md` exists with H2-per-version sections (created from git log if absent during phase kickoff); `docs/DEPLOY.ps1` syncs it into the plugin meta dir alongside `plugin.json`
    5. The version-change prompt (VERS-04) and SessionStart auto-pick (AUTO-03 from Phase 33) coexist deterministically under non-overlapping allow-list conditions; the sentinel write site lives at the top-level SessionStart entry (atomic write, no in-handoff trampoline writes) so a mid-handoff race cannot corrupt it
    6. End-of-milestone DEPLOY.ps1 `-Bump patch` ships all four phases together (no per-phase deploys during v1.7.1)
  **Plans:** TBD

  Plans:
  - [ ] TBD — discuss/plan-phase to break down (research flag: phase must begin with CHANGELOG.md existence check + create-from-git-log if absent)

  ## Progress

  ### Requirements Addressed (pre-amendment baseline; Plan 01 will amend VERS-02/04/06/08/09)

  - [ ] **VERS-01**: A new file `$SPT_HOME/last-seen-version.json` (sibling to `owlery/`, NOT inside it) stores the most recently seen plugin version per the running owl.exe.
  - [ ] **VERS-02**: On SessionStart (in `plugin_session_start.rs`), owl.exe compares `env!("CARGO_PKG_VERSION")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`.
  - [ ] **VERS-03**: First-install case (sentinel missing) silently writes the current version — NO prompt is emitted to the user.
  - [ ] **VERS-04**: Version-mismatch case emits a single SessionStart prompt: `SPT plugin updated v{old} → v{new}. Print changelog?` via AskUserQuestion (yes / no / dismiss).
  - [ ] **VERS-05**: Regardless of user response, the sentinel is atomically rewritten with the current version using existing `atomic_write_string` helper (tmp + rename) so the prompt does not re-fire next session.
  - [ ] **VERS-06**: Multi-version skip (e.g. v1.10.5 → v1.12.0) surfaces a version-list summary plus a pointer to `CHANGELOG.md`, not full prose for each interim version.
  - [ ] **VERS-07**: A repo-root `CHANGELOG.md` is created (or existing one is verified) with H2-per-version sections so excerpt parsing works; deploy script (`docs/DEPLOY.ps1`) syncs it into the plugin meta dir alongside `plugin.json`.
  - [ ] **VERS-08**: SessionStart auto-pick (AUTO-03) and version-change prompt (VERS-04) coexist without colliding — they fire under non-overlapping allow-list conditions and in deterministic order.
  - [ ] **VERS-09**: Version-sentinel write does not race against binary handoff (Phase 18.4/18.5) — single write site at the top-level SessionStart entry, after user sees the prompt; atomic; no in-handoff trampoline writes.


  ### User Decisions (CONTEXT.md)
  # Phase 34: Version-Change Changelog - Context

  **Gathered:** 2026-05-17
  **Status:** Ready for planning

  <domain>
  ## Phase Boundary

  A single end-of-turn `Stop` hook prompt that surfaces an old→new changelog excerpt to the user the first time the agent finishes a turn after the plugin version has changed underneath them. Scope:

  1. **Sentinel + version-mismatch detection** — `$SPT_HOME/last-seen-version.json` (sibling to `owlery/`, NOT inside it) stores the most recently seen version. `hook_idle.rs` (Stop hook) compares `env!("CARGO_PKG_VERSION")` against the sentinel after perch resolution; on mismatch, emits a `<spt-version-changelog>` block via `decision:"block"` + `reason:"..."` and atomically rewrites the sentinel at the same site.
  2. **Repo-root `CHANGELOG.md`** — created from git-log history at Plan 01 start in Keep-a-Changelog format (`## [X.Y.Z] - YYYY-MM-DD`). DEPLOY.ps1 `-Bump` appends a stub H2 without committing and refuses to deploy until author fills + commits.
  3. **DEPLOY.ps1 sync** — copies repo-root `CHANGELOG.md` into `{Marketplace}/plugin/spt/CHANGELOG.md` alongside the existing binary/skills/hooks/plugin.json sync step. Hook resolves at runtime via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.
  4. **Block payload + AskUserQuestion shape** — Rust hook parses CHANGELOG.md once, builds `{old, new, old_date, new_date, step_count}` payload, embeds in block. Claude renders a 4-option AskUserQuestion (`Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`); on `Remind me later` invokes new `$OWL version-remind` subcommand that rolls the sentinel back to `old`, re-arming the next-Stop emission.
  5. **Hook-pivot triggers REQUIREMENTS.md + ROADMAP.md amendment** (Plan 01 first commit, mirrors Phase 31 D-11 / Phase 33 D-03/D-04 pattern) — strikes the SessionStart-entry assumption baked into VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md success criteria 1, 2, 5.

  Touches:
  - `src/owl/hook_idle.rs` — version-mismatch detection + emission + sentinel atomic write at top of `run()`, after perch resolution and before `set_idle_ready` / `spawn_echo_commune_if_live`. Recursion guard: skip when `OWL_ECHO_COMMUNE` set (mirror line 64-69).
  - New `src/owl/version_changelog.rs` (or co-located in `hook_idle.rs`) — sentinel read/write helpers, CHANGELOG.md parser (regex `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`), payload builder.
  - New bin subcommand `owl version-remind <old>` — atomically writes `{"version":"<old>"}` back to the sentinel using existing `atomic_write_string`. ~10-line implementation.
  - New repo-root `CHANGELOG.md` — Keep-a-Changelog format, authored by hand at Plan 01 from `git log --oneline --grep='chore: bump'`.
  - `docs/DEPLOY.ps1` — (a) new sync step copying `CHANGELOG.md` to marketplace, (b) `-Bump` extension that appends stub H2 to repo-root CHANGELOG.md uncommitted and aborts on unchanged-but-required content.
  - `.planning/REQUIREMENTS.md` — amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 per decisions D-01..D-09.
  - `.planning/ROADMAP.md` Phase 34 — revise success criteria 1, 2, 5 in lockstep with REQUIREMENTS.md.

  No new on-disk artifacts beyond the sentinel + CHANGELOG.md. No new info.json fields. No skill surface (no `/spt:changelog` skill — block-body inline instructions drive Claude through the AUQ + remind flow).

  </domain>

  <decisions>
  ## Implementation Decisions

  ### Hook Pivot (architectural foundation)

  - **D-01: Stop hook (`hook_idle.rs`), not SessionStart.** Surfacing the version-change prompt at end-of-turn lands the AskUserQuestion exactly when the agent is between tasks. Rationale: a SessionStart or UserPromptSubmit prompt risks the user issuing "go do X" → walking away → AskUserQuestion blocking the agent → returning many minutes later to find zero progress. Stop hook guarantees the question fires after the agent's current work is complete and before any new turn starts. This invalidates VERS-02's "On SessionStart (in `plugin_session_start.rs`)" wording — amendment scope below.

  - **D-02: Predicate = `owl_id` resolves in `hook_idle.rs::run()`.** Piggybacks on the existing perch-resolution gate at hook_idle.rs:25-32. If no perch matches `OWL_SESSION_ID` or `parent_pid`, hook early-returns and the version-change check never runs — exactly the "spt is in use" semantic the user requested (`spt listen` / `spt live` / `spt revive` all create perches; live-agent sessions are perches). No new predicate code; no new info.json field.

  - **D-03: Recursion guard reuses `OWL_ECHO_COMMUNE` env-var skip** (hook_idle.rs:64-69 pattern). Haiku echo-commune subprocesses MUST NOT see a version-change block. New version-changelog code MUST run after the OWL_ECHO_COMMUNE check or be itself guarded equivalently.

  ### Block Emission + Sentinel Write Site

  - **D-04: Block emission via `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"`** at the top of `hook_idle.rs::run()`, immediately after perch resolution and the OWL_ECHO_COMMUNE guard, before `set_idle_ready` / `spawn_echo_commune_if_live`. Block content is the rendered XML payload (D-05). Stop hook's `decision:"block"` + `reason` schema is the natural injection channel for Stop (which has no `hookSpecificOutput.additionalContext` per the existing hook_idle.rs:1-5 comment) — research flag: confirm Stop's `reason` text reaches Claude as injected context on re-engagement, not as a silent abort. See Research Flags below.

  - **D-05: Sentinel write site = same `run()` top, BEFORE returning the block.** Atomic via existing `atomic_write_string` (src/common/owlery.rs:248). Default behavior: writes the NEW version. Re-fire on next Stop only happens if the user picks `Remind me later`, which calls `$OWL version-remind <old>` to roll the sentinel back. This inverts the "ack on yes/skip" model: the hook commits sentinel pre-emission; only the explicit remind path rolls back. Rationale (per user): most-used paths (Yes-full / Yes-highlights / Skip) have zero UX bloat; the less-common Remind path pays the round-trip.

  - **D-06: Fire-once cadence = once per version transition.** With D-05 sentinel pre-write in place, the next Stop in the same session sees `sentinel == CARGO_PKG_VERSION` and emits nothing. Matches VERS-05's "does not re-fire next session" intent — also doesn't re-fire next Stop in same session.

  ### Block Payload + AskUserQuestion Shape

  - **D-07: Payload shape = `{old, new, old_date, new_date, step_count}`.** Embedded JSON-ish inside the `<spt-version-changelog>` block. Dates parsed from CHANGELOG.md H2 lines (Keep-a-Changelog `## [X.Y.Z] - YYYY-MM-DD` regex). `step_count` = number of H2 sections between `old` (exclusive) and `new` (inclusive). Surfaces in the AUQ question text (`SPT plugin updated v{old} → v{new} ({step_count} version{s}). Print changelog?`); does NOT change the option set.

  - **D-08: AskUserQuestion 4-option shape:** `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`.
    - `Yes, full changelog` — Claude prints ALL H2 sections from CHANGELOG.md where `old < v ≤ new`, verbatim markdown, concatenated. Single-step and multi-step rendered identically (overrides VERS-06's "never full prose for each interim version" constraint — see amendment scope D-09).
    - `Yes, highlights only` — Claude's discretion: extracts the most impactful feature adds + fixes from the same range, prose-style. No fixed rubric; agent uses judgment. Pointer to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` appended for users who want full detail.
    - `Remind me later` — Claude invokes `$OWL version-remind <old version>` (e.g. `$OWL version-remind 1.10.9`), passing the `old` value from the block payload as a literal positional arg. `$OWL` is the env-var-injected owl-binary path (matches existing `$OWL deliver` / `$LIVE pick-spec` conventions across `/spt:*` skills). The subcommand rewrites `$SPT_HOME/last-seen-version.json` to `{"version":"<old version>"}` so the next Stop hook re-detects mismatch and re-fires the block. No body rendered. **The block body MUST surface the exact `$OWL version-remind <old>` invocation string verbatim** so Claude has no ambiguity about which arg to pass — Claude cannot infer `<old>` from anything other than the block payload it just received.
    - `Skip` — no action. Sentinel already updated by hook at run-entry; no re-fire.

  ### CHANGELOG.md Genesis + Maintenance

  - **D-09: Repo-root `CHANGELOG.md` authored one-shot at Plan 01 start.** Plan 01 step 1 (after the REQUIREMENTS.md/ROADMAP.md amendment commit): author CHANGELOG.md by hand using `git log --oneline --grep='chore: bump'` as the version-bump skeleton. Curated prose per version, Keep-a-Changelog format. One commit; never auto-regenerated. No new `owl changelog backfill` subcommand — out of scope.

  - **D-10: Forward maintenance = DEPLOY.ps1 `-Bump` stub injection + commit gate.** When `-Bump patch|minor|major` runs, DEPLOY.ps1 appends a stub `## [NEW_VERSION] - YYYY-MM-DD\n\n- TODO: changelog entry\n` to repo-root CHANGELOG.md **without committing**. Outputs an explicit notice that the user must fill + commit before deploy will succeed. On the next `-Bump` invocation: if CHANGELOG.md still contains the TODO marker for `NEW_VERSION` (or is otherwise unchanged from prior commit), DEPLOY aborts with the same notice. Combines stub-injection (option 1) with the "abort if unchanged" gate (option 2) — guaranteed CHANGELOG.md update before any bump can ship.

  - **D-11: DEPLOY.ps1 sync target = `{Marketplace}/plugin/spt/CHANGELOG.md`** (sibling to `plugin.json`, NOT inside `.claude-plugin/`). Resolves at hook time as `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`. Add to DEPLOY.ps1's sync step alongside `owl.exe` + `skills/` + `hooks/` + `plugin.json`. Mirrors `psyche.md` placement convention (plugin root, not subdir).

  ### REQUIREMENTS.md + ROADMAP.md Amendment Scope (Plan 01 first commit)

  - **D-12: Amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md success criteria 1, 2, 5.** Mirrors Phase 31 D-11 and Phase 33 D-03/D-04 amendment-commit pattern.
    - **VERS-02:** "On SessionStart (in `plugin_session_start.rs`)" → "On Stop hook (in `src/owl/hook_idle.rs`), gated on active perch resolution via existing `find_perch_by_session` / `find_perch_by_parent_pid`. Hook compares `env!("CARGO_PKG_VERSION")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`." (compiled-in source clause preserved)
    - **VERS-04:** yes/no/dismiss → "AskUserQuestion with four options: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Question text includes `step_count` when > 1."
    - **VERS-06:** drop "never full prose for each interim version" constraint. New wording: "Multi-version skip surfaces the same 4-option AskUserQuestion as single-step; user-chosen `Yes, full changelog` renders all interim H2 sections verbatim, `Yes, highlights only` defers to agent discretion. The CHANGELOG.md pointer is appended to the highlights body."
    - **VERS-08:** AUTO-03 (SessionStart) and VERS-04 (Stop) fire on different hooks; coexistence is enforced by hook-separation, not allow-list overlap. New wording: "Version-change prompt (VERS-04) fires from Stop hook and SessionStart auto-pick (AUTO-03 from Phase 33) fires from SessionStart hook. The two never share a hook invocation; no allow-list reconciliation required."
    - **VERS-09:** "single write site at the top-level SessionStart entry... no in-handoff trampoline writes" → "Single write site at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard. Atomic via existing `atomic_write_string`. Binary handoff (Phase 18.4/18.5) does not race because Stop hooks do not fire during in-flight binary swap (Stop fires between turns, handoff happens between processes)."
    - **ROADMAP.md SC1:** "On a fresh install, SessionStart silently writes..." → "...on first Stop hook fire in any spt-bearing session, sentinel silently writes..."
    - **ROADMAP.md SC2:** "next SessionStart emits..." → "next Stop hook fire in an spt-bearing session emits..."
    - **ROADMAP.md SC5:** drop coexistence language → "VERS-04 fires from Stop; AUTO-03 fires from SessionStart; collision impossible by hook separation."
    - Plan 01 begins with this amendment commit before any Rust code lands.

  ### Claude's Discretion

  - **Stop hook `decision:"block"` + `reason` injection semantics** — Confirm via Claude Code hooks reference (`~/.claude/reference_docs/claude-code-hooks.md`) that the `reason` text in a blocked Stop response reaches Claude as injected context on the next turn re-engagement. The hook_idle.rs:1-5 comment notes Stop "has no hookSpecificOutput schema" — but `decision:"block"` is a separate output shape that may carry user-visible context. Flag for research phase. If `decision:"block"` does NOT propagate text to Claude usefully, fall back: emit via PreToolUse (next tool call) or revisit UserPromptSubmit despite the earlier "walk-away" concern.
  - **`<spt-version-changelog>` block exact XML shape** — child elements vs JSON-as-text payload vs hybrid. Locked decisions: must carry `old`, `new`, `old_date`, `new_date`, `step_count`. Open: also embed full CHANGELOG.md excerpt for `old < v ≤ new`, or have Claude read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` at AUQ-yes time? D-05 sentinel-pre-write removes the "block must be self-contained" pressure; either is workable. Planner / researcher decides — leaning toward Claude reads at yes-time so block payload stays small.
  - **`version-remind` subcommand exact signature** — Locked: `$OWL version-remind <old version>` (bare positional arg, e.g. `$OWL version-remind 1.10.9`). Block-body MUST surface this exact invocation string with the `old` value substituted so Claude passes it verbatim; the subcommand cannot infer the rollback target from any other source.
  - **CHANGELOG.md parser regex tightness** — Keep-a-Changelog allows `## [Unreleased]` and link references (`[1.10.10]: <url>`). The phase 34 parser SHOULD reject `[Unreleased]` (no date, not a real release) and ignore link reference lines (not H2 markers — they're `[ref]:` form). Add to RESEARCH.md test corpus.
  - **Highlights extraction policy** — D-08 says "Claude's discretion". Skill-body wording (or block-body wording, since no skill is planned) should give Claude enough framing: "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes". Final wording in Plan 02 or wherever the block-body authoring lives.
  - **Plan split** — likely 3 plans: (1) REQUIREMENTS.md/ROADMAP.md amendment + CHANGELOG.md genesis + DEPLOY.ps1 sync/stub, (2) Rust hook + sentinel + parser + `version-remind` subcommand + tests, (3) block-body Claude instructions + integration test on a synthesized version transition. Final boundary at planner's discretion.
  - **Tests** — version-mismatch detection (single-step, multi-step, first-install silent, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard, no-perch guard); CHANGELOG.md parser (Keep-a-Changelog happy path, `[Unreleased]` reject, malformed H2 graceful); `version-remind` rollback round-trip; DEPLOY.ps1 stub injection + abort-on-unchanged.

  </decisions>

  <canonical_refs>
  ## Canonical References

  **Downstream agents MUST read these before planning or implementing.**

  ### Phase requirements + planning artifacts
  - `.planning/REQUIREMENTS.md` VERS-01..VERS-09 — 9 requirements. **VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 to be amended per D-12.** Plan 01 begins with the amendment commit.
  - `.planning/ROADMAP.md` Phase 34 — goal + 6 success criteria. **Success criteria 1, 2, 5 to be revised in lockstep with REQUIREMENTS.md amendment.** Success criteria 3, 4, 6 unchanged.

  ### Cross-references to prior phase decisions
  - `.planning/phases/33-fresh-start-commune-auto-resume/33-CONTEXT.md` — Phase 33 D-11 amendment-commit pattern (Plan 01 mirrors this for D-12). AUTO-03 SessionStart emission pattern (`emit_auto_pick` at plugin_session_start.rs:139-155) — informational; Phase 34 does NOT emit from SessionStart but the Pattern S2 envelope shape is reusable for any future SessionStart additions. Phase 33 D-04 / FRESH-06 rewording precedent for VERS-08's coexistence-by-hook-separation rewording.
  - `.planning/phases/31-picker-correctness/31-CONTEXT.md` D-11 — doc-amendment-first plan structure (Plan 01 amends REQUIREMENTS.md + ROADMAP.md before any code).
  - `.planning/phases/32-list-overhaul-skill-hint-audit/32-CONTEXT.md` — DEPLOY.ps1 multi-file sync pattern (informational; Phase 34 extends the sync step).

  ### Stop hook surface (D-01..D-06)
  - `src/owl/hook_idle.rs:14-43` — `run()` entry. Version-change emission site is AFTER perch resolution (lines 25-32) and AFTER the OWL_ECHO_COMMUNE guard (mirror lines 64-69 — guard MUST run before any version-change work), BEFORE `set_idle_ready` (line 39) and `spawn_echo_commune_if_live` (line 42).
  - `src/owl/hook_idle.rs:1-5` — comment asserts "Stop hooks have no hookSpecificOutput schema". This is research-relevant: Phase 34 uses `decision:"block"` + `reason` instead, which is a different output channel. Confirm semantics against `~/.claude/reference_docs/claude-code-hooks.md` during research.
  - `src/owl/hook_idle.rs:64-69` — `OWL_ECHO_COMMUNE` recursion guard. Reuse pattern verbatim for the version-change code path.

  ### Claude Code hooks reference (D-04 research target)
  - `~/.claude/reference_docs/claude-code-hooks.md` — Stop hook output schema. Confirm `decision:"block"` + `reason` propagates `reason` text to Claude as re-engagement context (not as a silent abort).

  ### Atomic write + sentinel path (VERS-01, D-05)
  - `src/common/owlery.rs:248` — `atomic_write_string(path, body)` (tmp + rename). Use verbatim for both Stop-hook sentinel write (D-05) and `version-remind` rollback write.
  - `src/common/owlery.rs:18-32` — `spt_home()` resolves `$SPT_HOME` env override or platform default. Sentinel lives at `spt_home().join("last-seen-version.json")` — sibling to `spt_home().join("owlery")` per VERS-01.

  ### Compiled-in version source (VERS-02)
  - `src/owl/poll.rs:75`, `src/owl/setup.rs:35`, `src/live/stop.rs:199` — existing call sites for `env!("CARGO_PKG_VERSION")`. Pattern for the Phase 34 hook: same `env!` macro at compile time; no runtime `plugin.json` read.

  ### CHANGELOG.md format + parser surface (D-08, D-09, D-11)
  - `https://keepachangelog.com/en/1.1.0/` — Keep-a-Changelog v1.1.0 spec. Reference for `## [X.Y.Z] - YYYY-MM-DD` H2 shape. Phase 34 parser regex: `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`. Reject `[Unreleased]`; ignore link reference lines.
  - `psyche.md` (repo root, embedded via `include_str!`) — placement precedent for repo-root markdown files synced to plugin meta dir. Phase 34's CHANGELOG.md follows this placement convention but is NOT embedded (D-11: runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`).

  ### DEPLOY.ps1 surfaces (D-10, D-11)
  - `docs/DEPLOY.ps1:154-176` — plugin.json version resolution + `-Bump` entry. Stub-injection logic (D-10) hooks in alongside the existing Cargo.toml sync at lines 224-269.
  - `docs/DEPLOY.ps1:171-292` — `-Bump` block. New CHANGELOG.md stub-append + unchanged-abort gate slots in after the Cargo.toml/plugin.json bump but before commit (lines 277-287).
  - `docs/DEPLOY.ps1` sync step (post-build) — exact lines vary; CHANGELOG.md sync target is `{Marketplace}/plugin/spt/CHANGELOG.md` per D-11.

  ### Existing version-text precedent
  - `src/owl/poll.rs:75` — `READY:{id} (spt v{})` format. Informational only; Phase 34 prompt text uses `v{old} → v{new}` shape from D-07/D-08.

  ### AskUserQuestion native "Other" field
  - AskUserQuestion automatically provides an "Other" free-text option. Phase 34 D-08's 4-option set explicitly chooses NOT to use "Other" because Remind / Skip already cover the non-print branches. If a user provides freeform input, Claude should map continuation phrases ("not now", "later", "skip for now") to `Remind me later` and dismissal phrases ("ignore", "stop asking") to `Skip` — matches Phase 33 D-08 / advisor mode pattern.

  </canonical_refs>

  <code_context>
  ## Existing Code Insights

  ### Reusable Assets
  - **`atomic_write_string`** (src/common/owlery.rs:248) — tmp + rename atomic write. Used verbatim for sentinel writes from both Stop hook (D-05) and `version-remind` subcommand.
  - **`spt_home()` + `owlery_dir()`** (src/common/owlery.rs:18-32, 65) — already resolve `$SPT_HOME` / platform default. Sentinel path = `spt_home().join("last-seen-version.json")`. No new path helper needed.
  - **`hook_output::parse_hook_stdin` + `find_perch_by_session` + `find_perch_by_parent_pid` + `find_perch_cached`** — perch-resolution path already proven across `hook_idle.rs`, `hook_prompt.rs`, `hook_check.rs`. Phase 34 reuses verbatim.
  - **`env!("CARGO_PKG_VERSION")`** — compile-time version macro already used in 3 existing call sites (poll.rs, setup.rs, stop.rs). Identical usage for Phase 34's hook check.
  - **`OWL_ECHO_COMMUNE` recursion guard** (hook_idle.rs:64-69) — copy/paste pattern for the version-change code path. Haiku subprocesses MUST NOT see a version-change block.
  - **AskUserQuestion native "Other"** — Phase 34 deliberately ignores; 4-option set is complete.
  - **DEPLOY.ps1 `-Bump` Cargo.toml/plugin.json sync** (DEPLOY.ps1:224-269) — atomic-rewrite-then-verify pattern. CHANGELOG.md stub-injection (D-10) follows the same regex-replace-then-verify shape.

  ### Established Patterns
  - **Doc-amendment-first plan structure** (Phase 31 Plan 01, Phase 32 Plan 01, Phase 33 Plan 01): when a phase's discovered design contradicts a requirement-as-written, Plan 01's first commit amends REQUIREMENTS.md + ROADMAP.md before any code. Phase 34 follows this for D-12.
  - **Pattern S2: hookSpecificOutput envelope** (plugin_session_start.rs:139-155 `emit_auto_pick`) — informational only for Phase 34; Stop uses `decision:"block"` + `reason` instead. Confirmation needed during research.
  - **`include_str!` precedent for repo-root markdown** (psyche.md): Phase 34 chooses NOT to follow this for CHANGELOG.md (per D-11: runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` instead). Rationale: changelog grows monotonically; binary should not bloat with history.
  - **Sync-then-verify in DEPLOY.ps1** (DEPLOY.ps1:256-273): each file write verifies via re-read. CHANGELOG.md sync follows the same pattern.
  - **Schema additivity** (Phase 26 PLAN-OVERVIEW Q4, Phase 31 D-12, Phase 33): no new info.json fields; no new on-disk artifacts beyond the sentinel; the `version-remind` subcommand is purely additive.

  ### Integration Points
  - **`src/owl/hook_idle.rs::run()`** — version-change emission lands between perch resolution (lines 25-32) and `set_idle_ready` (line 39). OWL_ECHO_COMMUNE guard MUST precede any new logic.
  - **`docs/DEPLOY.ps1` `-Bump` flow** — CHANGELOG.md stub-injection + unchanged-abort gate inserts after Cargo.toml verification (line 238), before the regex-replace block (line 247).
  - **`docs/DEPLOY.ps1` sync step** — `{Marketplace}/plugin/spt/CHANGELOG.md` copy slots alongside the existing binary/skills/hooks/plugin.json sync.
  - **`Cargo.toml` `[[bin]]` table** — new `version-remind` subcommand attaches as a `clap` subcommand on the existing `owl` binary; no new binary target.
  - **`tests/`** — version-change detection tests live in a new `tests/version_changelog.rs` integration test file (mirrors `tests/skill_hints.rs` from Phase 32 regression-guard pattern). Unit tests for the CHANGELOG.md parser co-locate with `src/owl/version_changelog.rs` (or wherever the parser lands).

  </code_context>

  <specifics>
  ## Specific Ideas

  - **Sentinel JSON shape (locked):** `{"version":"X.Y.Z"}`. Single field, no timestamp, no plugin name. Minimal for `serde_json::from_str` round-trip. Path: `$SPT_HOME/last-seen-version.json` (sibling to `owlery/` per VERS-01).
  - **CHANGELOG.md H2 regex (locked):** `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`. Case-sensitive. Reject `## [Unreleased]` (no date matched). Ignore link reference lines (start with `[` not `## [`).
  - **Prompt question text template:**
    - Single-step: `SPT plugin updated v{old} → v{new}. Print changelog?`
    - Multi-step: `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`
    - Dates surface in the AUQ description / Claude's pre-question framing, not in the question line itself.
  - **4-option labels (locked, verbatim per D-08):**
    - `Yes, full changelog`
    - `Yes, highlights only`
    - `Remind me later`
    - `Skip`
  - **Highlights body framing (suggested wording for block-body or skill-body):** "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes. Append `See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail.`"
  - **`version-remind` subcommand signature (locked):** `$OWL version-remind <old version>` — bare positional arg (e.g. `$OWL version-remind 1.10.9`). Writes `{"version":"<old version>"}` to `$SPT_HOME/last-seen-version.json` via `atomic_write_string`. Exits 0 on success, 1 on missing arg or write failure. The `<old version>` arg is REQUIRED — the subcommand has no way to infer it; the block-body MUST instruct Claude to pass the literal `old` value from the block payload it received. `$OWL` is the env-var-injected owl-binary path per existing `/spt:*` skill conventions; subcommand also callable directly via `owl version-remind <old version>` for tests / scripted invocation.
  - **DEPLOY.ps1 stub format (locked):** Appended block at end of repo-root CHANGELOG.md:
    ```markdown
    ## [{NEW_VERSION}] - {YYYY-MM-DD}

    - TODO: changelog entry
    ```
    Date = bump-time `Get-Date -Format 'yyyy-MM-dd'`. The literal `TODO: changelog entry` marker is what the unchanged-abort gate scans for on the next `-Bump` invocation.
  - **DEPLOY.ps1 unchanged-abort message (suggested):** `ERROR: CHANGELOG.md contains 'TODO: changelog entry' for v{NEW_VERSION}. Fill in the entry and commit before re-running -Bump.`

  </specifics>

  <deferred>
  ## Deferred Ideas

  - **`owl changelog backfill` subcommand** — would auto-generate CHANGELOG.md from `git log --grep='chore: bump'`. Rejected for Phase 34 per D-09 (one-shot manual author at Plan 01). Re-evaluate if a future fresh-fork bootstrap is needed.
  - **`/spt:changelog` skill** — would own AUQ + render + remind flow as a skill body. Rejected for Phase 34 per overall scope (no new skill surface). Block-body inline instructions to Claude are sufficient. Re-evaluate if the inline-instructions pattern becomes brittle or if other phases want to compose with the changelog flow.
  - **Sentinel with reminder flag** — alternative to `version-remind`-rolls-back: sentinel could carry `{"version":"X.Y.Z","reminder":bool}` and version-remind sets `reminder:true` while keeping the new version. Rejected for Phase 34 to keep sentinel shape minimal (D-07). Could reconsider if a future requirement needs richer sentinel metadata.
  - **Telemetry for AUQ option distribution** — log how often each of the 4 options is picked. Useful for tuning highlights framing. Out of scope; no telemetry infrastructure in spt today.
  - **`Yes, highlights only` rubric formalization** — current decision is "Claude's discretion". A future phase could specify an exact rubric (e.g., "include all `## Added`, `## Fixed` H3 subsections; exclude `## Changed (internal)`"). Requires CHANGELOG.md to use H3 subsections per Keep-a-Changelog v1.1.0 — out of scope for Phase 34's minimal H2-per-version format.
  - **Block payload embedded full excerpt vs Claude-reads-at-yes-time** — D-04 / Claude's Discretion flagged this as planner choice. If planner picks embed-in-block, Phase 34 hook also parses the H2 sections during the hook run. Defer to RESEARCH.md.
  - **PreToolUse fallback if Stop's `decision:"block"` + `reason` doesn't propagate text to Claude** — flagged as research blocker in D-04 / Claude's Discretion. If confirmed broken, escalate during planning.
  - **UserPromptSubmit revisit** — the "walk-away mid-task" rationale (D-01) is the reason we pivoted from UserPromptSubmit to Stop. If a future requirement needs the prompt to land at session-start time (e.g., a critical security update notice), UserPromptSubmit becomes viable again with a different UX framing. Not in Phase 34.

  </deferred>

  ---

  *Phase: 34-version-change-changelog*
  *Context gathered: 2026-05-17*

  ### Research Findings
  # Phase 34: Version-Change Changelog - Research

  **Researched:** 2026-05-17
  **Domain:** Claude Code Stop hook semantics, CHANGELOG.md parsing, Windows atomic-write races, DEPLOY.ps1 stub-injection
  **Confidence:** HIGH (primary research blocker D-04 RESOLVED with multi-source agreement)

  ---

  ## Summary

  - **Q1 / D-04 GREEN:** Stop hook `decision:"block"` + `reason` propagates `reason` text to Claude as continuation instruction — Claude generates a new assistant turn responding to it. No silent abort. Multi-source agreement (Anthropic schema text "`reason` tells Claude how to proceed", disler hooks-mastery, claude.com blog, claudefa.st). The architectural choice in D-04 is sound; no fallback to PreToolUse / UserPromptSubmit is required.
  - **Q2 nuance:** Claude CAN invoke AskUserQuestion immediately on re-engagement (the block IS the new-turn trigger; no user prompt needed first). The `reason` text drives Claude's response, and Claude is free to call any tool — exactly the pattern that "stop hook forces task completion" examples in the ecosystem rely on.
  - **Stop hook has NO `hookSpecificOutput` schema** — confirmed. Only top-level `decision` + `reason` + universal fields (`continue`, `stopReason`, `suppressOutput`, `systemMessage`). The existing `hook_idle.rs:1-5` comment is accurate. Block-body content MUST live inside `reason` verbatim. Payload size budget: `reason` is a single string; no documented hard cap but practical limit aligns with the 10,000-char JSON-output cap. Our payload is tiny (a few hundred chars).
  - **`stop_hook_active` infinite-loop guard:** Industry-standard `Stop` input flag. Set to `true` on Claude's re-engagement after a previous block in the same continuation chain. Our hook MUST short-circuit on `stop_hook_active==true` regardless of version mismatch, as a defensive guard. With D-05 sentinel pre-write the version-mismatch path naturally won't re-fire, but the guard prevents pathological cases (e.g., sentinel write failure → mismatch persists → infinite block loop).
  - **`$CLAUDE_PLUGIN_ROOT` confirmed available** to plugin-bundled hooks at invocation. Documented in Claude Code hooks reference.
  - **Plan boundary recommendation:** 3 plans, refined boundary below (Q8). Plan 01 = amendment-commit + CHANGELOG.md genesis + DEPLOY.ps1 (doc/script only, no Rust). Plan 02 = Rust hook + sentinel + parser + `version-remind` subcommand + unit tests. Plan 03 = block-body authoring + integration test on synthesized version transition.

  **Primary recommendation:** Proceed with the design exactly as captured in CONTEXT.md D-01..D-12. The single research blocker (D-04) resolves favorably; no architectural pivot required.

  ---

  ## User Constraints (from CONTEXT.md)

  ### Locked Decisions (D-01..D-12 — verbatim authority)

  - **D-01:** Stop hook (`hook_idle.rs`), not SessionStart.
  - **D-02:** Predicate = `owl_id` resolves in `hook_idle.rs::run()` (piggyback existing perch resolution).
  - **D-03:** Recursion guard reuses `OWL_ECHO_COMMUNE` env-var skip (hook_idle.rs:64-69 pattern).
  - **D-04:** Block emission via top-level `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"`. RESEARCH-CONFIRMED.
  - **D-05:** Sentinel write site = same `run()` top, BEFORE returning the block. Atomic via `atomic_write_string`. Default behavior: writes the NEW version. Rollback only via explicit `version-remind`.
  - **D-06:** Fire-once cadence = once per version transition (sentinel pre-write enforces this).
  - **D-07:** Payload shape = `{old, new, old_date, new_date, step_count}`.
  - **D-08:** AskUserQuestion 4-option shape: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Block body MUST surface the exact `$OWL version-remind <old>` invocation string verbatim.
  - **D-09:** Repo-root `CHANGELOG.md` authored one-shot at Plan 01 start from `git log --grep='chore: bump'`. No `owl changelog backfill` subcommand.
  - **D-10:** Forward maintenance = DEPLOY.ps1 `-Bump` stub injection + commit gate (combined: append `TODO: changelog entry` stub uncommitted; next `-Bump` aborts if marker still present).
  - **D-11:** DEPLOY.ps1 sync target = `{Marketplace}/plugin/spt/CHANGELOG.md` (sibling to plugin.json). Resolves at hook time via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.
  - **D-12:** Amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md SC1/SC2/SC5 per the rewordings in CONTEXT.md.

  ### Claude's Discretion (planner picks)

  - `<spt-version-changelog>` block exact XML shape (planner picks JSON-as-text vs child elements).
  - Block payload: embed full excerpt vs Claude-reads-at-yes-time (researcher leans toward Claude-reads, see Q11 below).
  - CHANGELOG.md parser regex tightness (`[Unreleased]` reject, link reference lines ignored) — locked in test corpus below.
  - `Yes, highlights only` extraction policy framing — final wording in Plan 02 or 03.
  - Plan split (3 plans) — final boundary at planner's discretion; researcher recommendation in Q8.

  ### Deferred Ideas (OUT OF SCOPE)

  - `owl changelog backfill` subcommand
  - `/spt:changelog` skill
  - Sentinel with reminder flag (richer schema)
  - Telemetry for AUQ option distribution
  - `Yes, highlights only` rubric formalization (H3 subsections)
  - PreToolUse fallback (not needed — D-04 confirmed)
  - UserPromptSubmit revisit
  - Block payload embedded full excerpt — researcher recommends NOT embedding (see Q11)

  ---

  ## Phase Requirements

  | ID | Description | Research Support |
  |----|-------------|------------------|
  | VERS-01 | `$SPT_HOME/last-seen-version.json` sentinel sibling to `owlery/` | `src/common/owlery.rs:18-32` spt_home resolution verified; sentinel path = `spt_home().join("last-seen-version.json")` |
  | VERS-02 | Stop hook compares `env!("CARGO_PKG_VERSION")` against sentinel (AMENDED per D-12) | hook_idle.rs:14-43 emission site verified; CARGO_PKG_VERSION precedent at poll.rs:75, setup.rs:35, stop.rs:199 |
  | VERS-03 | First-install silent write | Sentinel-missing path: read returns None/Err → atomic_write_string current version, return without block emission |
  | VERS-04 | AskUserQuestion 4-option shape (AMENDED per D-12) | Block-body inline instructions to Claude; AUQ invoked by Claude on re-engagement |
  | VERS-05 | Atomic write via `atomic_write_string` | Verified at src/common/owlery.rs:248; tmp + rename; NTFS same-volume safe |
  | VERS-06 | Multi-version skip (AMENDED per D-12) — 4-option AUQ applies uniformly | `step_count` field in payload; Yes-full renders all H2 sections verbatim |
  | VERS-07 | Repo-root `CHANGELOG.md` synced via DEPLOY.ps1 to plugin meta dir | DEPLOY.ps1 sync step extended; resolves to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` per D-11 |
  | VERS-08 | AUTO-03 vs VERS-04 coexistence (AMENDED per D-12) — separate hooks | AUTO-03 is SessionStart (plugin_session_start.rs); VERS-04 is Stop (hook_idle.rs). No collision possible. |
  | VERS-09 | Single write site, no in-handoff race (AMENDED per D-12) | hook_idle.rs::run() top after perch resolution + OWL_ECHO_COMMUNE guard. Stop hooks do not fire mid-handoff. |

  ---

  ## Architectural Responsibility Map

  | Capability | Primary Tier | Secondary Tier | Rationale |
  |------------|-------------|----------------|-----------|
  | Version-mismatch detection | Rust hook (`hook_idle.rs`) | — | Compile-time `env!("CARGO_PKG_VERSION")` is the only handoff-safe source per Out-of-Scope §3 |
  | Sentinel read/write | Rust hook + `version-remind` subcommand | `atomic_write_string` (owlery.rs:248) | Both write sites share the same helper; rollback is a discrete subcommand for explicit user intent |
  | CHANGELOG.md parse | Rust hook | — | "Rust hook does it once" chosen explicitly in Discussion Log Round 2 |
  | Block payload composition | Rust hook | — | Hook builds `{old, new, old_date, new_date, step_count}` from parser output; embeds verbatim into `reason` |
  | AUQ render + branching | Claude (block-body instructions) | — | Stop hook has no AUQ-emission channel; Claude invokes AUQ on re-engagement |
  | `Yes, full changelog` render | Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` | — | See Q11 recommendation — block payload stays small |
  | `Yes, highlights only` synthesis | Claude (discretion + framing) | — | D-08; no fixed rubric |
  | `Remind me later` rollback | `$OWL version-remind <old>` subcommand | `atomic_write_string` | Bare positional arg; ~10-line implementation |
  | CHANGELOG.md authorship (genesis) | Human (Plan 01 step 1) | git log skeleton | One-shot, never auto-regenerated |
  | Forward CHANGELOG.md maintenance | DEPLOY.ps1 `-Bump` stub + commit gate | Human fills | Combined enforcement: stub-append + unchanged-abort |

  ---

  ## Standard Stack

  ### Core

  | Library | Version | Purpose | Why Standard |
  |---------|---------|---------|--------------|
  | `serde_json` | (existing, bundled with `preserve_order`) | Sentinel JSON read/write | Already in tree; minimal `{"version":"X.Y.Z"}` shape needs only string round-trip |
  | `clap` | (existing) | `version-remind` subcommand registration | Already in tree; bare positional arg slots in alongside existing owl subcommands |

  ### Supporting

  | Library | Purpose | When to Use |
  |---------|---------|-------------|
  | `std::fs::read_to_string` | CHANGELOG.md slurp at hook time | One-time read; file ~kB scale |
  | `std::env::var` | `$CLAUDE_PLUGIN_ROOT` resolution | Hook fallback path: walk up from hook script if env var unset (defensive) |

  ### Alternatives Considered

  | Instead of | Could Use | Tradeoff |
  |------------|-----------|----------|
  | Manual H2 regex parser | `pulldown-cmark` markdown crate | Adds dep weight; Phase 34 H2-only parsing is ~10 LOC of regex — overkill |
  | `include_str!` embed CHANGELOG.md | Runtime read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` | Explicitly rejected in Discussion Round 1: binary bloat with history |
  | `owl changelog backfill` subcommand | One-shot manual author | Discussion Round 1: deferred (D-09) |

  **Verified versions:** No new crates required. All work uses already-installed `serde_json`, `clap`, and `std` — no version drift risk.

  ---

  ## Per-Question Findings

  ### Q1 (CRITICAL): Stop hook `decision:"block"` + `reason` propagation

  **Status:** GREEN — RESOLVED.

  **Finding:** When a Stop hook emits `{"decision":"block","reason":"..."}`, the `reason` text is propagated to Claude as continuation instruction. Claude does NOT silently abort — Claude generates a new assistant turn responding to the reason text, exactly as if the reason had been delivered as a system message instructing Claude to continue working.

  **Sources:**
  - Anthropic's hooks reference `~/.claude/reference_docs/claude-code-hooks.md` (lines 389-399): Stop hook output is `{"decision":"block","reason":"Reason to block completion"}` (note the schema text positions `reason` as actionable instruction).
  - [Claude Code Hooks Mastery (disler)](https://github.com/disler/claude-code-hooks-mastery): "Yes, when `decision: "block"` is returned with a reason field in the Stop hook, the reason text is fed back to Claude as a continuation instruction." `[CITED]`
  - [Anthropic blog: How to configure hooks](https://claude.com/blog/how-to-configure-hooks): documents `decision:"block"` with `reason` as "shown to Claude" `[CITED]`
  - [Claudefa.st Stop Hook guide](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement): "When you use `decision: block` in a Stop hook, it automatically prompts Claude with the reason provided, which is crucial because the reason field is fed back to Claude" `[CITED]`
  - [Steve Kinney AI Tools course](https://stevekinney.com/courses/ai-development/claude-code-hook-control-flow): confirms same mechanic
  - Practical evidence: the ecosystem "force task completion" pattern (e.g. "must pass tests before stopping") relies on this exact propagation. If `reason` did not reach Claude, that entire pattern category would be inoperative — and it is documented as working across multiple guides.

  **Rendered shape on Claude's side:** Claude sees the reason text as the new continuation context. The block IS the new-turn trigger. No user prompt is required. Claude's response then renders the block body inline, then proceeds to call AskUserQuestion (matching the pattern other Stop-hook-driven flows use).

  **Counter-evidence considered:** One WebFetch response claimed the reason is "shown to the user only, not fed back to Claude" — this was anomalous and contradicted by 4+ other sources including the official hooks reference. Discounted as parsing artifact.

  **No fallback required.** PreToolUse and UserPromptSubmit alternatives in Deferred Ideas remain dormant.

  ### Q2: AskUserQuestion invocation from blocked Stop context

  **Status:** GREEN.

  **Finding:** Once Claude re-engages with the `reason` text in context, Claude can immediately invoke AskUserQuestion in the same turn — without waiting for a new user prompt. The block IS the new-turn trigger; the new turn is bounded by Claude's response, which can include any tool call.

  **Sources:**
  - Inference from Q1: if reason drives Claude's new turn, Claude is free to invoke tools per the normal assistant-turn model.
  - Ecosystem pattern: "stop hook → forced continuation → tool call → new stop hook" loop is well-attested in the disler/claudefa.st examples. The `stop_hook_active` flag exists precisely because Claude can and will fire tools in the forced-continuation turn.

  **Implication for D-08:** The block body's inline instructions to "render the AUQ now with these 4 options" will be honored on the same turn — AUQ fires immediately, before any new user message.

  **Caveat:** One Anthropic-docs WebFetch snippet suggested "Claude would need a new user prompt OR the hook to provide additionalContext" — but since the Stop hook does NOT support `additionalContext` (Q3) and the ecosystem clearly demonstrates Claude responding to `reason`, the practical conclusion is that `reason` IS the new context channel for Stop hooks. The disler/claudefa.st consensus wins.

  ### Q3: Stop hook output schema — `hookSpecificOutput.additionalContext`?

  **Status:** Confirmed NEGATIVE.

  **Finding:** Stop hook output supports ONLY top-level `decision` + `reason` + universal fields (`continue`, `stopReason`, `suppressOutput`, `systemMessage`, `terminalSequence`). NO `hookSpecificOutput` envelope. NO `additionalContext` field.

  **Sources:**
  - `~/.claude/reference_docs/claude-code-hooks.md` lines 389-399 (Stop section) — only `decision` + `reason` documented.
  - Anthropic hooks reference page (WebFetch verified): "Stop is explicitly listed as supporting only `decision` and `reason` at the top level—not `hookSpecificOutput`."
  - Existing `src/owl/hook_idle.rs:1-5` comment is accurate and predates our research.

  **Implication:** ALL block-body content (the `<spt-version-changelog>` XML, the AUQ instructions, the `$OWL version-remind <old>` invocation string) MUST live inside the single `reason` string. This is fine — payload is tiny, well under any practical limit.

  ### Q4: `$CLAUDE_PLUGIN_ROOT` availability inside Stop hook process

  **Status:** GREEN.

  **Finding:** `$CLAUDE_PLUGIN_ROOT` is set by Claude Code on the spawned hook process for ALL plugin-bundled hooks. Documented in the hooks reference (line 607) and verified via authoritative Anthropic docs.

  **Sources:**
  - `~/.claude/reference_docs/claude-code-hooks.md` line 607: "`$CLAUDE_PLUGIN_ROOT` -- Plugin directory" listed under "Environment Variables Available in Hooks".
  - Anthropic hooks page (WebFetch verified): "Both forms support the same path placeholders, and both export them as the environment variables `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, and `CLAUDE_PLUGIN_DATA` on the spawned process."

  **Implication:** Hook resolves CHANGELOG.md path as:
  ```rust
  let plugin_root = std::env::var("CLAUDE_PLUGIN_ROOT")
      .map_err(|_| "CLAUDE_PLUGIN_ROOT must be set by Claude Code")?;
  let changelog_path = std::path::Path::new(&plugin_root).join("CHANGELOG.md");
  ```

  **Defensive fallback:** If `$CLAUDE_PLUGIN_ROOT` is unset (e.g., direct invocation in tests, or theoretical CC bug), the hook should fail soft — skip emission, do not panic. Tests cover both branches.

  ### Q5: `$OWL` env var availability inside AUQ-renderer Claude turn

  **Status:** GREEN.

  **Finding:** `$OWL` is injected into `~/.claude/settings.json` by SPT's SessionStart hook (per CLAUDE.md §"Env vars: `$OWL` and `$LIVE` are auto-injected into `~/.claude/settings.json` at SessionStart"). Settings env vars persist for the full session, across all turns — Bash tool invocations resolve `$OWL` to the absolute owl.exe path.

  **Sources:**
  - `CLAUDE.md` §Technology Stack: "Env vars: `$OWL` and `$LIVE` are auto-injected into `~/.claude/settings.json` at SessionStart."
  - Existing skill convention: every `/spt:*` skill (e.g., `commune/SKILL.md`, `signoff/SKILL.md`) uses `$OWL deliver` / `$LIVE pick-spec` without first having to resolve the path — confirms the env var is honored at tool-invocation time.

  **Implication:** Claude can call `$OWL version-remind 1.10.9` in a Bash tool invocation directly. The block body should surface the exact invocation string verbatim per D-08.

  ### Q6: `atomic_write_string` Windows semantics under concurrent writers

  **Status:** GREEN — acceptable for our use case.

  **Finding:** Rust's `std::fs::rename` on Windows uses `MoveFileEx` with `MOVEFILE_REPLACE_EXISTING` semantics. On NTFS same-volume renames (our case — both source `.tmp` and destination live under `$SPT_HOME`), the rename is atomic with respect to the file pointer: readers either see the old file or the new file, never a torn read.

  **Concurrent writers behavior:** Last-writer-wins. If two Stop hooks race to write `last-seen-version.json` (e.g., two concurrent spt-bearing agents both transitioning v1.10.9 → v1.10.10 simultaneously), both write the same value `{"version":"1.10.10"}` — the final on-disk content is identical regardless of which rename "won". No torn reads possible.

  **Race-condition window:** Between the two `fs::write(&tmp, body)?` calls and the two `fs::rename` calls, there is a window where both `.tmp` files exist. The temp filename is shared (`last-seen-version.json.tmp` — see `atomic_write_string` source at owlery.rs:249) which CAN cause the second writer to fail with `ERROR_SHARING_VIOLATION` or overwrite the first's `.tmp`. Per existing call-site pattern (`write_last_commune_epoch` in owlery.rs:264) this is treated as best-effort: errors are swallowed at the caller layer. Our Phase 34 hook should match this — wrap `atomic_write_string` in a best-effort pattern; on failure, log to stderr and continue. Worst case: sentinel doesn't update this cycle → block re-fires on next Stop in same session (degraded but not broken).

  **Sources:**
  - `src/common/owlery.rs:238-247` — existing comment asserts NTFS+POSIX same-volume atomic rename.
  - [Rust std::fs::rename docs](https://doc.rust-lang.org/std/fs/fn.rename.html) — Windows uses MoveFileEx.
  - [Microsoft Learn: MoveFileEx + MOVEFILE_REPLACE_EXISTING + NTFS + same volume = atomic](https://learn.microsoft.com/en-us/archive/msdn-technet-forums/449bb49d-8acc-48dc-a46f-0760ceddbfc3) — confirmed atomic for our use case.
  - [rust-lang/rust PR#131072](https://github.com/rust-lang/rust/pull/131072) — Recent rust now uses POSIX rename semantics on Windows 1601+ via `FileRenameInfoEx`.

  **Test coverage required:** Unit test that simulates double-write and asserts final content is correct. Not a race test (impossible to deterministically reproduce on a unit-test timeline), just a sequential equivalence check.

  ### Q7: DEPLOY.ps1 stub-injection idempotency

  **Status:** GREEN with abort-gate design locked.

  **Finding:** Per D-10 the design is "stub + commit gate": first `-Bump` appends the stub uncommitted; next `-Bump` aborts if the unchanged TODO marker is still present. This is the correct semantics. The DEPLOY.ps1 existing patterns at lines 199-238 (refuse-on-dirty-plugin.json and refuse-on-dirty-Cargo.toml) provide the precedent template.

  **Mapping to DEPLOY.ps1 structure:**

  Insertion site:
  - **Bump abort gate:** Insert at lines 199-222, after the dirty-plugin.json / dirty-Cargo.toml refuse-on-dirty gates. New check: `if (CHANGELOG.md contains "TODO: changelog entry") { Write-Host "ERROR:..."; exit 1 }`. The scan is a substring or regex match for the literal `TODO: changelog entry`.
  - **Stub injection:** Insert at lines 277-287, after Cargo.toml/plugin.json regex-rewrites (line 274) but BEFORE the `git add` + `git commit` (lines 279-282). The new stub MUST be appended to repo-root `CHANGELOG.md` AFTER the bump verification (lines 257-274) succeeds. Critically: `git add` MUST NOT stage the CHANGELOG.md — the stub stays in working-tree only. The commit at line 281 should explicitly list `plugin.json` and `Cargo.toml` (already does — line 279) and NOT add CHANGELOG.md to the path list.

  **Idempotency property:** A user running `-Bump` twice without filling the CHANGELOG entry:
  1. First run: bumps Cargo.toml/plugin.json, commits, appends stub to CHANGELOG.md (unstaged, uncommitted).
  2. Second run: hits the abort gate at the dirty-state check OR the new TODO-marker check — depending on whether the user committed Cargo.toml/plugin.json from run 1 (they did; we commit it ourselves), so the dirty-state check passes; the TODO-marker check then fires and aborts.

  **Wait — there's a gap:** Between run 1 and run 2, the user may have edited CHANGELOG.md AND committed it. If they committed it without filling in real prose, the TODO marker would still be present. Our check would still fire (correct). If they committed it WITH real prose, the TODO marker is gone, the check passes, and the next `-Bump` proceeds (correct). The gate works in all branches.

  **Edge case:** If the user manually deletes the stub WITHOUT committing real content, the next `-Bump` sees no TODO marker but ALSO no entry for `NewVersion`. Should we check for both? Recommendation: also check that an H2 line matching `## [<NewVersion>] - YYYY-MM-DD` exists in CHANGELOG.md. If neither the stub nor a real entry for the bumped version is present, abort. This catches the "user deleted the stub but forgot to write a real entry" case.

  **Recommended check shape** (insert at ~line 215):
  ```powershell
  # Check that the previous bump's CHANGELOG entry was filled in.
  # A "TODO: changelog entry" marker means the previous bump's stub is still pending.
  $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  if (Test-Path $ChangelogPath) {
      $ChangelogContent = Get-Content $ChangelogPath -Raw
      if ($ChangelogContent -match 'TODO: changelog entry') {
          Write-Host "ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump." -ForegroundColor Red
          Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
          exit 1
      }
  }
  ```

  ### Q8: Plan boundary recommendation

  **Status:** RECOMMEND 3 plans, with refined boundary below.

  **Recommendation:**

  **Plan 01 — Doc Amendment + CHANGELOG.md Genesis + DEPLOY.ps1 (NO Rust)**
  - Step 1 (FIRST COMMIT): REQUIREMENTS.md + ROADMAP.md amendment per D-12 (mirrors Phase 31 / Phase 33 doc-amendment-first pattern).
  - Step 2: Author `CHANGELOG.md` at repo root by hand from `git log --grep='chore: bump'` skeleton (38 versions surfaced; see Appendix A below). Single commit with curated prose per version.
  - Step 3: Extend `docs/DEPLOY.ps1`:
    - Sync step: copy repo-root `CHANGELOG.md` to `{Marketplace}/plugin/spt/CHANGELOG.md` alongside the existing binary/skills/hooks/plugin.json sync (~line 368, new `Invoke-Mutation` block).
    - `-Bump` flow: insert TODO-marker abort gate at ~line 215 (after dirty-state checks); insert stub append at ~line 287 (after Cargo.toml regex-rewrite verification, before `git add`). Critically: the stub append MUST NOT stage CHANGELOG.md.
  - Verification: dry-run `-Bump patch` on a test branch; confirm stub appended, not committed; confirm `-Bump patch` again aborts with the new error.

  **Plan 02 — Rust Hook + Sentinel + Parser + `version-remind` Subcommand (NO Claude-side block-body)**
  - Author `src/owl/version_changelog.rs` (new module): sentinel read/write helpers, CHANGELOG.md parser, payload builder. Co-located unit tests for parser corpus (Q3 below).
  - Wire into `src/owl/hook_idle.rs::run()`: after perch resolution (lines 25-32), after `OWL_ECHO_COMMUNE` guard (mirrored from spawn_echo_commune_if_live:64-69), BEFORE `set_idle_ready` (line 39). On version-mismatch: emit `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"` via stdout JSON, then exit 0. Sentinel write happens at hook-entry top, BEFORE the block emission (per D-05).
  - `stop_hook_active` guard: parse from stdin JSON; short-circuit if true.
  - Add `version-remind <old>` subcommand to clap subcommand dispatch in `src/main.rs` (or wherever owl's bin entry is wired). ~10-line implementation: parse positional `<old>`, atomic_write_string `{"version":"<old>"}` to sentinel path, exit 0.
  - Unit tests in `src/owl/version_changelog.rs::tests`: parser happy path, parser reject `[Unreleased]`, parser ignore link references, sentinel read missing → None, sentinel read present → Some, sentinel write atomic correctness.
  - Hook-emission integration tests in `tests/version_changelog.rs` (NEW file): version-mismatch detection (single-step, multi-step, first-install silent, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard skip, no-perch guard skip, stop_hook_active guard skip); `version-remind` rollback round-trip.

  **Plan 03 — Block Body Authoring + End-to-End Integration Test**
  - Author the block-body text (the content inside `reason`'s `<spt-version-changelog>` XML). Includes: the 4-option AUQ instructions verbatim, the highlights-extraction framing wording (D-08), the exact `$OWL version-remind <old>` invocation string with `{old}` slot, the pointer to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` for `Yes, highlights only` and `Yes, full changelog` paths.
  - This text lives in the Rust payload-builder helper as a `format!` template. Plan 03 owns the wording; Plan 02 owns the scaffold.
  - End-to-end integration test in `tests/version_changelog.rs`: synthesized version transition (sentinel set to v1.10.9, CARGO_PKG_VERSION mock'd or env-var-overridden to v1.10.10), assert stdout JSON has `decision:"block"` and `reason` matches expected XML envelope (snapshot-style assertion via golden fixture in `tests/golden/`).
  - UAT: manually run `cargo build --release`, deploy via DEPLOY.ps1, manually trigger a version mismatch, verify the AUQ surfaces in a real Claude Code session.

  **Rationale for boundary:**
  - Plan 01 is doc/script only — no Rust, no test framework dependency. Can ship independently.
  - Plan 02 is Rust internals — testable in isolation. Subcommand pairs with the hook because both share the parser+sentinel module; splitting would force the subcommand to duplicate the path-helper.
  - Plan 03 is the user-facing surface — wording iterations don't churn Plan 02's scaffold. Integration test lives here because it depends on the block-body text Plan 03 owns.

  **Dependency graph:** Plan 01 → Plan 02 → Plan 03 (linear). Plan 02 cannot start until CHANGELOG.md exists (parser needs sample data for unit tests). Plan 03 cannot start until Plan 02's payload builder exists.

  **Alternative considered: 4 plans.** Splitting CHANGELOG.md genesis from amendment + DEPLOY.ps1 (separate Plan 0/Plan 1). Rejected — genesis is a single commit of one file; it doesn't warrant a separate plan boundary.

  ### Q9: Test fixture for synthesized version transition

  **Status:** RECOMMEND env-var override seam.

  **Recommendation:** Introduce a test-only env var `SPT_OVERRIDE_PKG_VERSION` (cfg-gated under `#[cfg(test)]` OR runtime-read). When set and non-empty, the version-changelog module reads it INSTEAD of `env!("CARGO_PKG_VERSION")`. Otherwise falls through to `env!`.

  **Why this approach:**
  - **Least invasive:** No rebuild required per test case. A single binary can simulate any version transition.
  - **Matches existing patterns:** `SPT_HOME` env override (owlery.rs:18-32) is the established seam pattern in this codebase. `SPT_OVERRIDE_PKG_VERSION` is symmetric.
  - **No production risk:** Document explicitly as "test-only; never set in production". Optionally gate behind a runtime check that emits a stderr warning if set in non-test contexts.

  **Alternatives rejected:**
  - **Mock seam (trait + impl injection):** Overkill for a single `env!` call site. Rust mocking ergonomics are poor for compile-time macros.
  - **Helper that writes sentinel to temp `$SPT_HOME`:** Already needed for sentinel-side tests, but doesn't solve the CARGO_PKG_VERSION side — the binary version is baked in at compile time.
  - **Per-test custom binary build:** Slow, flaky, increases test-runtime cost dramatically.

  **Implementation seam (in `version_changelog.rs`):**
  ```rust
  fn current_version() -> &'static str {
      // Test seam: SPT_OVERRIDE_PKG_VERSION lets tests simulate any version
      // without rebuilding. Production code paths see env!("CARGO_PKG_VERSION").
      if let Ok(v) = std::env::var("SPT_OVERRIDE_PKG_VERSION") {
          if !v.is_empty() {
              // SAFETY: leak is bounded by test count; acceptable for test-only seam.
              return Box::leak(v.into_boxed_str());
          }
      }
      env!("CARGO_PKG_VERSION")
  }
  ```

  Note: the `Box::leak` is intentional for the test-only path; in practice tests setting this env var run a finite number of times per binary lifecycle. If the leak offends, the function can return `String` and accept the allocation cost on every call.

  ### Q10: Keep-a-Changelog 1.1.0 — `[Unreleased]` and format details

  **Status:** GREEN with `[Unreleased]` omitted from our format.

  **Findings:**

  1. **`## [X.Y.Z] - YYYY-MM-DD` format is canonical.** Verified at [keepachangelog.com/en/1.1.0](https://keepachangelog.com/en/1.1.0/).
  2. **`[Unreleased]` section is conventional but not strictly required.** The spec recommends it for projects with active development; we explicitly omit it (D-09 says we author from `git log` retroactively — no current "unreleased" state at genesis).
  3. **Spec recommends categories `Added/Changed/Deprecated/Removed/Fixed/Security`.** Phase 34's parser doesn't care about H3 subsections — it only locates H2 version headers. The body content between H2 boundaries is preserved verbatim for the `Yes, full changelog` path. Our author MAY use H3 subsections in prose, MAY use plain bullet lists, MAY use prose paragraphs — parser is body-content-agnostic.
  4. **Link references at file bottom (`[1.10.10]: https://...`)** are valid per spec but irrelevant for our use case (we don't render comparison links). Parser MUST ignore them by virtue of the `^## \[` anchor (link refs start with `[`, not `## [`).

  **Parser implications:** The regex `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$` rejects `[Unreleased]` (no version match) and ignores link references (no `## ` prefix). No additional spec compliance needed.

  ### Q11: Block payload — embed full excerpt vs Claude-reads-at-yes-time

  **Status:** RECOMMENDATION: Claude-reads-at-yes-time.

  **Rationale:**

  | Approach | Block size | Hook complexity | Token cost | Multi-version impact |
  |----------|-----------|-----------------|------------|---------------------|
  | Embed full excerpt in block | Large (every visible session pays for all transition prose) | Rust hook parses + concatenates H2 sections | High on every transition, even if user picks Skip | Worst with multi-version skip (e.g. 10 versions = 10x prose) |
  | Claude-reads-at-yes-time | Small (just `{old,new,old_date,new_date,step_count}` + invocation string) | Rust hook parses only H2 headers (cheap) | Low on Skip / Remind paths; cost paid only on Yes paths | Multi-version still cheap (one CHANGELOG.md read regardless of step count) |

  Block payload stays compact. Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` only when user picks `Yes, full changelog` or `Yes, highlights only`. Cost is bounded by user choice.

  **Block-body XML shape (recommendation):**
  ```xml
  <spt-version-changelog>
    <old>1.10.9</old>
    <new>1.10.10</new>
    <old_date>2026-05-15</old_date>
    <new_date>2026-05-16</new_date>
    <step_count>1</step_count>
    <changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>
    <invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>
    <instructions>
      Render an AskUserQuestion with question: "SPT plugin updated v1.10.9 → v1.10.10. Print changelog?"
      Options (exactly 4):
      - "Yes, full changelog": read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section
        where 1.10.9 &lt; version &lt;= 1.10.10, verbatim markdown.
      - "Yes, highlights only": read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md, extract 2-5 most impactful
        feature adds and fixes from the same range; prefer user-facing behavior changes;
        skip purely-internal refactors and test-only changes. Append: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."
      - "Remind me later": invoke Bash: `$OWL version-remind 1.10.9` — pass the literal old version.
      - "Skip": no action.
    </instructions>
  </spt-version-changelog>
  ```

  Child elements (not JSON-as-text) recommended. Rationale: Claude parses XML reliably; child elements are unambiguous; readable in transcripts; matches the existing Phase 33 `<spt-live-auto-pick>` pattern convention.

  `step_count` surfaces in the question text when `> 1` per D-07: `SPT plugin updated v1.10.5 → v1.12.0 (12 versions). Print changelog?`

  ---

  ## CHANGELOG.md Parser — Test Corpus

  | Test Case | Input | Expected |
  |-----------|-------|----------|
  | **Happy path: single-version H2** | `## [1.10.10] - 2026-05-16` | Match: version=1.10.10, date=2026-05-16 |
  | **Happy path: pre-1.0** | `## [0.9.3] - 2025-12-01` | Match: version=0.9.3 |
  | **Reject: Unreleased** | `## [Unreleased]` | No match |
  | **Reject: no date** | `## [1.10.10]` | No match |
  | **Reject: incomplete semver (no patch)** | `## [1.10] - 2026-05-16` | No match |
  | **Reject: incomplete semver (no minor)** | `## [1] - 2026-05-16` | No match |
  | **Reject: link reference line** | `[1.10.10]: https://github.com/org/repo/compare/v1.10.9...v1.10.10` | No match (no `## ` prefix) |
  | **Reject: H1 with same body** | `# [1.10.10] - 2026-05-16` | No match (single `#`) |
  | **Reject: H3 with same body** | `### [1.10.10] - 2026-05-16` | No match (three `#`) |
  | **Trailing whitespace tolerance** | `## [1.10.10] - 2026-05-16  ` (2 trailing spaces) | Researcher recommends: **accept** (loose anchor: `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})\s*$`). Reject only if extra non-whitespace content. |
  | **BOM tolerance** | `\u{feff}## [1.10.10] - 2026-05-16` | Match — parser strips BOM at file-read time |
  | **Edge: extra text after date** | `## [1.10.10] - 2026-05-16 (yanked)` | Researcher recommends: **reject** (strict-trailing-text rejection keeps parser deterministic; if a release is yanked, author should rewrite the H2 line or use a separate convention) |
  | **Edge: lowercase version** | `## [1.10.10] - 2026-05-16` | Match (case-sensitive comparison) |
  | **Edge: pre-release suffix** | `## [1.10.10-rc1] - 2026-05-16` | No match (our format is plain semver; pre-release out of scope) |
  | **Edge: empty H2** | `## ` | No match |
  | **Whitespace inside brackets** | `## [ 1.10.10 ] - 2026-05-16` | No match (strict bracket-hugging) |
  | **Date format variation** | `## [1.10.10] - 2026/05/16` | No match (ISO-8601 hyphen-only) |
  | **Range query (parser-driver level)** | Parser asked for all H2 sections between 1.10.5 (exclusive) and 1.10.7 (inclusive). File contains 1.10.5, 1.10.6, 1.10.7, 1.10.8. | Return body content of 1.10.6 and 1.10.7 only, concatenated. |
  | **Body content preservation** | H2 section body includes nested H3 headers, code fences, lists | Body returned verbatim (parser DOES NOT modify body content) |

  **Recommended regex:**
  ```rust
  const H2_RE: &str = r"^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$";
  ```

  Three capture groups for semver components plus three for date components. Multiline mode; case-sensitive. Strict ISO-8601 date format.

  **Parser API (in `src/owl/version_changelog.rs`):**
  ```rust
  pub struct ChangelogEntry {
      pub version: (u32, u32, u32),
      pub date: String, // "YYYY-MM-DD" verbatim
      pub body: String, // content between this H2 and next H2 / EOF, verbatim
      pub line_no: usize, // for error messages
  }

  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry>;
  pub fn sections_between(entries: &[ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&ChangelogEntry>;
  ```

  ---

  ## Integration Test Corpus (tests/version_changelog.rs)

  | Test | Setup | Assertion |
  |------|-------|-----------|
  | `first_install_silent_writes_sentinel` | Empty `$SPT_HOME`; bake `SPT_OVERRIDE_PKG_VERSION=1.10.10`; run hook | Sentinel written `{"version":"1.10.10"}`; stdout JSON contains no `decision:"block"` |
  | `single_step_mismatch_emits_block` | Sentinel `{"version":"1.10.9"}`; `SPT_OVERRIDE_PKG_VERSION=1.10.10`; CHANGELOG.md has both versions | stdout JSON `decision:"block"`, `reason` contains `<spt-version-changelog>` with `<old>1.10.9</old>` and `<new>1.10.10</new>` and `<step_count>1</step_count>`; sentinel now `1.10.10` |
  | `multi_step_mismatch_emits_block_with_step_count` | Sentinel `1.10.5`; PKG_VERSION `1.10.10`; CHANGELOG has 1.10.5-1.10.10 | `step_count` = 5 in payload; question text mentions "5 versions" |
  | `sentinel_equal_no_emit` | Sentinel `1.10.10`; PKG_VERSION `1.10.10` | No `decision:"block"`; sentinel unchanged (no write at all — write only on mismatch path) |
  | `owl_echo_commune_guard_skips_emission` | Mismatch present; `OWL_ECHO_COMMUNE=1` set | No `decision:"block"`; sentinel unchanged |
  | `no_perch_skips_emission` | Mismatch present; NO perch found (perch resolution returns None) | No `decision:"block"`; sentinel unchanged (per D-02 predicate) |
  | `stop_hook_active_skips_emission` | Mismatch present; stdin JSON has `stop_hook_active: true` | No `decision:"block"`; sentinel unchanged (defensive infinite-loop guard) |
  | `version_remind_rollback_round_trip` | Set sentinel to 1.10.10; invoke `owl version-remind 1.10.9`; assert sentinel now 1.10.9 | Sentinel content `{"version":"1.10.9"}` |
  | `version_remind_missing_arg_exits_1` | Invoke `owl version-remind` with no arg | exit code 1; stderr non-empty |
  | `version_remind_atomic_write` | Invoke `owl version-remind` twice in succession | No `.tmp` sibling left behind |
  | `changelog_parser_unit_corpus` | Feed parser the 18 test-corpus rows above | All pass per expected behavior |

  ---

  ## Don't Hand-Roll

  | Problem | Don't Build | Use Instead | Why |
  |---------|-------------|-------------|-----|
  | Atomic file write | Custom tmp+rename | `atomic_write_string` (owlery.rs:248) | Already battle-tested in 8+ call sites; NTFS+POSIX semantics encoded |
  | Spt-home path resolution | New env helper | `spt_home()` (owlery.rs:18-32) | Handles SPT_HOME override + platform default + create-if-missing |
  | Perch resolution | New session-id lookup | `find_perch_by_session` / `find_perch_by_parent_pid` (existing) | Same path proven across hook_idle.rs, hook_prompt.rs, hook_check.rs |
  | Markdown parsing | Full markdown crate | Regex H2 matcher (~10 LOC) | We only need H2 headers; body content is preserved verbatim |
  | Version-equality comparison | Semver crate | Tuple `(u32, u32, u32)` comparison | Plain semver only; no pre-release / build-metadata in our format |
  | JSON sentinel | Hand-roll serializer | `serde_json::to_string` / `from_str` | Single field; round-trip trivial |
  | Recursion guard | New env var | `OWL_ECHO_COMMUNE` (hook_idle.rs:64-69) | Reuse existing — same haiku-subprocess gate applies |
  | AUQ rendering in Rust | Try to emit AUQ JSON | Claude renders AUQ via block-body instructions | Stop hook has no AUQ-emission channel; Claude is the only AUQ surface |

  ---

  ## Common Pitfalls

  ### Pitfall 1: Sentinel write before block emission must NOT fail silently in ways that hide the mismatch

  **What goes wrong:** If `atomic_write_string` returns `Err` (e.g., disk full, perm denied) and the code swallows the error and proceeds to emit the block, the user sees the block, but the sentinel stays at the OLD value → next Stop fires again → infinite block loop (mitigated by `stop_hook_active` but still alarming).

  **Why it happens:** Existing best-effort pattern in `write_last_commune_epoch` (owlery.rs:264) swallows errors.

  **How to avoid:** Either (a) swallow the error AND skip emission if sentinel write fails (degraded UX: no notification this session; correct next session), OR (b) emit anyway AND rely on `stop_hook_active` to break the loop. Recommendation: (a) — fail silent is the established codebase idiom and matches the "best-effort sentinel" philosophy from Phase 18.8.

  **Warning signs:** Tests should include a "sentinel write fails (mock IO error)" case → assert no block emitted.

  ### Pitfall 2: `$CLAUDE_PLUGIN_ROOT` unset in non-plugin invocations

  **What goes wrong:** During tests or direct CLI invocation (`owl hook-idle`), `$CLAUDE_PLUGIN_ROOT` is not set. If the parser fails to find CHANGELOG.md, the block emission either crashes or emits a degraded payload.

  **Why it happens:** Tests bypass the Claude Code plugin invocation path.

  **How to avoid:** Block emission should ALSO require `$CLAUDE_PLUGIN_ROOT` to resolve to a real file. If the env var is unset OR the file is missing, treat as "no changelog available" → still emit the block but with a degraded `<changelog_path>NOT_RESOLVED</changelog_path>` AND skip the `step_count` / `old_date` / `new_date` fields. Claude's AUQ surface still works (with just version numbers, no dates). Better than crashing.

  **Warning signs:** Tests should explicitly include the "CLAUDE_PLUGIN_ROOT unset" branch.

  ### Pitfall 3: Parser ignores body content with `## ` at column 0 inside fenced code blocks

  **What goes wrong:** A CHANGELOG.md entry's body contains a markdown code block with `## [something]` inside (e.g., showing a doc example) → parser incorrectly matches it as an H2.

  **Why it happens:** Regex matches line-by-line without code-fence awareness.

  **How to avoid:** Either (a) accept this edge case as author responsibility (don't include `## [` at line start in code blocks), OR (b) track code-fence state across lines. Recommendation: (a) — Phase 34 CHANGELOG.md will be hand-authored prose; if we ever auto-generate, revisit. Document in code comment so future maintainers know.

  ### Pitfall 4: `stop_hook_active` semantics — we should respect it even though D-05 sentinel pre-write prevents normal re-fire

  **What goes wrong:** Edge case where sentinel write fails (Pitfall 1) — without `stop_hook_active` guard, hook would re-fire infinitely if write keeps failing.

  **Why it happens:** Sentinel write is fallible; `stop_hook_active` is Claude Code's documented infinite-loop guard.

  **How to avoid:** Parse `stop_hook_active` from stdin JSON. If true, short-circuit — exit 0 with no block emission, regardless of version mismatch. Belt-and-suspenders with the sentinel pre-write.

  ### Pitfall 5: DEPLOY.ps1 stub append at WRONG point causes git add to stage CHANGELOG.md

  **What goes wrong:** If stub append runs BEFORE `git add 'plugin/spt/.claude-plugin/plugin.json' 'Cargo.toml'` (DEPLOY.ps1 line 279), and a user later runs `git add -A` reflexively, CHANGELOG.md gets committed in a different commit and the unchanged-abort gate breaks (TODO marker now committed, not in working tree).

  **Why it happens:** Author confusion about where in the `-Bump` sequence to insert the stub.

  **How to avoid:** Stub append MUST happen AFTER `git commit` (line 281) — the bump commit excludes CHANGELOG.md. The stub then lives in working-tree only. Document this constraint with a code comment.

  ### Pitfall 6: Block payload's `step_count` is for SURFACE TEXT only — full-changelog body still reads ALL intermediate versions

  **What goes wrong:** Claude sees `step_count=5` and prints only 5 version headers without reading the full content from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.

  **Why it happens:** Claude is being asked to surface a count, not a body.

  **How to avoid:** Block-body instructions for `Yes, full changelog` MUST explicitly say "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version ≤ {new}". The `step_count` is for the AUQ question text only, not the body-rendering surface.

  ---

  ## Runtime State Inventory

  | Category | Items Found | Action Required |
  |----------|-------------|------------------|
  | Stored data | New sentinel `$SPT_HOME/last-seen-version.json` — single field `{"version":"X.Y.Z"}`. **Pre-existing data:** none (new artifact in Phase 34). | Code edit: create read/write helpers in `version_changelog.rs`. No data migration needed (first-install silent path handles missing sentinel per VERS-03). |
  | Live service config | `$CLAUDE_PLUGIN_ROOT` resolves to `~/.claude/plugins/cache/cplugs/spt/<version>/` at runtime; CHANGELOG.md must be present at that path. | DEPLOY.ps1 sync step copies CHANGELOG.md alongside other plugin files (per D-11). Hook reads at invocation time. |
  | OS-registered state | None — no Task Scheduler, pm2, systemd, or launchd registrations involve Phase 34 artifacts. | None. |
  | Secrets/env vars | New test-only env var `SPT_OVERRIDE_PKG_VERSION` (test seam per Q9). `$OWL` env var dependency confirmed (per Q5). `$CLAUDE_PLUGIN_ROOT` confirmed (per Q4). | None — test seam is additive. |
  | Build artifacts | `target/release/owl.exe` rebuilds with new `version_changelog.rs` module. Marketplace copy via DEPLOY.ps1 (existing pattern). | Standard `cargo build --release` + DEPLOY.ps1 sync. |

  **The canonical question:** *After every file in the repo is updated, what runtime systems still have the old string cached, stored, or registered?*

  Phase 34 is greenfield (new artifact), not a rename. No legacy cached state needs migration. The only runtime-state interaction is the sentinel itself, which is created on first hook fire after deploy.

  ---

  ## Environment Availability

  | Dependency | Required By | Available | Version | Fallback |
  |------------|------------|-----------|---------|----------|
  | Rust toolchain (cargo) | Plan 02, Plan 03 build | ✓ | (existing) | — |
  | PowerShell 5.1+ | Plan 01 DEPLOY.ps1 changes | ✓ | (existing) | — |
  | `git` CLI | Plan 01 CHANGELOG genesis (`git log --grep`) | ✓ | (existing) | — |
  | `serde_json` crate | Sentinel JSON | ✓ | (existing in Cargo.toml) | — |
  | `clap` crate | `version-remind` subcommand | ✓ | (existing in Cargo.toml) | — |
  | Claude Code (`claude` CLI) | UAT verification of AUQ surface | ✓ | (existing) | Manual test via direct hook invocation possible but lower-fidelity |

  No missing dependencies. No blocking gaps.

  ---

  ## Validation Architecture

  ### Test Framework

  | Property | Value |
  |----------|-------|
  | Framework | Rust `cargo test` (built-in, no extra crate) |
  | Config file | `Cargo.toml` (existing) |
  | Quick run command | `cargo test --lib version_changelog` |
  | Full suite command | `cargo test` |

  ### Phase Requirements → Test Map

  | Req ID | Behavior | Test Type | Automated Command | File Exists? |
  |--------|----------|-----------|-------------------|-------------|
  | VERS-01 | Sentinel at `$SPT_HOME/last-seen-version.json` | unit | `cargo test sentinel_path` | ❌ Wave 2 (new module) |
  | VERS-02 | Compile-time CARGO_PKG_VERSION read; never reads plugin.json at runtime | unit + crate-wide audit | `cargo test current_version_uses_env_macro` + grep audit | ❌ Wave 2 |
  | VERS-03 | First-install silent write | integration | `cargo test --test version_changelog first_install_silent_writes_sentinel` | ❌ Wave 2 (new integration file) |
  | VERS-04 | 4-option AUQ shape (per D-12 amendment) | integration | `cargo test --test version_changelog single_step_mismatch_emits_block` | ❌ Wave 2 |
  | VERS-05 | Atomic write via `atomic_write_string` | unit | `cargo test sentinel_write_atomic` | ❌ Wave 2 |
  | VERS-06 | Multi-version uses same 4-option (per D-12) | integration | `cargo test --test version_changelog multi_step_mismatch_emits_block_with_step_count` | ❌ Wave 2 |
  | VERS-07 | DEPLOY.ps1 syncs CHANGELOG.md to plugin meta dir | manual (DEPLOY.ps1 dry-run) | `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun` | n/a — script |
  | VERS-08 | AUTO-03 + VERS-04 coexistence by hook separation | integration | manual UAT — both hooks present in fixture, neither collides | n/a — UAT |
  | VERS-09 | Single write site at hook-entry top | manual code review + grep audit | `grep -r "last-seen-version" src/` should yield 2 hits (hook + subcommand) | n/a — review |

  ### Sampling Rate
  - **Per task commit:** `cargo test --lib version_changelog` (parser unit tests; fast)
  - **Per wave merge:** `cargo test` (full suite including integration)
  - **Phase gate:** Full suite green + UAT against real Claude Code session

  ### Wave 0 Gaps
  - `src/owl/version_changelog.rs` — new module file with parser + sentinel helpers
  - `tests/version_changelog.rs` — new integration test file (mirrors `tests/skill_hints.rs` from Phase 32)
  - `tests/golden/version_changelog_block.txt` — golden fixture for block-body snapshot assertion (Plan 03)

  ---

  ## Security Domain

  | ASVS Category | Applies | Standard Control |
  |---------------|---------|-----------------|
  | V2 Authentication | no | n/a — local-only sentinel; no auth boundary |
  | V3 Session Management | no | n/a |
  | V4 Access Control | no | n/a — sentinel lives under user's own `$SPT_HOME` |
  | V5 Input Validation | yes | Parse `<old version>` arg in `version-remind`: validate against semver regex BEFORE writing sentinel. If not matching `^\d+\.\d+\.\d+$`, exit 1. Prevents arbitrary content injection into the sentinel JSON. |
  | V6 Cryptography | no | n/a — no secrets, no signing |

  ### Known Threat Patterns

  | Pattern | STRIDE | Standard Mitigation |
  |---------|--------|---------------------|
  | `version-remind` arg injection | Tampering | Validate arg matches `^\d+\.\d+\.\d+$` regex; reject any other input |
  | Sentinel path traversal | Tampering | `$SPT_HOME` resolution is authoritative; path is constructed via `spt_home().join("last-seen-version.json")` — no user input in the path |
  | CHANGELOG.md content injection | Tampering | Author-controlled file; content goes into block `reason` text → reaches Claude, but Claude is the only consumer and the format is markdown. No XSS / code-exec surface. |
  | `OWL_ECHO_COMMUNE` recursion guard bypass | DoS (loop) | Guard MUST come BEFORE version-changelog logic (D-03); mirror hook_idle.rs:64-69 exactly |

  **Argument validation MUST happen in the `version-remind` subcommand entry, BEFORE any filesystem write.**

  ---

  ## Code Examples (Verified Patterns)

  ### Atomic write of sentinel (Plan 02)

  ```rust
  // Source: src/common/owlery.rs:248 (existing helper)
  use crate::common::owlery::{atomic_write_string, spt_home};

  fn write_sentinel(version: &str) -> std::io::Result<()> {
      let path = spt_home().join("last-seen-version.json");
      let body = format!("{{\"version\":\"{}\"}}", version);
      atomic_write_string(&path, &body)
  }
  ```

  ### Sentinel read with first-install handling (Plan 02)

  ```rust
  // First-install: read returns None; caller writes current version silently.
  fn read_sentinel() -> Option<String> {
      let path = spt_home().join("last-seen-version.json");
      let content = std::fs::read_to_string(&path).ok()?;
      let v: serde_json::Value = serde_json::from_str(&content).ok()?;
      v.get("version")?.as_str().map(String::from)
  }
  ```

  ### Hook emission via stdout JSON (Plan 02)

  ```rust
  // Source: existing pattern in src/common/hook_output.rs
  fn emit_block(reason: &str) {
      let output = serde_json::json!({
          "decision": "block",
          "reason": reason,
      });
      println!("{}", output);
  }
  ```

  ### Stop hook stdin parsing (Plan 02 — extend existing parse_hook_stdin)

  ```rust
  // Stop hook input includes stop_hook_active per ecosystem convention
  #[derive(Deserialize)]
  struct StopHookInput {
      #[serde(default)]
      session_id: Option<String>,
      #[serde(default)]
      stop_hook_active: bool,
  }
  ```

  ### `version-remind` subcommand body (Plan 02)

  ```rust
  // New subcommand. Bare positional arg per D-08.
  pub fn version_remind(old: &str) -> std::process::ExitCode {
      // V5 input validation: reject non-semver
      let semver_re = regex::Regex::new(r"^\d+\.\d+\.\d+$").unwrap();
      if !semver_re.is_match(old) {
          eprintln!("error: <old version> must match X.Y.Z semver");
          return std::process::ExitCode::from(1);
      }
      let path = spt_home().join("last-seen-version.json");
      let body = format!("{{\"version\":\"{}\"}}", old);
      match atomic_write_string(&path, &body) {
          Ok(_) => std::process::ExitCode::SUCCESS,
          Err(e) => {
              eprintln!("error: sentinel write failed: {}", e);
              std::process::ExitCode::from(1)
          }
      }
  }
  ```

  Note: `regex` crate may not be in Cargo.toml today. If not, use a hand-rolled validator (`old.split('.').count() == 3 && all parts parse as u32`).

  ---

  ## Assumptions Log

  | # | Claim | Section | Risk if Wrong |
  |---|-------|---------|---------------|
  | A1 | Stop hook `reason` text reaches Claude as continuation context strong enough for Claude to invoke AUQ in same turn | Q1 / Q2 | If wrong: AUQ never fires; user never sees changelog prompt. Mitigation: cross-source confirmation (4+ sources agree); ecosystem-wide "stop hook task completion" pattern relies on this exactly. Confidence: HIGH. |
  | A2 | `regex` crate not yet in Cargo.toml | Q9 / code example | If wrong: minor — just use it. Researcher did not verify Cargo.toml deps. Planner should check before Plan 02 implementation. |
  | A3 | `step_count` calculation: count of H2 sections where `old < version <= new` (exclusive of old, inclusive of new) | Q11 / Q3 corpus | If wrong: off-by-one in question text. Locked in CONTEXT.md D-07; aligns with Discussion Log Round 2. Confidence: HIGH (locked in CONTEXT). |
  | A4 | Test seam env var `SPT_OVERRIDE_PKG_VERSION` is acceptable codebase pattern | Q9 | If user/maintainer rejects test-only env var approach, fall back to a `cfg(test)` module replacement (slightly more invasive). Confidence: MEDIUM — pattern matches `SPT_HOME` precedent but is novel for VERSION specifically. |
  | A5 | DEPLOY.ps1 `-Bump` insertion sites at lines 215 (abort gate) and 287 (stub append) | Q7 | If wrong: stub appended at wrong sequence point may either be staged into the bump commit (bad) or fail to detect dirty state (bad). Mitigation: Plan 01 must dry-run DEPLOY.ps1 and verify the sequence holds. Confidence: MEDIUM — based on reading the script, not on test execution. |
  | A6 | `$OWL` env var persists across Claude Code turns via `~/.claude/settings.json` injection | Q5 | If wrong: Claude's `Bash($OWL version-remind 1.10.9)` invocation would fail with "$OWL: command not found". Mitigation: every existing `/spt:*` skill uses `$OWL` and works — strong empirical evidence. Confidence: HIGH. |

  ---

  ## Open Questions

  1. **Should the integration test golden-fixture include a multi-version scenario?**
     - What we know: single-step and multi-step are both required test cases (Q3, integration corpus).
     - What's unclear: how many H2 sections to include in the test fixture CHANGELOG.md, and whether to snapshot the full `reason` string or just key fields.
     - Recommendation: Snapshot just `<old>`, `<new>`, `<step_count>` and the invocation string. Body content varies with CHANGELOG.md edits and would make tests fragile.

  2. **Should `version-remind` log its action to stderr for debugging?**
     - What we know: success exit 0, failure exit 1.
     - What's unclear: whether a single-line stderr "rolled back sentinel to 1.10.9" is helpful or noise.
     - Recommendation: emit a single line on success (matches `READY:id` / `SENT:id` status-tag convention from CLAUDE.md §Conventions). E.g., `REMINDED:1.10.9`. Final wording in Plan 02.

  3. **Should the block body include the dates in the AUQ question text, or only in the AUQ description / pre-question framing?**
     - What we know: D-07 says dates surface in "AUQ description / Claude's pre-question framing, not in the question line itself".
     - What's unclear: AskUserQuestion's API may or may not have a description slot per option vs a global header.
     - Recommendation: Claude renders dates in the prose surface BEFORE invoking AUQ (e.g., "v1.10.9 (released 2026-05-15) → v1.10.10 (released 2026-05-16). Print changelog?"). AUQ itself uses just the version-number question.

  ---

  ## State of the Art

  | Old Approach (in REQUIREMENTS.md as written) | Current Approach (per CONTEXT.md D-01..D-12) | When Changed | Impact |
  |--------------|------------------|--------------|--------|
  | SessionStart hook emission | Stop hook emission | Phase 34 discuss-phase | Pivots VERS-02 from `plugin_session_start.rs` to `hook_idle.rs`; user is between turns when prompted, not at session boot |
  | 3-option AUQ (yes/no/dismiss) | 4-option AUQ (yes-full / yes-highlights / remind / skip) | Phase 34 discuss-phase | VERS-04 amendment; richer UX, splits "yes" into "all detail" vs "discretion summary" |
  | Multi-version = version-list + pointer (never full prose) | Multi-version = same 4-option as single; `Yes, full` renders all interim H2 sections verbatim | Phase 34 discuss-phase | VERS-06 amendment; user-driven choice rather than format-driven constraint |
  | Sentinel write coupled to user response | Sentinel pre-written at hook entry; `version-remind` rolls back on `Remind me later` | Phase 34 discuss-phase Round 5 | Inverts ack/dismiss flow — most-used paths (Yes-full / Yes-highlights / Skip) have zero round-trip; less-common Remind pays the cost |
  | `last-seen-version.json` inside `owlery/` | `last-seen-version.json` SIBLING to `owlery/` | VERS-01 (original requirement) | Per VERS-01 wording; respected in D-05 |

  **No deprecated items.** Phase 34 is greenfield.

  ---

  ## Plan-Boundary Recommendation Summary

  **3 plans** in linear sequence:

  | Plan | Title | Touches | Tests |
  |------|-------|---------|-------|
  | Plan 01 | Doc amendment + CHANGELOG.md genesis + DEPLOY.ps1 stub/sync | REQUIREMENTS.md, ROADMAP.md, CHANGELOG.md (new), docs/DEPLOY.ps1 | DEPLOY.ps1 dry-run; manual review |
  | Plan 02 | Rust hook + sentinel + parser + `version-remind` subcommand | src/owl/version_changelog.rs (new), src/owl/hook_idle.rs, src/main.rs (subcommand wiring), tests/version_changelog.rs | Unit tests for parser corpus; integration tests for hook emission |
  | Plan 03 | Block body authoring + end-to-end integration test + UAT | src/owl/version_changelog.rs (block-body template), tests/version_changelog.rs (snapshot), tests/golden/ | Golden snapshot; manual UAT against real Claude Code |

  **Dependencies:** 01 → 02 → 03 (linear). Plan 02 needs CHANGELOG.md to exist (for parser tests against real content). Plan 03 needs Plan 02's payload-builder helper.

  ---

  ## Open Risks / Blockers for Planner

  1. **(LOW)** DEPLOY.ps1 stub-append site exact insertion may need adjustment after Plan 01 dry-run. Recommend: dry-run before committing the script change.
  2. **(LOW)** `regex` crate availability — verify before Plan 02 implementation. Easy fix if missing.
  3. **(LOW)** Yanked-version handling deferred (parser rejects `## [1.10.10] - 2026-05-16 (yanked)`). If a real yank ever happens, author edits CHANGELOG.md to remove the H2 line entirely. Document as a constraint in CHANGELOG.md authoring guidelines.
  4. **(MEDIUM)** AUQ-instructions wording in block body (Plan 03) needs careful authoring to ensure Claude reliably invokes the right tools in the right order. Recommend: explicit numbered steps inside `<instructions>` element.
  5. **(LOW)** The block-body XML shape (Q11 recommendation) is researcher's pick — planner is empowered to revise. Constraint: must carry `old`, `new`, `old_date`, `new_date`, `step_count`, and the exact `$OWL version-remind <old>` invocation string verbatim.

  ---

  ## Appendix A: CHANGELOG.md Genesis Scope (Plan 01 Step 2)

  Versions identified from `git log --grep='chore: bump'` (38 entries):

  ```
  v1.10.10  (most recent — Phase 33 completion era)
  v1.10.9
  v1.10.8
  v1.10.7
  v1.10.6
  v1.10.5
  v1.10.4
  v1.10.3
  v1.10.2
  v1.10.1
  v1.10.0
  v1.9.15
  v1.9.14
  v1.9.13
  v1.9.12
  v1.9.10  (note: 1.9.11 missing — skip in CHANGELOG)
  v1.9.9
  v1.9.8
  v1.9.7
  v1.9.5   (note: 1.9.6 missing — skip)
  v1.9.2   (note: 1.9.0/1/3/4 missing — skip)
  v1.8.11
  v1.8.10
  v1.8.9
  v1.8.8
  v1.8.7
  v1.8.6
  v1.8.5
  v1.8.4
  v1.8.3
  v1.8.2
  v1.8.1
  v1.8.0
  v1.7.5
  v1.7.4
  v1.7.3
  v1.7.2
  v1.5.4   (older format: "bump plugin version to 1.5.4 and add test artifacts")
  ```

  **Author note for Plan 01:** Per D-09, author curated prose per version using `git log <range>` to scope the changes between each bump. The CHANGELOG.md is NOT a literal log dump — it is a human-readable summary. Skipped versions (1.9.11, 1.9.6, etc.) need no entry. Format: Keep-a-Changelog 1.1.0; H2 `## [X.Y.Z] - YYYY-MM-DD`; body uses bullet lists or prose paragraphs (author's discretion — parser doesn't care about body structure).

  ---

  ## Sources

  ### Primary (HIGH confidence)
  - `~/.claude/reference_docs/claude-code-hooks.md` — Stop hook schema, environment variables, exit codes
  - `src/owl/hook_idle.rs` — Stop hook emission site, OWL_ECHO_COMMUNE guard pattern
  - `src/common/owlery.rs:18-32, 65, 248` — spt_home, owlery_dir, atomic_write_string
  - `docs/DEPLOY.ps1:154-292` — `-Bump` flow, sync step
  - `.planning/phases/34-version-change-changelog/34-CONTEXT.md` — 12 locked decisions (D-01..D-12)
  - `.planning/phases/34-version-change-changelog/34-DISCUSSION-LOG.md` — alternatives + rationale
  - `.planning/REQUIREMENTS.md` — VERS-01..VERS-09 + Out-of-Scope clauses

  ### Secondary (MEDIUM-HIGH confidence — multi-source verified)
  - [Anthropic Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) — Stop hook output schema, environment variables
  - [disler/claude-code-hooks-mastery](https://github.com/disler/claude-code-hooks-mastery) — Stop hook `decision:"block"` semantics, ecosystem patterns
  - [Anthropic blog: How to configure hooks](https://claude.com/blog/how-to-configure-hooks) — Stop hook reason propagation
  - [Claudefa.st Stop Hook task enforcement](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement) — reason field "fed back to Claude"
  - [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — format specification

  ### Tertiary (LOW confidence — single-source / training data)
  - Rust `std::fs::rename` Windows behavior — verified via [doc.rust-lang.org](https://doc.rust-lang.org/std/fs/fn.rename.html) and [Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-technet-forums/449bb49d-8acc-48dc-a46f-0760ceddbfc3); HIGH confidence on same-volume NTFS atomicity, LOWER on concurrent-write edge cases

  ---

  ## Metadata

  **Confidence breakdown:**
  - Stop hook `decision:"block"` semantics (Q1/Q2): HIGH (4+ sources agree; ecosystem-wide pattern; CONTEXT.md design is sound)
  - Standard stack: HIGH (no new deps; all existing crates)
  - Architecture (3-plan boundary): HIGH (mirrors Phase 31/32/33 amendment-first pattern)
  - Parser corpus: HIGH (test cases derived from Keep-a-Changelog spec + common edge cases)
  - DEPLOY.ps1 stub injection sites: MEDIUM (based on script reading, not dry-run execution)
  - Test seam (SPT_OVERRIDE_PKG_VERSION): MEDIUM (novel pattern; need user confirm before Plan 02)
  - Pitfalls: HIGH (derived from established codebase patterns: best-effort IO, recursion guard, atomic write)

  **Research date:** 2026-05-17
  **Valid until:** 2026-06-16 (30 days — Claude Code hooks API is stable-ish but new event types added periodically)

  ### Patterns Map
  # Phase 34: Version-Change Changelog - Pattern Map

  **Mapped:** 2026-05-17
  **Files analyzed:** 8 (4 new, 4 modified)
  **Analogs found:** 8 / 8

  ## File Classification

  | New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
  |-------------------|------|-----------|----------------|---------------|
  | NEW `src/owl/version_changelog.rs` | utility (sentinel I/O + parser + payload builder) | file-I/O + transform | `src/common/owlery.rs:248-255` (atomic_write_string) + `src/owl/hook_idle.rs:71-98` (sentinel read pattern) | exact (composite) |
  | NEW `owl version-remind <old>` subcommand | controller (one-shot CLI handler) | request-response (argv → file write) | `src/cli.rs:142-153` + `src/owl/mod.rs:116-130` (`NewAlarm`) | exact |
  | NEW repo-root `CHANGELOG.md` | config (static asset) | n/a | `psyche.md` (repo root) + no `include_str!` (D-11) | placement-only analog |
  | NEW `tests/version_changelog.rs` | test (integration) | event-driven (subprocess + stdin) | `tests/auto_pick_integration.rs:1-120` (SPT_HOME sandbox + assert_cmd) | exact |
  | MOD `src/owl/hook_idle.rs` | controller (Stop hook) | event-driven (hook stdin → JSON stdout) | self — append at run() top mirroring own `OWL_ECHO_COMMUNE` guard (lines 64-69) | self-reference |
  | MOD `docs/DEPLOY.ps1` | utility (build script) | batch (file rewrite + verify) | `docs/DEPLOY.ps1:246-274` (regex-replace-then-verify) + `:359-370` (Copy-Item to marketplace) | exact |
  | MOD `.planning/REQUIREMENTS.md` | config (spec doc) | n/a | Phase 31 D-11 / Phase 33 D-03/D-04 amendment commit | structural |
  | MOD `.planning/ROADMAP.md` | config (spec doc) | n/a | Phase 31 D-11 / Phase 33 D-03/D-04 amendment commit | structural |

  ---

  ## Pattern Assignments

  ### NEW `src/owl/version_changelog.rs` (utility, file-I/O + transform)

  **Analog A — sentinel atomic write** (`src/common/owlery.rs:248-255`):
  ```rust
  pub fn atomic_write_string(path: &std::path::Path, body: &str) -> std::io::Result<()> {
      let tmp = path.with_file_name(format!(
          "{}.tmp",
          path.file_name().and_then(|s| s.to_str()).unwrap_or("epoch")
      ));
      fs::write(&tmp, body)?;
      fs::rename(&tmp, path)
  }
  ```
  **Reuse verbatim.** Sentinel path: `owlery::spt_home().join("last-seen-version.json")`. Body: `serde_json::to_string(&SentinelV1 { version: new })?`.

  **Analog B — sentinel read with graceful degrade** (`src/owl/hook_idle.rs:71-83`):
  ```rust
  let info_path = owlery::info_file(owl_id);
  let content = match std::fs::read_to_string(&info_path) {
      Ok(c) => c,
      Err(_) => return,
  };
  let info: types::InfoJson = match serde_json::from_str(&content) {
      Ok(i) => i,
      Err(_) => return,
  };
  ```
  **Copy shape.** For sentinel: missing-file (first-install) → silently write NEW + skip emission; malformed JSON → log nothing, treat as missing.

  **Analog C — compile-time version source** (`src/owl/poll.rs:75`, `setup.rs:35`, `live/stop.rs:199`):
  ```rust
  env!("CARGO_PKG_VERSION")
  ```
  **Use identically.** Stored result for sentinel comparison; NEVER read runtime `plugin.json`.

  **Sentinel struct (locked per CONTEXT specifics):**
  ```rust
  #[derive(serde::Serialize, serde::Deserialize)]
  struct SentinelV1 { version: String }
  ```

  **CHANGELOG parser regex (locked):** `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$` — case-sensitive, rejects `[Unreleased]`, ignores link-reference lines (`[1.2.3]: <url>` form). No external `regex` crate dep — use the `regex` crate already in `Cargo.toml` if present; otherwise hand-roll (mirror `tests/skill_hints.rs:29-56` zero-dep YAML parser style).

  **Unit-test pattern** (co-located `#[cfg(test)]`, mirror `src/owl/hook_idle.rs:100-264`):
  - `ENV_LOCK: Mutex<()>` static + `EnvSnapshot` RAII for `SPT_HOME` mutation (lines 108-135)
  - `setup_spt_home(&tmp)` helper (lines 162-165)
  - Per-test tempdir via `tempfile::tempdir()`

  ---

  ### NEW `owl version-remind <old>` subcommand (controller, request-response)

  **Clap registration analog** (`src/cli.rs:142-153` — `NewAlarm`, the most recent positional-arg subcommand):
  ```rust
  /// Schedule a one-shot timed reminder ...
  #[command(name = "new-alarm")]
  NewAlarm {
      self_id: String,
      time_spec: String,
      ...
  },
  ```

  **Pattern to copy for Phase 34** — add to `src/cli.rs` `Commands` enum (no `hide = true`; visible like `NewAlarm`):
  ```rust
  /// Roll the last-seen version sentinel back to <old> so the next Stop
  /// hook re-fires the version-change prompt. Invoked by Claude when the
  /// user picks `Remind me later` from the AUQ.
  #[command(name = "version-remind")]
  VersionRemind {
      /// The previous version to roll the sentinel back to (e.g. `1.10.9`).
      /// MUST match the `old` value from the <spt-version-changelog> block
      /// Claude received — the subcommand cannot infer this otherwise.
      old: String,
  },
  ```

  **Dispatch arm** (`src/owl/mod.rs:37-160` `handle_command` match — add alongside `Commands::EchoCommune` at ~line 153):
  ```rust
  Some(Commands::VersionRemind { old }) => {
      version_changelog::run_version_remind(&old);
  }
  ```

  **Handler body** (~10 lines, mirrors `src/common/owlery.rs:264-269` `write_last_commune_epoch`):
  ```rust
  pub fn run_version_remind(old: &str) {
      let path = owlery::spt_home().join("last-seen-version.json");
      let body = serde_json::to_string(&SentinelV1 { version: old.to_string() })
          .expect("SentinelV1 serialization is infallible");
      match owlery::atomic_write_string(&path, &body) {
          Ok(_) => std::process::exit(0),
          Err(e) => {
              eprintln!("[owl] version-remind: failed to rewrite {}: {}", path.display(), e);
              std::process::exit(1);
          }
      }
  }
  ```

  **Clap parse-test pattern** (`src/cli.rs:351-411` — co-locate in `#[cfg(test)] mod tests`):
  ```rust
  #[test]
  fn parses_version_remind_positional() {
      let cli = Cli::try_parse_from(["owl", "version-remind", "1.10.9"]).unwrap();
      match cli.command {
          Some(Commands::VersionRemind { old }) => assert_eq!(old, "1.10.9"),
          other => panic!("expected VersionRemind, got {:?}", other),
      }
  }
  ```

  ---

  ### NEW repo-root `CHANGELOG.md` (config, static asset)

  **Placement analog:** `psyche.md` (repo root, copied into plugin meta dir). Phase 34 explicitly diverges from `psyche.md` in ONE respect: NOT embedded via `include_str!` (D-11 — runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`).

  **Format (locked, Keep-a-Changelog 1.1.0):**
  ```markdown
  # Changelog

  ## [1.11.0] - 2026-05-17

  - Added: ...
  - Fixed: ...

  ## [1.10.10] - 2026-05-13

  - ...
  ```

  **Genesis** (Plan 01 step 1, after REQUIREMENTS/ROADMAP amendment commit): author by hand using `git log --oneline --grep='chore: bump'` as the version skeleton. One commit. No auto-regeneration.

  ---

  ### NEW `tests/version_changelog.rs` (test, event-driven integration)

  **Analog:** `tests/auto_pick_integration.rs:1-120` — the closest existing subprocess-driven hook test.

  **Imports + harness pattern** (lines 42-99 verbatim):
  ```rust
  use std::fs;
  use std::sync::Mutex;
  static ENV_LOCK: Mutex<()> = Mutex::new(());

  struct SptHomeGuard { prior: Option<String> }
  impl SptHomeGuard {
      fn set(path: &std::path::Path) -> Self {
          let prior = std::env::var("SPT_HOME").ok();
          std::env::set_var("SPT_HOME", path);
          Self { prior }
      }
  }
  impl Drop for SptHomeGuard {
      fn drop(&mut self) {
          match &self.prior {
              Some(v) => std::env::set_var("SPT_HOME", v),
              None => std::env::remove_var("SPT_HOME"),
          }
      }
  }
  ```

  **Hook invocation pattern** (lines 81-99 — `assert_cmd::Command::cargo_bin("owl")`):
  ```rust
  let mut cmd = assert_cmd::Command::cargo_bin("owl").expect("owl binary must be built");
  cmd.arg("hook-idle")
      .env("SPT_HOME", tmp)
      .env("HOME", tmp)
      .env("USERPROFILE", tmp)
      .env_remove("OWL_ECHO_COMMUNE");
  // stdin pipe via .write_stdin(json) per assert_cmd
  ```

  **Perch-fixture pattern** (lines 105-120 verbatim — `write_live_perch`):
  - create `{spt_home}/owlery/{id}/ready`
  - write `info.json` with `parent_pid = std::process::id() as u64`
  - so `find_perch_by_parent_pid` resolves to this perch when the subprocess inherits the test PID as its ppid

  **Test cases to cover (per CONTEXT Claude's Discretion → Tests bullet):**
  1. version-mismatch single-step → emits block
  2. version-mismatch multi-step → emits block with `step_count > 1`
  3. first-install (no sentinel) → silent write, no emit
  4. sentinel == compile-time version → no emit
  5. `OWL_ECHO_COMMUNE=1` → no emit (recursion guard)
  6. no perch resolved → no emit
  7. `version-remind 1.10.9` → sentinel rewritten round-trip
  8. CHANGELOG.md parser: happy path, `[Unreleased]` reject, malformed H2 graceful, link-reference lines ignored

  ---

  ### MOD `src/owl/hook_idle.rs` (controller, event-driven)

  **This is a self-reference — append a new block at the TOP of `run()` between perch resolution (line 32) and `set_idle_ready` (line 39).**

  **Recursion guard (mirror lines 64-69 EXACTLY — MUST be first):**
  ```rust
  if std::env::var("OWL_ECHO_COMMUNE")
      .map(|v| !v.is_empty())
      .unwrap_or(false)
  {
      // skip version-change check in haiku echo-commune subprocess
  } else {
      // version-change emission block here
  }
  ```
  **OR** reorder so OWL_ECHO_COMMUNE guard runs at the very top of `run()` itself (cleaner — the existing guard at line 64-69 then becomes a no-op or is removed since `spawn_echo_commune_if_live` is called after). Planner's choice; CONTEXT D-03 accepts either.

  **Block emission (D-04 — Stop hook output schema):**
  ```rust
  // Stop hooks have no hookSpecificOutput.additionalContext channel
  // (per the hook_idle.rs:1-5 file-doc comment). Phase 34 uses
  // decision:"block" + reason instead. Research-confirmed: reason
  // text propagates to Claude as re-engagement context.
  let response = serde_json::json!({
      "decision": "block",
      "reason": format!(
          "<spt-version-changelog>\n{}\n</spt-version-changelog>",
          payload_xml
      ),
  });
  println!("{}", serde_json::to_string(&response).unwrap());
  ```
  **NOTE:** This is the channel Phase 34 picks — NOT the Pattern S2 `hookSpecificOutput` envelope from `plugin_session_start.rs:139-155` (`emit_auto_pick`). The S2 pattern is for SessionStart/PreToolUse/UserPromptSubmit hooks. Stop's only context-injection channel is `decision:"block"` + `reason`.

  **Sentinel write site (D-05):**
  ```rust
  // Write the NEW version BEFORE emitting the block. The `Remind me later`
  // AUQ option calls `$OWL version-remind <old>` to roll back; all other
  // branches (Yes-full / Yes-highlights / Skip) leave the sentinel at NEW.
  let _ = owlery::atomic_write_string(
      &owlery::spt_home().join("last-seen-version.json"),
      &serde_json::to_string(&SentinelV1 { version: current.to_string() }).unwrap(),
  );
  ```

  **SessionStart's `emit_auto_pick` envelope for comparison** (`plugin_session_start.rs:139-155`):
  ```rust
  pub(crate) fn emit_auto_pick(pick_spec_json: &str) {
      let context = format!(
          "<spt-live-auto-pick>\n{}\n</spt-live-auto-pick>",
          pick_spec_json
      );
      let response = serde_json::json!({
          "hookSpecificOutput": {
              "hookEventName": "SessionStart",
              "additionalContext": context,
          }
      });
      let out = serde_json::to_string(&response).unwrap_or_else(|_| {
          r#"{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":""}}"#.to_string()
      });
      println!("{}", out);
  }
  ```
  **Phase 34 does NOT mirror this shape.** Stop uses `decision:"block"` per the file-doc comment at `hook_idle.rs:1-5`. Confirm semantics during research per CONTEXT D-04 / Open Question.

  ---

  ### MOD `docs/DEPLOY.ps1` (utility, batch file rewrite)

  **Analog A — stub-injection (regex-replace-then-verify)** at `docs/DEPLOY.ps1:246-274`:
  ```powershell
  # Regex-rewrite preserves existing formatting (indent, key order, trailing newline)
  $pattern = '"version"\s*:\s*"' + [regex]::Escape($CurrentVersion) + '"'
  $replacement = '"version": "' + $NewVersion + '"'
  $newRaw = [regex]::Replace($PluginRaw, $pattern, $replacement)
  if ($newRaw -eq $PluginRaw) {
      throw "Failed to locate ""version"": ""$CurrentVersion"" in plugin.json -- aborting bump"
  }
  [System.IO.File]::WriteAllText($PluginJson, $newRaw)

  # Verify the write took
  $verify = (Get-Content $PluginJson -Raw | ConvertFrom-Json).version
  if ($verify -ne $NewVersion) {
      throw "plugin.json post-write reads version='$verify', expected '$NewVersion'"
  }
  ```

  **Pattern to copy for CHANGELOG.md stub-append (D-10 — slots in after line 274, before commit at line 279):**
  ```powershell
  # Phase 34 D-10: append stub H2 to repo-root CHANGELOG.md (no commit)
  $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  $ChangelogRaw  = Get-Content $ChangelogPath -Raw
  $todoMarker    = "TODO: changelog entry"
  # Abort if PRIOR -Bump's stub for current version is still un-filled
  if ($ChangelogRaw -match [regex]::Escape("## [$CurrentVersion]") -and `
      $ChangelogRaw -match [regex]::Escape($todoMarker)) {
      Write-Host "ERROR: CHANGELOG.md contains '$todoMarker' for v$CurrentVersion." -ForegroundColor Red
      Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
      exit 1
  }
  $today = Get-Date -Format 'yyyy-MM-dd'
  $stub = "`n## [$NewVersion] - $today`n`n- $todoMarker`n"
  $newChangelog = $ChangelogRaw.TrimEnd("`r","`n") + $stub
  [System.IO.File]::WriteAllText($ChangelogPath, $newChangelog)
  Write-SubStep "Appended CHANGELOG.md stub for v$NewVersion (fill in + commit before next -Bump)"
  # Intentionally NOT staged/committed — gates next -Bump on author filling
  ```

  **Analog B — sync to marketplace (Copy-Item)** at `docs/DEPLOY.ps1:359-370`:
  ```powershell
  Invoke-Mutation "Copy owl.exe -> $MarketSpt" {
      Copy-Item -Path $OwlBinary -Destination (Join-Path $MarketSpt 'owl.exe') -Force
  }
  Invoke-Mutation "Copy plugin.json -> $MarketPluginMeta" {
      Copy-Item -Path $PluginJson -Destination (Join-Path $MarketPluginMeta 'plugin.json') -Force
  }
  ```

  **Pattern to copy for CHANGELOG.md sync (D-11 — `{Marketplace}/plugin/spt/CHANGELOG.md`, sibling to `plugin.json`, NOT inside `.claude-plugin/`):**
  ```powershell
  # Phase 34 D-11: sync repo-root CHANGELOG.md to marketplace plugin root
  Invoke-Mutation "Copy CHANGELOG.md -> $MarketSpt" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $MarketSpt 'CHANGELOG.md') -Force
  }
  ```
  **Insert site:** at line 367 (after `Copy hooks/* -> $MarketHooks`, before `Copy plugin.json`).

  **Cache-tier sync** (mirror at `docs/DEPLOY.ps1:489-500` — same `Copy-Item -Force` shape):
  ```powershell
  Invoke-Mutation "Copy CHANGELOG.md -> $CacheVer" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $CacheVer 'CHANGELOG.md') -Force
  }
  ```
  **Insert site:** at line 497 (after `Copy hooks/*`, before `Copy plugin.json -> $CacheVer/.claude-plugin`).

  ---

  ### MOD `.planning/REQUIREMENTS.md` + `.planning/ROADMAP.md` (config, spec doc)

  **Analog:** Phase 31 D-11 + Phase 33 D-03/D-04 amendment-commit precedent. Plan 01 step 0 is a single commit that rewrites VERS-02/04/06/08/09 per CONTEXT D-12 and ROADMAP SC1/SC2/SC5 per CONTEXT D-12 — BEFORE any Rust code lands. Commit message: `docs(34): amend VERS-02..09 + SC1/2/5 for Stop-hook pivot`.

  ---

  ## Shared Patterns

  ### Recursion Guard (OWL_ECHO_COMMUNE)
  **Source:** `src/owl/hook_idle.rs:64-69`
  **Apply to:** version-change emission block + sentinel write (must precede both)
  ```rust
  if std::env::var("OWL_ECHO_COMMUNE")
      .map(|v| !v.is_empty())
      .unwrap_or(false)
  {
      return;
  }
  ```

  ### Atomic File Write
  **Source:** `src/common/owlery.rs:248-255`
  **Apply to:** sentinel write (Stop-hook path AND `version-remind` handler)
  ```rust
  owlery::atomic_write_string(&path, &body)
  ```

  ### Sentinel Read (graceful degrade)
  **Source:** `src/owl/hook_idle.rs:71-83` pattern
  **Apply to:** Stop-hook sentinel read
  - Missing file → treat as first-install (write NEW, no emit)
  - Malformed JSON → treat as missing (best-effort, no panic)

  ### Compile-Time Version
  **Source:** `src/owl/poll.rs:75`, `setup.rs:35`, `live/stop.rs:199`
  **Apply to:** all version comparisons in Phase 34 code
  ```rust
  env!("CARGO_PKG_VERSION")
  ```
  **NEVER read runtime `plugin.json`** (per VERS-02 amended wording).

  ### Perch-Resolution Gate
  **Source:** `src/owl/hook_idle.rs:24-32` (already in place in `hook_idle.rs::run()`)
  **Apply to:** version-change emission — only fires AFTER `owl_id = ... or return`. No new predicate code per D-02.

  ### Hook Stdin Parsing
  **Source:** `src/common/hook_output.rs:26-29` (`parse_hook_stdin`)
  **Apply to:** any new hook-handler stdin reads (version-change code doesn't need it — it reads no stdin fields beyond what perch-resolution already consumes).

  ### Test Env-Mutation Hygiene
  **Source:** `src/owl/hook_idle.rs:108-135` (in-file unit tests) + `tests/auto_pick_integration.rs:46-65` (integration tests)
  **Apply to:** all unit + integration tests touching `SPT_HOME`
  - `static ENV_LOCK: Mutex<()> = Mutex::new(())`
  - `EnvSnapshot::capture()` RAII (Drop restores)
  - `cmd.env_remove(...)` for all gate-listed env vars before subprocess invocation

  ### DEPLOY.ps1 Mutation Wrapping
  **Source:** `docs/DEPLOY.ps1:359-370` (`Invoke-Mutation "...desc..." { Copy-Item ... }`)
  **Apply to:** every new file write in DEPLOY.ps1 — wrap in `Invoke-Mutation` so `-DryRun` is auto-respected and step descriptions print uniformly.

  ---

  ## Risks: Deviations from Established Patterns

  1. **Stop-hook output channel — `decision:"block"` + `reason` is unprecedented in this codebase.** Every other hook (`hook_check`, `hook_prompt`, `plugin_session_start`) uses `hookSpecificOutput.additionalContext`. The file-doc comment at `hook_idle.rs:1-5` explicitly says Stop has no such schema. Phase 34 picks `decision:"block"` per CONTEXT D-04 but this is a research blocker — confirm with `~/.claude/reference_docs/claude-code-hooks.md` that `reason` text actually propagates to Claude on re-engagement. **Fallback if broken:** PreToolUse (next tool call) or revisit UserPromptSubmit. Flagged in CONTEXT Deferred.

  2. **Sentinel lives at `$SPT_HOME/last-seen-version.json`, NOT under `$SPT_HOME/owlery/`.** Per VERS-01 the sentinel is a SIBLING of `owlery/`, breaking the "everything spt-related lives under `owlery_dir()`" convention. There is no existing precedent for files at `spt_home()` root. Other dirs (`bin/`, `psyches/`, `pulses/`, `status/`, `logs_latest/`) are all subdirs. CONTEXT locks this; the sentinel is intentionally OUT of owlery to avoid coupling with perch lifecycle (e.g., `owl cleanup`). Add helper `pub fn sentinel_path() -> PathBuf { spt_home().join("last-seen-version.json") }` to `owlery.rs` to keep the convention encapsulated.

  3. **`version-remind` is the first subcommand whose only purpose is to mutate a single sentinel file.** Most subcommands wrap richer logic (`Setup`, `Poll`, `Live`, `Send`). The closest precedent is `NewAlarm` (one-shot file write) but `NewAlarm` also threads through resolve + pulse-dir registration. `version-remind` is ~10 lines and a single fs write — keep it tiny; don't over-engineer.

  4. **No `regex` crate currently in `Cargo.toml`** (verify during planning). The CHANGELOG.md H2 parser regex is small enough to hand-roll (5-10 lines, mirror the parser style at `tests/skill_hints.rs:29-56` for zero-dep parsing). Adding `regex` for this one use would be a heavy dep; planner should default to hand-rolled.

  5. **DEPLOY.ps1 stub-injection does not commit.** Every other DEPLOY.ps1 mutation either commits immediately (`-Bump` at line 281) or is part of the marketplace push (line 403). The Phase 34 CHANGELOG.md stub-append is deliberately uncommitted to gate the next `-Bump` on author action — this is a NEW interaction shape for DEPLOY.ps1. Surface it clearly in the post-bump notice so users aren't surprised by `git status` showing dirty CHANGELOG.md after `-Bump`.

  6. **CHANGELOG.md is read at runtime from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` rather than embedded via `include_str!`** (D-11). `psyche.md` is the established precedent and IS embedded. CONTEXT calls out the rationale: changelog grows monotonically, binary should not bloat. Tradeoff: runtime file read can fail (missing file, IO error) → must degrade gracefully (skip emission, log silently — mirror the `hook_idle.rs:71-83` read-then-bail pattern).

  7. **AskUserQuestion + `$OWL version-remind <old>` round-trip relies on Claude faithfully passing the literal `old` value from the block payload.** No structural guarantee — purely instructional. Block-body MUST surface the exact invocation string verbatim with `<old>` substituted (CONTEXT D-08). The only enforcement is the `version-remind` subcommand rejecting a missing arg.

  ---

  ## Metadata

  **Analog search scope:** `src/owl/`, `src/common/`, `src/live/`, `tests/`, `docs/DEPLOY.ps1`, `src/cli.rs`, `src/main.rs`
  **Files scanned:** 12 read in full or in targeted ranges
  **Pattern extraction date:** 2026-05-17

  ### Plans to Review
  #### 34-01-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 01
  type: execute
  wave: 1
  depends_on: []
  files_modified:
    - .planning/REQUIREMENTS.md
    - .planning/ROADMAP.md
    - CHANGELOG.md
    - docs/DEPLOY.ps1
  autonomous: true
  requirements:
    - VERS-02
    - VERS-04
    - VERS-06
    - VERS-07
    - VERS-08
    - VERS-09
  user_setup: []

  must_haves:
    truths:
      - "REQUIREMENTS.md VERS-02/04/06/08/09 reflect the Stop-hook pivot + 4-option AUQ + AUTO-03-coexistence-by-hook-separation wording per CONTEXT D-12"
      - "ROADMAP.md Phase 34 Success Criteria 1, 2, 5 reflect the Stop-hook pivot wording per CONTEXT D-12"
      - "Repo-root CHANGELOG.md exists in Keep-a-Changelog 1.1.0 format with an H2 section per historical version bump (38 candidate versions surfaced by `git log --grep='chore: bump'`)"
      - "Running `docs/DEPLOY.ps1 -Bump patch -DryRun` (after this plan) shows the new TODO-marker abort gate and CHANGELOG.md stub-append substeps in the dry-run trace"
      - "Running `docs/DEPLOY.ps1` (deploy without bump) shows a new substep that copies repo-root CHANGELOG.md into `{Marketplace}/plugin/spt/CHANGELOG.md` AND into `{CacheVer}/CHANGELOG.md`"
    artifacts:
      - path: "CHANGELOG.md"
        provides: "Keep-a-Changelog 1.1.0 root document; consumed at runtime by Plan 02's parser via $CLAUDE_PLUGIN_ROOT/CHANGELOG.md"
        contains: "## ["
      - path: ".planning/REQUIREMENTS.md"
        provides: "Amended VERS-02/04/06/08/09 per D-12"
        contains: "Stop hook"
      - path: ".planning/ROADMAP.md"
        provides: "Amended Phase 34 SC1/SC2/SC5 per D-12"
        contains: "Stop hook"
      - path: "docs/DEPLOY.ps1"
        provides: "(a) -Bump TODO-marker abort gate, (b) -Bump CHANGELOG.md stub-append (uncommitted, after the bump commit), (c) CHANGELOG.md sync to MarketSpt + CacheVer"
        contains: "CHANGELOG.md"
    key_links:
      - from: "docs/DEPLOY.ps1 -Bump flow"
        to: "repo-root CHANGELOG.md"
        via: "stub-append after `git commit` at line 282, before any further work"
        pattern: "TODO: changelog entry"
      - from: "docs/DEPLOY.ps1 deploy/sync flow"
        to: "{Marketplace}/plugin/spt/CHANGELOG.md AND {CacheVer}/CHANGELOG.md"
        via: "two new Invoke-Mutation `Copy-Item` blocks"
        pattern: "Copy CHANGELOG.md"
  ---

  <objective>
  Phase 34 doc-amendment-first plan + CHANGELOG.md genesis + DEPLOY.ps1 extension.
  PURE doc/script work — NO Rust code in this plan.

  Purpose: Land three preconditions Plan 02 + Plan 03 depend on:
    1. REQUIREMENTS.md + ROADMAP.md no longer assert SessionStart wording (CONTEXT D-12; mirrors Phase 31 D-11 / Phase 33 D-03/D-04 amendment-first pattern).
    2. Repo-root CHANGELOG.md exists in Keep-a-Changelog 1.1.0 format so Plan 02's parser has real content to consume in unit + integration tests.
    3. DEPLOY.ps1 (a) gates future bumps on a filled CHANGELOG entry and (b) syncs the file to the marketplace + cache so the hook's runtime path (`$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`) resolves.

  Output: 3 commits (amendment, CHANGELOG genesis, DEPLOY.ps1 extension). No Rust touched.
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @docs/DEPLOY.ps1

  <interfaces>
  <!-- Locked invariants this plan must preserve / not contradict. -->

  REQUIREMENTS.md VERS-02 — REPLACEMENT WORDING (CONTEXT D-12):
    "On Stop hook (in `src/owl/hook_idle.rs`), gated on active perch resolution via existing `find_perch_by_session` / `find_perch_by_parent_pid`. Hook compares `env!(\"CARGO_PKG_VERSION\")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`."

  REQUIREMENTS.md VERS-04 — REPLACEMENT WORDING (CONTEXT D-12):
    "AskUserQuestion with four options: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Question text includes `step_count` when > 1."

  REQUIREMENTS.md VERS-06 — REPLACEMENT WORDING (CONTEXT D-12):
    "Multi-version skip surfaces the same 4-option AskUserQuestion as single-step; user-chosen `Yes, full changelog` renders all interim H2 sections verbatim, `Yes, highlights only` defers to agent discretion. The CHANGELOG.md pointer is appended to the highlights body."

  REQUIREMENTS.md VERS-08 — REPLACEMENT WORDING (CONTEXT D-12):
    "Version-change prompt (VERS-04) fires from Stop hook and SessionStart auto-pick (AUTO-03 from Phase 33) fires from SessionStart hook. The two never share a hook invocation; no allow-list reconciliation required."

  REQUIREMENTS.md VERS-09 — REPLACEMENT WORDING (CONTEXT D-12):
    "Single write site at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard. Atomic via existing `atomic_write_string`. Binary handoff (Phase 18.4/18.5) does not race because Stop hooks do not fire during in-flight binary swap (Stop fires between turns, handoff happens between processes)."

  ROADMAP.md Phase 34 SC1 — REPLACEMENT WORDING (CONTEXT D-12):
    "On a fresh install (no `$SPT_HOME/last-seen-version.json` present), on first Stop hook fire in any spt-bearing session, the sentinel silently writes the current `CARGO_PKG_VERSION` and emits NO changelog prompt to the user."

  ROADMAP.md Phase 34 SC2 — REPLACEMENT WORDING (CONTEXT D-12):
    "After the plugin version changes (compiled-in `CARGO_PKG_VERSION` differs from sentinel contents), the next Stop hook fire in an spt-bearing session emits a `<spt-version-changelog>` block via `decision:\"block\"` + `reason`; Claude renders an AskUserQuestion with four options (`Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`); the sentinel is atomically rewritten with the current version at hook entry (before the block emission), so the prompt does not re-fire next session unless the user picks `Remind me later`, which invokes `$OWL version-remind <old>` to roll the sentinel back."

  ROADMAP.md Phase 34 SC5 — REPLACEMENT WORDING (CONTEXT D-12):
    "VERS-04 fires from Stop hook (`hook_idle.rs`); AUTO-03 (from Phase 33) fires from SessionStart hook (`plugin_session_start.rs`); collision is impossible by hook separation. The sentinel write site lives at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard (atomic via existing `atomic_write_string`); Stop hooks do not fire mid-binary-handoff, so a race cannot corrupt it."

  ROADMAP.md Phase 34 SC3, SC4, SC6 — UNCHANGED (CONTEXT D-12).

  CHANGELOG.md format (Keep-a-Changelog 1.1.0, RESEARCH Q10):
    H2 line shape: `## [X.Y.Z] - YYYY-MM-DD`
    Parser regex (Plan 02 will enforce): `^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$`
    NO `[Unreleased]` section (parser rejects it; we author retroactively from git log).
    H3 subsections / bullets / prose paragraphs all acceptable in body (parser is body-content-agnostic).
    Link-reference lines at file bottom are valid per spec but unused; do not add them.

  DEPLOY.ps1 stub format (CONTEXT specifics):
  ```
  ## [{NEW_VERSION}] - {YYYY-MM-DD}

  - TODO: changelog entry
  ```
  Literal `TODO: changelog entry` is the scan token for the abort gate.
  Date computed at bump-time via `Get-Date -Format 'yyyy-MM-dd'`.

  DEPLOY.ps1 insertion sites (verified via Read against current docs/DEPLOY.ps1):
  - Abort gate: insert AFTER existing Cargo.toml dirty-state check (current line 222), BEFORE the `# Read Cargo.toml ...` comment (current line 224). The check runs only when `$Bump` is truthy.
  - Stub append: insert AFTER the `Push-Location $RepoRoot ... } finally { Pop-Location } Write-SubStep "Committed bump: $bumpSha (not pushed)"` block (current line 287), BEFORE `$Version = $NewVersion` (current line 290). CRITICAL: must run AFTER `git commit` so the stub stays in working tree only — Pitfall 5 in RESEARCH.md.
  - Sync to MarketSpt: insert AFTER `Copy hooks/* -> $MarketHooks` block (current lines 365-367), BEFORE `Copy plugin.json -> $MarketPluginMeta` (current lines 368-370).
  - Sync to CacheVer: insert AFTER `Copy hooks/* -> $CacheVer/hooks` block (current lines 495-497), BEFORE `Copy plugin.json -> $CacheVer/.claude-plugin` (current lines 498-500).

  CHANGELOG.md author-time scope (RESEARCH Appendix A): 38 versions surfaced by `git log --oneline --grep='chore: bump'`, ranging from v1.5.4 to current (v1.10.10 or later — verify at author time). Skipped versions (1.9.11, 1.9.6, 1.9.0/1/3/4) get no entry. NOT a literal log dump — human-readable curated prose per version. One commit.
  </interfaces>
  </context>

  <tasks>

  <task type="auto">
    <name>Task 1: Amend REQUIREMENTS.md VERS-02/04/06/08/09 + ROADMAP.md Phase 34 SC1/SC2/SC5 per D-12</name>
    <files>.planning/REQUIREMENTS.md, .planning/ROADMAP.md</files>
    <action>
  Mirror Phase 31 D-11 + Phase 33 D-03/D-04 amendment-commit precedent (CONTEXT canonical_refs).

  In `.planning/REQUIREMENTS.md`, edit the five VERS-* bullets currently at lines 72-79 (verify line numbers by reading the file first — they may have drifted; the canonical anchors are the bullet IDs `**VERS-02**`, `**VERS-04**`, `**VERS-06**`, `**VERS-08**`, `**VERS-09**`):
    - Replace VERS-02 body with the verbatim REPLACEMENT WORDING block in `<interfaces>` above (CONTEXT D-12).
    - Replace VERS-04 body with its REPLACEMENT WORDING.
    - Replace VERS-06 body with its REPLACEMENT WORDING.
    - Replace VERS-08 body with its REPLACEMENT WORDING.
    - Replace VERS-09 body with its REPLACEMENT WORDING.
    - DO NOT touch VERS-01, VERS-03, VERS-05, VERS-07. They remain valid as-written.
    - DO NOT touch the bullet IDs themselves (`- [ ] **VERS-02**:` etc.) — only the prose after the colon.
    - Keep the `*Updated YYYY-MM-DD — ...*` footer convention: append a new line directly under the existing `*Updated 2026-05-16 — FRESH-04 and FRESH-05 struck per Phase 33 CONTEXT D-03; FRESH-06 reworded per D-04.*` line saying: `*Updated 2026-05-17 — VERS-02/04/06/08/09 amended per Phase 34 CONTEXT D-12 (Stop-hook pivot, 4-option AUQ, AUTO-03-coexistence-by-hook-separation).*`

  In `.planning/ROADMAP.md`, edit the Phase 34 Success Criteria block (currently at lines 643-655, verify by reading first; anchor is `### Phase 34: Version-Change Changelog`):
    - Replace SC1 body with the verbatim REPLACEMENT WORDING (CONTEXT D-12).
    - Replace SC2 body with its REPLACEMENT WORDING.
    - Replace SC5 body with its REPLACEMENT WORDING.
    - DO NOT touch SC3, SC4, SC6 — they remain unchanged.
    - DO NOT touch the **Goal**, **Milestone**, **Depends on**, **Requirements**, **Plans:** lines.

  After both edits, run:
  ```
  git diff --stat .planning/REQUIREMENTS.md .planning/ROADMAP.md
  ```
  Verify exactly two files changed.

  Commit (in repo root) with message:
  ```
  docs(34): amend VERS-02/04/06/08/09 + ROADMAP SC1/2/5 per Phase 34 D-12

  Pivots from the original SessionStart wording to Stop-hook + 4-option AUQ
  + AUTO-03-coexistence-by-hook-separation. Mirrors Phase 31 D-11 and
  Phase 33 D-03/D-04 doc-amendment-first commit pattern.

  No code touched in this commit (Plan 01 step 1).
  ```

  DO NOT stage other files; use `git add .planning/REQUIREMENTS.md .planning/ROADMAP.md` explicitly.
    </action>
    <verify>
      <automated>git log -1 --format=%s | grep -q "amend VERS-02/04/06/08/09" &amp;&amp; grep -q "Stop hook" .planning/REQUIREMENTS.md &amp;&amp; grep -q "VERS-04 fires from Stop" .planning/ROADMAP.md</automated>
    </verify>
    <done>Commit landed on HEAD; REQUIREMENTS.md contains the five amended VERS bodies (search confirms `Stop hook` substring); ROADMAP.md SC5 contains the literal "VERS-04 fires from Stop" phrase; no other files in the commit.</done>
  </task>

  <task type="auto">
    <name>Task 2: Author repo-root CHANGELOG.md from git-log version-bump skeleton</name>
    <files>CHANGELOG.md</files>
    <action>
  Per CONTEXT D-09: author by hand. NO `owl changelog backfill` subcommand (deferred). One commit.

  Step A: Generate version skeleton from git log:
  ```
  git log --oneline --grep='chore: bump' > /tmp/version-bumps.txt
  ```
  Expect ~38 entries (RESEARCH Appendix A surfaced this count). Read the output; map each `chore: bump spt plugin to vX.Y.Z` line to a target H2 section. Skipped versions (1.9.11, 1.9.6, 1.9.0/1/3/4) get NO entry — they were never released.

  Step B: For each released version, derive curated prose by inspecting the commit RANGE between consecutive bumps:
  ```
  git log --oneline <previous_bump_sha>..<this_bump_sha>
  ```
  Summarize the user-facing changes (feature adds, fixes, breaking changes if any) in 1-5 bullet points per version. Format guidance:
    - Top-level title: `# Changelog` followed by a one-line description and a link to Keep-a-Changelog 1.1.0 + Semantic Versioning per spec convention.
    - Each version: H2 line `## [X.Y.Z] - YYYY-MM-DD` (date = the bump commit's author date in ISO-8601, get via `git show -s --format=%ad --date=short <sha>`).
    - Body: prose paragraphs OR bullet lists OR H3 subsections (`### Added` / `### Fixed` / etc. per Keep-a-Changelog 1.1.0) — author's discretion; the Plan 02 parser does NOT care about body shape.
    - NEWEST version first, OLDEST last (Keep-a-Changelog convention).
    - NO `[Unreleased]` section — we author retroactively.
    - NO link-reference lines (`[1.10.10]: <url>`) — unused by our parser.

  Step C: Validate format by hand-running the parser regex against each H2 line:
  ```
  grep -E '^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$' CHANGELOG.md | wc -l
  ```
  The match count MUST equal the number of H2 sections you authored. If any H2 line fails this regex, the Plan 02 parser will skip that version → re-author the offending line to match.

  Step D: Read back the file once with `wc -l CHANGELOG.md` and confirm it is non-empty and "reasonable size" (expect 100-400 lines for ~30+ versions of curated prose).

  Step E: Commit:
  ```
  git add CHANGELOG.md
  git commit -m "docs(34): create repo-root CHANGELOG.md (Keep-a-Changelog 1.1.0)"
  ```

  CRITICAL CONSTRAINTS:
  - The CHANGELOG content itself is curated prose — you are authoring history, not auto-generating. Quality bar: each entry should read as a useful release note for a future user.
  - Date format MUST be ISO-8601 `YYYY-MM-DD` with hyphens. Slashes will fail the parser regex.
  - Patch-level semver only (`X.Y.Z`). NO pre-release suffixes (`-rc1`) — parser rejects them per RESEARCH Q10.
  - No commit-message-style prefixes inside the body (no `feat:` / `fix:` prefixes — those go in the git log, not the CHANGELOG).
    </action>
    <verify>
      <!-- N2 fix: research surfaced 38 historical bumps (RESEARCH Appendix A). Raise the
           floor from 20 to 30 to better track reality, AND cross-check against the live
           `git log --grep='chore: bump'` count so the floor self-calibrates if more bumps
           have landed since RESEARCH was written. Skipped versions (1.9.11/6/0/1/3/4) mean
           the H2 count can legitimately be a few less than the raw git-log count — the
           tolerance is `>= (bump_count - 8)` to absorb skips without false negatives. -->
      <automated>test -f CHANGELOG.md &amp;&amp; H2_COUNT=$(grep -cE '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$' CHANGELOG.md) &amp;&amp; test "$H2_COUNT" -ge 30 &amp;&amp; BUMP_COUNT=$(git log --grep='chore: bump' --oneline | wc -l | tr -d ' ') &amp;&amp; test "$H2_COUNT" -ge "$((BUMP_COUNT - 8))" &amp;&amp; ! grep -qE '^## \[Unreleased\]' CHANGELOG.md &amp;&amp; ! grep -qE '^## \[[0-9]+\.[0-9]+\.[0-9]+-' CHANGELOG.md</automated>
    </verify>
    <done>CHANGELOG.md exists at repo root; ≥30 valid Keep-a-Changelog H2 sections (and within 8 of the live `git log --grep='chore: bump'` count); no `[Unreleased]`; no pre-release suffix versions; committed.</done>
  </task>

  <task type="auto">
    <name>Task 3: Extend docs/DEPLOY.ps1 — TODO-marker abort gate + stub-append + CHANGELOG.md sync (MarketSpt + CacheVer)</name>
    <files>docs/DEPLOY.ps1</files>
    <action>
  Four discrete insertions per the verified line citations in `<interfaces>`. Read the file FIRST and confirm the anchor lines before editing — DEPLOY.ps1 evolves between phases.

  === Insertion 1: TODO-marker abort gate (inside -Bump branch) ===

  Pattern source: existing dirty-state checks at docs/DEPLOY.ps1:213-222 (CONTEXT canonical_refs).
  Insert AFTER the existing Cargo.toml dirty-state `if` block (currently ending at line 222 `exit 1 }`), BEFORE the `# Read Cargo.toml and locate its current [package] version` comment (currently line 224).

  Insert this block verbatim:
  ```powershell
      # Phase 34 D-10 abort gate: refuse to bump if the previous -Bump's CHANGELOG.md
      # stub is still un-filled. Scans for the literal TODO marker the stub injects.
      $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
      if (Test-Path $ChangelogPath) {
          $ChangelogContent = Get-Content $ChangelogPath -Raw
          if ($ChangelogContent -match 'TODO: changelog entry') {
              Write-Host "ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump." -ForegroundColor Red
              Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
              exit 1
          }
      } else {
          Write-Host "ERROR: CHANGELOG.md not found at $ChangelogPath" -ForegroundColor Red
          Write-Host "       Phase 34 requires a repo-root CHANGELOG.md; create one before -Bump." -ForegroundColor Red
          exit 1
      }
  ```

  === Insertion 2: CHANGELOG.md stub-append (uncommitted, after `git commit`) ===

  Pattern source: existing Cargo.toml regex-rewrite-then-verify at docs/DEPLOY.ps1:246-274 (CONTEXT canonical_refs).
  CRITICAL ORDERING per Pitfall 5 in RESEARCH.md: stub append MUST run AFTER `git commit` (current line 281) so CHANGELOG.md stays in working tree only — NEVER staged into the bump commit.

  Insert AFTER `Write-SubStep "Committed bump: $bumpSha (not pushed)"` (current line 287), BEFORE the closing `}` of the `-not $DryRun` else branch (current line 288). Wrap in an `if ($DryRun)` / `else` to honor the existing dry-run discipline.

  Actually — to keep the indent and ordering clean, insert AFTER the `} else {` ... `Write-SubStep "Committed bump: $bumpSha (not pushed)"` ... `}` closing brace of the `if ($DryRun) { ... } else { ... }` block (current line 288), and BEFORE `$Version = $NewVersion` (current line 290).

  Insert this block:
  ```powershell
      # Phase 34 D-10 stub-append: drop a TODO H2 into repo-root CHANGELOG.md
      # WITHOUT staging or committing. The next -Bump's abort gate (insertion 1)
      # will refuse to proceed until the author fills the entry and commits it.
      $today = Get-Date -Format 'yyyy-MM-dd'
      $todoMarker = "TODO: changelog entry"
      $stub = "`n## [$NewVersion] - $today`n`n- $todoMarker`n"
      if ($DryRun) {
          Write-Host "    [DRY-RUN] append stub to CHANGELOG.md: ## [$NewVersion] - $today (uncommitted)" -ForegroundColor DarkYellow
      } else {
          $ChangelogRaw = Get-Content $ChangelogPath -Raw
          $newChangelog = $ChangelogRaw.TrimEnd("`r","`n") + $stub
          [System.IO.File]::WriteAllText($ChangelogPath, $newChangelog)
          Write-SubStep "Appended CHANGELOG.md stub for v$NewVersion (fill in + commit before next -Bump)"
          Write-SubStep "  NOTE: CHANGELOG.md is intentionally unstaged. Edit it, then 'git add CHANGELOG.md && git commit'."
      }
  ```

  NOTE: `$ChangelogPath` is in scope from insertion 1's variable declaration. If the variable is not assigned outside the `if ($Bump)` block (insertion 1 lives inside `if ($Bump) { ... }`), redeclare it here:
  ```powershell
      $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  ```

  === Insertion 3: CHANGELOG.md sync -> MarketSpt ===

  Pattern source: existing `Copy-Item -Force` wrapped in `Invoke-Mutation` at docs/DEPLOY.ps1:359-370 (CONTEXT canonical_refs).
  Insert AFTER the `Copy hooks/* -> $MarketHooks` Invoke-Mutation block (current lines 365-367), BEFORE the `Copy plugin.json -> $MarketPluginMeta` block (current lines 368-370).

  Insert this block:
  ```powershell
      Invoke-Mutation "Copy CHANGELOG.md -> $MarketSpt" {
          Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $MarketSpt 'CHANGELOG.md') -Force
      }
  ```

  Per CONTEXT D-11: target is `{Marketplace}/plugin/spt/CHANGELOG.md` — sibling to plugin.json, NOT inside `.claude-plugin/`. `$MarketSpt` already resolves to that directory (verify by reading the variable assignment earlier in the script; do not re-derive).

  === Insertion 4: CHANGELOG.md sync -> CacheVer ===

  Pattern source: existing CacheVer `Copy-Item -Force` blocks at docs/DEPLOY.ps1:489-500 (CONTEXT canonical_refs).
  Insert AFTER the `Copy hooks/* -> $CacheVer/hooks` block (current lines 495-497), BEFORE the `Copy plugin.json -> $CacheVer/.claude-plugin` block (current lines 498-500).

  Insert this block:
  ```powershell
  Invoke-Mutation "Copy CHANGELOG.md -> $CacheVer" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $CacheVer 'CHANGELOG.md') -Force
  }
  ```

  NOTE: this is the cache-tier sync; target is `$CacheVer/CHANGELOG.md` (plugin root, NOT `$CacheVer/.claude-plugin/CHANGELOG.md`). Hook resolves at runtime via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`, and `$CLAUDE_PLUGIN_ROOT` points to `$CacheVer` for plugin-bundled hooks.

  === Verification dry-run ===

  After all four insertions, run:
  ```
  powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun
  ```
  Expect to see (in some order):
    - `[DRY-RUN] Copy CHANGELOG.md -> {MarketSpt path}` (insertion 3)
    - `[DRY-RUN] Copy CHANGELOG.md -> {CacheVer path}` (insertion 4)

  Then run:
  ```
  powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun
  ```
  Expect to see (in order):
    - The new `ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump.` is NOT triggered (CHANGELOG just authored has no TODO marker).
    - `[DRY-RUN] append stub to CHANGELOG.md: ## [X.Y.Z+1] - YYYY-MM-DD (uncommitted)` (insertion 2 dry-run line).
    - The two sync substeps above also surface.

  === Commit ===

  ```
  git add docs/DEPLOY.ps1
  git commit -m "feat(34): DEPLOY.ps1 CHANGELOG.md sync + bump stub/abort gate"
  ```

  Body suggestion:
  ```
  - New abort gate refuses -Bump when prior CHANGELOG stub still has 'TODO: changelog entry' marker.
  - New stub-append drops `## [NEW] - YYYY-MM-DD\n\n- TODO: changelog entry` into CHANGELOG.md AFTER the bump commit, intentionally unstaged so the next -Bump's gate fires until the author fills + commits.
  - New sync substeps copy repo-root CHANGELOG.md to {Marketplace}/plugin/spt/CHANGELOG.md AND to {CacheVer}/CHANGELOG.md (sibling to plugin.json, NOT inside .claude-plugin/) per CONTEXT D-11.
  ```
    </action>
    <verify>
      <!-- PRECONDITION: working tree MUST be clean OR this task MUST run in a tempdir-cloned repo.
           DEPLOY.ps1 -Bump (and even bare -DryRun in some branches) aborts on a dirty Cargo.toml
           dirty-state check; we cannot reliably exercise the -Bump dry-run mid-plan-execution.
           The blocking verify therefore runs the no-bump dry-run only (which surfaces insertions 3+4)
           and uses comment-stripped grep counts (per grep-gate hygiene: bare `grep -c` counts
           self-referential comment lines like the canonical_refs prose, inflating the count).
           Insertion 1's abort gate (literal TODO scan) and insertion 2's stub-append dry-run line
           are observed in the non-blocking <done> below. -->
      <automated>TODO_COUNT=$(grep -v '^[[:space:]]*#' docs/DEPLOY.ps1 | grep -c "TODO: changelog entry") &amp;&amp; test "$TODO_COUNT" -ge 2 &amp;&amp; COPY_COUNT=$(grep -v '^[[:space:]]*#' docs/DEPLOY.ps1 | grep -c "Copy CHANGELOG.md") &amp;&amp; test "$COPY_COUNT" -ge 2 &amp;&amp; powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun 2>&amp;1 | tee /tmp/d.log &amp;&amp; grep -q "Copy CHANGELOG.md -> .*MarketSpt\|Copy CHANGELOG.md -> .*spt" /tmp/d.log &amp;&amp; grep -c "Copy CHANGELOG.md" /tmp/d.log | grep -qE '^[2-9]'</automated>
    </verify>
    <done>docs/DEPLOY.ps1 contains both `TODO: changelog entry` references (gate scan + stub literal) and both `Copy CHANGELOG.md` Invoke-Mutation blocks (MarketSpt + CacheVer); the no-bump `-DryRun` surfaces the two new sync substeps; commit landed.

  NON-BLOCKING OBSERVATIONS (manually verify in a clean-tree environment OR a tempdir clone — do not gate the task on these, since `-Bump patch -DryRun` may abort on the live working tree's dirty state):
  - `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun` (against a clean tree) emits `[DRY-RUN] append stub to CHANGELOG.md: ## [X.Y.Z+1] - YYYY-MM-DD (uncommitted)` (insertion 2).
  - The abort gate (insertion 1) is reachable: temporarily inject `TODO: changelog entry` into CHANGELOG.md, run `-Bump patch -DryRun`, and observe the `ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump.` exit-1 path. Remove the injection after observation.</done>
  </task>

  </tasks>

  <verification>
  After all 3 tasks:

  1. `git log --oneline -5` shows three new commits in order:
     - `docs(34): amend VERS-02/04/06/08/09 + ROADMAP SC1/2/5 per Phase 34 D-12`
     - `docs(34): create repo-root CHANGELOG.md (Keep-a-Changelog 1.1.0)`
     - `feat(34): DEPLOY.ps1 CHANGELOG.md sync + bump stub/abort gate`

  2. `git diff --name-only HEAD~3` shows exactly 4 paths:
     - `.planning/REQUIREMENTS.md`
     - `.planning/ROADMAP.md`
     - `CHANGELOG.md`
     - `docs/DEPLOY.ps1`

  3. `grep -cE '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$' CHANGELOG.md` ≥ 20 (curated history).

  4. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun` exits 0 AND emits both new `Copy CHANGELOG.md` substeps.

  5. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun` exits 0 AND emits the new `[DRY-RUN] append stub to CHANGELOG.md` substep.

  6. Cargo tests untouched: `cargo test --no-run` builds (smoke check that nothing accidentally introduced a Rust syntax break — Plan 01 is pure doc/script, no Rust expected).
  </verification>

  <success_criteria>
  - Plan 01 maps to (post-amendment) VERS-02 (REQUIREMENTS.md wording landed), VERS-04 (wording landed), VERS-06 (wording landed), VERS-07 (CHANGELOG.md exists + sync syncs it), VERS-08 (wording landed), VERS-09 (wording landed).
  - Plan 01 unblocks Plan 02 by establishing CHANGELOG.md with real content for parser unit + integration tests.
  - Plan 01 unblocks future end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6) by installing the maintenance gate.
  - ROADMAP.md Phase 34 SC1, SC2, SC5 read coherently against the implementation Plan 02/03 will deliver.
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | author -> repo | hand-authored CHANGELOG.md content reaches plugin users via deploy; trusted because author IS the user |
  | deploy script -> filesystem | DEPLOY.ps1 writes to marketplace + cache; local-only, no network surface |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-01 | Tampering | CHANGELOG.md author content | accept | Hand-authored prose under git review; no automated injection path |
  | T-34-02 | Tampering | DEPLOY.ps1 abort gate bypass | accept | Local script, single-user trust boundary; the gate is UX-not-security |
  | T-34-03 | Repudiation | Bump commit not including CHANGELOG.md | mitigate | Stub-append runs AFTER `git commit` (insertion 2 ordering, Pitfall 5) so stub stays unstaged and the bump commit is provably clean |
  | T-34-04 | DoS | Abort gate infinite-loop (author cannot escape) | accept | Gate prints clear instructions: "Fill in the entry and commit before re-running -Bump." Author has a direct remedy. |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-01-SUMMARY.md` documenting:
  - Three commit SHAs
  - CHANGELOG.md final H2 section count
  - Any deviations from the line-citation anchors in `<interfaces>` (DEPLOY.ps1 may have drifted; record the actual insertion line numbers used)
  - Confirmation that `-Bump patch -DryRun` surfaces the new substeps in expected order
  </output>

  #### 34-02-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 02
  type: execute
  wave: 2
  depends_on: [34-01]
  files_modified:
    - src/owl/version_changelog.rs
    - src/owl/mod.rs
    - src/owl/hook_idle.rs
    - src/cli.rs
    - src/common/owlery.rs
  autonomous: true
  requirements:
    - VERS-01
    - VERS-02
    - VERS-03
    - VERS-05
    - VERS-09
  user_setup: []

  must_haves:
    truths:
      - "A new `src/owl/version_changelog.rs` module owns sentinel I/O, the Keep-a-Changelog parser, and the payload-XML builder"
      - "`hook_idle.rs::run()` calls the version-change check AFTER perch resolution (lines 25-32) AND after an OWL_ECHO_COMMUNE guard, BEFORE `set_idle_ready` (line 39)"
      - "On version mismatch the hook emits `{\"decision\":\"block\",\"reason\":\"<spt-version-changelog>...</spt-version-changelog>\"}` to stdout"
      - "Sentinel is pre-written to NEW version atomically via `owlery::atomic_write_string` BEFORE the block is emitted; rollback ONLY via `$OWL version-remind <old>`"
      - "First-install path (sentinel missing) silently writes the current version and emits NO block (VERS-03)"
      - "`stop_hook_active=true` from stdin short-circuits the check (RESEARCH Pitfall 4 — defensive infinite-loop guard)"
      - "A new clap subcommand `owl version-remind <old>` writes `{\"version\":\"<old>\"}` to the sentinel and exits 0; missing-arg or non-semver arg exits 1 (RESEARCH security V5)"
      - "A test-only env var `SPT_OVERRIDE_PKG_VERSION` lets unit + integration tests simulate any version without rebuilding (RESEARCH Q9)"
      - "Co-located unit tests in `src/owl/version_changelog.rs::tests` cover the 18-row parser corpus + sentinel read/write round-trips"
      - "`tests/version_changelog.rs` integration tests cover: first-install silent, single-step mismatch, multi-step mismatch, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard, no-perch guard, stop_hook_active guard, version-remind round-trip, version-remind missing-arg, version-remind non-semver-arg"
    artifacts:
      - path: "src/owl/version_changelog.rs"
        provides: "Sentinel + parser + payload builder + `run_version_remind` handler"
        min_lines: 200
      - path: "src/owl/hook_idle.rs"
        provides: "Version-change emission call site at top of `run()` after perch resolution + OWL_ECHO_COMMUNE guard"
        contains: "version_changelog::"
      - path: "src/cli.rs"
        provides: "`VersionRemind { old: String }` clap variant + clap parse-test"
        contains: "VersionRemind"
      - path: "src/owl/mod.rs"
        provides: "Module declaration `pub mod version_changelog;` + dispatch arm `Some(Commands::VersionRemind { old }) => ...`"
        contains: "VersionRemind"
      - path: "src/common/owlery.rs"
        provides: "New helper `pub fn sentinel_path() -> PathBuf` to encapsulate the spt_home().join(\"last-seen-version.json\") expression (RESEARCH Pitfall 2 / Risk #2)"
        contains: "sentinel_path"
      - path: "tests/version_changelog.rs"
        provides: "Integration tests covering all 10 cases above; mirrors `tests/auto_pick_integration.rs` SPT_HOME sandbox + perch-fixture pattern"
        min_lines: 300
    key_links:
      - from: "src/owl/hook_idle.rs::run()"
        to: "src/owl/version_changelog.rs::maybe_emit_version_change_block"
        via: "function call after perch resolution + OWL_ECHO_COMMUNE guard"
        pattern: "version_changelog::maybe_emit"
      - from: "src/cli.rs Commands::VersionRemind"
        to: "src/owl/version_changelog.rs::run_version_remind"
        via: "dispatch arm in src/owl/mod.rs::handle_command"
        pattern: "Commands::VersionRemind"
      - from: "Stop hook block emission"
        to: "sentinel pre-write"
        via: "atomic_write_string is called BEFORE the `println!` that emits the block"
        pattern: "atomic_write_string"
  ---

  <objective>
  Phase 34 Rust internals: sentinel + parser + payload builder + Stop-hook wiring + `version-remind` subcommand.

  Purpose: Land all the deterministic, testable code surface for Phase 34. No user-facing wording yet — the block-body XML scaffold is emitted but the natural-language instructions inside `<instructions>` get authored in Plan 03.

  Output: One new module, three modified files, one new integration-test file, ~30+ tests (parser corpus + sentinel + hook + subcommand + integration). Cargo tests green.
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @.planning/phases/34-version-change-changelog/34-01-SUMMARY.md
  @CHANGELOG.md
  @src/owl/hook_idle.rs
  @src/cli.rs
  @src/owl/mod.rs
  @src/common/owlery.rs

  <interfaces>
  <!-- Verified line citations from Read tool — confirm by re-reading before editing. -->

  src/owl/hook_idle.rs::run() (verified lines 14-43, file may evolve):
    Line 14: `pub fn run() {`
    Lines 17-22: stdin parse + session_id resolve
    Lines 25-32: perch resolution -> early return on None
    Line 34-39: comment + `inbox::set_idle_ready(&owl_id);`
    Line 42: `spawn_echo_commune_if_live(&owl_id);`

  src/owl/hook_idle.rs::spawn_echo_commune_if_live() (verified lines 57-98):
    Lines 64-69: OWL_ECHO_COMMUNE recursion guard pattern (COPY THIS SHAPE EXACTLY)

  src/common/owlery.rs::atomic_write_string (verified line 248):
    Signature: `pub fn atomic_write_string(path: &std::path::Path, body: &str) -> std::io::Result<()>`
    Tmp + rename; NTFS+POSIX same-volume atomic.

  src/common/owlery.rs::spt_home (verified lines 18-32):
    Signature: `pub fn spt_home() -> PathBuf`
    Resolves `$SPT_HOME` env override or platform default; create-if-missing.

  src/cli.rs Commands::NewAlarm (verified lines 139-153) — POSITIONAL-ARG ANALOG:
  ```rust
  #[command(name = "new-alarm")]
  NewAlarm {
      self_id: String,
      time_spec: String,
      ...
  },
  ```

  src/cli.rs Commands::HookIdle (verified line 197):
    `HookIdle,`  // variant only, no fields; dispatch handled in src/owl/mod.rs

  src/owl/mod.rs::handle_command (verified line 37) — DISPATCH ARMS:
    Line 116: `Some(Commands::NewAlarm { self_id, time_spec, message_file, message }) => { ... }`
    Line 143: `Some(Commands::HookIdle) => { ... }`

  src/main.rs (verified): `let cli = Cli::parse(); handle_command(cli.command);` (line 40-41)
    No edits needed to main.rs.

  NO `regex` crate in Cargo.toml — parser MUST be hand-rolled.

  Sentinel JSON shape (LOCKED — CONTEXT specifics):
  ```json
  {"version":"X.Y.Z"}
  ```
  Single field. No timestamp. No plugin name.

  Sentinel path (LOCKED — VERS-01 + CONTEXT):
    `spt_home().join("last-seen-version.json")` — SIBLING to `owlery/`, NOT inside it.

  Compile-time version source (LOCKED — VERS-02, RESEARCH Q9):
  ```rust
  fn current_version() -> String {
      // Test seam: SPT_OVERRIDE_PKG_VERSION lets tests simulate any version
      // without rebuilding. Production paths see env!("CARGO_PKG_VERSION").
      if let Ok(v) = std::env::var("SPT_OVERRIDE_PKG_VERSION") {
          if !v.is_empty() {
              return v;
          }
      }
      env!("CARGO_PKG_VERSION").to_string()
  }
  ```
  Return `String` (not `&'static str`) to avoid the `Box::leak` discussed in RESEARCH Q9. The allocation cost is paid once per hook invocation; immaterial.

  Parser regex (LOCKED — RESEARCH Q10 corpus):
    Regex equivalent: `^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$`
    Hand-rolled MUST:
      - Require literal prefix `## [` at column 0
      - Capture three `\d+` semver components separated by `.`
      - Require literal `] - ` between version and date
      - Capture `\d{4}-\d{2}-\d{2}` ISO-8601 date
      - Allow trailing whitespace, reject any other trailing content
      - Reject `[Unreleased]`, `[1.10]`, `[1.10.10-rc1]`, `# [1.10.10] - ...`, `### [1.10.10] - ...`, `[1.10.10]: <url>`, `## [1.10.10] - 2026/05/16`
    See RESEARCH "CHANGELOG.md Parser — Test Corpus" for the full 18-row case set.

  ChangelogEntry struct (RESEARCH parser API):
  ```rust
  pub struct ChangelogEntry {
      pub version: (u32, u32, u32),
      pub date: String,        // "YYYY-MM-DD" verbatim
      pub body: String,        // content between this H2 and next H2 / EOF, verbatim
      pub line_no: usize,      // 1-indexed line where the H2 lives
  }
  ```

  Public API surface (RESEARCH):
  ```rust
  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry>;
  pub fn sections_between(entries: &[ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&ChangelogEntry>;
  ```
  `sections_between` returns entries E where `old < E.version <= new`, ordered ascending by version. Used by Plan 03 wording and by the payload's `step_count`.

  Block payload shape (LOCKED — CONTEXT D-07 + RESEARCH Q11):
  ```xml
  <spt-version-changelog>
    <old>X.Y.Z</old>
    <new>X.Y.Z</new>
    <old_date>YYYY-MM-DD</old_date>
    <new_date>YYYY-MM-DD</new_date>
    <step_count>N</step_count>
    <changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>
    <invocation_for_remind>$OWL version-remind X.Y.Z</invocation_for_remind>
    <instructions>
      [PLAN 03 OWNS THIS WORDING; Plan 02 emits a placeholder string here]
    </instructions>
  </spt-version-changelog>
  ```
  `<old_date>` / `<new_date>` may be absent if CHANGELOG.md is unresolvable (Pitfall 2 graceful-degrade); emit empty elements `<old_date></old_date>` rather than skipping the tag.

  Plan 02 emits literal placeholder for `<instructions>` block (Plan 03 replaces). Use this exact placeholder so Plan 03 can grep+replace deterministically:
  ```
  [PLAN_03_INSTRUCTIONS_PLACEHOLDER]
  ```

  Recursion guard ordering (LOCKED — CONTEXT D-03 / RESEARCH Pitfall 4):
    OWL_ECHO_COMMUNE guard MUST run BEFORE the version-change check. Stop_hook_active guard MUST also run before the check. Both short-circuit the check; perch resolution is the only predicate that allows the check to proceed.

  Block emission MUST use `serde_json::json!` + `println!` (RESEARCH code examples):
  ```rust
  let response = serde_json::json!({
      "decision": "block",
      "reason": reason_xml,
  });
  println!("{}", serde_json::to_string(&response).unwrap());
  ```
  This writes to stdout; Claude Code reads the JSON envelope and propagates `reason` to Claude on re-engagement (RESEARCH Q1 GREEN).

  CRITICAL — `hook_idle.rs::run()` currently DOES NOT print anything to stdout (it's a side-effect hook). Adding the version-change emission changes that. Document this in the new function's doc-comment.

  Sentinel write ordering (LOCKED — CONTEXT D-05):
    1. Write NEW version to sentinel atomically.
    2. Emit block.
    Order matters: if step 2 fails to print (impossible barring OOM), the sentinel still reflects truth. If step 1 fails, RESEARCH Pitfall 1 says: skip emission AND do not panic. The user sees no prompt this turn; next Stop will re-check.

  `$CLAUDE_PLUGIN_ROOT` resolution (RESEARCH Q4 + Pitfall 2):
  ```rust
  fn resolve_changelog_path() -> Option<std::path::PathBuf> {
      let root = std::env::var("CLAUDE_PLUGIN_ROOT").ok()?;
      let path = std::path::PathBuf::from(root).join("CHANGELOG.md");
      if path.is_file() { Some(path) } else { None }
  }
  ```
  Hook may emit even when CHANGELOG.md is unresolvable — the block then carries empty `<old_date>` / `<new_date>` / `<changelog_path>NOT_RESOLVED</changelog_path>` and `<step_count>` derived from raw version diff (e.g., minor-version delta as a rough proxy). Researcher's recommendation per Pitfall 2.

  Stop_hook_active parsing:
    Extend `hook_output::parse_hook_stdin` (in src/common/hook_output.rs) to capture `stop_hook_active: bool` (default false). If the existing parser doesn't already surface it, add the field. Reuse if present.

  `version-remind` security validation (LOCKED — RESEARCH security V5):
    Validate `<old>` against semver regex `^\d+\.\d+\.\d+$` BEFORE writing the sentinel. Hand-rolled validator (no `regex` crate):
  ```rust
  fn is_plain_semver(s: &str) -> bool {
      let parts: Vec<&str> = s.split('.').collect();
      parts.len() == 3 && parts.iter().all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
  }
  ```
  On non-match: stderr `error: <old version> must match X.Y.Z semver`, exit 1.
  On missing arg: clap handles natively (positional arg required).

  Test-helper for SPT_HOME sandbox + perch fixture (PATTERN MAP — `tests/auto_pick_integration.rs:1-120`):
    Replicate verbatim. Key fields:
      - `static ENV_LOCK: Mutex<()> = Mutex::new(())`
      - `SptHomeGuard` RAII for SPT_HOME mutation
      - `write_live_perch(spt_home, id)` helper writing `{spt_home}/owlery/{id}/ready` + `info.json` with `parent_pid = std::process::id() as u64`
      - `assert_cmd::Command::cargo_bin("owl")` to invoke the subprocess
      - `.env("SPT_HOME", tmp).env("HOME", tmp).env("USERPROFILE", tmp).env_remove("OWL_ECHO_COMMUNE")` env hygiene

  `tests/version_changelog.rs` integration cases (LOCKED — RESEARCH Integration Test Corpus + CONTEXT):
    1. `first_install_silent_writes_sentinel` — empty SPT_HOME, SPT_OVERRIDE_PKG_VERSION=1.10.10; assert sentinel written, no `decision:"block"` in stdout
    2. `single_step_mismatch_emits_block` — sentinel `{"version":"1.10.9"}`, OVERRIDE=1.10.10; CHANGELOG fixture has both versions; assert block emitted with `<old>1.10.9</old>`, `<new>1.10.10</new>`, `<step_count>1</step_count>`; sentinel now 1.10.10
    3. `multi_step_mismatch_emits_block_with_step_count` — sentinel 1.10.5, OVERRIDE=1.10.10, CHANGELOG has 1.10.5..1.10.10; step_count=5
    4. `sentinel_equal_no_emit` — sentinel 1.10.10, OVERRIDE=1.10.10; no block; sentinel unchanged
    5. `owl_echo_commune_guard_skips_emission` — mismatch + OWL_ECHO_COMMUNE=1; no block; sentinel unchanged
    6. `no_perch_skips_emission` — mismatch but no perch fixture written; no block; sentinel unchanged
    7. `stop_hook_active_skips_emission` — mismatch + stdin `{"stop_hook_active":true}`; no block; sentinel unchanged
    8. `version_remind_rollback_round_trip` — set sentinel to 1.10.10, invoke `owl version-remind 1.10.9`, assert sentinel reads `{"version":"1.10.9"}`
    9. `version_remind_missing_arg_exits_1` — `owl version-remind` no arg; exit code != 0 (clap-emitted)
    10. `version_remind_non_semver_exits_1` — `owl version-remind not-a-version`; exit code 1; stderr matches "semver"

    Test fixtures: write a synthetic `CHANGELOG.md` into the SPT_HOME tempdir under a path the test sets `CLAUDE_PLUGIN_ROOT` to. Suggested fixture: a string constant inside the test module with 6-10 Keep-a-Changelog entries spanning 1.10.5..1.10.10.

  Co-located unit tests in `src/owl/version_changelog.rs::tests` (LOCKED — RESEARCH parser corpus):
    Cover the full 18-row test corpus from RESEARCH Q10. One test per row OR table-driven test. Each row asserts parse-or-reject + extracted version+date if parsed.
  </interfaces>
  </context>

  <tasks>

  <task type="auto" tdd="true">
    <name>Task 1: Create src/owl/version_changelog.rs (sentinel + parser + payload + version-remind handler) with unit tests</name>
    <files>src/owl/version_changelog.rs, src/owl/mod.rs, src/common/owlery.rs, src/cli.rs</files>
    <behavior>
  Unit tests (co-located `#[cfg(test)] mod tests`):
  - Parser corpus (RESEARCH Q10, 18 rows):
    - Accept `## [1.10.10] - 2026-05-16` -> version=(1,10,10), date="2026-05-16"
    - Accept `## [0.9.3] - 2025-12-01` (pre-1.0)
    - Reject `## [Unreleased]`
    - Reject `## [1.10.10]` (no date)
    - Reject `## [1.10] - 2026-05-16` (no patch)
    - Reject `## [1] - 2026-05-16` (no minor)
    - Reject `[1.10.10]: https://...` (link reference)
    - Reject `# [1.10.10] - 2026-05-16` (H1)
    - Reject `### [1.10.10] - 2026-05-16` (H3)
    - Accept `## [1.10.10] - 2026-05-16  ` (trailing whitespace tolerance)
    - Accept BOM-prefixed line (strip BOM at file-read time)
    - Reject `## [1.10.10] - 2026-05-16 (yanked)` (strict trailing-text rejection)
    - Reject `## [1.10.10-rc1] - 2026-05-16` (pre-release suffix)
    - Reject `## ` (empty H2)
    - Reject `## [ 1.10.10 ] - 2026-05-16` (whitespace in brackets)
    - Reject `## [1.10.10] - 2026/05/16` (slash date)
  - `sections_between` corpus:
    - Range query 1.10.5 (exclusive) -> 1.10.7 (inclusive) over entries [1.10.5, 1.10.6, 1.10.7, 1.10.8] returns [1.10.6, 1.10.7]
    - Body content preserved verbatim (no parser mutation)
  - Sentinel I/O:
    - `read_sentinel()` returns None when file missing
    - `read_sentinel()` returns None when JSON malformed
    - `read_sentinel()` returns Some("X.Y.Z") on happy path
    - `write_sentinel("1.10.10")` round-trips through `read_sentinel()` correctly
    - `write_sentinel` uses `atomic_write_string` (no .tmp residue after success)
  - `current_version()`:
    - With SPT_OVERRIDE_PKG_VERSION unset -> returns `env!("CARGO_PKG_VERSION")` value
    - With SPT_OVERRIDE_PKG_VERSION="1.99.99" -> returns "1.99.99"
    - With SPT_OVERRIDE_PKG_VERSION="" -> falls through to env! (empty treated as unset)
  - `is_plain_semver` validator:
    - Accept "1.10.10", "0.0.0", "999.999.999"
    - Reject "", "1.10", "1.10.10.10", "1.10.x", "1.10.10-rc1", "abc.def.ghi", " 1.10.10"
  - `run_version_remind`:
    - Valid semver arg writes sentinel + returns success
    - Non-semver arg returns ExitCode::from(1) without writing sentinel
  - `build_block_reason` builder:
    - Given old=(1,10,9), new=(1,10,10), step_count=1, optional old_date/new_date -> returns a string containing all required XML elements + literal `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]`
    - Element ordering: `<old>` before `<new>` before `<old_date>` before `<new_date>` before `<step_count>` before `<changelog_path>` before `<invocation_for_remind>` before `<instructions>`
    - When CHANGELOG path unresolvable: `<changelog_path>NOT_RESOLVED</changelog_path>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>` is verbatim (no escaping; literal `$OWL` substring)
    </behavior>
    <action>
  PRE-EDIT ANCHOR GUARD (C1 — line-citations in `<interfaces>` drift between phases). Run these greps FIRST; if either fails, ABORT the task and re-read the cited files to resync anchors before proceeding:
  ```
  grep -q "Some(Commands::NewAlarm" src/owl/mod.rs
  grep -q "NewAlarm" src/cli.rs
  ```
  The first confirms the dispatch arm pattern for `Some(Commands::NewAlarm { ... })` is still present in `src/owl/mod.rs` (anchor for inserting the new `Some(Commands::VersionRemind { old })` arm). The second confirms the clap `NewAlarm` variant still exists in `src/cli.rs` (anchor for inserting `VersionRemind` after it). If either grep fails (exit != 0), STOP — the task's line citations are stale and the insertions risk landing in the wrong place. Resync by re-reading the files, then update the per-step line numbers below, then re-run the greps as a final sanity check.

  After the guard passes, proceed:

  Create the new module file `src/owl/version_changelog.rs`. Hand-roll all parsing (NO `regex` crate per Cargo.toml verification).

  Structure the file as:
  ```rust
  // Phase 34 (D-01..D-12): version-change detection for Stop hook.
  // Public surface:
  //   - read_sentinel / write_sentinel — atomic sentinel I/O at $SPT_HOME/last-seen-version.json
  //   - current_version — env!("CARGO_PKG_VERSION") with SPT_OVERRIDE_PKG_VERSION test seam (RESEARCH Q9)
  //   - parse_changelog / sections_between — Keep-a-Changelog 1.1.0 H2 parser
  //   - build_block_reason — emit the <spt-version-changelog> payload XML for hook_idle.rs
  //   - maybe_emit_version_change_block — Stop-hook entry point (called from hook_idle.rs::run)
  //   - run_version_remind — clap-dispatched handler for `owl version-remind <old>`
  //
  // NO regex crate dependency — all parsing is hand-rolled. Sentinel JSON shape
  // is `{"version":"X.Y.Z"}` and the writer formats it directly (no serde_json
  // round-trip needed for the write path; the reader uses serde_json::Value
  // to gracefully degrade on malformed input per RESEARCH Pitfall 1).
  ```

  Implement these functions (signatures verbatim):
  ```rust
  pub fn current_version() -> String { ... }                                     // see <interfaces>
  pub fn sentinel_path() -> std::path::PathBuf { ... }                           // delegates to owlery::sentinel_path
  pub fn read_sentinel() -> Option<String> { ... }                               // graceful None on missing/malformed
  pub fn write_sentinel(version: &str) -> std::io::Result<()> { ... }            // atomic_write_string

  pub struct ChangelogEntry { ... }                                              // see <interfaces>
  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry> { ... }           // hand-rolled
  pub fn parse_h2_line(line: &str) -> Option<((u32,u32,u32), String)> { ... }    // single-line helper
  pub fn sections_between<'a>(entries: &'a [ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&'a ChangelogEntry> { ... }

  pub fn is_plain_semver(s: &str) -> bool { ... }                                // V5 input validation
  pub fn parse_semver_tuple(s: &str) -> Option<(u32,u32,u32)> { ... }            // for hook side

  pub fn build_block_reason(
      old: &str,
      new: &str,
      old_date: Option<&str>,
      new_date: Option<&str>,
      step_count: usize,
      changelog_path_resolved: bool,
  ) -> String { ... }

  pub fn resolve_changelog_path() -> Option<std::path::PathBuf> { ... }          // reads $CLAUDE_PLUGIN_ROOT
  pub fn read_changelog() -> Option<String> { ... }                              // reads file at resolve_changelog_path
  pub fn maybe_emit_version_change_block() { ... }                               // hook entry point

  pub fn run_version_remind(old: &str) -> std::process::ExitCode { ... }         // subcommand handler
  ```

  `maybe_emit_version_change_block` body algorithm (CONTEXT D-04..D-06 + RESEARCH Pitfall 1/2/4):
  1. If `current_version()` == `read_sentinel()` -> return (no mismatch).
  2. If `read_sentinel()` is None (first install) -> write sentinel = current; return (silent per VERS-03).
  3. Mismatch path:
     a. Capture `old = sentinel`, `new = current`.
     b. Try `read_changelog()` -> parse_changelog -> sections_between(old, new) -> compute `step_count` + `old_date` + `new_date`.
     c. If CHANGELOG unresolvable: step_count = 1 (rough proxy), dates = None, changelog_path_resolved = false.
     d. Write sentinel = new (atomic). On Err: log stderr, return (no emit — Pitfall 1).
     e. Build reason XML via `build_block_reason(...)`.
     f. Emit `serde_json::json!({"decision":"block","reason":reason})` to stdout via println!.

  Add `pub mod version_changelog;` to `src/owl/mod.rs`. The module declarations live alphabetically near other `pub mod` lines (find the existing `pub mod hook_idle;` or `pub mod poll;` — insert alongside).

  In `src/owl/mod.rs::handle_command` (line 37 entry, NewAlarm dispatch arm at line 116, HookIdle arm at line 143): add a new arm after `Some(Commands::NewAlarm { ... }) => { ... }` (around line 130) and BEFORE the HookIdle arm:
  ```rust
  Some(Commands::VersionRemind { old }) => {
      std::process::exit(u8::from(version_changelog::run_version_remind(&old)) as i32);
  }
  ```
  (Match the existing dispatch idiom — some arms call into functions that themselves call `std::process::exit`; mirror whichever convention the surrounding NewAlarm arm uses. The `version-remind` handler returns `ExitCode` per the public signature; convert to process exit code.)

  In `src/cli.rs`: add a new variant to the `Commands` enum AFTER `NewAlarm` (line 153) and BEFORE `HookSubagentStart` (line 156):
  ```rust
  /// Roll the last-seen version sentinel back to <old> so the next Stop
  /// hook re-fires the version-change prompt. Invoked by Claude when the
  /// user picks `Remind me later` from the AskUserQuestion. Phase 34 D-08.
  #[command(name = "version-remind")]
  VersionRemind {
      /// The previous version to roll the sentinel back to (e.g. `1.10.9`).
      /// MUST match the `old` value from the <spt-version-changelog> block
      /// Claude received — the subcommand has no way to infer it otherwise.
      old: String,
  },
  ```

  Also add to the `#[cfg(test)] mod tests` block at the bottom of `src/cli.rs` (line 351+), a clap parse test:
  ```rust
  #[test]
  fn parses_version_remind_positional() {
      let cli = Cli::try_parse_from(["owl", "version-remind", "1.10.9"]).unwrap();
      match cli.command {
          Some(Commands::VersionRemind { old }) => assert_eq!(old, "1.10.9"),
          other => panic!("expected VersionRemind, got {:?}", other),
      }
  }

  #[test]
  fn version_remind_requires_positional() {
      let result = Cli::try_parse_from(["owl", "version-remind"]);
      assert!(result.is_err());
  }
  ```

  In `src/common/owlery.rs`: add the encapsulating helper (RESEARCH Risk #2):
  ```rust
  /// Phase 34: sentinel path for the version-change detector. Sibling to `owlery/`,
  /// NOT inside it (per VERS-01). Centralized here so the convention is encapsulated
  /// rather than scattered across hook + subcommand call sites.
  pub fn sentinel_path() -> std::path::PathBuf {
      spt_home().join("last-seen-version.json")
  }
  ```
  Insert alongside the existing `pub fn spt_home()` / `pub fn owlery_dir()` definitions (around lines 18-32 / 65). Add doc comment per the existing style in the file.

  Author the co-located `#[cfg(test)] mod tests` block in `src/owl/version_changelog.rs` covering ALL behaviors listed in `<behavior>`. Reuse the `ENV_LOCK` + `EnvSnapshot` pattern from `src/owl/hook_idle.rs:100-264` (lines 108-135 are the canonical shape). For SPT_HOME mutations, use tempfile::tempdir() per test.

  Final step: run `cargo test --lib version_changelog` and confirm all tests green. Then `cargo test --lib` (full library suite) to confirm no regressions elsewhere. Then `cargo test --test version_changelog` will fail (file not yet created — that's Task 3).

  Commit:
  ```
  feat(34): add version_changelog module + version-remind subcommand

  - New src/owl/version_changelog.rs: sentinel I/O, hand-rolled Keep-a-Changelog
    parser, payload XML builder, hook entry point, version-remind handler.
  - src/cli.rs: new VersionRemind { old } variant + clap parse tests.
  - src/owl/mod.rs: dispatch arm + module declaration.
  - src/common/owlery.rs: pub fn sentinel_path() encapsulation helper.

  NO regex crate — all parsing hand-rolled. Block-body <instructions>
  element emits placeholder [PLAN_03_INSTRUCTIONS_PLACEHOLDER]; Plan 03
  will replace with the AUQ + render wording.
  ```
    </action>
    <verify>
      <automated>cargo test --lib version_changelog 2>&amp;1 | tee /tmp/t.log &amp;&amp; grep -qE "test result: ok\. [0-9]+ passed" /tmp/t.log &amp;&amp; cargo test --lib 2>&amp;1 | grep -qE "test result: ok"</automated>
    </verify>
    <done>Module file exists; all co-located unit tests pass; clap parse tests pass; full lib test suite green; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 2: Wire version-change check into hook_idle.rs::run() (after perch resolution, before set_idle_ready)</name>
    <files>src/owl/hook_idle.rs, src/common/hook_output.rs</files>
    <behavior>
  - `hook_idle.rs::run()` continues to early-return on missing perch (existing behavior, lines 25-32 unchanged).
  - After perch resolution succeeds, `OWL_ECHO_COMMUNE` is checked at the top of the version-change code path; if set, skip the version-change check entirely.
  - If `stop_hook_active` is true in stdin JSON, skip the version-change check entirely (RESEARCH Pitfall 4 defensive guard).
  - Otherwise call `version_changelog::maybe_emit_version_change_block()` which handles its own internal mismatch logic + sentinel write + block emission.
  - After the version-change call, `inbox::set_idle_ready(&owl_id)` continues to fire (unchanged behavior — sentinel + version-change is additive, not a short-circuit).
  - After `set_idle_ready`, `spawn_echo_commune_if_live(&owl_id)` continues to fire (unchanged).

  Behavior tests live in `tests/version_changelog.rs` (Task 3). Co-located unit tests in hook_idle.rs are extended only if existing tests depend on `run()` not printing to stdout — verify and adjust as needed.
    </behavior>
    <action>
  Step A: confirm `parse_hook_stdin` surface in `src/common/hook_output.rs` includes a `stop_hook_active: bool` field. Read the file; if absent, add it:
  ```rust
  #[derive(Deserialize, Default)]
  pub struct ParsedHookStdin {
      ...existing fields...,
      #[serde(default)]
      pub stop_hook_active: bool,
  }
  ```
  If the struct shape already covers it via a different name, use that name. Keep changes minimal; this is the smallest possible additive change.

  Step B: edit `src/owl/hook_idle.rs::run()`:

  Current shape (lines 14-43):
  ```rust
  pub fn run() {
      let stdin = hook_output::parse_hook_stdin();
      let session_id = std::env::var("OWL_SESSION_ID")
          .ok()
          .filter(|s| !s.is_empty())
          .or(stdin.session_id);
      let owl_id = match &session_id {
          Some(sid) => hook_output::find_perch_cached(sid),
          None => hook_output::find_perch_by_parent_pid(),
      };
      let owl_id = match owl_id {
          Some(id) => id,
          None => return,
      };
      inbox::set_idle_ready(&owl_id);
      spawn_echo_commune_if_live(&owl_id);
  }
  ```

  Modify to:
  ```rust
  pub fn run() {
      // 1. Parse stdin JSON — Stop has common fields only.
      let stdin = hook_output::parse_hook_stdin();

      let session_id = std::env::var("OWL_SESSION_ID")
          .ok()
          .filter(|s| !s.is_empty())
          .or(stdin.session_id.clone());

      // 2. Find perch — perch resolution is the active-spt-session predicate
      //    for both legacy set_idle_ready / spawn_echo_commune AND the Phase 34
      //    version-change emission (CONTEXT D-02).
      let owl_id = match &session_id {
          Some(sid) => hook_output::find_perch_cached(sid),
          None => hook_output::find_perch_by_parent_pid(),
      };
      let owl_id = match owl_id {
          Some(id) => id,
          None => return,
      };

      // Phase 34 (D-01..D-06): version-change detection + block emission.
      // MUST run BEFORE set_idle_ready / spawn_echo_commune_if_live so the
      // block emission (println! to stdout) is the only thing on stdout this
      // turn — set_idle_ready and spawn_echo_commune are side-effect calls
      // and emit nothing to stdout.
      //
      // OWL_ECHO_COMMUNE recursion guard: haiku echo-commune subprocesses
      // MUST NOT see a version-change block. Mirrors lines 64-69 below.
      // stop_hook_active defensive guard: prevent infinite-loop pathologies
      // (RESEARCH Pitfall 4).
      let skip_version_change =
          std::env::var("OWL_ECHO_COMMUNE")
              .map(|v| !v.is_empty())
              .unwrap_or(false)
          || stdin.stop_hook_active;
      if !skip_version_change {
          crate::owl::version_changelog::maybe_emit_version_change_block();
      }

      // 3. Set .idle-ready so poll can deliver (per 18.1 D-02, D-04).
      inbox::set_idle_ready(&owl_id);

      // 4. Phase 18.3 D-01..D-03: write .more-done sentinel if live+psyche-ready.
      spawn_echo_commune_if_live(&owl_id);
  }
  ```

  Step C: update the file-doc comment at lines 1-5:
  ```rust
  // hook-idle subcommand -- Stop hook handler
  // Sets .idle-ready so poll can deliver messages.
  // Phase 34 (D-04): also emits a `<spt-version-changelog>` block via
  // `decision:"block"` + `reason` on version mismatch (compile-time
  // CARGO_PKG_VERSION vs $SPT_HOME/last-seen-version.json sentinel).
  // Stop hook output schema supports `decision`/`reason` only — no
  // hookSpecificOutput envelope. The block IS the only stdout this turn.
  // On no-mismatch / first-install / echo-commune guard hit: silent (no stdout).
  //
  // Phase 18.3 (D-01..D-04): no longer spawns `_echo-commune` directly.
  // (rest of existing comment unchanged)
  ```

  Step D: existing unit tests in `src/owl/hook_idle.rs::tests` (lines 100-264) MAY rely on `run()` producing no stdout. Read those tests; if any assert no stdout, they will need adjustment because the version-change path now CAN write to stdout when there's a mismatch. Strategy:
    - Add an `OWL_ECHO_COMMUNE=1` env var to those tests (or `stop_hook_active=true` stdin) to bypass the version-change path. This keeps existing tests focused on the `.more-done` sentinel behavior.
    - Alternatively, set `SPT_OVERRIDE_PKG_VERSION` to match the sentinel value (or set sentinel to current version via `write_sentinel(env!("CARGO_PKG_VERSION"))`).
    - Preserve existing test semantics — Phase 18.3's `.more-done` behavior MUST keep working.

  Run `cargo test --lib hook_idle` and confirm green. Run `cargo test --lib` and confirm full suite green.

  Commit:
  ```
  feat(34): wire version-change emission into hook_idle.rs::run()

  Adds version_changelog::maybe_emit_version_change_block call after
  perch resolution, gated by OWL_ECHO_COMMUNE (D-03) and stop_hook_active
  (RESEARCH Pitfall 4). Block emission precedes set_idle_ready so stdout
  carries only the JSON envelope when a mismatch fires.

  Updates hook_idle file-doc comment to reflect the new stdout channel
  (Stop hooks now CAN emit on this hook path; previously side-effect-only).
  ```
    </action>
    <verify>
      <automated>cargo test --lib hook_idle 2>&amp;1 | grep -qE "test result: ok" &amp;&amp; cargo test --lib 2>&amp;1 | grep -qE "test result: ok" &amp;&amp; grep -q "version_changelog::maybe_emit_version_change_block" src/owl/hook_idle.rs &amp;&amp; grep -q "stop_hook_active" src/owl/hook_idle.rs</automated>
    </verify>
    <done>hook_idle.rs::run() invokes maybe_emit_version_change_block before set_idle_ready; OWL_ECHO_COMMUNE + stop_hook_active guards in place; all library tests green; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 3: Create tests/version_changelog.rs integration suite (10 cases) using SPT_HOME sandbox + perch fixture pattern</name>
    <files>tests/version_changelog.rs</files>
    <behavior>
  Integration tests (subprocess-driven via `assert_cmd::Command::cargo_bin("owl")`):

  1. `first_install_silent_writes_sentinel`
     - Setup: empty SPT_HOME, OVERRIDE=1.10.10, no sentinel, perch fixture written, synthetic CHANGELOG.md present.
     - Invoke: `owl hook-idle` with stdin `{}`.
     - Assert: exit 0; stdout does NOT contain `"decision":"block"`; sentinel file now contains `{"version":"1.10.10"}`.

  2. `single_step_mismatch_emits_block`
     - Setup: sentinel `{"version":"1.10.9"}`, OVERRIDE=1.10.10, perch fixture, CHANGELOG with both versions.
     - Invoke: `owl hook-idle` with stdin `{}`.
     - Assert: exit 0; stdout JSON has `decision=block`; reason contains `<old>1.10.9</old>`, `<new>1.10.10</new>`, `<step_count>1</step_count>`; sentinel post-call is `1.10.10`.

  3. `multi_step_mismatch_emits_block_with_step_count`
     - Setup: sentinel `1.10.5`, OVERRIDE=1.10.10, CHANGELOG with H2 sections for 1.10.5, 1.10.6, 1.10.7, 1.10.8, 1.10.9, 1.10.10.
     - Assert: step_count == 5; reason has `<step_count>5</step_count>`.

  4. `sentinel_equal_no_emit`
     - Setup: sentinel `1.10.10`, OVERRIDE=1.10.10.
     - Assert: no `decision:"block"` in stdout; sentinel unchanged.

  5. `owl_echo_commune_guard_skips_emission`
     - Setup: mismatch present; cmd.env("OWL_ECHO_COMMUNE", "1").
     - Assert: no block emitted; sentinel UNCHANGED (guard runs before mismatch check + sentinel write).

  6. `no_perch_skips_emission`
     - Setup: SPT_HOME tempdir BUT no perch fixture written.
     - Assert: no block; sentinel unchanged.

  7. `stop_hook_active_skips_emission`
     - Setup: mismatch present; stdin `{"stop_hook_active":true}`.
     - Assert: no block; sentinel unchanged.

  8. `version_remind_rollback_round_trip`
     - Setup: sentinel = 1.10.10.
     - Invoke: `owl version-remind 1.10.9`.
     - Assert: exit 0; sentinel reads `{"version":"1.10.9"}`.

  9. `version_remind_missing_arg_exits_nonzero`
     - Invoke: `owl version-remind` (no positional).
     - Assert: exit code != 0; stderr non-empty (clap default).

  10. `version_remind_non_semver_exits_1`
      - Invoke: `owl version-remind not-a-version`.
      - Assert: exit code == 1; stderr matches `semver` substring; sentinel unchanged (or doesn't exist).
    </behavior>
    <action>
  Create `tests/version_changelog.rs` mirroring `tests/auto_pick_integration.rs:1-120` SPT_HOME sandbox pattern verbatim.

  File outline:
  ```rust
  //! Phase 34 integration tests: version-change detection + sentinel + version-remind.
  //! Mirrors tests/auto_pick_integration.rs SPT_HOME sandbox pattern.

  use std::fs;
  use std::path::Path;
  use std::sync::Mutex;

  static ENV_LOCK: Mutex<()> = Mutex::new(());

  const CHANGELOG_FIXTURE: &str = r#"# Changelog

  ## [1.10.10] - 2026-05-16

  - Multi-step test entry for 1.10.10.

  ## [1.10.9] - 2026-05-15

  - Entry for 1.10.9.

  ## [1.10.8] - 2026-05-14

  - Entry for 1.10.8.

  ## [1.10.7] - 2026-05-13

  - Entry for 1.10.7.

  ## [1.10.6] - 2026-05-12

  - Entry for 1.10.6.

  ## [1.10.5] - 2026-05-11

  - Entry for 1.10.5.
  "#;

  struct SptHomeGuard {
      prior_spt: Option<String>,
      prior_plugin_root: Option<String>,
      prior_override: Option<String>,
  }

  impl SptHomeGuard {
      fn set(spt_home: &Path, plugin_root: &Path, override_version: Option<&str>) -> Self {
          let prior_spt = std::env::var("SPT_HOME").ok();
          let prior_plugin_root = std::env::var("CLAUDE_PLUGIN_ROOT").ok();
          let prior_override = std::env::var("SPT_OVERRIDE_PKG_VERSION").ok();
          std::env::set_var("SPT_HOME", spt_home);
          std::env::set_var("CLAUDE_PLUGIN_ROOT", plugin_root);
          match override_version {
              Some(v) => std::env::set_var("SPT_OVERRIDE_PKG_VERSION", v),
              None => std::env::remove_var("SPT_OVERRIDE_PKG_VERSION"),
          }
          Self { prior_spt, prior_plugin_root, prior_override }
      }
  }

  impl Drop for SptHomeGuard {
      fn drop(&mut self) {
          match &self.prior_spt {
              Some(v) => std::env::set_var("SPT_HOME", v),
              None => std::env::remove_var("SPT_HOME"),
          }
          match &self.prior_plugin_root {
              Some(v) => std::env::set_var("CLAUDE_PLUGIN_ROOT", v),
              None => std::env::remove_var("CLAUDE_PLUGIN_ROOT"),
          }
          match &self.prior_override {
              Some(v) => std::env::set_var("SPT_OVERRIDE_PKG_VERSION", v),
              None => std::env::remove_var("SPT_OVERRIDE_PKG_VERSION"),
          }
      }
  }

  fn write_live_perch(spt_home: &Path, id: &str) {
      let perch_dir = spt_home.join("owlery").join(id);
      fs::create_dir_all(&perch_dir).unwrap();
      fs::write(perch_dir.join("ready"), "").unwrap();
      let info = serde_json::json!({
          "owl_id": id,
          "pid": std::process::id() as u64,
          // N1: parent_pid = test-process PID so hook_output::find_perch_by_parent_pid()
          // resolves THIS test's perch when the owl subprocess (spawned by assert_cmd
          // under the test process) walks up the parent chain. Mirrors the
          // auto_pick_integration.rs fixture convention.
          "parent_pid": std::process::id() as u64,
          "mode": "listen",
          "session_id": "test-session",
          "state": "Ready",
          "project_history": [],
      });
      fs::write(perch_dir.join("info.json"), serde_json::to_string(&info).unwrap()).unwrap();
  }

  fn write_changelog_fixture(plugin_root: &Path) {
      fs::write(plugin_root.join("CHANGELOG.md"), CHANGELOG_FIXTURE).unwrap();
  }

  fn write_sentinel_fixture(spt_home: &Path, version: &str) {
      let body = format!("{{\"version\":\"{}\"}}", version);
      fs::write(spt_home.join("last-seen-version.json"), body).unwrap();
  }

  // ... 10 #[test] fns covering the behaviors above ...
  ```

  For each test:
    - `let _lock = ENV_LOCK.lock().unwrap();`
    - `let tmp = tempfile::tempdir().unwrap();`
    - Both SPT_HOME and CLAUDE_PLUGIN_ROOT point at `tmp.path()` for simplicity (CHANGELOG.md sits at `tmp.path()/CHANGELOG.md`; sentinel at `tmp.path()/last-seen-version.json`; perch at `tmp.path()/owlery/{id}/`).
    - `let _guard = SptHomeGuard::set(tmp.path(), tmp.path(), Some("1.10.10"));`
    - Write fixtures: `write_changelog_fixture`, `write_live_perch("test-owl")`, `write_sentinel_fixture(..)` as required by the test case.
    - Invoke the subprocess:
      ```rust
      let mut cmd = assert_cmd::Command::cargo_bin("owl").expect("owl binary must be built");
      let output = cmd
          .arg("hook-idle")
          .env("SPT_HOME", tmp.path())
          .env("CLAUDE_PLUGIN_ROOT", tmp.path())
          .env("SPT_OVERRIDE_PKG_VERSION", "1.10.10")
          .env("HOME", tmp.path())
          .env("USERPROFILE", tmp.path())
          .env_remove("OWL_ECHO_COMMUNE")    // per-test may re-add
          .write_stdin("{}")
          .output()
          .expect("owl subprocess");
      ```
    - For the `version-remind` tests: replace the args with `["version-remind", "<arg>"]` and skip stdin/perch fixtures.
    - Assert via `output.status.success()`, `String::from_utf8_lossy(&output.stdout)`, and reading the sentinel file post-call.

  For test 5 (`owl_echo_commune_guard_skips_emission`):
    ```rust
    .env("OWL_ECHO_COMMUNE", "1")
    ```

  For test 7 (`stop_hook_active_skips_emission`):
    ```rust
    .write_stdin(r#"{"stop_hook_active":true}"#)
    ```

  For tests 9 + 10 (subcommand error cases): use `output.status.code()` and `String::from_utf8_lossy(&output.stderr)`.

  CRITICAL: `assert_cmd` requires the binary to be built. Add a `cargo build` step in the test setup OR rely on `cargo test`'s implicit build. The `cargo_bin` helper points at the test binary's `target/debug/owl` — `cargo test` builds it automatically.

  After authoring, run `cargo test --test version_changelog` and confirm all 10 tests pass.

  Commit:
  ```
  test(34): integration tests for version-change hook + version-remind subcommand

  10 cases covering first-install silent, single + multi-step mismatch,
  sentinel-equal no-emit, OWL_ECHO_COMMUNE + stop_hook_active + no-perch
  guards, version-remind round-trip + missing-arg + non-semver-arg.

  Uses SPT_OVERRIDE_PKG_VERSION test seam (no rebuild required per case).
  Fixture CHANGELOG.md spans 1.10.5..1.10.10 to exercise step_count math.
  ```
    </action>
    <verify>
      <automated>cargo test --test version_changelog 2>&amp;1 | tee /tmp/it.log &amp;&amp; grep -qE "test result: ok\. 10 passed" /tmp/it.log</automated>
    </verify>
    <done>tests/version_changelog.rs exists with 10 #[test] fns; all pass; commit landed.</done>
  </task>

  </tasks>

  <verification>
  After all 3 tasks:

  1. Full test suite green:
     ```
     cargo test
     ```
     Expect `test result: ok` for both lib and integration test runs.

  2. New module wired correctly:
     ```
     grep -c "version_changelog" src/owl/mod.rs src/owl/hook_idle.rs src/cli.rs
     ```
     Expect at least 4 hits (mod declaration + dispatch arm + hook call + Plan 03 placeholder reference if Plan 03 hasn't started).

  3. Single sentinel write site (VERS-09 audit):
     ```
     grep -rn "last-seen-version" src/
     ```
     Expect exactly 2 hits: one in `src/common/owlery.rs::sentinel_path()` and one in tests OR one in `src/owl/version_changelog.rs` (the consumer of `sentinel_path`).
     No `last-seen-version` literal in `hook_idle.rs` (must go through the module API).

  4. No regex crate added (Cargo.toml audit):
     ```
     grep -c "^regex" Cargo.toml
     ```
     Expect 0.

  5. Version-change emission ordering preserved:
     ```
     grep -n "version_changelog\|set_idle_ready\|spawn_echo_commune" src/owl/hook_idle.rs
     ```
     Manual check: `maybe_emit_version_change_block` line number is LESS than `set_idle_ready` line number, which is LESS than `spawn_echo_commune_if_live` line number.

  6. Three commits landed:
     ```
     git log --oneline -3
     ```
     Expect:
     - `test(34): integration tests for version-change hook + version-remind subcommand`
     - `feat(34): wire version-change emission into hook_idle.rs::run()`
     - `feat(34): add version_changelog module + version-remind subcommand`
  </verification>

  <success_criteria>
  - Plan 02 maps to VERS-01 (sentinel path encapsulated in owlery::sentinel_path), VERS-02 (CARGO_PKG_VERSION via current_version() with test seam; NEVER reads plugin.json at runtime), VERS-03 (first-install silent path tested), VERS-05 (atomic_write_string used at sentinel write site), VERS-09 (single write site at hook entry; grep audit confirms 2 callers via sentinel_path).
  - Block-body XML scaffold complete with placeholder for Plan 03's wording.
  - All 30+ unit tests + 10 integration tests pass.
  - No new crate dependencies.
  - hook_idle.rs change preserves Phase 18.3 `.more-done` sentinel behavior (existing tests still pass).
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | Claude Code -> hook subprocess | stdin JSON (untrusted shape but trust-boundary-internal — Claude Code is local) |
  | Claude -> $OWL bash invocation | `version-remind <old>` argv crosses Claude's tool-call boundary to the owl subprocess |
  | disk -> hook | CHANGELOG.md content at $CLAUDE_PLUGIN_ROOT (author-controlled) is read and inlined into the block reason |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-05 | Tampering | `version-remind <old>` argv injection | mitigate | `is_plain_semver` validator rejects any non-`X.Y.Z` arg BEFORE sentinel write (RESEARCH security V5); test 10 covers this |
  | T-34-06 | Tampering | Sentinel path traversal | mitigate | Path constructed via `spt_home().join("last-seen-version.json")` — no user input in the path; encapsulated in `owlery::sentinel_path()` |
  | T-34-07 | DoS | Infinite Stop-hook block loop | mitigate | `stop_hook_active` guard short-circuits regardless of mismatch (RESEARCH Pitfall 4); test 7 covers this; belt-and-suspenders with sentinel pre-write per CONTEXT D-05 |
  | T-34-08 | DoS | OWL_ECHO_COMMUNE recursion (haiku subprocesses) | mitigate | Guard runs BEFORE the version-change call (lines 64-69 pattern mirrored); test 5 covers this |
  | T-34-09 | Information disclosure | CHANGELOG.md content injection | accept | Author-controlled file; content reaches Claude only (no XSS / code-exec surface); plaintext markdown |
  | T-34-10 | Tampering | Sentinel write failure leaves stale value | mitigate | Best-effort pattern per RESEARCH Pitfall 1: on Err, skip emission (no false notification); next Stop re-checks; stop_hook_active is the bounded loop guard |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-02-SUMMARY.md` documenting:
  - Three commit SHAs
  - Final test counts (unit + integration)
  - Confirmation that the `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` literal appears exactly once in `src/owl/version_changelog.rs`
  - Any deviations from the line-citation anchors in `<interfaces>` (hook_idle.rs / cli.rs / owlery.rs may have drifted; record actual line numbers)
  - Confirmation `cargo test` ends green with no warnings introduced by Plan 02 code
  </output>

  #### 34-03-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 03
  type: execute
  wave: 3
  depends_on: [34-01, 34-02]
  files_modified:
    - src/owl/version_changelog.rs
    - tests/version_changelog.rs
    - tests/golden/version_changelog_block.txt
  autonomous: false
  requirements:
    - VERS-04
    - VERS-06
  user_setup: []

  must_haves:
    truths:
      - "The `<instructions>` element inside the `<spt-version-changelog>` block carries the verbatim 4-option AUQ wording locked in CONTEXT D-08: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`"
      - "The `Yes, full changelog` instruction tells Claude to read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and print every H2 section where `{old} < version <= {new}`, verbatim markdown (RESEARCH Pitfall 6)"
      - "The `Yes, highlights only` instruction tells Claude to extract 2-5 most impactful feature adds and fixes across the range, preferring user-facing behavior changes, skipping internal refactors and test-only changes; appends pointer to $CLAUDE_PLUGIN_ROOT/CHANGELOG.md (CONTEXT D-08 / specifics)"
      - "The `Remind me later` instruction surfaces the literal invocation string `$OWL version-remind <old_version_substituted_verbatim>` so Claude passes the `old` value with no ambiguity (CONTEXT D-08 + follow-up clarification)"
      - "The question text uses the `step_count` field: single-step `SPT plugin updated v{old} → v{new}. Print changelog?`; multi-step `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`"
      - "Dates surface in Claude's pre-question prose, NOT in the AUQ question line (RESEARCH open question Q3)"
      - "A golden fixture at `tests/golden/version_changelog_block.txt` snapshots the canonical block reason string for a fixed 1.10.9->1.10.10 transition; an integration test diffs the live output against it"
      - "End-to-end UAT documentation captures the manual test protocol for confirming the AUQ surfaces correctly in a real Claude Code session"
    artifacts:
      - path: "src/owl/version_changelog.rs"
        provides: "Replaced `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` with the final wording inside `build_block_reason`"
        contains: "Yes, full changelog"
      - path: "tests/version_changelog.rs"
        provides: "New `block_reason_matches_golden_snapshot` test covering the 1.10.9->1.10.10 canonical case"
        contains: "version_changelog_block.txt"
      - path: "tests/golden/version_changelog_block.txt"
        provides: "Canonical snapshot of the `reason` string for the 1.10.9->1.10.10 transition fixture"
        min_lines: 25
    key_links:
      - from: "build_block_reason output"
        to: "tests/golden/version_changelog_block.txt"
        via: "snapshot equality assertion (with template substitution)"
        pattern: "version_changelog_block.txt"
      - from: "<instructions> element text"
        to: "Claude's AUQ rendering at re-engagement"
        via: "Stop hook reason -> Claude continuation -> AskUserQuestion tool call (RESEARCH Q1/Q2 GREEN)"
        pattern: "AskUserQuestion"
  ---

  <objective>
  Phase 34 user-facing wording + golden snapshot + UAT.

  Purpose: Replace Plan 02's placeholder with the production AUQ instruction text. This is the only file in the repo whose exact prose determines whether Claude reliably invokes AskUserQuestion with the four locked options on hook re-engagement. Wording quality matters more than code volume.

  Output: One module edit (replace placeholder), one new test (golden snapshot), one new fixture file. End-to-end UAT documented for `cargo build --release` + `DEPLOY.ps1` + manual Claude Code session verification.

  This plan has ONE checkpoint at the end (manual UAT against a real Claude Code session — `checkpoint:human-verify`).
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @.planning/phases/34-version-change-changelog/34-01-SUMMARY.md
  @.planning/phases/34-version-change-changelog/34-02-SUMMARY.md
  @src/owl/version_changelog.rs
  @tests/version_changelog.rs

  <interfaces>
  <!-- Locked invariants from CONTEXT D-08 + RESEARCH Q11 -->

  AUQ 4-option labels (LOCKED — CONTEXT D-08, verbatim):
    1. `Yes, full changelog`
    2. `Yes, highlights only`
    3. `Remind me later`
    4. `Skip`

  `Yes, full changelog` behavior (LOCKED — CONTEXT D-08 + RESEARCH Pitfall 6):
    Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and prints EVERY H2 section where `{old} < version <= {new}`, VERBATIM markdown. Single-step renders one section; multi-step renders all interim sections in order (newest first or oldest first — author chooses ONE convention and locks it in the instructions text).

  `Yes, highlights only` framing (LOCKED — CONTEXT specifics, suggested wording):
    "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes. Append `See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail.`"

  `Remind me later` invocation (LOCKED — CONTEXT D-08 + clarification):
    Claude invokes `$OWL version-remind <old>` via Bash, where `<old>` is the LITERAL value from the `<old>` element in the block. Block MUST surface the exact invocation string verbatim with the old value substituted — Claude cannot infer the rollback target from any other source.
    Example for 1.10.9->1.10.10 transition: `$OWL version-remind 1.10.9`

  `Skip` behavior (LOCKED):
    No action. Sentinel was already updated by the hook at run-entry; no re-fire. Claude acknowledges briefly and proceeds.

  Question text template (LOCKED — CONTEXT specifics):
    Single-step (`step_count == 1`): `SPT plugin updated v{old} → v{new}. Print changelog?`
    Multi-step (`step_count > 1`): `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`
    (Note: `versions` is always plural in the multi-step variant; pluralization is unconditional because step_count > 1 means at least 2.)

  Pre-question prose (Claude's discretion, but the instructions SHOULD recommend a shape per RESEARCH open question Q3):
    Before invoking AskUserQuestion, Claude states a one-line context line such as:
    `SPT plugin updated v1.10.9 (released 2026-05-15) → v1.10.10 (released 2026-05-16). Print changelog?`
    Dates are taken from `<old_date>` / `<new_date>` elements; omit the parenthetical when the element is empty (CHANGELOG.md unresolvable case).

  XML element ordering (LOCKED — Plan 02's `build_block_reason`):
    `<old>` -> `<new>` -> `<old_date>` -> `<new_date>` -> `<step_count>` -> `<changelog_path>` -> `<invocation_for_remind>` -> `<instructions>`

  NO ESCAPING of `$OWL` / `$CLAUDE_PLUGIN_ROOT` in the XML body — these are LITERAL substrings Claude reads and passes to Bash. The Bash tool expands them at invocation time. If we escape `$` to `\$`, Claude will pass the escaped form to Bash and the invocation will fail.

  `<instructions>` element wording — DRAFT (planner-authored, locked in this plan; refine inside the action below):
  ```
  Render an AskUserQuestion to confirm whether the user wants to see the changelog for this SPT plugin update.

  Before invoking AskUserQuestion, write one short pre-question line that names the version transition (and dates if both <old_date> and <new_date> are non-empty), e.g. "SPT plugin updated v{old} (released {old_date}) → v{new} (released {new_date}). Print changelog?". Omit the dates if either element is empty.

  Then invoke AskUserQuestion with EXACTLY these four options (labels verbatim, do not paraphrase):

    1. "Yes, full changelog" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version <= {new}, verbatim markdown (preserve the original H2 lines, bullet structure, and any H3 subsections). Render newest-first (the order entries appear in the file). Do not summarize, do not paraphrase.

    2. "Yes, highlights only" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and synthesize 2-5 highlight bullets across the H2 sections where {old} < version <= {new}. Prefer items that change user-facing behavior. Skip purely-internal refactors and test-only changes. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."

    3. "Remind me later" — On selection: invoke Bash with EXACTLY this command (substitute the {old} value verbatim from the <old> element above, do NOT paraphrase or compute):
       $OWL version-remind {old}
       This rolls the sentinel back so the next Stop hook re-fires this prompt. Do not run any other commands as part of this option.

    4. "Skip" — On selection: take no action. Acknowledge briefly ("OK, skipping.") and continue with the user's prior intent.

  If the user provides freeform input via AskUserQuestion's "Other" option:
    - "not now" / "later" / "skip for now" → treat as "Remind me later" (invoke the Bash command above).
    - "ignore" / "stop asking" / "no" → treat as "Skip".

  When the question text mentions {step_count} versions (i.e. step_count > 1), the multi-version skip rendering applies: "Yes, full changelog" prints ALL interim H2 sections, NOT just the endpoints.

  After completing the user's choice, return control to whatever the user was working on before this prompt.
  ```

  CRITICAL — `{old}` and `{new}` and `{old_date}` and `{new_date}` and `{step_count}` are TEMPLATE TOKENS in the instructions element that `build_block_reason` substitutes at emit time. The actual emitted text MUST have the real values inline (not `{old}` placeholders). E.g., for a 1.10.9->1.10.10 transition the `<instructions>` block reads `$OWL version-remind 1.10.9` LITERALLY — with `1.10.9` baked in.

  Golden snapshot strategy (RESEARCH open question Q1):
    Snapshot just the parts of the reason string that are stable: the XML element shape + the literal AUQ option labels + the literal invocation string. Body content of the highlights instructions is fragile (any wording tweak invalidates the snapshot), so the test asserts SUBSTRING matches for stable invariants rather than full-string equality:
      - `<old>1.10.9</old>`
      - `<new>1.10.10</new>`
      - `<step_count>1</step_count>`
      - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
      - Literal labels: `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"`
      - `Pick 2-5 most impactful` (highlights rubric anchor)
      - `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md` (instructions for both Yes options)
      - `$OWL version-remind 1.10.9` (instructions text for Remind, with substitution)

    The golden FILE stores the full expected reason string for reference / human review, but the TEST uses substring assertions for resilience.

  UAT protocol (CONTEXT canonical_refs + RESEARCH plan-boundary Q8 Plan 03):
    1. `cargo build --release` — clean build with all Phase 34 code.
    2. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1` — sync to marketplace + cache.
    3. `claude plugin install spt@cplugs` (DEPLOY.ps1 does this; verify it ran).
    4. Run `/reload-plugins` manually inside Claude Code.
    5. Synthesize a version mismatch: `node -e 'fs.writeFileSync(process.env.LOCALAPPDATA + "/spt/last-seen-version.json", JSON.stringify({version:"0.0.1"}))'` (Windows) or equivalent Unix path. Now the sentinel claims v0.0.1 and the installed plugin claims its real current version.
    6. Inside Claude Code: start an spt session (e.g., run `/spt:listen` or `/spt:live`).
    7. End the agent's turn naturally. Stop hook fires.
    8. EXPECTED: Claude responds with the pre-question line + AskUserQuestion showing the four locked options.
    9. Test each branch:
       - Pick `Yes, full changelog`: Claude prints CHANGELOG.md sections.
       - Pick `Yes, highlights only`: Claude renders 2-5 bullets + pointer line.
       - Pick `Remind me later`: Claude invokes `$OWL version-remind 0.0.1` via Bash; sentinel rolls back; next Stop re-fires.
       - Pick `Skip`: Claude says "OK, skipping" and proceeds; sentinel stays at current version; next Stop does NOT re-fire.
    10. Confirm sentinel state at each step via `cat $SPT_HOME/last-seen-version.json` (or `type %LOCALAPPDATA%\spt\last-seen-version.json` on Windows).
  </interfaces>
  </context>

  <tasks>

  <task type="auto" tdd="true">
    <name>Task 1: Replace [PLAN_03_INSTRUCTIONS_PLACEHOLDER] in build_block_reason with the locked 4-option AUQ wording (with template substitution)</name>
    <files>src/owl/version_changelog.rs</files>
    <behavior>
  - `build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true)` returns a string containing:
    - `<old>1.10.9</old>`
    - `<new>1.10.10</new>`
    - `<old_date>2026-05-15</old_date>`
    - `<new_date>2026-05-16</new_date>`
    - `<step_count>1</step_count>`
    - `<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
    - Inside `<instructions>`: literal substrings `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"`, `Pick 2-5 most impactful`, `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md`, `$OWL version-remind 1.10.9` (with the 1.10.9 substituted verbatim).
  - The literal token `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` is REMOVED from the file (grep -n returns no results).
  - For the multi-step case `build_block_reason("1.10.5", "1.10.10", ..., step_count=5, ...)`:
    - `<step_count>5</step_count>`
    - The pre-question hint inside instructions surfaces the multi-step form: `SPT plugin updated v1.10.5 → v1.10.10 (5 versions). Print changelog?`
    - The invocation_for_remind substitutes `1.10.5`: `$OWL version-remind 1.10.5`
  - For the unresolvable-CHANGELOG case `build_block_reason("1.10.9", "1.10.10", None, None, 1, false)`:
    - `<old_date></old_date>` (empty element)
    - `<new_date></new_date>` (empty element)
    - `<changelog_path>NOT_RESOLVED</changelog_path>`
    - Instructions still surface the four options; the Yes-full / Yes-highlights branches degrade gracefully (Claude sees NOT_RESOLVED and skips the CHANGELOG read, just states a generic "no changelog detail available" message). The instructions text MUST handle this branch explicitly so Claude doesn't try to read a nonexistent file.
    </behavior>
    <action>
  Step A: Read `src/owl/version_changelog.rs` to confirm the current shape of `build_block_reason` (Plan 02 left a placeholder `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` inside the `<instructions>` element).

  Step B: Replace the placeholder with the verbatim instructions wording in `<interfaces>` above, applying string templating for `{old}`, `{new}`, `{old_date}`, `{new_date}`, `{step_count}`. The `format!` macro is the natural fit:

  ```rust
  let question_text = if step_count == 1 {
      format!("SPT plugin updated v{old} → v{new}. Print changelog?")
  } else {
      format!("SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?")
  };

  let date_hint = match (old_date, new_date) {
      (Some(od), Some(nd)) if !od.is_empty() && !nd.is_empty() => {
          format!(" (released {od} → released {nd})")
      }
      _ => String::new(),
  };

  let changelog_path_element = if changelog_path_resolved {
      "$CLAUDE_PLUGIN_ROOT/CHANGELOG.md".to_string()
  } else {
      "NOT_RESOLVED".to_string()
  };

  let resolved_branch_extra = if changelog_path_resolved {
      String::new()  // Yes-full / Yes-highlights work normally
  } else {
      "  NOTE: <changelog_path> is NOT_RESOLVED — $CLAUDE_PLUGIN_ROOT/CHANGELOG.md was not readable at hook time. For 'Yes, full changelog' and 'Yes, highlights only', state briefly that the changelog file was not available and offer to retry after the user verifies the plugin install. Do not attempt to read the file.\n".to_string()
  };

  let instructions = format!(r#"Render an AskUserQuestion to confirm whether the user wants to see the changelog for this SPT plugin update.

  Before invoking AskUserQuestion, write one short pre-question line that names the version transition (and dates if both <old_date> and <new_date> are non-empty), e.g. "{question_text}"{date_hint}.

  Then invoke AskUserQuestion with EXACTLY these four options (labels verbatim, do not paraphrase):

    1. "Yes, full changelog" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version <= {new}, verbatim markdown (preserve the original H2 lines, bullet structure, and any H3 subsections). Render in the order they appear in the file (newest first). Do not summarize, do not paraphrase.

    2. "Yes, highlights only" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and synthesize 2-5 highlight bullets across the H2 sections where {old} < version <= {new}. Prefer items that change user-facing behavior. Skip purely-internal refactors and test-only changes. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."

    3. "Remind me later" — On selection: invoke Bash with EXACTLY this command (the {old} value is already substituted; pass it verbatim, do not paraphrase or recompute):
       $OWL version-remind {old}
       This rolls the sentinel back so the next Stop hook re-fires this prompt. Do not run any other commands as part of this option.

    4. "Skip" — On selection: take no action. Acknowledge briefly ("OK, skipping.") and continue with the user's prior intent.

  If the user provides freeform input via AskUserQuestion's "Other" option:
    - "not now" / "later" / "skip for now" → treat as "Remind me later" (invoke the Bash command in option 3).
    - "ignore" / "stop asking" / "no" → treat as "Skip".

  When the question text mentions multiple versions (step_count > 1), the multi-version skip rendering applies: "Yes, full changelog" prints ALL interim H2 sections (not just the endpoints).
  {resolved_branch_extra}
  After completing the user's choice, return control to whatever the user was working on before this prompt."#);
  ```

  CRITICAL: do NOT escape `$` in the `$OWL` or `$CLAUDE_PLUGIN_ROOT` substrings. These literally need to reach Claude as `$OWL` and `$CLAUDE_PLUGIN_ROOT` for Bash expansion. Rust's `format!` macro does NOT touch `$` characters — they pass through verbatim. Sanity-check: print the output of `build_block_reason("1.10.9", "1.10.10", None, None, 1, true)` once locally during development and confirm `$OWL version-remind 1.10.9` appears as-is.

  CRITICAL: do NOT include `{old}`/`{new}`/etc. AS LITERAL TEXT in the final emitted string — `format!` substitutes them. If you need the LITERAL characters `{old}` in the instructions (for some reason), escape with `{{old}}`. Audit the output: a successful build_block_reason call should have NO unsubstituted `{name}` patterns in the output (a regex `\{[a-z_]+\}` should match zero times).

  Step C: extend the co-located `#[cfg(test)] mod tests` block with three additional tests covering the substitution invariants in `<behavior>`:
  ```rust
  #[test]
  fn build_block_reason_single_step_with_dates_substitutes_correctly() {
      let out = build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true);
      assert!(out.contains("<old>1.10.9</old>"));
      assert!(out.contains("<new>1.10.10</new>"));
      assert!(out.contains("<old_date>2026-05-15</old_date>"));
      assert!(out.contains("<new_date>2026-05-16</new_date>"));
      assert!(out.contains("<step_count>1</step_count>"));
      assert!(out.contains("<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>"));
      assert!(out.contains("<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>"));
      assert!(out.contains("\"Yes, full changelog\""));
      assert!(out.contains("\"Yes, highlights only\""));
      assert!(out.contains("\"Remind me later\""));
      assert!(out.contains("\"Skip\""));
      assert!(out.contains("Pick 2-5 most impactful") || out.contains("2-5 highlight bullets"));
      assert!(out.contains("$OWL version-remind 1.10.9"));
      assert!(!out.contains("[PLAN_03_INSTRUCTIONS_PLACEHOLDER]"));
      // No unsubstituted template tokens:
      let token_re_hits = out.matches('{').filter(|_| true).count();
      // A handful of literal `{` are acceptable (none in our wording though)
      // — assert no `{old}` / `{new}` / `{step_count}` literal remains:
      assert!(!out.contains("{old}"));
      assert!(!out.contains("{new}"));
      assert!(!out.contains("{step_count}"));
      assert!(!out.contains("{old_date}"));
      assert!(!out.contains("{new_date}"));
  }

  #[test]
  fn build_block_reason_multi_step_substitutes_step_count() {
      let out = build_block_reason("1.10.5", "1.10.10", Some("2026-05-11"), Some("2026-05-16"), 5, true);
      assert!(out.contains("<step_count>5</step_count>"));
      assert!(out.contains("(5 versions)"));
      assert!(out.contains("$OWL version-remind 1.10.5"));
  }

  #[test]
  fn build_block_reason_unresolved_changelog_degrades_gracefully() {
      let out = build_block_reason("1.10.9", "1.10.10", None, None, 1, false);
      assert!(out.contains("<old_date></old_date>"));
      assert!(out.contains("<new_date></new_date>"));
      assert!(out.contains("<changelog_path>NOT_RESOLVED</changelog_path>"));
      assert!(out.contains("NOT_RESOLVED") || out.contains("not available"));
      // Four options still present even when CHANGELOG unresolvable:
      assert!(out.contains("\"Yes, full changelog\""));
      assert!(out.contains("\"Skip\""));
  }
  ```

  Step D: run `cargo test --lib version_changelog` and confirm all tests pass (the existing Plan 02 tests + the three new ones).

  Step E: Audit for unsubstituted placeholders across the entire file:
  ```
  grep -c "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/owl/version_changelog.rs
  ```
  MUST return 0. (Plan 02 left exactly one occurrence; Plan 03 must remove it.)

  Step F: Commit:
  ```
  feat(34): author 4-option AUQ instructions for version-change block

  Replaces Plan 02's [PLAN_03_INSTRUCTIONS_PLACEHOLDER] with the locked
  AUQ wording per CONTEXT D-08:
  - "Yes, full changelog" / "Yes, highlights only" / "Remind me later" / "Skip"
  - Substitutes {old}/{new}/{step_count}/{old_date}/{new_date} verbatim
  - Surfaces $OWL version-remind {old} invocation literally (Claude has no
    way to infer the rollback target otherwise — CONTEXT D-08 clarification)
  - Highlights rubric: 2-5 most impactful items, user-facing-behavior bias
  - Multi-step path renders ALL interim H2 sections (RESEARCH Pitfall 6)
  - NOT_RESOLVED branch degrades gracefully when CHANGELOG.md unreadable

  Three new unit tests assert substitution invariants + no unsubstituted
  tokens remain.
  ```
    </action>
    <verify>
      <automated>! grep -q "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/owl/version_changelog.rs &amp;&amp; cargo test --lib version_changelog 2>&amp;1 | grep -qE "test result: ok"</automated>
    </verify>
    <done>Placeholder removed; three new substitution tests pass; all prior Plan 02 tests still pass; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 2: Add golden snapshot fixture + integration test covering the end-to-end block reason string</name>
    <files>tests/golden/version_changelog_block.txt, tests/version_changelog.rs</files>
    <behavior>
  - `tests/golden/version_changelog_block.txt` exists with the full expected reason string for the canonical 1.10.9 -> 1.10.10 transition (step_count=1, both dates present, CHANGELOG resolved).
  - New integration test `block_reason_matches_golden_snapshot` invokes `owl hook-idle` with a mismatch fixture mirroring the golden's input parameters; captures stdout; parses the JSON envelope; extracts the `reason` field; asserts it equals the golden file content (after stripping leading/trailing whitespace) OR asserts a curated set of substring invariants per the RESEARCH open-question Q1 recommendation (snapshot just the stable parts).
  - Choose the SUBSTRING approach for resilience: any wording tweak in Task 1 invalidates a full-string snapshot but should NOT break the test. Substring invariants:
    - `<old>1.10.9</old>`
    - `<new>1.10.10</new>`
    - `<step_count>1</step_count>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
    - `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"` (literal labels)
    - `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md`
    - `$OWL version-remind 1.10.9` (with substituted old version)
  - The golden FILE serves as human-reviewable reference but the test's PASS/FAIL is on the substring set.
    </behavior>
    <action>
  Step A: Generate the canonical reason string by running locally:
  ```
  cargo test --lib version_changelog::tests::build_block_reason_single_step_with_dates_substitutes_correctly -- --nocapture 2>&1 | grep -A 200 "^---"
  ```
  OR write a tiny helper test that prints the canonical output:
  ```rust
  #[test]
  #[ignore]  // run with `cargo test -- --ignored` to regenerate golden
  fn print_canonical_block_reason_for_golden() {
      let out = build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true);
      println!("---GOLDEN-BEGIN---");
      println!("{}", out);
      println!("---GOLDEN-END---");
  }
  ```
  Run with `cargo test --lib print_canonical_block_reason_for_golden -- --ignored --nocapture`, capture the section between the markers, write to `tests/golden/version_changelog_block.txt`.

  Step B: Add the integration test to `tests/version_changelog.rs`:
  ```rust
  #[test]
  fn block_reason_matches_golden_snapshot() {
      let _lock = ENV_LOCK.lock().unwrap();
      let tmp = tempfile::tempdir().unwrap();

      // Fixture: sentinel at 1.10.9, OVERRIDE to 1.10.10, CHANGELOG covering both.
      let _guard = SptHomeGuard::set(tmp.path(), tmp.path(), Some("1.10.10"));
      write_changelog_fixture(tmp.path());
      write_live_perch(tmp.path(), "golden-perch");
      write_sentinel_fixture(tmp.path(), "1.10.9");

      let output = assert_cmd::Command::cargo_bin("owl")
          .expect("owl binary must be built")
          .arg("hook-idle")
          .env("SPT_HOME", tmp.path())
          .env("CLAUDE_PLUGIN_ROOT", tmp.path())
          .env("SPT_OVERRIDE_PKG_VERSION", "1.10.10")
          .env("HOME", tmp.path())
          .env("USERPROFILE", tmp.path())
          .env_remove("OWL_ECHO_COMMUNE")
          .write_stdin("{}")
          .output()
          .expect("owl subprocess");

      assert!(output.status.success(), "owl exited non-zero: {:?}", output);
      let stdout = String::from_utf8_lossy(&output.stdout);

      // Parse the JSON envelope and extract `reason`.
      let envelope: serde_json::Value = serde_json::from_str(&stdout.trim())
          .expect(&format!("stdout is not valid JSON: {}", stdout));
      assert_eq!(envelope["decision"], "block");
      let reason = envelope["reason"]
          .as_str()
          .expect("reason field missing or not a string");

      // Substring invariants per RESEARCH Q1 recommendation. Any one failure
      // means a wording regression vs the locked CONTEXT D-08 surface.
      let invariants: &[&str] = &[
          "<spt-version-changelog>",
          "</spt-version-changelog>",
          "<old>1.10.9</old>",
          "<new>1.10.10</new>",
          "<step_count>1</step_count>",
          "<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>",
          "<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>",
          "\"Yes, full changelog\"",
          "\"Yes, highlights only\"",
          "\"Remind me later\"",
          "\"Skip\"",
          "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md",
          "$OWL version-remind 1.10.9",
      ];
      for inv in invariants {
          assert!(
              reason.contains(inv),
              "reason missing required substring: {:?}\nfull reason:\n{}",
              inv,
              reason
          );
      }

      // Golden file is a human-readable reference. Diff-check it for completeness,
      // but allow drift (the test passes on substring invariants alone).
      let golden_path = std::path::Path::new("tests/golden/version_changelog_block.txt");
      if let Ok(golden) = std::fs::read_to_string(golden_path) {
          if golden.trim() != reason.trim() {
              eprintln!("NOTE: golden file at {} differs from live output. Regenerate via `cargo test -- --ignored print_canonical_block_reason_for_golden`.", golden_path.display());
          }
      } else {
          eprintln!("NOTE: golden file at {} missing. Regenerate via `cargo test -- --ignored print_canonical_block_reason_for_golden`.", golden_path.display());
      }
  }
  ```

  Step C: ensure `tests/golden/` directory exists; commit both the fixture and the test together.

  Step D: run `cargo test --test version_changelog` and confirm all 11 integration tests pass (10 from Plan 02 + this new one).

  Step E: commit:
  ```
  test(34): golden snapshot for version-change block reason

  Adds tests/golden/version_changelog_block.txt as human-reviewable
  reference for the canonical 1.10.9 -> 1.10.10 transition reason
  string. Integration test asserts a curated set of substring
  invariants (XML elements + literal AUQ labels + $OWL invocation)
  rather than full-string equality, per RESEARCH open question Q1
  recommendation: snapshot the stable parts, allow prose drift.
  ```
    </action>
    <verify>
      <!-- C2 fix: original regex `test result: ok\. (11|12) passed` over-constrained the
       count, coupling the gate to a fragile arithmetic invariant. Relax to `[0-9]+ passed`
       for the count assertion AND add a SEPARATE substring assertion that the new test
       name `block_reason_matches_golden_snapshot` actually appears in the output. This
       decouples "tests pass" from "the specific new behavior was tested" and survives
       future test additions in this file. -->
  <automated>test -f tests/golden/version_changelog_block.txt &amp;&amp; cargo test --test version_changelog 2>&amp;1 | tee /tmp/it2.log &amp;&amp; grep -qE "test result: ok\. [0-9]+ passed" /tmp/it2.log &amp;&amp; grep -q "block_reason_matches_golden_snapshot" /tmp/it2.log</automated>
    </verify>
    <done>Golden fixture committed; new integration test passes; full integration suite (now 11 tests) green; commit landed.</done>
  </task>

  <task type="checkpoint:human-verify" gate="blocking">
    <name>Task 3: UAT — manually verify the AUQ surfaces correctly in a real Claude Code session</name>
    <what-built>
  The full Phase 34 stack:
  - Stop-hook version-change detection with sentinel at `$SPT_HOME/last-seen-version.json`
  - `<spt-version-changelog>` block emission via `decision:"block"` + `reason`
  - 4-option AUQ flow rendered by Claude on re-engagement
  - `$OWL version-remind <old>` rollback subcommand
  - DEPLOY.ps1 syncs CHANGELOG.md alongside the binary
    </what-built>
    <how-to-verify>
  1. **Clean build + deploy:**
     ```
     cargo build --release
     powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1
     ```
     Verify the deploy log shows the two new `Copy CHANGELOG.md ->` substeps. After deploy, run `/reload-plugins` in Claude Code.

  2. **Verify CHANGELOG.md landed in plugin cache:**
     Windows:
     ```
     type %LOCALAPPDATA%\..\..\.claude\plugins\cache\cplugs\spt\<version>\CHANGELOG.md
     ```
     (Path varies; verify against `installed_plugins.json`.) The file should contain the curated Keep-a-Changelog content authored in Plan 01.

  3. **Synthesize a version mismatch by rewriting the sentinel to a fake old version:**
     Windows PowerShell:
     ```
     Set-Content -Path "$env:LOCALAPPDATA\spt\last-seen-version.json" -Value '{"version":"0.0.1"}'
     ```
     Unix:
     ```
     echo '{"version":"0.0.1"}' > ~/.spt/last-seen-version.json
     ```

  4. **Trigger a Stop hook:** open a Claude Code session in any spt-aware repo (this one works). Run a no-op task that ends a turn (e.g., "what files are in this directory?"). When Claude finishes, the Stop hook fires.

  5. **EXPECTED behavior on re-engagement:**
     a. Claude writes a pre-question line naming the version transition (and dates, if visible in the block payload).
     b. Claude invokes AskUserQuestion with EXACTLY four options (labels verbatim):
        - `Yes, full changelog`
        - `Yes, highlights only`
        - `Remind me later`
        - `Skip`
     c. Note: the question text mentions "X versions" if step_count > 1 (when 0.0.1 -> current, step_count will be large because CHANGELOG.md has many entries between).

  6. **Test each branch (in 4 separate Claude Code sessions or by re-synthesizing the mismatch between each):**
     - **Yes, full changelog:** Claude prints H2 sections from CHANGELOG.md verbatim. Confirm formatting preserved (bullets, H3 subsections, etc.). Sentinel after: current version.
     - **Yes, highlights only:** Claude renders 2-5 highlight bullets across the range + literal pointer line "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail." Sentinel after: current version.
     - **Remind me later:** Claude invokes `$OWL version-remind 0.0.1` via Bash. Sentinel after: `{"version":"0.0.1"}` (rolled back). The next Stop hook re-fires the block.
     - **Skip:** Claude says "OK, skipping." or similar; sentinel after: current version; next Stop does NOT re-fire.

  7. **Verify sentinel state after each branch** by running `type %LOCALAPPDATA%\spt\last-seen-version.json` (Windows) or `cat ~/.spt/last-seen-version.json` (Unix).

  8. **Test the "Other" freeform fallback** (one of the branches): when the AUQ surfaces, type something like "not now" into Other. Verify Claude maps it to `Remind me later` and invokes `$OWL version-remind 0.0.1`.

  9. **Regression check** — confirm the existing Phase 18.3 `.more-done` sentinel still works:
     - Start a `/spt:live` session with a psyche.
     - End a turn.
     - Verify `$SPT_HOME/owlery/<psyche_id>/.more-done` exists after Stop fires.
     - Verify the version-change block did NOT cancel out this behavior.

  10. **OWL_ECHO_COMMUNE regression check** — manually invoke `OWL_ECHO_COMMUNE=1 owl hook-idle <<< '{}'` after re-synthesizing a mismatch. Verify stdout is EMPTY (no block emitted; guard fires) and sentinel is UNCHANGED.

  If all 10 checks pass: type `approved` to gate-pass this checkpoint.

  If any check fails: describe the failure precisely (which step, what was expected, what actually happened, any stderr/stdout captured). The orchestrator will route the failure back through `/gsd-plan-phase --gaps` for closure.
    </how-to-verify>
    <resume-signal>Type "approved" if all 10 UAT checks pass, or describe the failure for gap closure routing.</resume-signal>
  </task>

  </tasks>

  <verification>
  After all 3 tasks (Tasks 1+2 automated, Task 3 manual UAT):

  1. Placeholder fully removed:
     ```
     grep -r "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/ tests/
     ```
     Expect 0 hits.

  2. Full test suite green:
     ```
     cargo test
     ```
     Expect ~30+ unit tests + 11 integration tests, all passing.

  3. Golden fixture present and non-empty:
     ```
     test -s tests/golden/version_changelog_block.txt
     ```
     Plus a manual eyeball: does the golden content read as the AUQ wording you authored?

  4. Two new commits in addition to Plan 01 + Plan 02:
     ```
     git log --oneline -2
     ```
     Expect:
     - `test(34): golden snapshot for version-change block reason`
     - `feat(34): author 4-option AUQ instructions for version-change block`

  5. UAT approved by human via Task 3 checkpoint.

  6. End-to-end stack: `cargo build --release && powershell docs/DEPLOY.ps1 -Bump patch -DryRun` exits 0 and surfaces the new CHANGELOG.md stub-append substep (regression check against Plan 01 work).
  </verification>

  <success_criteria>
  - Plan 03 maps to VERS-04 (4-option AUQ wording authored verbatim; AskUserQuestion surface verified in UAT) and VERS-06 (multi-version path uses the same 4-option AUQ; "Yes, full changelog" renders all interim H2 sections per Pitfall 6).
  - The `<instructions>` element in the block reason is the only file where wording lives — change-control is centralized.
  - Golden snapshot resilience strategy (substring invariants, not full-string) accommodates future wording tweaks without breaking the test.
  - UAT confirms the entire Phase 34 surface works in a real Claude Code session.
  - After approval, Phase 34 is shippable in the next end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6).
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | hook reason -> Claude continuation | the `<instructions>` element text drives Claude's next turn; wording reliability determines whether the AUQ fires correctly |
  | Claude -> Bash | `$OWL version-remind <old>` invocation crosses Claude's Bash tool boundary; `<old>` is substituted by the hook, not by Claude |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-11 | Tampering | Wording drift in `<instructions>` causes Claude to invoke wrong tool / wrong args | mitigate | Substring-invariant integration test (Task 2) asserts the 4 option labels + literal invocation string survive any prose tweak; UAT (Task 3) catches semantic regressions |
  | T-34-12 | Spoofing | Claude paraphrases `$OWL version-remind <old>` to a different value | mitigate | Instructions text explicitly says "substitute the {old} value verbatim, do NOT paraphrase or recompute"; the `<old>` value is pre-substituted by the hook so Claude only needs to copy-paste; the `is_plain_semver` validator in version-remind rejects garbage arg |
  | T-34-13 | Information disclosure | Pre-question prose leaks unintended detail | accept | Author-controlled wording; only surfaces version numbers + dates from CHANGELOG.md (already public artifact) |
  | T-34-14 | DoS | Highlights branch reads CHANGELOG.md for every turn | accept | Read only happens on user-explicit "Yes, highlights only" pick, bounded by user action; no automated re-read |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-03-SUMMARY.md` documenting:
  - Two commit SHAs (Task 1 wording, Task 2 golden)
  - UAT result (approved / failed-with-detail)
  - Final substring invariant count in the integration test
  - Snapshot file SHA-256 (for future drift detection)
  - Confirmation `grep -r PLAN_03_INSTRUCTIONS_PLACEHOLDER src/ tests/` returns 0 hits
  - Any wording tweaks made between Task 1 draft and the final committed text (preserve the audit trail)
  - Confirmation Phase 34 is ready for the next end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6)
  </output>

  ## Review Instructions

  Analyze each plan and provide:
  1. Summary — one-paragraph assessment
  2. Strengths (bullets)
  3. Concerns (bullets, severity HIGH/MEDIUM/LOW)
  4. Suggestions (specific bullets)
  5. Risk Assessment (LOW/MEDIUM/HIGH + justification)

  Focus on: missing edge cases, dependency ordering, scope creep, security, performance, whether plans achieve phase goals.
  Output in markdown.

first_user_message:
  # Cross-AI Plan Review Request

  You are reviewing implementation plans for software project phase 34.
  Provide structured feedback on plan quality, completeness, and risks.

  ## Project Context
  # Owl Skill (SPT — Spacetime / Sentience Pocket Transacter)

  ## What This Is

  A zero-dependency inter-agent messaging and live-agent lifecycle system for Claude Code, shipped as the `spt` plugin via the `cplugs` marketplace. Native Rust binary (`owl.exe`) uses TCP-primary delivery with SQLite spool fallback. Live agents run a detached Psyche wrapper (periodic `claude -p --resume` sessions) that absorbs `COMMUNE` updates from Self and reacts to scheduled pulses, file-drops, and SessionStart boundaries. Listener now streams `<EVENT>` envelopes via Claude Code's Monitor tool. Used by multiple people — Windows-native portability with no runtime dependencies is a hard constraint.

  ## Core Value

  Reliable, zero-dependency agent-to-agent messaging that just works on any machine, with a live-agent layer that survives `/clear`, `/compact`, binary handoffs, and orphan boundaries without losing context.

  ## Current Milestone: v1.7.1 Seamlessification II

  **Goal:** Polish round on live-agent friction — listing/picker correctness, skill hint surface, fresh-start commune prompt, auto-resume affordance, plugin-update changelog notification.

  **Target features:**
  - `list` fn overhaul (`--all` / `--offline` / `--here`; default = online-only) for `$OWL list` + `$LIVE list`
  - `argument-hint` frontmatter coverage on every `/spt:*` skill
  - Fresh-start live-agent commune flow — when agent identity is known-new (via pick-spec OR psyche-download empty), surface first-commune summary and prompt user before init
  - `$LIVE pick-spec` correctness — return live agents in repo even when all are online; calling flow enforces "new name or cancel"
  - `/spt:live --auto` flag — SessionStart auto-surfaces pick-spec JSON for non-live/non-listening agents; `--auto` picks most-recent + immediately resumes any clear next body of work; activates on casual prompts ("pick up where we left off", "keep going", "resume work")
  - Version-change changelog notification — sentinel + `%LOCALAPPDATA%` current-version file + SessionStart/PreToolUse hook prompts user with old→new diff

  ## Current State

  **Shipped: v1.7 Seamlessification** (2026-05-16; deployed v1.10.10)

  Five phases (26-30) focused on friction removal across the live-agent lifecycle:
  - **Phase 26** — `/spt:live` auto-pick + AskUserQuestion fork + free-text resolve
  - **Phase 27** — Listener poll migrated from Bash to Monitor tool (streaming `<EVENT>` envelopes)
  - **Phase 28** — SessionStart injects `<psyche-context>` on live perches (`/clear` & `/compact`)
  - **Phase 29** — Auto-fire echo-commune at boundaries + typed `<EVENT type="echo_commune">` envelope cutover
  - **Phase 30** — Commune/signoff file-drop protocol (`.claude/{id}-{commune,signoff}.md`) replaces stdin piping

  26 plans, 211 commits, 7-day cycle. Off-roadmap sub-iterations 18.x continued through v1.10.x defense-in-depth deploys (stale-signoff drain, init_signoff envelope-shape predicate, FileDropOutcome::BreakLoop).

  **In flight: v1.7.1 Seamlessification II** — Phase 31 complete (2026-05-17): `$LIVE pick-spec` now emits `kind:"all-live"` when every known agent in the repo is online (covers PICK-01/02/04/05/06; PICK-03 deferred per D-10/D-11). Phases 32 (list overhaul + skill hint audit), 33 (fresh-start commune + auto-resume, depends on 31), and 34 (version-change changelog) remain.

  **Next milestone:** v1.8 Psyche Restructure — Phases 23/24/25 (commune/signoff project-root + HEAD SHA stamping, tracked-dir per-agent layout + sessions audit log, psyche perch nesting + general/project context split).

  ## Requirements

  ### Validated

  - ✓ Harden owl.sh against race conditions and edge cases — v1.0
  - ✓ Unified output formatting: compact symbols, ANSI color codes, no emoji — v1.0
  - ✓ Auto-setup env vars and session cleanup hook — v1.0
  - ✓ Live Agent start + Psyche subagent + periodic pulse + COMMUNE format — v1.0
  - ✓ Native Rust binary with P2P TCP messaging + SQLite offline spool — v1.1
  - ✓ Golden test suite + cross-platform CI (4 targets) — v1.1
  - ✓ Timed pulses + Spine supervisor + single-binary drop-in — v1.1
  - ✓ Plugin migration to `/spt` namespace — v1.5
  - ✓ Spacetime filesystem (`%LOCALAPPDATA%\spt` / `~/.spt`) with `SPT_HOME` override — v1.5
  - ✓ Persistent offline perches + indefinite queueing + reconnection — v1.5
  - ✓ Memformat-driven Psyche brainstorming + INSIGHT messages — v1.5
  - ✓ PerchState enum + owl doctor + session detection + optional agent_id — v1.5
  - ✓ Hand-rolled MCP server (JSON-RPC over stdio) — v1.6
  - ✓ Spool unification (peek_all + mark_delivered) + Result variant refactor — v1.6
  - ✓ 13 MCP tools + 5 MCP resources + check-on-request notifications — v1.6
  - ✓ Seamless binary handoff for long-running spt processes — v1.6 (Phase 18.4/18.5 sub-iterations)
  - ✓ Echo-commune fresh-session jsonl excerpt + 15-min cadence gate — v1.6 (Phase 18.8 sub-iteration)
  - ✓ `/spt:live` auto-pick + AskUserQuestion fork + `$LIVE pick-spec`/`$LIVE fork` — v1.7
  - ✓ Listener poll: Monitor tool primary + `<EVENT>` envelopes + Bash `--once` fallback — v1.7
  - ✓ SessionStart `<psyche-context>` injection on live perches — v1.7
  - ✓ Auto-fire echo-commune on `/clear`+`/compact`+orphan; typed envelope cutover — v1.7
  - ✓ Commune/signoff file-drop protocol (`.claude/{id}-{commune,signoff}.md`) — v1.7

  ### Active

  - Phase 23 — Commune/signoff payload includes `project_root` + `HEAD` SHA (v1.8 candidate)
  - Phase 24 — `psyches/tracked/<self_id>/` per-agent layout + sessions audit log (v1.8)
  - Phase 25 — Psyche perch nesting + general.md / proj-{project}.md context split (v1.8)
  - Phase 19-22 — Capsule (psmux-based sendkeys pipeline, abrupt messages, GSD proceed via sendkeys) (v1.9)
  - Resolve `psyche-stale-after-clear` debug session (root cause known, fix pending)
  - 2 Phase 28 raw-stdout /clear-cycle Human-UAT tests (deferred, non-blocking)

  ### Out of Scope

  - macOS/Linux-specific features — cross-platform CI covers builds but not platform-specific optimizations
  - External dependencies beyond the single binary — copy-one-file install is core constraint
  - MCP transport prioritization — deprioritized post-v1.6 in favor of CLI-first model (see `feedback_mcp_performance.md`); MCP layer remains but Phase 18.1+ sub-iterations refactored away from MCP-centric flows

  ## Phase 34: Version-Change Changelog
  ### Roadmap Section
  ### Phase 34: Version-Change Changelog
  **Goal**: When a user's installed `spt` plugin version changes underneath them, the next SessionStart surfaces an old→new changelog prompt (via AskUserQuestion) sourced from compiled-in `CARGO_PKG_VERSION` against an `$SPT_HOME/last-seen-version.json` sentinel — without firing on first-install, without colliding with the SessionStart auto-pick (AUTO-03), and without racing the binary handoff (Phase 18.4/18.5)
  **Milestone**: v1.7.1 Seamlessification II
  **Depends on**: Nothing in v1.7.1 (independent at the Rust level); sequenced AFTER Phase 33 to keep the two SessionStart-hook extensions (AUTO-03 + VERS-04) in separate phases
  **Requirements**: VERS-01, VERS-02, VERS-03, VERS-04, VERS-05, VERS-06, VERS-07, VERS-08, VERS-09
  **Success Criteria** (what must be TRUE):
    1. On a fresh install (no `$SPT_HOME/last-seen-version.json` present), SessionStart silently writes the current `CARGO_PKG_VERSION` and emits NO changelog prompt to the user
    2. After the plugin version changes (compiled-in `CARGO_PKG_VERSION` differs from sentinel contents), the next SessionStart emits a single `SPT plugin updated v{old} → v{new}. Print changelog?` AskUserQuestion (yes/no/dismiss); the sentinel is atomically rewritten with the current version regardless of user response, so the prompt does not re-fire next session
    3. A multi-version skip (e.g. v1.10.5 → v1.12.0) surfaces a version-list summary plus a pointer to `CHANGELOG.md` — never full prose for each interim version
    4. Repo-root `CHANGELOG.md` exists with H2-per-version sections (created from git log if absent during phase kickoff); `docs/DEPLOY.ps1` syncs it into the plugin meta dir alongside `plugin.json`
    5. The version-change prompt (VERS-04) and SessionStart auto-pick (AUTO-03 from Phase 33) coexist deterministically under non-overlapping allow-list conditions; the sentinel write site lives at the top-level SessionStart entry (atomic write, no in-handoff trampoline writes) so a mid-handoff race cannot corrupt it
    6. End-of-milestone DEPLOY.ps1 `-Bump patch` ships all four phases together (no per-phase deploys during v1.7.1)
  **Plans:** TBD

  Plans:
  - [ ] TBD — discuss/plan-phase to break down (research flag: phase must begin with CHANGELOG.md existence check + create-from-git-log if absent)

  ## Progress

  ### Requirements Addressed (pre-amendment baseline; Plan 01 will amend VERS-02/04/06/08/09)

  - [ ] **VERS-01**: A new file `$SPT_HOME/last-seen-version.json` (sibling to `owlery/`, NOT inside it) stores the most recently seen plugin version per the running owl.exe.
  - [ ] **VERS-02**: On SessionStart (in `plugin_session_start.rs`), owl.exe compares `env!("CARGO_PKG_VERSION")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`.
  - [ ] **VERS-03**: First-install case (sentinel missing) silently writes the current version — NO prompt is emitted to the user.
  - [ ] **VERS-04**: Version-mismatch case emits a single SessionStart prompt: `SPT plugin updated v{old} → v{new}. Print changelog?` via AskUserQuestion (yes / no / dismiss).
  - [ ] **VERS-05**: Regardless of user response, the sentinel is atomically rewritten with the current version using existing `atomic_write_string` helper (tmp + rename) so the prompt does not re-fire next session.
  - [ ] **VERS-06**: Multi-version skip (e.g. v1.10.5 → v1.12.0) surfaces a version-list summary plus a pointer to `CHANGELOG.md`, not full prose for each interim version.
  - [ ] **VERS-07**: A repo-root `CHANGELOG.md` is created (or existing one is verified) with H2-per-version sections so excerpt parsing works; deploy script (`docs/DEPLOY.ps1`) syncs it into the plugin meta dir alongside `plugin.json`.
  - [ ] **VERS-08**: SessionStart auto-pick (AUTO-03) and version-change prompt (VERS-04) coexist without colliding — they fire under non-overlapping allow-list conditions and in deterministic order.
  - [ ] **VERS-09**: Version-sentinel write does not race against binary handoff (Phase 18.4/18.5) — single write site at the top-level SessionStart entry, after user sees the prompt; atomic; no in-handoff trampoline writes.


  ### User Decisions (CONTEXT.md)
  # Phase 34: Version-Change Changelog - Context

  **Gathered:** 2026-05-17
  **Status:** Ready for planning

  <domain>
  ## Phase Boundary

  A single end-of-turn `Stop` hook prompt that surfaces an old→new changelog excerpt to the user the first time the agent finishes a turn after the plugin version has changed underneath them. Scope:

  1. **Sentinel + version-mismatch detection** — `$SPT_HOME/last-seen-version.json` (sibling to `owlery/`, NOT inside it) stores the most recently seen version. `hook_idle.rs` (Stop hook) compares `env!("CARGO_PKG_VERSION")` against the sentinel after perch resolution; on mismatch, emits a `<spt-version-changelog>` block via `decision:"block"` + `reason:"..."` and atomically rewrites the sentinel at the same site.
  2. **Repo-root `CHANGELOG.md`** — created from git-log history at Plan 01 start in Keep-a-Changelog format (`## [X.Y.Z] - YYYY-MM-DD`). DEPLOY.ps1 `-Bump` appends a stub H2 without committing and refuses to deploy until author fills + commits.
  3. **DEPLOY.ps1 sync** — copies repo-root `CHANGELOG.md` into `{Marketplace}/plugin/spt/CHANGELOG.md` alongside the existing binary/skills/hooks/plugin.json sync step. Hook resolves at runtime via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.
  4. **Block payload + AskUserQuestion shape** — Rust hook parses CHANGELOG.md once, builds `{old, new, old_date, new_date, step_count}` payload, embeds in block. Claude renders a 4-option AskUserQuestion (`Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`); on `Remind me later` invokes new `$OWL version-remind` subcommand that rolls the sentinel back to `old`, re-arming the next-Stop emission.
  5. **Hook-pivot triggers REQUIREMENTS.md + ROADMAP.md amendment** (Plan 01 first commit, mirrors Phase 31 D-11 / Phase 33 D-03/D-04 pattern) — strikes the SessionStart-entry assumption baked into VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md success criteria 1, 2, 5.

  Touches:
  - `src/owl/hook_idle.rs` — version-mismatch detection + emission + sentinel atomic write at top of `run()`, after perch resolution and before `set_idle_ready` / `spawn_echo_commune_if_live`. Recursion guard: skip when `OWL_ECHO_COMMUNE` set (mirror line 64-69).
  - New `src/owl/version_changelog.rs` (or co-located in `hook_idle.rs`) — sentinel read/write helpers, CHANGELOG.md parser (regex `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`), payload builder.
  - New bin subcommand `owl version-remind <old>` — atomically writes `{"version":"<old>"}` back to the sentinel using existing `atomic_write_string`. ~10-line implementation.
  - New repo-root `CHANGELOG.md` — Keep-a-Changelog format, authored by hand at Plan 01 from `git log --oneline --grep='chore: bump'`.
  - `docs/DEPLOY.ps1` — (a) new sync step copying `CHANGELOG.md` to marketplace, (b) `-Bump` extension that appends stub H2 to repo-root CHANGELOG.md uncommitted and aborts on unchanged-but-required content.
  - `.planning/REQUIREMENTS.md` — amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 per decisions D-01..D-09.
  - `.planning/ROADMAP.md` Phase 34 — revise success criteria 1, 2, 5 in lockstep with REQUIREMENTS.md.

  No new on-disk artifacts beyond the sentinel + CHANGELOG.md. No new info.json fields. No skill surface (no `/spt:changelog` skill — block-body inline instructions drive Claude through the AUQ + remind flow).

  </domain>

  <decisions>
  ## Implementation Decisions

  ### Hook Pivot (architectural foundation)

  - **D-01: Stop hook (`hook_idle.rs`), not SessionStart.** Surfacing the version-change prompt at end-of-turn lands the AskUserQuestion exactly when the agent is between tasks. Rationale: a SessionStart or UserPromptSubmit prompt risks the user issuing "go do X" → walking away → AskUserQuestion blocking the agent → returning many minutes later to find zero progress. Stop hook guarantees the question fires after the agent's current work is complete and before any new turn starts. This invalidates VERS-02's "On SessionStart (in `plugin_session_start.rs`)" wording — amendment scope below.

  - **D-02: Predicate = `owl_id` resolves in `hook_idle.rs::run()`.** Piggybacks on the existing perch-resolution gate at hook_idle.rs:25-32. If no perch matches `OWL_SESSION_ID` or `parent_pid`, hook early-returns and the version-change check never runs — exactly the "spt is in use" semantic the user requested (`spt listen` / `spt live` / `spt revive` all create perches; live-agent sessions are perches). No new predicate code; no new info.json field.

  - **D-03: Recursion guard reuses `OWL_ECHO_COMMUNE` env-var skip** (hook_idle.rs:64-69 pattern). Haiku echo-commune subprocesses MUST NOT see a version-change block. New version-changelog code MUST run after the OWL_ECHO_COMMUNE check or be itself guarded equivalently.

  ### Block Emission + Sentinel Write Site

  - **D-04: Block emission via `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"`** at the top of `hook_idle.rs::run()`, immediately after perch resolution and the OWL_ECHO_COMMUNE guard, before `set_idle_ready` / `spawn_echo_commune_if_live`. Block content is the rendered XML payload (D-05). Stop hook's `decision:"block"` + `reason` schema is the natural injection channel for Stop (which has no `hookSpecificOutput.additionalContext` per the existing hook_idle.rs:1-5 comment) — research flag: confirm Stop's `reason` text reaches Claude as injected context on re-engagement, not as a silent abort. See Research Flags below.

  - **D-05: Sentinel write site = same `run()` top, BEFORE returning the block.** Atomic via existing `atomic_write_string` (src/common/owlery.rs:248). Default behavior: writes the NEW version. Re-fire on next Stop only happens if the user picks `Remind me later`, which calls `$OWL version-remind <old>` to roll the sentinel back. This inverts the "ack on yes/skip" model: the hook commits sentinel pre-emission; only the explicit remind path rolls back. Rationale (per user): most-used paths (Yes-full / Yes-highlights / Skip) have zero UX bloat; the less-common Remind path pays the round-trip.

  - **D-06: Fire-once cadence = once per version transition.** With D-05 sentinel pre-write in place, the next Stop in the same session sees `sentinel == CARGO_PKG_VERSION` and emits nothing. Matches VERS-05's "does not re-fire next session" intent — also doesn't re-fire next Stop in same session.

  ### Block Payload + AskUserQuestion Shape

  - **D-07: Payload shape = `{old, new, old_date, new_date, step_count}`.** Embedded JSON-ish inside the `<spt-version-changelog>` block. Dates parsed from CHANGELOG.md H2 lines (Keep-a-Changelog `## [X.Y.Z] - YYYY-MM-DD` regex). `step_count` = number of H2 sections between `old` (exclusive) and `new` (inclusive). Surfaces in the AUQ question text (`SPT plugin updated v{old} → v{new} ({step_count} version{s}). Print changelog?`); does NOT change the option set.

  - **D-08: AskUserQuestion 4-option shape:** `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`.
    - `Yes, full changelog` — Claude prints ALL H2 sections from CHANGELOG.md where `old < v ≤ new`, verbatim markdown, concatenated. Single-step and multi-step rendered identically (overrides VERS-06's "never full prose for each interim version" constraint — see amendment scope D-09).
    - `Yes, highlights only` — Claude's discretion: extracts the most impactful feature adds + fixes from the same range, prose-style. No fixed rubric; agent uses judgment. Pointer to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` appended for users who want full detail.
    - `Remind me later` — Claude invokes `$OWL version-remind <old version>` (e.g. `$OWL version-remind 1.10.9`), passing the `old` value from the block payload as a literal positional arg. `$OWL` is the env-var-injected owl-binary path (matches existing `$OWL deliver` / `$LIVE pick-spec` conventions across `/spt:*` skills). The subcommand rewrites `$SPT_HOME/last-seen-version.json` to `{"version":"<old version>"}` so the next Stop hook re-detects mismatch and re-fires the block. No body rendered. **The block body MUST surface the exact `$OWL version-remind <old>` invocation string verbatim** so Claude has no ambiguity about which arg to pass — Claude cannot infer `<old>` from anything other than the block payload it just received.
    - `Skip` — no action. Sentinel already updated by hook at run-entry; no re-fire.

  ### CHANGELOG.md Genesis + Maintenance

  - **D-09: Repo-root `CHANGELOG.md` authored one-shot at Plan 01 start.** Plan 01 step 1 (after the REQUIREMENTS.md/ROADMAP.md amendment commit): author CHANGELOG.md by hand using `git log --oneline --grep='chore: bump'` as the version-bump skeleton. Curated prose per version, Keep-a-Changelog format. One commit; never auto-regenerated. No new `owl changelog backfill` subcommand — out of scope.

  - **D-10: Forward maintenance = DEPLOY.ps1 `-Bump` stub injection + commit gate.** When `-Bump patch|minor|major` runs, DEPLOY.ps1 appends a stub `## [NEW_VERSION] - YYYY-MM-DD\n\n- TODO: changelog entry\n` to repo-root CHANGELOG.md **without committing**. Outputs an explicit notice that the user must fill + commit before deploy will succeed. On the next `-Bump` invocation: if CHANGELOG.md still contains the TODO marker for `NEW_VERSION` (or is otherwise unchanged from prior commit), DEPLOY aborts with the same notice. Combines stub-injection (option 1) with the "abort if unchanged" gate (option 2) — guaranteed CHANGELOG.md update before any bump can ship.

  - **D-11: DEPLOY.ps1 sync target = `{Marketplace}/plugin/spt/CHANGELOG.md`** (sibling to `plugin.json`, NOT inside `.claude-plugin/`). Resolves at hook time as `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`. Add to DEPLOY.ps1's sync step alongside `owl.exe` + `skills/` + `hooks/` + `plugin.json`. Mirrors `psyche.md` placement convention (plugin root, not subdir).

  ### REQUIREMENTS.md + ROADMAP.md Amendment Scope (Plan 01 first commit)

  - **D-12: Amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md success criteria 1, 2, 5.** Mirrors Phase 31 D-11 and Phase 33 D-03/D-04 amendment-commit pattern.
    - **VERS-02:** "On SessionStart (in `plugin_session_start.rs`)" → "On Stop hook (in `src/owl/hook_idle.rs`), gated on active perch resolution via existing `find_perch_by_session` / `find_perch_by_parent_pid`. Hook compares `env!("CARGO_PKG_VERSION")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`." (compiled-in source clause preserved)
    - **VERS-04:** yes/no/dismiss → "AskUserQuestion with four options: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Question text includes `step_count` when > 1."
    - **VERS-06:** drop "never full prose for each interim version" constraint. New wording: "Multi-version skip surfaces the same 4-option AskUserQuestion as single-step; user-chosen `Yes, full changelog` renders all interim H2 sections verbatim, `Yes, highlights only` defers to agent discretion. The CHANGELOG.md pointer is appended to the highlights body."
    - **VERS-08:** AUTO-03 (SessionStart) and VERS-04 (Stop) fire on different hooks; coexistence is enforced by hook-separation, not allow-list overlap. New wording: "Version-change prompt (VERS-04) fires from Stop hook and SessionStart auto-pick (AUTO-03 from Phase 33) fires from SessionStart hook. The two never share a hook invocation; no allow-list reconciliation required."
    - **VERS-09:** "single write site at the top-level SessionStart entry... no in-handoff trampoline writes" → "Single write site at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard. Atomic via existing `atomic_write_string`. Binary handoff (Phase 18.4/18.5) does not race because Stop hooks do not fire during in-flight binary swap (Stop fires between turns, handoff happens between processes)."
    - **ROADMAP.md SC1:** "On a fresh install, SessionStart silently writes..." → "...on first Stop hook fire in any spt-bearing session, sentinel silently writes..."
    - **ROADMAP.md SC2:** "next SessionStart emits..." → "next Stop hook fire in an spt-bearing session emits..."
    - **ROADMAP.md SC5:** drop coexistence language → "VERS-04 fires from Stop; AUTO-03 fires from SessionStart; collision impossible by hook separation."
    - Plan 01 begins with this amendment commit before any Rust code lands.

  ### Claude's Discretion

  - **Stop hook `decision:"block"` + `reason` injection semantics** — Confirm via Claude Code hooks reference (`~/.claude/reference_docs/claude-code-hooks.md`) that the `reason` text in a blocked Stop response reaches Claude as injected context on the next turn re-engagement. The hook_idle.rs:1-5 comment notes Stop "has no hookSpecificOutput schema" — but `decision:"block"` is a separate output shape that may carry user-visible context. Flag for research phase. If `decision:"block"` does NOT propagate text to Claude usefully, fall back: emit via PreToolUse (next tool call) or revisit UserPromptSubmit despite the earlier "walk-away" concern.
  - **`<spt-version-changelog>` block exact XML shape** — child elements vs JSON-as-text payload vs hybrid. Locked decisions: must carry `old`, `new`, `old_date`, `new_date`, `step_count`. Open: also embed full CHANGELOG.md excerpt for `old < v ≤ new`, or have Claude read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` at AUQ-yes time? D-05 sentinel-pre-write removes the "block must be self-contained" pressure; either is workable. Planner / researcher decides — leaning toward Claude reads at yes-time so block payload stays small.
  - **`version-remind` subcommand exact signature** — Locked: `$OWL version-remind <old version>` (bare positional arg, e.g. `$OWL version-remind 1.10.9`). Block-body MUST surface this exact invocation string with the `old` value substituted so Claude passes it verbatim; the subcommand cannot infer the rollback target from any other source.
  - **CHANGELOG.md parser regex tightness** — Keep-a-Changelog allows `## [Unreleased]` and link references (`[1.10.10]: <url>`). The phase 34 parser SHOULD reject `[Unreleased]` (no date, not a real release) and ignore link reference lines (not H2 markers — they're `[ref]:` form). Add to RESEARCH.md test corpus.
  - **Highlights extraction policy** — D-08 says "Claude's discretion". Skill-body wording (or block-body wording, since no skill is planned) should give Claude enough framing: "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes". Final wording in Plan 02 or wherever the block-body authoring lives.
  - **Plan split** — likely 3 plans: (1) REQUIREMENTS.md/ROADMAP.md amendment + CHANGELOG.md genesis + DEPLOY.ps1 sync/stub, (2) Rust hook + sentinel + parser + `version-remind` subcommand + tests, (3) block-body Claude instructions + integration test on a synthesized version transition. Final boundary at planner's discretion.
  - **Tests** — version-mismatch detection (single-step, multi-step, first-install silent, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard, no-perch guard); CHANGELOG.md parser (Keep-a-Changelog happy path, `[Unreleased]` reject, malformed H2 graceful); `version-remind` rollback round-trip; DEPLOY.ps1 stub injection + abort-on-unchanged.

  </decisions>

  <canonical_refs>
  ## Canonical References

  **Downstream agents MUST read these before planning or implementing.**

  ### Phase requirements + planning artifacts
  - `.planning/REQUIREMENTS.md` VERS-01..VERS-09 — 9 requirements. **VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 to be amended per D-12.** Plan 01 begins with the amendment commit.
  - `.planning/ROADMAP.md` Phase 34 — goal + 6 success criteria. **Success criteria 1, 2, 5 to be revised in lockstep with REQUIREMENTS.md amendment.** Success criteria 3, 4, 6 unchanged.

  ### Cross-references to prior phase decisions
  - `.planning/phases/33-fresh-start-commune-auto-resume/33-CONTEXT.md` — Phase 33 D-11 amendment-commit pattern (Plan 01 mirrors this for D-12). AUTO-03 SessionStart emission pattern (`emit_auto_pick` at plugin_session_start.rs:139-155) — informational; Phase 34 does NOT emit from SessionStart but the Pattern S2 envelope shape is reusable for any future SessionStart additions. Phase 33 D-04 / FRESH-06 rewording precedent for VERS-08's coexistence-by-hook-separation rewording.
  - `.planning/phases/31-picker-correctness/31-CONTEXT.md` D-11 — doc-amendment-first plan structure (Plan 01 amends REQUIREMENTS.md + ROADMAP.md before any code).
  - `.planning/phases/32-list-overhaul-skill-hint-audit/32-CONTEXT.md` — DEPLOY.ps1 multi-file sync pattern (informational; Phase 34 extends the sync step).

  ### Stop hook surface (D-01..D-06)
  - `src/owl/hook_idle.rs:14-43` — `run()` entry. Version-change emission site is AFTER perch resolution (lines 25-32) and AFTER the OWL_ECHO_COMMUNE guard (mirror lines 64-69 — guard MUST run before any version-change work), BEFORE `set_idle_ready` (line 39) and `spawn_echo_commune_if_live` (line 42).
  - `src/owl/hook_idle.rs:1-5` — comment asserts "Stop hooks have no hookSpecificOutput schema". This is research-relevant: Phase 34 uses `decision:"block"` + `reason` instead, which is a different output channel. Confirm semantics against `~/.claude/reference_docs/claude-code-hooks.md` during research.
  - `src/owl/hook_idle.rs:64-69` — `OWL_ECHO_COMMUNE` recursion guard. Reuse pattern verbatim for the version-change code path.

  ### Claude Code hooks reference (D-04 research target)
  - `~/.claude/reference_docs/claude-code-hooks.md` — Stop hook output schema. Confirm `decision:"block"` + `reason` propagates `reason` text to Claude as re-engagement context (not as a silent abort).

  ### Atomic write + sentinel path (VERS-01, D-05)
  - `src/common/owlery.rs:248` — `atomic_write_string(path, body)` (tmp + rename). Use verbatim for both Stop-hook sentinel write (D-05) and `version-remind` rollback write.
  - `src/common/owlery.rs:18-32` — `spt_home()` resolves `$SPT_HOME` env override or platform default. Sentinel lives at `spt_home().join("last-seen-version.json")` — sibling to `spt_home().join("owlery")` per VERS-01.

  ### Compiled-in version source (VERS-02)
  - `src/owl/poll.rs:75`, `src/owl/setup.rs:35`, `src/live/stop.rs:199` — existing call sites for `env!("CARGO_PKG_VERSION")`. Pattern for the Phase 34 hook: same `env!` macro at compile time; no runtime `plugin.json` read.

  ### CHANGELOG.md format + parser surface (D-08, D-09, D-11)
  - `https://keepachangelog.com/en/1.1.0/` — Keep-a-Changelog v1.1.0 spec. Reference for `## [X.Y.Z] - YYYY-MM-DD` H2 shape. Phase 34 parser regex: `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`. Reject `[Unreleased]`; ignore link reference lines.
  - `psyche.md` (repo root, embedded via `include_str!`) — placement precedent for repo-root markdown files synced to plugin meta dir. Phase 34's CHANGELOG.md follows this placement convention but is NOT embedded (D-11: runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`).

  ### DEPLOY.ps1 surfaces (D-10, D-11)
  - `docs/DEPLOY.ps1:154-176` — plugin.json version resolution + `-Bump` entry. Stub-injection logic (D-10) hooks in alongside the existing Cargo.toml sync at lines 224-269.
  - `docs/DEPLOY.ps1:171-292` — `-Bump` block. New CHANGELOG.md stub-append + unchanged-abort gate slots in after the Cargo.toml/plugin.json bump but before commit (lines 277-287).
  - `docs/DEPLOY.ps1` sync step (post-build) — exact lines vary; CHANGELOG.md sync target is `{Marketplace}/plugin/spt/CHANGELOG.md` per D-11.

  ### Existing version-text precedent
  - `src/owl/poll.rs:75` — `READY:{id} (spt v{})` format. Informational only; Phase 34 prompt text uses `v{old} → v{new}` shape from D-07/D-08.

  ### AskUserQuestion native "Other" field
  - AskUserQuestion automatically provides an "Other" free-text option. Phase 34 D-08's 4-option set explicitly chooses NOT to use "Other" because Remind / Skip already cover the non-print branches. If a user provides freeform input, Claude should map continuation phrases ("not now", "later", "skip for now") to `Remind me later` and dismissal phrases ("ignore", "stop asking") to `Skip` — matches Phase 33 D-08 / advisor mode pattern.

  </canonical_refs>

  <code_context>
  ## Existing Code Insights

  ### Reusable Assets
  - **`atomic_write_string`** (src/common/owlery.rs:248) — tmp + rename atomic write. Used verbatim for sentinel writes from both Stop hook (D-05) and `version-remind` subcommand.
  - **`spt_home()` + `owlery_dir()`** (src/common/owlery.rs:18-32, 65) — already resolve `$SPT_HOME` / platform default. Sentinel path = `spt_home().join("last-seen-version.json")`. No new path helper needed.
  - **`hook_output::parse_hook_stdin` + `find_perch_by_session` + `find_perch_by_parent_pid` + `find_perch_cached`** — perch-resolution path already proven across `hook_idle.rs`, `hook_prompt.rs`, `hook_check.rs`. Phase 34 reuses verbatim.
  - **`env!("CARGO_PKG_VERSION")`** — compile-time version macro already used in 3 existing call sites (poll.rs, setup.rs, stop.rs). Identical usage for Phase 34's hook check.
  - **`OWL_ECHO_COMMUNE` recursion guard** (hook_idle.rs:64-69) — copy/paste pattern for the version-change code path. Haiku subprocesses MUST NOT see a version-change block.
  - **AskUserQuestion native "Other"** — Phase 34 deliberately ignores; 4-option set is complete.
  - **DEPLOY.ps1 `-Bump` Cargo.toml/plugin.json sync** (DEPLOY.ps1:224-269) — atomic-rewrite-then-verify pattern. CHANGELOG.md stub-injection (D-10) follows the same regex-replace-then-verify shape.

  ### Established Patterns
  - **Doc-amendment-first plan structure** (Phase 31 Plan 01, Phase 32 Plan 01, Phase 33 Plan 01): when a phase's discovered design contradicts a requirement-as-written, Plan 01's first commit amends REQUIREMENTS.md + ROADMAP.md before any code. Phase 34 follows this for D-12.
  - **Pattern S2: hookSpecificOutput envelope** (plugin_session_start.rs:139-155 `emit_auto_pick`) — informational only for Phase 34; Stop uses `decision:"block"` + `reason` instead. Confirmation needed during research.
  - **`include_str!` precedent for repo-root markdown** (psyche.md): Phase 34 chooses NOT to follow this for CHANGELOG.md (per D-11: runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` instead). Rationale: changelog grows monotonically; binary should not bloat with history.
  - **Sync-then-verify in DEPLOY.ps1** (DEPLOY.ps1:256-273): each file write verifies via re-read. CHANGELOG.md sync follows the same pattern.
  - **Schema additivity** (Phase 26 PLAN-OVERVIEW Q4, Phase 31 D-12, Phase 33): no new info.json fields; no new on-disk artifacts beyond the sentinel; the `version-remind` subcommand is purely additive.

  ### Integration Points
  - **`src/owl/hook_idle.rs::run()`** — version-change emission lands between perch resolution (lines 25-32) and `set_idle_ready` (line 39). OWL_ECHO_COMMUNE guard MUST precede any new logic.
  - **`docs/DEPLOY.ps1` `-Bump` flow** — CHANGELOG.md stub-injection + unchanged-abort gate inserts after Cargo.toml verification (line 238), before the regex-replace block (line 247).
  - **`docs/DEPLOY.ps1` sync step** — `{Marketplace}/plugin/spt/CHANGELOG.md` copy slots alongside the existing binary/skills/hooks/plugin.json sync.
  - **`Cargo.toml` `[[bin]]` table** — new `version-remind` subcommand attaches as a `clap` subcommand on the existing `owl` binary; no new binary target.
  - **`tests/`** — version-change detection tests live in a new `tests/version_changelog.rs` integration test file (mirrors `tests/skill_hints.rs` from Phase 32 regression-guard pattern). Unit tests for the CHANGELOG.md parser co-locate with `src/owl/version_changelog.rs` (or wherever the parser lands).

  </code_context>

  <specifics>
  ## Specific Ideas

  - **Sentinel JSON shape (locked):** `{"version":"X.Y.Z"}`. Single field, no timestamp, no plugin name. Minimal for `serde_json::from_str` round-trip. Path: `$SPT_HOME/last-seen-version.json` (sibling to `owlery/` per VERS-01).
  - **CHANGELOG.md H2 regex (locked):** `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`. Case-sensitive. Reject `## [Unreleased]` (no date matched). Ignore link reference lines (start with `[` not `## [`).
  - **Prompt question text template:**
    - Single-step: `SPT plugin updated v{old} → v{new}. Print changelog?`
    - Multi-step: `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`
    - Dates surface in the AUQ description / Claude's pre-question framing, not in the question line itself.
  - **4-option labels (locked, verbatim per D-08):**
    - `Yes, full changelog`
    - `Yes, highlights only`
    - `Remind me later`
    - `Skip`
  - **Highlights body framing (suggested wording for block-body or skill-body):** "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes. Append `See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail.`"
  - **`version-remind` subcommand signature (locked):** `$OWL version-remind <old version>` — bare positional arg (e.g. `$OWL version-remind 1.10.9`). Writes `{"version":"<old version>"}` to `$SPT_HOME/last-seen-version.json` via `atomic_write_string`. Exits 0 on success, 1 on missing arg or write failure. The `<old version>` arg is REQUIRED — the subcommand has no way to infer it; the block-body MUST instruct Claude to pass the literal `old` value from the block payload it received. `$OWL` is the env-var-injected owl-binary path per existing `/spt:*` skill conventions; subcommand also callable directly via `owl version-remind <old version>` for tests / scripted invocation.
  - **DEPLOY.ps1 stub format (locked):** Appended block at end of repo-root CHANGELOG.md:
    ```markdown
    ## [{NEW_VERSION}] - {YYYY-MM-DD}

    - TODO: changelog entry
    ```
    Date = bump-time `Get-Date -Format 'yyyy-MM-dd'`. The literal `TODO: changelog entry` marker is what the unchanged-abort gate scans for on the next `-Bump` invocation.
  - **DEPLOY.ps1 unchanged-abort message (suggested):** `ERROR: CHANGELOG.md contains 'TODO: changelog entry' for v{NEW_VERSION}. Fill in the entry and commit before re-running -Bump.`

  </specifics>

  <deferred>
  ## Deferred Ideas

  - **`owl changelog backfill` subcommand** — would auto-generate CHANGELOG.md from `git log --grep='chore: bump'`. Rejected for Phase 34 per D-09 (one-shot manual author at Plan 01). Re-evaluate if a future fresh-fork bootstrap is needed.
  - **`/spt:changelog` skill** — would own AUQ + render + remind flow as a skill body. Rejected for Phase 34 per overall scope (no new skill surface). Block-body inline instructions to Claude are sufficient. Re-evaluate if the inline-instructions pattern becomes brittle or if other phases want to compose with the changelog flow.
  - **Sentinel with reminder flag** — alternative to `version-remind`-rolls-back: sentinel could carry `{"version":"X.Y.Z","reminder":bool}` and version-remind sets `reminder:true` while keeping the new version. Rejected for Phase 34 to keep sentinel shape minimal (D-07). Could reconsider if a future requirement needs richer sentinel metadata.
  - **Telemetry for AUQ option distribution** — log how often each of the 4 options is picked. Useful for tuning highlights framing. Out of scope; no telemetry infrastructure in spt today.
  - **`Yes, highlights only` rubric formalization** — current decision is "Claude's discretion". A future phase could specify an exact rubric (e.g., "include all `## Added`, `## Fixed` H3 subsections; exclude `## Changed (internal)`"). Requires CHANGELOG.md to use H3 subsections per Keep-a-Changelog v1.1.0 — out of scope for Phase 34's minimal H2-per-version format.
  - **Block payload embedded full excerpt vs Claude-reads-at-yes-time** — D-04 / Claude's Discretion flagged this as planner choice. If planner picks embed-in-block, Phase 34 hook also parses the H2 sections during the hook run. Defer to RESEARCH.md.
  - **PreToolUse fallback if Stop's `decision:"block"` + `reason` doesn't propagate text to Claude** — flagged as research blocker in D-04 / Claude's Discretion. If confirmed broken, escalate during planning.
  - **UserPromptSubmit revisit** — the "walk-away mid-task" rationale (D-01) is the reason we pivoted from UserPromptSubmit to Stop. If a future requirement needs the prompt to land at session-start time (e.g., a critical security update notice), UserPromptSubmit becomes viable again with a different UX framing. Not in Phase 34.

  </deferred>

  ---

  *Phase: 34-version-change-changelog*
  *Context gathered: 2026-05-17*

  ### Research Findings
  # Phase 34: Version-Change Changelog - Research

  **Researched:** 2026-05-17
  **Domain:** Claude Code Stop hook semantics, CHANGELOG.md parsing, Windows atomic-write races, DEPLOY.ps1 stub-injection
  **Confidence:** HIGH (primary research blocker D-04 RESOLVED with multi-source agreement)

  ---

  ## Summary

  - **Q1 / D-04 GREEN:** Stop hook `decision:"block"` + `reason` propagates `reason` text to Claude as continuation instruction — Claude generates a new assistant turn responding to it. No silent abort. Multi-source agreement (Anthropic schema text "`reason` tells Claude how to proceed", disler hooks-mastery, claude.com blog, claudefa.st). The architectural choice in D-04 is sound; no fallback to PreToolUse / UserPromptSubmit is required.
  - **Q2 nuance:** Claude CAN invoke AskUserQuestion immediately on re-engagement (the block IS the new-turn trigger; no user prompt needed first). The `reason` text drives Claude's response, and Claude is free to call any tool — exactly the pattern that "stop hook forces task completion" examples in the ecosystem rely on.
  - **Stop hook has NO `hookSpecificOutput` schema** — confirmed. Only top-level `decision` + `reason` + universal fields (`continue`, `stopReason`, `suppressOutput`, `systemMessage`). The existing `hook_idle.rs:1-5` comment is accurate. Block-body content MUST live inside `reason` verbatim. Payload size budget: `reason` is a single string; no documented hard cap but practical limit aligns with the 10,000-char JSON-output cap. Our payload is tiny (a few hundred chars).
  - **`stop_hook_active` infinite-loop guard:** Industry-standard `Stop` input flag. Set to `true` on Claude's re-engagement after a previous block in the same continuation chain. Our hook MUST short-circuit on `stop_hook_active==true` regardless of version mismatch, as a defensive guard. With D-05 sentinel pre-write the version-mismatch path naturally won't re-fire, but the guard prevents pathological cases (e.g., sentinel write failure → mismatch persists → infinite block loop).
  - **`$CLAUDE_PLUGIN_ROOT` confirmed available** to plugin-bundled hooks at invocation. Documented in Claude Code hooks reference.
  - **Plan boundary recommendation:** 3 plans, refined boundary below (Q8). Plan 01 = amendment-commit + CHANGELOG.md genesis + DEPLOY.ps1 (doc/script only, no Rust). Plan 02 = Rust hook + sentinel + parser + `version-remind` subcommand + unit tests. Plan 03 = block-body authoring + integration test on synthesized version transition.

  **Primary recommendation:** Proceed with the design exactly as captured in CONTEXT.md D-01..D-12. The single research blocker (D-04) resolves favorably; no architectural pivot required.

  ---

  ## User Constraints (from CONTEXT.md)

  ### Locked Decisions (D-01..D-12 — verbatim authority)

  - **D-01:** Stop hook (`hook_idle.rs`), not SessionStart.
  - **D-02:** Predicate = `owl_id` resolves in `hook_idle.rs::run()` (piggyback existing perch resolution).
  - **D-03:** Recursion guard reuses `OWL_ECHO_COMMUNE` env-var skip (hook_idle.rs:64-69 pattern).
  - **D-04:** Block emission via top-level `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"`. RESEARCH-CONFIRMED.
  - **D-05:** Sentinel write site = same `run()` top, BEFORE returning the block. Atomic via `atomic_write_string`. Default behavior: writes the NEW version. Rollback only via explicit `version-remind`.
  - **D-06:** Fire-once cadence = once per version transition (sentinel pre-write enforces this).
  - **D-07:** Payload shape = `{old, new, old_date, new_date, step_count}`.
  - **D-08:** AskUserQuestion 4-option shape: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Block body MUST surface the exact `$OWL version-remind <old>` invocation string verbatim.
  - **D-09:** Repo-root `CHANGELOG.md` authored one-shot at Plan 01 start from `git log --grep='chore: bump'`. No `owl changelog backfill` subcommand.
  - **D-10:** Forward maintenance = DEPLOY.ps1 `-Bump` stub injection + commit gate (combined: append `TODO: changelog entry` stub uncommitted; next `-Bump` aborts if marker still present).
  - **D-11:** DEPLOY.ps1 sync target = `{Marketplace}/plugin/spt/CHANGELOG.md` (sibling to plugin.json). Resolves at hook time via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.
  - **D-12:** Amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md SC1/SC2/SC5 per the rewordings in CONTEXT.md.

  ### Claude's Discretion (planner picks)

  - `<spt-version-changelog>` block exact XML shape (planner picks JSON-as-text vs child elements).
  - Block payload: embed full excerpt vs Claude-reads-at-yes-time (researcher leans toward Claude-reads, see Q11 below).
  - CHANGELOG.md parser regex tightness (`[Unreleased]` reject, link reference lines ignored) — locked in test corpus below.
  - `Yes, highlights only` extraction policy framing — final wording in Plan 02 or 03.
  - Plan split (3 plans) — final boundary at planner's discretion; researcher recommendation in Q8.

  ### Deferred Ideas (OUT OF SCOPE)

  - `owl changelog backfill` subcommand
  - `/spt:changelog` skill
  - Sentinel with reminder flag (richer schema)
  - Telemetry for AUQ option distribution
  - `Yes, highlights only` rubric formalization (H3 subsections)
  - PreToolUse fallback (not needed — D-04 confirmed)
  - UserPromptSubmit revisit
  - Block payload embedded full excerpt — researcher recommends NOT embedding (see Q11)

  ---

  ## Phase Requirements

  | ID | Description | Research Support |
  |----|-------------|------------------|
  | VERS-01 | `$SPT_HOME/last-seen-version.json` sentinel sibling to `owlery/` | `src/common/owlery.rs:18-32` spt_home resolution verified; sentinel path = `spt_home().join("last-seen-version.json")` |
  | VERS-02 | Stop hook compares `env!("CARGO_PKG_VERSION")` against sentinel (AMENDED per D-12) | hook_idle.rs:14-43 emission site verified; CARGO_PKG_VERSION precedent at poll.rs:75, setup.rs:35, stop.rs:199 |
  | VERS-03 | First-install silent write | Sentinel-missing path: read returns None/Err → atomic_write_string current version, return without block emission |
  | VERS-04 | AskUserQuestion 4-option shape (AMENDED per D-12) | Block-body inline instructions to Claude; AUQ invoked by Claude on re-engagement |
  | VERS-05 | Atomic write via `atomic_write_string` | Verified at src/common/owlery.rs:248; tmp + rename; NTFS same-volume safe |
  | VERS-06 | Multi-version skip (AMENDED per D-12) — 4-option AUQ applies uniformly | `step_count` field in payload; Yes-full renders all H2 sections verbatim |
  | VERS-07 | Repo-root `CHANGELOG.md` synced via DEPLOY.ps1 to plugin meta dir | DEPLOY.ps1 sync step extended; resolves to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` per D-11 |
  | VERS-08 | AUTO-03 vs VERS-04 coexistence (AMENDED per D-12) — separate hooks | AUTO-03 is SessionStart (plugin_session_start.rs); VERS-04 is Stop (hook_idle.rs). No collision possible. |
  | VERS-09 | Single write site, no in-handoff race (AMENDED per D-12) | hook_idle.rs::run() top after perch resolution + OWL_ECHO_COMMUNE guard. Stop hooks do not fire mid-handoff. |

  ---

  ## Architectural Responsibility Map

  | Capability | Primary Tier | Secondary Tier | Rationale |
  |------------|-------------|----------------|-----------|
  | Version-mismatch detection | Rust hook (`hook_idle.rs`) | — | Compile-time `env!("CARGO_PKG_VERSION")` is the only handoff-safe source per Out-of-Scope §3 |
  | Sentinel read/write | Rust hook + `version-remind` subcommand | `atomic_write_string` (owlery.rs:248) | Both write sites share the same helper; rollback is a discrete subcommand for explicit user intent |
  | CHANGELOG.md parse | Rust hook | — | "Rust hook does it once" chosen explicitly in Discussion Log Round 2 |
  | Block payload composition | Rust hook | — | Hook builds `{old, new, old_date, new_date, step_count}` from parser output; embeds verbatim into `reason` |
  | AUQ render + branching | Claude (block-body instructions) | — | Stop hook has no AUQ-emission channel; Claude invokes AUQ on re-engagement |
  | `Yes, full changelog` render | Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` | — | See Q11 recommendation — block payload stays small |
  | `Yes, highlights only` synthesis | Claude (discretion + framing) | — | D-08; no fixed rubric |
  | `Remind me later` rollback | `$OWL version-remind <old>` subcommand | `atomic_write_string` | Bare positional arg; ~10-line implementation |
  | CHANGELOG.md authorship (genesis) | Human (Plan 01 step 1) | git log skeleton | One-shot, never auto-regenerated |
  | Forward CHANGELOG.md maintenance | DEPLOY.ps1 `-Bump` stub + commit gate | Human fills | Combined enforcement: stub-append + unchanged-abort |

  ---

  ## Standard Stack

  ### Core

  | Library | Version | Purpose | Why Standard |
  |---------|---------|---------|--------------|
  | `serde_json` | (existing, bundled with `preserve_order`) | Sentinel JSON read/write | Already in tree; minimal `{"version":"X.Y.Z"}` shape needs only string round-trip |
  | `clap` | (existing) | `version-remind` subcommand registration | Already in tree; bare positional arg slots in alongside existing owl subcommands |

  ### Supporting

  | Library | Purpose | When to Use |
  |---------|---------|-------------|
  | `std::fs::read_to_string` | CHANGELOG.md slurp at hook time | One-time read; file ~kB scale |
  | `std::env::var` | `$CLAUDE_PLUGIN_ROOT` resolution | Hook fallback path: walk up from hook script if env var unset (defensive) |

  ### Alternatives Considered

  | Instead of | Could Use | Tradeoff |
  |------------|-----------|----------|
  | Manual H2 regex parser | `pulldown-cmark` markdown crate | Adds dep weight; Phase 34 H2-only parsing is ~10 LOC of regex — overkill |
  | `include_str!` embed CHANGELOG.md | Runtime read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` | Explicitly rejected in Discussion Round 1: binary bloat with history |
  | `owl changelog backfill` subcommand | One-shot manual author | Discussion Round 1: deferred (D-09) |

  **Verified versions:** No new crates required. All work uses already-installed `serde_json`, `clap`, and `std` — no version drift risk.

  ---

  ## Per-Question Findings

  ### Q1 (CRITICAL): Stop hook `decision:"block"` + `reason` propagation

  **Status:** GREEN — RESOLVED.

  **Finding:** When a Stop hook emits `{"decision":"block","reason":"..."}`, the `reason` text is propagated to Claude as continuation instruction. Claude does NOT silently abort — Claude generates a new assistant turn responding to the reason text, exactly as if the reason had been delivered as a system message instructing Claude to continue working.

  **Sources:**
  - Anthropic's hooks reference `~/.claude/reference_docs/claude-code-hooks.md` (lines 389-399): Stop hook output is `{"decision":"block","reason":"Reason to block completion"}` (note the schema text positions `reason` as actionable instruction).
  - [Claude Code Hooks Mastery (disler)](https://github.com/disler/claude-code-hooks-mastery): "Yes, when `decision: "block"` is returned with a reason field in the Stop hook, the reason text is fed back to Claude as a continuation instruction." `[CITED]`
  - [Anthropic blog: How to configure hooks](https://claude.com/blog/how-to-configure-hooks): documents `decision:"block"` with `reason` as "shown to Claude" `[CITED]`
  - [Claudefa.st Stop Hook guide](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement): "When you use `decision: block` in a Stop hook, it automatically prompts Claude with the reason provided, which is crucial because the reason field is fed back to Claude" `[CITED]`
  - [Steve Kinney AI Tools course](https://stevekinney.com/courses/ai-development/claude-code-hook-control-flow): confirms same mechanic
  - Practical evidence: the ecosystem "force task completion" pattern (e.g. "must pass tests before stopping") relies on this exact propagation. If `reason` did not reach Claude, that entire pattern category would be inoperative — and it is documented as working across multiple guides.

  **Rendered shape on Claude's side:** Claude sees the reason text as the new continuation context. The block IS the new-turn trigger. No user prompt is required. Claude's response then renders the block body inline, then proceeds to call AskUserQuestion (matching the pattern other Stop-hook-driven flows use).

  **Counter-evidence considered:** One WebFetch response claimed the reason is "shown to the user only, not fed back to Claude" — this was anomalous and contradicted by 4+ other sources including the official hooks reference. Discounted as parsing artifact.

  **No fallback required.** PreToolUse and UserPromptSubmit alternatives in Deferred Ideas remain dormant.

  ### Q2: AskUserQuestion invocation from blocked Stop context

  **Status:** GREEN.

  **Finding:** Once Claude re-engages with the `reason` text in context, Claude can immediately invoke AskUserQuestion in the same turn — without waiting for a new user prompt. The block IS the new-turn trigger; the new turn is bounded by Claude's response, which can include any tool call.

  **Sources:**
  - Inference from Q1: if reason drives Claude's new turn, Claude is free to invoke tools per the normal assistant-turn model.
  - Ecosystem pattern: "stop hook → forced continuation → tool call → new stop hook" loop is well-attested in the disler/claudefa.st examples. The `stop_hook_active` flag exists precisely because Claude can and will fire tools in the forced-continuation turn.

  **Implication for D-08:** The block body's inline instructions to "render the AUQ now with these 4 options" will be honored on the same turn — AUQ fires immediately, before any new user message.

  **Caveat:** One Anthropic-docs WebFetch snippet suggested "Claude would need a new user prompt OR the hook to provide additionalContext" — but since the Stop hook does NOT support `additionalContext` (Q3) and the ecosystem clearly demonstrates Claude responding to `reason`, the practical conclusion is that `reason` IS the new context channel for Stop hooks. The disler/claudefa.st consensus wins.

  ### Q3: Stop hook output schema — `hookSpecificOutput.additionalContext`?

  **Status:** Confirmed NEGATIVE.

  **Finding:** Stop hook output supports ONLY top-level `decision` + `reason` + universal fields (`continue`, `stopReason`, `suppressOutput`, `systemMessage`, `terminalSequence`). NO `hookSpecificOutput` envelope. NO `additionalContext` field.

  **Sources:**
  - `~/.claude/reference_docs/claude-code-hooks.md` lines 389-399 (Stop section) — only `decision` + `reason` documented.
  - Anthropic hooks reference page (WebFetch verified): "Stop is explicitly listed as supporting only `decision` and `reason` at the top level—not `hookSpecificOutput`."
  - Existing `src/owl/hook_idle.rs:1-5` comment is accurate and predates our research.

  **Implication:** ALL block-body content (the `<spt-version-changelog>` XML, the AUQ instructions, the `$OWL version-remind <old>` invocation string) MUST live inside the single `reason` string. This is fine — payload is tiny, well under any practical limit.

  ### Q4: `$CLAUDE_PLUGIN_ROOT` availability inside Stop hook process

  **Status:** GREEN.

  **Finding:** `$CLAUDE_PLUGIN_ROOT` is set by Claude Code on the spawned hook process for ALL plugin-bundled hooks. Documented in the hooks reference (line 607) and verified via authoritative Anthropic docs.

  **Sources:**
  - `~/.claude/reference_docs/claude-code-hooks.md` line 607: "`$CLAUDE_PLUGIN_ROOT` -- Plugin directory" listed under "Environment Variables Available in Hooks".
  - Anthropic hooks page (WebFetch verified): "Both forms support the same path placeholders, and both export them as the environment variables `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, and `CLAUDE_PLUGIN_DATA` on the spawned process."

  **Implication:** Hook resolves CHANGELOG.md path as:
  ```rust
  let plugin_root = std::env::var("CLAUDE_PLUGIN_ROOT")
      .map_err(|_| "CLAUDE_PLUGIN_ROOT must be set by Claude Code")?;
  let changelog_path = std::path::Path::new(&plugin_root).join("CHANGELOG.md");
  ```

  **Defensive fallback:** If `$CLAUDE_PLUGIN_ROOT` is unset (e.g., direct invocation in tests, or theoretical CC bug), the hook should fail soft — skip emission, do not panic. Tests cover both branches.

  ### Q5: `$OWL` env var availability inside AUQ-renderer Claude turn

  **Status:** GREEN.

  **Finding:** `$OWL` is injected into `~/.claude/settings.json` by SPT's SessionStart hook (per CLAUDE.md §"Env vars: `$OWL` and `$LIVE` are auto-injected into `~/.claude/settings.json` at SessionStart"). Settings env vars persist for the full session, across all turns — Bash tool invocations resolve `$OWL` to the absolute owl.exe path.

  **Sources:**
  - `CLAUDE.md` §Technology Stack: "Env vars: `$OWL` and `$LIVE` are auto-injected into `~/.claude/settings.json` at SessionStart."
  - Existing skill convention: every `/spt:*` skill (e.g., `commune/SKILL.md`, `signoff/SKILL.md`) uses `$OWL deliver` / `$LIVE pick-spec` without first having to resolve the path — confirms the env var is honored at tool-invocation time.

  **Implication:** Claude can call `$OWL version-remind 1.10.9` in a Bash tool invocation directly. The block body should surface the exact invocation string verbatim per D-08.

  ### Q6: `atomic_write_string` Windows semantics under concurrent writers

  **Status:** GREEN — acceptable for our use case.

  **Finding:** Rust's `std::fs::rename` on Windows uses `MoveFileEx` with `MOVEFILE_REPLACE_EXISTING` semantics. On NTFS same-volume renames (our case — both source `.tmp` and destination live under `$SPT_HOME`), the rename is atomic with respect to the file pointer: readers either see the old file or the new file, never a torn read.

  **Concurrent writers behavior:** Last-writer-wins. If two Stop hooks race to write `last-seen-version.json` (e.g., two concurrent spt-bearing agents both transitioning v1.10.9 → v1.10.10 simultaneously), both write the same value `{"version":"1.10.10"}` — the final on-disk content is identical regardless of which rename "won". No torn reads possible.

  **Race-condition window:** Between the two `fs::write(&tmp, body)?` calls and the two `fs::rename` calls, there is a window where both `.tmp` files exist. The temp filename is shared (`last-seen-version.json.tmp` — see `atomic_write_string` source at owlery.rs:249) which CAN cause the second writer to fail with `ERROR_SHARING_VIOLATION` or overwrite the first's `.tmp`. Per existing call-site pattern (`write_last_commune_epoch` in owlery.rs:264) this is treated as best-effort: errors are swallowed at the caller layer. Our Phase 34 hook should match this — wrap `atomic_write_string` in a best-effort pattern; on failure, log to stderr and continue. Worst case: sentinel doesn't update this cycle → block re-fires on next Stop in same session (degraded but not broken).

  **Sources:**
  - `src/common/owlery.rs:238-247` — existing comment asserts NTFS+POSIX same-volume atomic rename.
  - [Rust std::fs::rename docs](https://doc.rust-lang.org/std/fs/fn.rename.html) — Windows uses MoveFileEx.
  - [Microsoft Learn: MoveFileEx + MOVEFILE_REPLACE_EXISTING + NTFS + same volume = atomic](https://learn.microsoft.com/en-us/archive/msdn-technet-forums/449bb49d-8acc-48dc-a46f-0760ceddbfc3) — confirmed atomic for our use case.
  - [rust-lang/rust PR#131072](https://github.com/rust-lang/rust/pull/131072) — Recent rust now uses POSIX rename semantics on Windows 1601+ via `FileRenameInfoEx`.

  **Test coverage required:** Unit test that simulates double-write and asserts final content is correct. Not a race test (impossible to deterministically reproduce on a unit-test timeline), just a sequential equivalence check.

  ### Q7: DEPLOY.ps1 stub-injection idempotency

  **Status:** GREEN with abort-gate design locked.

  **Finding:** Per D-10 the design is "stub + commit gate": first `-Bump` appends the stub uncommitted; next `-Bump` aborts if the unchanged TODO marker is still present. This is the correct semantics. The DEPLOY.ps1 existing patterns at lines 199-238 (refuse-on-dirty-plugin.json and refuse-on-dirty-Cargo.toml) provide the precedent template.

  **Mapping to DEPLOY.ps1 structure:**

  Insertion site:
  - **Bump abort gate:** Insert at lines 199-222, after the dirty-plugin.json / dirty-Cargo.toml refuse-on-dirty gates. New check: `if (CHANGELOG.md contains "TODO: changelog entry") { Write-Host "ERROR:..."; exit 1 }`. The scan is a substring or regex match for the literal `TODO: changelog entry`.
  - **Stub injection:** Insert at lines 277-287, after Cargo.toml/plugin.json regex-rewrites (line 274) but BEFORE the `git add` + `git commit` (lines 279-282). The new stub MUST be appended to repo-root `CHANGELOG.md` AFTER the bump verification (lines 257-274) succeeds. Critically: `git add` MUST NOT stage the CHANGELOG.md — the stub stays in working-tree only. The commit at line 281 should explicitly list `plugin.json` and `Cargo.toml` (already does — line 279) and NOT add CHANGELOG.md to the path list.

  **Idempotency property:** A user running `-Bump` twice without filling the CHANGELOG entry:
  1. First run: bumps Cargo.toml/plugin.json, commits, appends stub to CHANGELOG.md (unstaged, uncommitted).
  2. Second run: hits the abort gate at the dirty-state check OR the new TODO-marker check — depending on whether the user committed Cargo.toml/plugin.json from run 1 (they did; we commit it ourselves), so the dirty-state check passes; the TODO-marker check then fires and aborts.

  **Wait — there's a gap:** Between run 1 and run 2, the user may have edited CHANGELOG.md AND committed it. If they committed it without filling in real prose, the TODO marker would still be present. Our check would still fire (correct). If they committed it WITH real prose, the TODO marker is gone, the check passes, and the next `-Bump` proceeds (correct). The gate works in all branches.

  **Edge case:** If the user manually deletes the stub WITHOUT committing real content, the next `-Bump` sees no TODO marker but ALSO no entry for `NewVersion`. Should we check for both? Recommendation: also check that an H2 line matching `## [<NewVersion>] - YYYY-MM-DD` exists in CHANGELOG.md. If neither the stub nor a real entry for the bumped version is present, abort. This catches the "user deleted the stub but forgot to write a real entry" case.

  **Recommended check shape** (insert at ~line 215):
  ```powershell
  # Check that the previous bump's CHANGELOG entry was filled in.
  # A "TODO: changelog entry" marker means the previous bump's stub is still pending.
  $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  if (Test-Path $ChangelogPath) {
      $ChangelogContent = Get-Content $ChangelogPath -Raw
      if ($ChangelogContent -match 'TODO: changelog entry') {
          Write-Host "ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump." -ForegroundColor Red
          Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
          exit 1
      }
  }
  ```

  ### Q8: Plan boundary recommendation

  **Status:** RECOMMEND 3 plans, with refined boundary below.

  **Recommendation:**

  **Plan 01 — Doc Amendment + CHANGELOG.md Genesis + DEPLOY.ps1 (NO Rust)**
  - Step 1 (FIRST COMMIT): REQUIREMENTS.md + ROADMAP.md amendment per D-12 (mirrors Phase 31 / Phase 33 doc-amendment-first pattern).
  - Step 2: Author `CHANGELOG.md` at repo root by hand from `git log --grep='chore: bump'` skeleton (38 versions surfaced; see Appendix A below). Single commit with curated prose per version.
  - Step 3: Extend `docs/DEPLOY.ps1`:
    - Sync step: copy repo-root `CHANGELOG.md` to `{Marketplace}/plugin/spt/CHANGELOG.md` alongside the existing binary/skills/hooks/plugin.json sync (~line 368, new `Invoke-Mutation` block).
    - `-Bump` flow: insert TODO-marker abort gate at ~line 215 (after dirty-state checks); insert stub append at ~line 287 (after Cargo.toml regex-rewrite verification, before `git add`). Critically: the stub append MUST NOT stage CHANGELOG.md.
  - Verification: dry-run `-Bump patch` on a test branch; confirm stub appended, not committed; confirm `-Bump patch` again aborts with the new error.

  **Plan 02 — Rust Hook + Sentinel + Parser + `version-remind` Subcommand (NO Claude-side block-body)**
  - Author `src/owl/version_changelog.rs` (new module): sentinel read/write helpers, CHANGELOG.md parser, payload builder. Co-located unit tests for parser corpus (Q3 below).
  - Wire into `src/owl/hook_idle.rs::run()`: after perch resolution (lines 25-32), after `OWL_ECHO_COMMUNE` guard (mirrored from spawn_echo_commune_if_live:64-69), BEFORE `set_idle_ready` (line 39). On version-mismatch: emit `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"` via stdout JSON, then exit 0. Sentinel write happens at hook-entry top, BEFORE the block emission (per D-05).
  - `stop_hook_active` guard: parse from stdin JSON; short-circuit if true.
  - Add `version-remind <old>` subcommand to clap subcommand dispatch in `src/main.rs` (or wherever owl's bin entry is wired). ~10-line implementation: parse positional `<old>`, atomic_write_string `{"version":"<old>"}` to sentinel path, exit 0.
  - Unit tests in `src/owl/version_changelog.rs::tests`: parser happy path, parser reject `[Unreleased]`, parser ignore link references, sentinel read missing → None, sentinel read present → Some, sentinel write atomic correctness.
  - Hook-emission integration tests in `tests/version_changelog.rs` (NEW file): version-mismatch detection (single-step, multi-step, first-install silent, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard skip, no-perch guard skip, stop_hook_active guard skip); `version-remind` rollback round-trip.

  **Plan 03 — Block Body Authoring + End-to-End Integration Test**
  - Author the block-body text (the content inside `reason`'s `<spt-version-changelog>` XML). Includes: the 4-option AUQ instructions verbatim, the highlights-extraction framing wording (D-08), the exact `$OWL version-remind <old>` invocation string with `{old}` slot, the pointer to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` for `Yes, highlights only` and `Yes, full changelog` paths.
  - This text lives in the Rust payload-builder helper as a `format!` template. Plan 03 owns the wording; Plan 02 owns the scaffold.
  - End-to-end integration test in `tests/version_changelog.rs`: synthesized version transition (sentinel set to v1.10.9, CARGO_PKG_VERSION mock'd or env-var-overridden to v1.10.10), assert stdout JSON has `decision:"block"` and `reason` matches expected XML envelope (snapshot-style assertion via golden fixture in `tests/golden/`).
  - UAT: manually run `cargo build --release`, deploy via DEPLOY.ps1, manually trigger a version mismatch, verify the AUQ surfaces in a real Claude Code session.

  **Rationale for boundary:**
  - Plan 01 is doc/script only — no Rust, no test framework dependency. Can ship independently.
  - Plan 02 is Rust internals — testable in isolation. Subcommand pairs with the hook because both share the parser+sentinel module; splitting would force the subcommand to duplicate the path-helper.
  - Plan 03 is the user-facing surface — wording iterations don't churn Plan 02's scaffold. Integration test lives here because it depends on the block-body text Plan 03 owns.

  **Dependency graph:** Plan 01 → Plan 02 → Plan 03 (linear). Plan 02 cannot start until CHANGELOG.md exists (parser needs sample data for unit tests). Plan 03 cannot start until Plan 02's payload builder exists.

  **Alternative considered: 4 plans.** Splitting CHANGELOG.md genesis from amendment + DEPLOY.ps1 (separate Plan 0/Plan 1). Rejected — genesis is a single commit of one file; it doesn't warrant a separate plan boundary.

  ### Q9: Test fixture for synthesized version transition

  **Status:** RECOMMEND env-var override seam.

  **Recommendation:** Introduce a test-only env var `SPT_OVERRIDE_PKG_VERSION` (cfg-gated under `#[cfg(test)]` OR runtime-read). When set and non-empty, the version-changelog module reads it INSTEAD of `env!("CARGO_PKG_VERSION")`. Otherwise falls through to `env!`.

  **Why this approach:**
  - **Least invasive:** No rebuild required per test case. A single binary can simulate any version transition.
  - **Matches existing patterns:** `SPT_HOME` env override (owlery.rs:18-32) is the established seam pattern in this codebase. `SPT_OVERRIDE_PKG_VERSION` is symmetric.
  - **No production risk:** Document explicitly as "test-only; never set in production". Optionally gate behind a runtime check that emits a stderr warning if set in non-test contexts.

  **Alternatives rejected:**
  - **Mock seam (trait + impl injection):** Overkill for a single `env!` call site. Rust mocking ergonomics are poor for compile-time macros.
  - **Helper that writes sentinel to temp `$SPT_HOME`:** Already needed for sentinel-side tests, but doesn't solve the CARGO_PKG_VERSION side — the binary version is baked in at compile time.
  - **Per-test custom binary build:** Slow, flaky, increases test-runtime cost dramatically.

  **Implementation seam (in `version_changelog.rs`):**
  ```rust
  fn current_version() -> &'static str {
      // Test seam: SPT_OVERRIDE_PKG_VERSION lets tests simulate any version
      // without rebuilding. Production code paths see env!("CARGO_PKG_VERSION").
      if let Ok(v) = std::env::var("SPT_OVERRIDE_PKG_VERSION") {
          if !v.is_empty() {
              // SAFETY: leak is bounded by test count; acceptable for test-only seam.
              return Box::leak(v.into_boxed_str());
          }
      }
      env!("CARGO_PKG_VERSION")
  }
  ```

  Note: the `Box::leak` is intentional for the test-only path; in practice tests setting this env var run a finite number of times per binary lifecycle. If the leak offends, the function can return `String` and accept the allocation cost on every call.

  ### Q10: Keep-a-Changelog 1.1.0 — `[Unreleased]` and format details

  **Status:** GREEN with `[Unreleased]` omitted from our format.

  **Findings:**

  1. **`## [X.Y.Z] - YYYY-MM-DD` format is canonical.** Verified at [keepachangelog.com/en/1.1.0](https://keepachangelog.com/en/1.1.0/).
  2. **`[Unreleased]` section is conventional but not strictly required.** The spec recommends it for projects with active development; we explicitly omit it (D-09 says we author from `git log` retroactively — no current "unreleased" state at genesis).
  3. **Spec recommends categories `Added/Changed/Deprecated/Removed/Fixed/Security`.** Phase 34's parser doesn't care about H3 subsections — it only locates H2 version headers. The body content between H2 boundaries is preserved verbatim for the `Yes, full changelog` path. Our author MAY use H3 subsections in prose, MAY use plain bullet lists, MAY use prose paragraphs — parser is body-content-agnostic.
  4. **Link references at file bottom (`[1.10.10]: https://...`)** are valid per spec but irrelevant for our use case (we don't render comparison links). Parser MUST ignore them by virtue of the `^## \[` anchor (link refs start with `[`, not `## [`).

  **Parser implications:** The regex `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$` rejects `[Unreleased]` (no version match) and ignores link references (no `## ` prefix). No additional spec compliance needed.

  ### Q11: Block payload — embed full excerpt vs Claude-reads-at-yes-time

  **Status:** RECOMMENDATION: Claude-reads-at-yes-time.

  **Rationale:**

  | Approach | Block size | Hook complexity | Token cost | Multi-version impact |
  |----------|-----------|-----------------|------------|---------------------|
  | Embed full excerpt in block | Large (every visible session pays for all transition prose) | Rust hook parses + concatenates H2 sections | High on every transition, even if user picks Skip | Worst with multi-version skip (e.g. 10 versions = 10x prose) |
  | Claude-reads-at-yes-time | Small (just `{old,new,old_date,new_date,step_count}` + invocation string) | Rust hook parses only H2 headers (cheap) | Low on Skip / Remind paths; cost paid only on Yes paths | Multi-version still cheap (one CHANGELOG.md read regardless of step count) |

  Block payload stays compact. Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` only when user picks `Yes, full changelog` or `Yes, highlights only`. Cost is bounded by user choice.

  **Block-body XML shape (recommendation):**
  ```xml
  <spt-version-changelog>
    <old>1.10.9</old>
    <new>1.10.10</new>
    <old_date>2026-05-15</old_date>
    <new_date>2026-05-16</new_date>
    <step_count>1</step_count>
    <changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>
    <invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>
    <instructions>
      Render an AskUserQuestion with question: "SPT plugin updated v1.10.9 → v1.10.10. Print changelog?"
      Options (exactly 4):
      - "Yes, full changelog": read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section
        where 1.10.9 &lt; version &lt;= 1.10.10, verbatim markdown.
      - "Yes, highlights only": read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md, extract 2-5 most impactful
        feature adds and fixes from the same range; prefer user-facing behavior changes;
        skip purely-internal refactors and test-only changes. Append: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."
      - "Remind me later": invoke Bash: `$OWL version-remind 1.10.9` — pass the literal old version.
      - "Skip": no action.
    </instructions>
  </spt-version-changelog>
  ```

  Child elements (not JSON-as-text) recommended. Rationale: Claude parses XML reliably; child elements are unambiguous; readable in transcripts; matches the existing Phase 33 `<spt-live-auto-pick>` pattern convention.

  `step_count` surfaces in the question text when `> 1` per D-07: `SPT plugin updated v1.10.5 → v1.12.0 (12 versions). Print changelog?`

  ---

  ## CHANGELOG.md Parser — Test Corpus

  | Test Case | Input | Expected |
  |-----------|-------|----------|
  | **Happy path: single-version H2** | `## [1.10.10] - 2026-05-16` | Match: version=1.10.10, date=2026-05-16 |
  | **Happy path: pre-1.0** | `## [0.9.3] - 2025-12-01` | Match: version=0.9.3 |
  | **Reject: Unreleased** | `## [Unreleased]` | No match |
  | **Reject: no date** | `## [1.10.10]` | No match |
  | **Reject: incomplete semver (no patch)** | `## [1.10] - 2026-05-16` | No match |
  | **Reject: incomplete semver (no minor)** | `## [1] - 2026-05-16` | No match |
  | **Reject: link reference line** | `[1.10.10]: https://github.com/org/repo/compare/v1.10.9...v1.10.10` | No match (no `## ` prefix) |
  | **Reject: H1 with same body** | `# [1.10.10] - 2026-05-16` | No match (single `#`) |
  | **Reject: H3 with same body** | `### [1.10.10] - 2026-05-16` | No match (three `#`) |
  | **Trailing whitespace tolerance** | `## [1.10.10] - 2026-05-16  ` (2 trailing spaces) | Researcher recommends: **accept** (loose anchor: `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})\s*$`). Reject only if extra non-whitespace content. |
  | **BOM tolerance** | `\u{feff}## [1.10.10] - 2026-05-16` | Match — parser strips BOM at file-read time |
  | **Edge: extra text after date** | `## [1.10.10] - 2026-05-16 (yanked)` | Researcher recommends: **reject** (strict-trailing-text rejection keeps parser deterministic; if a release is yanked, author should rewrite the H2 line or use a separate convention) |
  | **Edge: lowercase version** | `## [1.10.10] - 2026-05-16` | Match (case-sensitive comparison) |
  | **Edge: pre-release suffix** | `## [1.10.10-rc1] - 2026-05-16` | No match (our format is plain semver; pre-release out of scope) |
  | **Edge: empty H2** | `## ` | No match |
  | **Whitespace inside brackets** | `## [ 1.10.10 ] - 2026-05-16` | No match (strict bracket-hugging) |
  | **Date format variation** | `## [1.10.10] - 2026/05/16` | No match (ISO-8601 hyphen-only) |
  | **Range query (parser-driver level)** | Parser asked for all H2 sections between 1.10.5 (exclusive) and 1.10.7 (inclusive). File contains 1.10.5, 1.10.6, 1.10.7, 1.10.8. | Return body content of 1.10.6 and 1.10.7 only, concatenated. |
  | **Body content preservation** | H2 section body includes nested H3 headers, code fences, lists | Body returned verbatim (parser DOES NOT modify body content) |

  **Recommended regex:**
  ```rust
  const H2_RE: &str = r"^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$";
  ```

  Three capture groups for semver components plus three for date components. Multiline mode; case-sensitive. Strict ISO-8601 date format.

  **Parser API (in `src/owl/version_changelog.rs`):**
  ```rust
  pub struct ChangelogEntry {
      pub version: (u32, u32, u32),
      pub date: String, // "YYYY-MM-DD" verbatim
      pub body: String, // content between this H2 and next H2 / EOF, verbatim
      pub line_no: usize, // for error messages
  }

  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry>;
  pub fn sections_between(entries: &[ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&ChangelogEntry>;
  ```

  ---

  ## Integration Test Corpus (tests/version_changelog.rs)

  | Test | Setup | Assertion |
  |------|-------|-----------|
  | `first_install_silent_writes_sentinel` | Empty `$SPT_HOME`; bake `SPT_OVERRIDE_PKG_VERSION=1.10.10`; run hook | Sentinel written `{"version":"1.10.10"}`; stdout JSON contains no `decision:"block"` |
  | `single_step_mismatch_emits_block` | Sentinel `{"version":"1.10.9"}`; `SPT_OVERRIDE_PKG_VERSION=1.10.10`; CHANGELOG.md has both versions | stdout JSON `decision:"block"`, `reason` contains `<spt-version-changelog>` with `<old>1.10.9</old>` and `<new>1.10.10</new>` and `<step_count>1</step_count>`; sentinel now `1.10.10` |
  | `multi_step_mismatch_emits_block_with_step_count` | Sentinel `1.10.5`; PKG_VERSION `1.10.10`; CHANGELOG has 1.10.5-1.10.10 | `step_count` = 5 in payload; question text mentions "5 versions" |
  | `sentinel_equal_no_emit` | Sentinel `1.10.10`; PKG_VERSION `1.10.10` | No `decision:"block"`; sentinel unchanged (no write at all — write only on mismatch path) |
  | `owl_echo_commune_guard_skips_emission` | Mismatch present; `OWL_ECHO_COMMUNE=1` set | No `decision:"block"`; sentinel unchanged |
  | `no_perch_skips_emission` | Mismatch present; NO perch found (perch resolution returns None) | No `decision:"block"`; sentinel unchanged (per D-02 predicate) |
  | `stop_hook_active_skips_emission` | Mismatch present; stdin JSON has `stop_hook_active: true` | No `decision:"block"`; sentinel unchanged (defensive infinite-loop guard) |
  | `version_remind_rollback_round_trip` | Set sentinel to 1.10.10; invoke `owl version-remind 1.10.9`; assert sentinel now 1.10.9 | Sentinel content `{"version":"1.10.9"}` |
  | `version_remind_missing_arg_exits_1` | Invoke `owl version-remind` with no arg | exit code 1; stderr non-empty |
  | `version_remind_atomic_write` | Invoke `owl version-remind` twice in succession | No `.tmp` sibling left behind |
  | `changelog_parser_unit_corpus` | Feed parser the 18 test-corpus rows above | All pass per expected behavior |

  ---

  ## Don't Hand-Roll

  | Problem | Don't Build | Use Instead | Why |
  |---------|-------------|-------------|-----|
  | Atomic file write | Custom tmp+rename | `atomic_write_string` (owlery.rs:248) | Already battle-tested in 8+ call sites; NTFS+POSIX semantics encoded |
  | Spt-home path resolution | New env helper | `spt_home()` (owlery.rs:18-32) | Handles SPT_HOME override + platform default + create-if-missing |
  | Perch resolution | New session-id lookup | `find_perch_by_session` / `find_perch_by_parent_pid` (existing) | Same path proven across hook_idle.rs, hook_prompt.rs, hook_check.rs |
  | Markdown parsing | Full markdown crate | Regex H2 matcher (~10 LOC) | We only need H2 headers; body content is preserved verbatim |
  | Version-equality comparison | Semver crate | Tuple `(u32, u32, u32)` comparison | Plain semver only; no pre-release / build-metadata in our format |
  | JSON sentinel | Hand-roll serializer | `serde_json::to_string` / `from_str` | Single field; round-trip trivial |
  | Recursion guard | New env var | `OWL_ECHO_COMMUNE` (hook_idle.rs:64-69) | Reuse existing — same haiku-subprocess gate applies |
  | AUQ rendering in Rust | Try to emit AUQ JSON | Claude renders AUQ via block-body instructions | Stop hook has no AUQ-emission channel; Claude is the only AUQ surface |

  ---

  ## Common Pitfalls

  ### Pitfall 1: Sentinel write before block emission must NOT fail silently in ways that hide the mismatch

  **What goes wrong:** If `atomic_write_string` returns `Err` (e.g., disk full, perm denied) and the code swallows the error and proceeds to emit the block, the user sees the block, but the sentinel stays at the OLD value → next Stop fires again → infinite block loop (mitigated by `stop_hook_active` but still alarming).

  **Why it happens:** Existing best-effort pattern in `write_last_commune_epoch` (owlery.rs:264) swallows errors.

  **How to avoid:** Either (a) swallow the error AND skip emission if sentinel write fails (degraded UX: no notification this session; correct next session), OR (b) emit anyway AND rely on `stop_hook_active` to break the loop. Recommendation: (a) — fail silent is the established codebase idiom and matches the "best-effort sentinel" philosophy from Phase 18.8.

  **Warning signs:** Tests should include a "sentinel write fails (mock IO error)" case → assert no block emitted.

  ### Pitfall 2: `$CLAUDE_PLUGIN_ROOT` unset in non-plugin invocations

  **What goes wrong:** During tests or direct CLI invocation (`owl hook-idle`), `$CLAUDE_PLUGIN_ROOT` is not set. If the parser fails to find CHANGELOG.md, the block emission either crashes or emits a degraded payload.

  **Why it happens:** Tests bypass the Claude Code plugin invocation path.

  **How to avoid:** Block emission should ALSO require `$CLAUDE_PLUGIN_ROOT` to resolve to a real file. If the env var is unset OR the file is missing, treat as "no changelog available" → still emit the block but with a degraded `<changelog_path>NOT_RESOLVED</changelog_path>` AND skip the `step_count` / `old_date` / `new_date` fields. Claude's AUQ surface still works (with just version numbers, no dates). Better than crashing.

  **Warning signs:** Tests should explicitly include the "CLAUDE_PLUGIN_ROOT unset" branch.

  ### Pitfall 3: Parser ignores body content with `## ` at column 0 inside fenced code blocks

  **What goes wrong:** A CHANGELOG.md entry's body contains a markdown code block with `## [something]` inside (e.g., showing a doc example) → parser incorrectly matches it as an H2.

  **Why it happens:** Regex matches line-by-line without code-fence awareness.

  **How to avoid:** Either (a) accept this edge case as author responsibility (don't include `## [` at line start in code blocks), OR (b) track code-fence state across lines. Recommendation: (a) — Phase 34 CHANGELOG.md will be hand-authored prose; if we ever auto-generate, revisit. Document in code comment so future maintainers know.

  ### Pitfall 4: `stop_hook_active` semantics — we should respect it even though D-05 sentinel pre-write prevents normal re-fire

  **What goes wrong:** Edge case where sentinel write fails (Pitfall 1) — without `stop_hook_active` guard, hook would re-fire infinitely if write keeps failing.

  **Why it happens:** Sentinel write is fallible; `stop_hook_active` is Claude Code's documented infinite-loop guard.

  **How to avoid:** Parse `stop_hook_active` from stdin JSON. If true, short-circuit — exit 0 with no block emission, regardless of version mismatch. Belt-and-suspenders with the sentinel pre-write.

  ### Pitfall 5: DEPLOY.ps1 stub append at WRONG point causes git add to stage CHANGELOG.md

  **What goes wrong:** If stub append runs BEFORE `git add 'plugin/spt/.claude-plugin/plugin.json' 'Cargo.toml'` (DEPLOY.ps1 line 279), and a user later runs `git add -A` reflexively, CHANGELOG.md gets committed in a different commit and the unchanged-abort gate breaks (TODO marker now committed, not in working tree).

  **Why it happens:** Author confusion about where in the `-Bump` sequence to insert the stub.

  **How to avoid:** Stub append MUST happen AFTER `git commit` (line 281) — the bump commit excludes CHANGELOG.md. The stub then lives in working-tree only. Document this constraint with a code comment.

  ### Pitfall 6: Block payload's `step_count` is for SURFACE TEXT only — full-changelog body still reads ALL intermediate versions

  **What goes wrong:** Claude sees `step_count=5` and prints only 5 version headers without reading the full content from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.

  **Why it happens:** Claude is being asked to surface a count, not a body.

  **How to avoid:** Block-body instructions for `Yes, full changelog` MUST explicitly say "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version ≤ {new}". The `step_count` is for the AUQ question text only, not the body-rendering surface.

  ---

  ## Runtime State Inventory

  | Category | Items Found | Action Required |
  |----------|-------------|------------------|
  | Stored data | New sentinel `$SPT_HOME/last-seen-version.json` — single field `{"version":"X.Y.Z"}`. **Pre-existing data:** none (new artifact in Phase 34). | Code edit: create read/write helpers in `version_changelog.rs`. No data migration needed (first-install silent path handles missing sentinel per VERS-03). |
  | Live service config | `$CLAUDE_PLUGIN_ROOT` resolves to `~/.claude/plugins/cache/cplugs/spt/<version>/` at runtime; CHANGELOG.md must be present at that path. | DEPLOY.ps1 sync step copies CHANGELOG.md alongside other plugin files (per D-11). Hook reads at invocation time. |
  | OS-registered state | None — no Task Scheduler, pm2, systemd, or launchd registrations involve Phase 34 artifacts. | None. |
  | Secrets/env vars | New test-only env var `SPT_OVERRIDE_PKG_VERSION` (test seam per Q9). `$OWL` env var dependency confirmed (per Q5). `$CLAUDE_PLUGIN_ROOT` confirmed (per Q4). | None — test seam is additive. |
  | Build artifacts | `target/release/owl.exe` rebuilds with new `version_changelog.rs` module. Marketplace copy via DEPLOY.ps1 (existing pattern). | Standard `cargo build --release` + DEPLOY.ps1 sync. |

  **The canonical question:** *After every file in the repo is updated, what runtime systems still have the old string cached, stored, or registered?*

  Phase 34 is greenfield (new artifact), not a rename. No legacy cached state needs migration. The only runtime-state interaction is the sentinel itself, which is created on first hook fire after deploy.

  ---

  ## Environment Availability

  | Dependency | Required By | Available | Version | Fallback |
  |------------|------------|-----------|---------|----------|
  | Rust toolchain (cargo) | Plan 02, Plan 03 build | ✓ | (existing) | — |
  | PowerShell 5.1+ | Plan 01 DEPLOY.ps1 changes | ✓ | (existing) | — |
  | `git` CLI | Plan 01 CHANGELOG genesis (`git log --grep`) | ✓ | (existing) | — |
  | `serde_json` crate | Sentinel JSON | ✓ | (existing in Cargo.toml) | — |
  | `clap` crate | `version-remind` subcommand | ✓ | (existing in Cargo.toml) | — |
  | Claude Code (`claude` CLI) | UAT verification of AUQ surface | ✓ | (existing) | Manual test via direct hook invocation possible but lower-fidelity |

  No missing dependencies. No blocking gaps.

  ---

  ## Validation Architecture

  ### Test Framework

  | Property | Value |
  |----------|-------|
  | Framework | Rust `cargo test` (built-in, no extra crate) |
  | Config file | `Cargo.toml` (existing) |
  | Quick run command | `cargo test --lib version_changelog` |
  | Full suite command | `cargo test` |

  ### Phase Requirements → Test Map

  | Req ID | Behavior | Test Type | Automated Command | File Exists? |
  |--------|----------|-----------|-------------------|-------------|
  | VERS-01 | Sentinel at `$SPT_HOME/last-seen-version.json` | unit | `cargo test sentinel_path` | ❌ Wave 2 (new module) |
  | VERS-02 | Compile-time CARGO_PKG_VERSION read; never reads plugin.json at runtime | unit + crate-wide audit | `cargo test current_version_uses_env_macro` + grep audit | ❌ Wave 2 |
  | VERS-03 | First-install silent write | integration | `cargo test --test version_changelog first_install_silent_writes_sentinel` | ❌ Wave 2 (new integration file) |
  | VERS-04 | 4-option AUQ shape (per D-12 amendment) | integration | `cargo test --test version_changelog single_step_mismatch_emits_block` | ❌ Wave 2 |
  | VERS-05 | Atomic write via `atomic_write_string` | unit | `cargo test sentinel_write_atomic` | ❌ Wave 2 |
  | VERS-06 | Multi-version uses same 4-option (per D-12) | integration | `cargo test --test version_changelog multi_step_mismatch_emits_block_with_step_count` | ❌ Wave 2 |
  | VERS-07 | DEPLOY.ps1 syncs CHANGELOG.md to plugin meta dir | manual (DEPLOY.ps1 dry-run) | `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun` | n/a — script |
  | VERS-08 | AUTO-03 + VERS-04 coexistence by hook separation | integration | manual UAT — both hooks present in fixture, neither collides | n/a — UAT |
  | VERS-09 | Single write site at hook-entry top | manual code review + grep audit | `grep -r "last-seen-version" src/` should yield 2 hits (hook + subcommand) | n/a — review |

  ### Sampling Rate
  - **Per task commit:** `cargo test --lib version_changelog` (parser unit tests; fast)
  - **Per wave merge:** `cargo test` (full suite including integration)
  - **Phase gate:** Full suite green + UAT against real Claude Code session

  ### Wave 0 Gaps
  - `src/owl/version_changelog.rs` — new module file with parser + sentinel helpers
  - `tests/version_changelog.rs` — new integration test file (mirrors `tests/skill_hints.rs` from Phase 32)
  - `tests/golden/version_changelog_block.txt` — golden fixture for block-body snapshot assertion (Plan 03)

  ---

  ## Security Domain

  | ASVS Category | Applies | Standard Control |
  |---------------|---------|-----------------|
  | V2 Authentication | no | n/a — local-only sentinel; no auth boundary |
  | V3 Session Management | no | n/a |
  | V4 Access Control | no | n/a — sentinel lives under user's own `$SPT_HOME` |
  | V5 Input Validation | yes | Parse `<old version>` arg in `version-remind`: validate against semver regex BEFORE writing sentinel. If not matching `^\d+\.\d+\.\d+$`, exit 1. Prevents arbitrary content injection into the sentinel JSON. |
  | V6 Cryptography | no | n/a — no secrets, no signing |

  ### Known Threat Patterns

  | Pattern | STRIDE | Standard Mitigation |
  |---------|--------|---------------------|
  | `version-remind` arg injection | Tampering | Validate arg matches `^\d+\.\d+\.\d+$` regex; reject any other input |
  | Sentinel path traversal | Tampering | `$SPT_HOME` resolution is authoritative; path is constructed via `spt_home().join("last-seen-version.json")` — no user input in the path |
  | CHANGELOG.md content injection | Tampering | Author-controlled file; content goes into block `reason` text → reaches Claude, but Claude is the only consumer and the format is markdown. No XSS / code-exec surface. |
  | `OWL_ECHO_COMMUNE` recursion guard bypass | DoS (loop) | Guard MUST come BEFORE version-changelog logic (D-03); mirror hook_idle.rs:64-69 exactly |

  **Argument validation MUST happen in the `version-remind` subcommand entry, BEFORE any filesystem write.**

  ---

  ## Code Examples (Verified Patterns)

  ### Atomic write of sentinel (Plan 02)

  ```rust
  // Source: src/common/owlery.rs:248 (existing helper)
  use crate::common::owlery::{atomic_write_string, spt_home};

  fn write_sentinel(version: &str) -> std::io::Result<()> {
      let path = spt_home().join("last-seen-version.json");
      let body = format!("{{\"version\":\"{}\"}}", version);
      atomic_write_string(&path, &body)
  }
  ```

  ### Sentinel read with first-install handling (Plan 02)

  ```rust
  // First-install: read returns None; caller writes current version silently.
  fn read_sentinel() -> Option<String> {
      let path = spt_home().join("last-seen-version.json");
      let content = std::fs::read_to_string(&path).ok()?;
      let v: serde_json::Value = serde_json::from_str(&content).ok()?;
      v.get("version")?.as_str().map(String::from)
  }
  ```

  ### Hook emission via stdout JSON (Plan 02)

  ```rust
  // Source: existing pattern in src/common/hook_output.rs
  fn emit_block(reason: &str) {
      let output = serde_json::json!({
          "decision": "block",
          "reason": reason,
      });
      println!("{}", output);
  }
  ```

  ### Stop hook stdin parsing (Plan 02 — extend existing parse_hook_stdin)

  ```rust
  // Stop hook input includes stop_hook_active per ecosystem convention
  #[derive(Deserialize)]
  struct StopHookInput {
      #[serde(default)]
      session_id: Option<String>,
      #[serde(default)]
      stop_hook_active: bool,
  }
  ```

  ### `version-remind` subcommand body (Plan 02)

  ```rust
  // New subcommand. Bare positional arg per D-08.
  pub fn version_remind(old: &str) -> std::process::ExitCode {
      // V5 input validation: reject non-semver
      let semver_re = regex::Regex::new(r"^\d+\.\d+\.\d+$").unwrap();
      if !semver_re.is_match(old) {
          eprintln!("error: <old version> must match X.Y.Z semver");
          return std::process::ExitCode::from(1);
      }
      let path = spt_home().join("last-seen-version.json");
      let body = format!("{{\"version\":\"{}\"}}", old);
      match atomic_write_string(&path, &body) {
          Ok(_) => std::process::ExitCode::SUCCESS,
          Err(e) => {
              eprintln!("error: sentinel write failed: {}", e);
              std::process::ExitCode::from(1)
          }
      }
  }
  ```

  Note: `regex` crate may not be in Cargo.toml today. If not, use a hand-rolled validator (`old.split('.').count() == 3 && all parts parse as u32`).

  ---

  ## Assumptions Log

  | # | Claim | Section | Risk if Wrong |
  |---|-------|---------|---------------|
  | A1 | Stop hook `reason` text reaches Claude as continuation context strong enough for Claude to invoke AUQ in same turn | Q1 / Q2 | If wrong: AUQ never fires; user never sees changelog prompt. Mitigation: cross-source confirmation (4+ sources agree); ecosystem-wide "stop hook task completion" pattern relies on this exactly. Confidence: HIGH. |
  | A2 | `regex` crate not yet in Cargo.toml | Q9 / code example | If wrong: minor — just use it. Researcher did not verify Cargo.toml deps. Planner should check before Plan 02 implementation. |
  | A3 | `step_count` calculation: count of H2 sections where `old < version <= new` (exclusive of old, inclusive of new) | Q11 / Q3 corpus | If wrong: off-by-one in question text. Locked in CONTEXT.md D-07; aligns with Discussion Log Round 2. Confidence: HIGH (locked in CONTEXT). |
  | A4 | Test seam env var `SPT_OVERRIDE_PKG_VERSION` is acceptable codebase pattern | Q9 | If user/maintainer rejects test-only env var approach, fall back to a `cfg(test)` module replacement (slightly more invasive). Confidence: MEDIUM — pattern matches `SPT_HOME` precedent but is novel for VERSION specifically. |
  | A5 | DEPLOY.ps1 `-Bump` insertion sites at lines 215 (abort gate) and 287 (stub append) | Q7 | If wrong: stub appended at wrong sequence point may either be staged into the bump commit (bad) or fail to detect dirty state (bad). Mitigation: Plan 01 must dry-run DEPLOY.ps1 and verify the sequence holds. Confidence: MEDIUM — based on reading the script, not on test execution. |
  | A6 | `$OWL` env var persists across Claude Code turns via `~/.claude/settings.json` injection | Q5 | If wrong: Claude's `Bash($OWL version-remind 1.10.9)` invocation would fail with "$OWL: command not found". Mitigation: every existing `/spt:*` skill uses `$OWL` and works — strong empirical evidence. Confidence: HIGH. |

  ---

  ## Open Questions

  1. **Should the integration test golden-fixture include a multi-version scenario?**
     - What we know: single-step and multi-step are both required test cases (Q3, integration corpus).
     - What's unclear: how many H2 sections to include in the test fixture CHANGELOG.md, and whether to snapshot the full `reason` string or just key fields.
     - Recommendation: Snapshot just `<old>`, `<new>`, `<step_count>` and the invocation string. Body content varies with CHANGELOG.md edits and would make tests fragile.

  2. **Should `version-remind` log its action to stderr for debugging?**
     - What we know: success exit 0, failure exit 1.
     - What's unclear: whether a single-line stderr "rolled back sentinel to 1.10.9" is helpful or noise.
     - Recommendation: emit a single line on success (matches `READY:id` / `SENT:id` status-tag convention from CLAUDE.md §Conventions). E.g., `REMINDED:1.10.9`. Final wording in Plan 02.

  3. **Should the block body include the dates in the AUQ question text, or only in the AUQ description / pre-question framing?**
     - What we know: D-07 says dates surface in "AUQ description / Claude's pre-question framing, not in the question line itself".
     - What's unclear: AskUserQuestion's API may or may not have a description slot per option vs a global header.
     - Recommendation: Claude renders dates in the prose surface BEFORE invoking AUQ (e.g., "v1.10.9 (released 2026-05-15) → v1.10.10 (released 2026-05-16). Print changelog?"). AUQ itself uses just the version-number question.

  ---

  ## State of the Art

  | Old Approach (in REQUIREMENTS.md as written) | Current Approach (per CONTEXT.md D-01..D-12) | When Changed | Impact |
  |--------------|------------------|--------------|--------|
  | SessionStart hook emission | Stop hook emission | Phase 34 discuss-phase | Pivots VERS-02 from `plugin_session_start.rs` to `hook_idle.rs`; user is between turns when prompted, not at session boot |
  | 3-option AUQ (yes/no/dismiss) | 4-option AUQ (yes-full / yes-highlights / remind / skip) | Phase 34 discuss-phase | VERS-04 amendment; richer UX, splits "yes" into "all detail" vs "discretion summary" |
  | Multi-version = version-list + pointer (never full prose) | Multi-version = same 4-option as single; `Yes, full` renders all interim H2 sections verbatim | Phase 34 discuss-phase | VERS-06 amendment; user-driven choice rather than format-driven constraint |
  | Sentinel write coupled to user response | Sentinel pre-written at hook entry; `version-remind` rolls back on `Remind me later` | Phase 34 discuss-phase Round 5 | Inverts ack/dismiss flow — most-used paths (Yes-full / Yes-highlights / Skip) have zero round-trip; less-common Remind pays the cost |
  | `last-seen-version.json` inside `owlery/` | `last-seen-version.json` SIBLING to `owlery/` | VERS-01 (original requirement) | Per VERS-01 wording; respected in D-05 |

  **No deprecated items.** Phase 34 is greenfield.

  ---

  ## Plan-Boundary Recommendation Summary

  **3 plans** in linear sequence:

  | Plan | Title | Touches | Tests |
  |------|-------|---------|-------|
  | Plan 01 | Doc amendment + CHANGELOG.md genesis + DEPLOY.ps1 stub/sync | REQUIREMENTS.md, ROADMAP.md, CHANGELOG.md (new), docs/DEPLOY.ps1 | DEPLOY.ps1 dry-run; manual review |
  | Plan 02 | Rust hook + sentinel + parser + `version-remind` subcommand | src/owl/version_changelog.rs (new), src/owl/hook_idle.rs, src/main.rs (subcommand wiring), tests/version_changelog.rs | Unit tests for parser corpus; integration tests for hook emission |
  | Plan 03 | Block body authoring + end-to-end integration test + UAT | src/owl/version_changelog.rs (block-body template), tests/version_changelog.rs (snapshot), tests/golden/ | Golden snapshot; manual UAT against real Claude Code |

  **Dependencies:** 01 → 02 → 03 (linear). Plan 02 needs CHANGELOG.md to exist (for parser tests against real content). Plan 03 needs Plan 02's payload-builder helper.

  ---

  ## Open Risks / Blockers for Planner

  1. **(LOW)** DEPLOY.ps1 stub-append site exact insertion may need adjustment after Plan 01 dry-run. Recommend: dry-run before committing the script change.
  2. **(LOW)** `regex` crate availability — verify before Plan 02 implementation. Easy fix if missing.
  3. **(LOW)** Yanked-version handling deferred (parser rejects `## [1.10.10] - 2026-05-16 (yanked)`). If a real yank ever happens, author edits CHANGELOG.md to remove the H2 line entirely. Document as a constraint in CHANGELOG.md authoring guidelines.
  4. **(MEDIUM)** AUQ-instructions wording in block body (Plan 03) needs careful authoring to ensure Claude reliably invokes the right tools in the right order. Recommend: explicit numbered steps inside `<instructions>` element.
  5. **(LOW)** The block-body XML shape (Q11 recommendation) is researcher's pick — planner is empowered to revise. Constraint: must carry `old`, `new`, `old_date`, `new_date`, `step_count`, and the exact `$OWL version-remind <old>` invocation string verbatim.

  ---

  ## Appendix A: CHANGELOG.md Genesis Scope (Plan 01 Step 2)

  Versions identified from `git log --grep='chore: bump'` (38 entries):

  ```
  v1.10.10  (most recent — Phase 33 completion era)
  v1.10.9
  v1.10.8
  v1.10.7
  v1.10.6
  v1.10.5
  v1.10.4
  v1.10.3
  v1.10.2
  v1.10.1
  v1.10.0
  v1.9.15
  v1.9.14
  v1.9.13
  v1.9.12
  v1.9.10  (note: 1.9.11 missing — skip in CHANGELOG)
  v1.9.9
  v1.9.8
  v1.9.7
  v1.9.5   (note: 1.9.6 missing — skip)
  v1.9.2   (note: 1.9.0/1/3/4 missing — skip)
  v1.8.11
  v1.8.10
  v1.8.9
  v1.8.8
  v1.8.7
  v1.8.6
  v1.8.5
  v1.8.4
  v1.8.3
  v1.8.2
  v1.8.1
  v1.8.0
  v1.7.5
  v1.7.4
  v1.7.3
  v1.7.2
  v1.5.4   (older format: "bump plugin version to 1.5.4 and add test artifacts")
  ```

  **Author note for Plan 01:** Per D-09, author curated prose per version using `git log <range>` to scope the changes between each bump. The CHANGELOG.md is NOT a literal log dump — it is a human-readable summary. Skipped versions (1.9.11, 1.9.6, etc.) need no entry. Format: Keep-a-Changelog 1.1.0; H2 `## [X.Y.Z] - YYYY-MM-DD`; body uses bullet lists or prose paragraphs (author's discretion — parser doesn't care about body structure).

  ---

  ## Sources

  ### Primary (HIGH confidence)
  - `~/.claude/reference_docs/claude-code-hooks.md` — Stop hook schema, environment variables, exit codes
  - `src/owl/hook_idle.rs` — Stop hook emission site, OWL_ECHO_COMMUNE guard pattern
  - `src/common/owlery.rs:18-32, 65, 248` — spt_home, owlery_dir, atomic_write_string
  - `docs/DEPLOY.ps1:154-292` — `-Bump` flow, sync step
  - `.planning/phases/34-version-change-changelog/34-CONTEXT.md` — 12 locked decisions (D-01..D-12)
  - `.planning/phases/34-version-change-changelog/34-DISCUSSION-LOG.md` — alternatives + rationale
  - `.planning/REQUIREMENTS.md` — VERS-01..VERS-09 + Out-of-Scope clauses

  ### Secondary (MEDIUM-HIGH confidence — multi-source verified)
  - [Anthropic Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) — Stop hook output schema, environment variables
  - [disler/claude-code-hooks-mastery](https://github.com/disler/claude-code-hooks-mastery) — Stop hook `decision:"block"` semantics, ecosystem patterns
  - [Anthropic blog: How to configure hooks](https://claude.com/blog/how-to-configure-hooks) — Stop hook reason propagation
  - [Claudefa.st Stop Hook task enforcement](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement) — reason field "fed back to Claude"
  - [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — format specification

  ### Tertiary (LOW confidence — single-source / training data)
  - Rust `std::fs::rename` Windows behavior — verified via [doc.rust-lang.org](https://doc.rust-lang.org/std/fs/fn.rename.html) and [Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-technet-forums/449bb49d-8acc-48dc-a46f-0760ceddbfc3); HIGH confidence on same-volume NTFS atomicity, LOWER on concurrent-write edge cases

  ---

  ## Metadata

  **Confidence breakdown:**
  - Stop hook `decision:"block"` semantics (Q1/Q2): HIGH (4+ sources agree; ecosystem-wide pattern; CONTEXT.md design is sound)
  - Standard stack: HIGH (no new deps; all existing crates)
  - Architecture (3-plan boundary): HIGH (mirrors Phase 31/32/33 amendment-first pattern)
  - Parser corpus: HIGH (test cases derived from Keep-a-Changelog spec + common edge cases)
  - DEPLOY.ps1 stub injection sites: MEDIUM (based on script reading, not dry-run execution)
  - Test seam (SPT_OVERRIDE_PKG_VERSION): MEDIUM (novel pattern; need user confirm before Plan 02)
  - Pitfalls: HIGH (derived from established codebase patterns: best-effort IO, recursion guard, atomic write)

  **Research date:** 2026-05-17
  **Valid until:** 2026-06-16 (30 days — Claude Code hooks API is stable-ish but new event types added periodically)

  ### Patterns Map
  # Phase 34: Version-Change Changelog - Pattern Map

  **Mapped:** 2026-05-17
  **Files analyzed:** 8 (4 new, 4 modified)
  **Analogs found:** 8 / 8

  ## File Classification

  | New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
  |-------------------|------|-----------|----------------|---------------|
  | NEW `src/owl/version_changelog.rs` | utility (sentinel I/O + parser + payload builder) | file-I/O + transform | `src/common/owlery.rs:248-255` (atomic_write_string) + `src/owl/hook_idle.rs:71-98` (sentinel read pattern) | exact (composite) |
  | NEW `owl version-remind <old>` subcommand | controller (one-shot CLI handler) | request-response (argv → file write) | `src/cli.rs:142-153` + `src/owl/mod.rs:116-130` (`NewAlarm`) | exact |
  | NEW repo-root `CHANGELOG.md` | config (static asset) | n/a | `psyche.md` (repo root) + no `include_str!` (D-11) | placement-only analog |
  | NEW `tests/version_changelog.rs` | test (integration) | event-driven (subprocess + stdin) | `tests/auto_pick_integration.rs:1-120` (SPT_HOME sandbox + assert_cmd) | exact |
  | MOD `src/owl/hook_idle.rs` | controller (Stop hook) | event-driven (hook stdin → JSON stdout) | self — append at run() top mirroring own `OWL_ECHO_COMMUNE` guard (lines 64-69) | self-reference |
  | MOD `docs/DEPLOY.ps1` | utility (build script) | batch (file rewrite + verify) | `docs/DEPLOY.ps1:246-274` (regex-replace-then-verify) + `:359-370` (Copy-Item to marketplace) | exact |
  | MOD `.planning/REQUIREMENTS.md` | config (spec doc) | n/a | Phase 31 D-11 / Phase 33 D-03/D-04 amendment commit | structural |
  | MOD `.planning/ROADMAP.md` | config (spec doc) | n/a | Phase 31 D-11 / Phase 33 D-03/D-04 amendment commit | structural |

  ---

  ## Pattern Assignments

  ### NEW `src/owl/version_changelog.rs` (utility, file-I/O + transform)

  **Analog A — sentinel atomic write** (`src/common/owlery.rs:248-255`):
  ```rust
  pub fn atomic_write_string(path: &std::path::Path, body: &str) -> std::io::Result<()> {
      let tmp = path.with_file_name(format!(
          "{}.tmp",
          path.file_name().and_then(|s| s.to_str()).unwrap_or("epoch")
      ));
      fs::write(&tmp, body)?;
      fs::rename(&tmp, path)
  }
  ```
  **Reuse verbatim.** Sentinel path: `owlery::spt_home().join("last-seen-version.json")`. Body: `serde_json::to_string(&SentinelV1 { version: new })?`.

  **Analog B — sentinel read with graceful degrade** (`src/owl/hook_idle.rs:71-83`):
  ```rust
  let info_path = owlery::info_file(owl_id);
  let content = match std::fs::read_to_string(&info_path) {
      Ok(c) => c,
      Err(_) => return,
  };
  let info: types::InfoJson = match serde_json::from_str(&content) {
      Ok(i) => i,
      Err(_) => return,
  };
  ```
  **Copy shape.** For sentinel: missing-file (first-install) → silently write NEW + skip emission; malformed JSON → log nothing, treat as missing.

  **Analog C — compile-time version source** (`src/owl/poll.rs:75`, `setup.rs:35`, `live/stop.rs:199`):
  ```rust
  env!("CARGO_PKG_VERSION")
  ```
  **Use identically.** Stored result for sentinel comparison; NEVER read runtime `plugin.json`.

  **Sentinel struct (locked per CONTEXT specifics):**
  ```rust
  #[derive(serde::Serialize, serde::Deserialize)]
  struct SentinelV1 { version: String }
  ```

  **CHANGELOG parser regex (locked):** `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$` — case-sensitive, rejects `[Unreleased]`, ignores link-reference lines (`[1.2.3]: <url>` form). No external `regex` crate dep — use the `regex` crate already in `Cargo.toml` if present; otherwise hand-roll (mirror `tests/skill_hints.rs:29-56` zero-dep YAML parser style).

  **Unit-test pattern** (co-located `#[cfg(test)]`, mirror `src/owl/hook_idle.rs:100-264`):
  - `ENV_LOCK: Mutex<()>` static + `EnvSnapshot` RAII for `SPT_HOME` mutation (lines 108-135)
  - `setup_spt_home(&tmp)` helper (lines 162-165)
  - Per-test tempdir via `tempfile::tempdir()`

  ---

  ### NEW `owl version-remind <old>` subcommand (controller, request-response)

  **Clap registration analog** (`src/cli.rs:142-153` — `NewAlarm`, the most recent positional-arg subcommand):
  ```rust
  /// Schedule a one-shot timed reminder ...
  #[command(name = "new-alarm")]
  NewAlarm {
      self_id: String,
      time_spec: String,
      ...
  },
  ```

  **Pattern to copy for Phase 34** — add to `src/cli.rs` `Commands` enum (no `hide = true`; visible like `NewAlarm`):
  ```rust
  /// Roll the last-seen version sentinel back to <old> so the next Stop
  /// hook re-fires the version-change prompt. Invoked by Claude when the
  /// user picks `Remind me later` from the AUQ.
  #[command(name = "version-remind")]
  VersionRemind {
      /// The previous version to roll the sentinel back to (e.g. `1.10.9`).
      /// MUST match the `old` value from the <spt-version-changelog> block
      /// Claude received — the subcommand cannot infer this otherwise.
      old: String,
  },
  ```

  **Dispatch arm** (`src/owl/mod.rs:37-160` `handle_command` match — add alongside `Commands::EchoCommune` at ~line 153):
  ```rust
  Some(Commands::VersionRemind { old }) => {
      version_changelog::run_version_remind(&old);
  }
  ```

  **Handler body** (~10 lines, mirrors `src/common/owlery.rs:264-269` `write_last_commune_epoch`):
  ```rust
  pub fn run_version_remind(old: &str) {
      let path = owlery::spt_home().join("last-seen-version.json");
      let body = serde_json::to_string(&SentinelV1 { version: old.to_string() })
          .expect("SentinelV1 serialization is infallible");
      match owlery::atomic_write_string(&path, &body) {
          Ok(_) => std::process::exit(0),
          Err(e) => {
              eprintln!("[owl] version-remind: failed to rewrite {}: {}", path.display(), e);
              std::process::exit(1);
          }
      }
  }
  ```

  **Clap parse-test pattern** (`src/cli.rs:351-411` — co-locate in `#[cfg(test)] mod tests`):
  ```rust
  #[test]
  fn parses_version_remind_positional() {
      let cli = Cli::try_parse_from(["owl", "version-remind", "1.10.9"]).unwrap();
      match cli.command {
          Some(Commands::VersionRemind { old }) => assert_eq!(old, "1.10.9"),
          other => panic!("expected VersionRemind, got {:?}", other),
      }
  }
  ```

  ---

  ### NEW repo-root `CHANGELOG.md` (config, static asset)

  **Placement analog:** `psyche.md` (repo root, copied into plugin meta dir). Phase 34 explicitly diverges from `psyche.md` in ONE respect: NOT embedded via `include_str!` (D-11 — runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`).

  **Format (locked, Keep-a-Changelog 1.1.0):**
  ```markdown
  # Changelog

  ## [1.11.0] - 2026-05-17

  - Added: ...
  - Fixed: ...

  ## [1.10.10] - 2026-05-13

  - ...
  ```

  **Genesis** (Plan 01 step 1, after REQUIREMENTS/ROADMAP amendment commit): author by hand using `git log --oneline --grep='chore: bump'` as the version skeleton. One commit. No auto-regeneration.

  ---

  ### NEW `tests/version_changelog.rs` (test, event-driven integration)

  **Analog:** `tests/auto_pick_integration.rs:1-120` — the closest existing subprocess-driven hook test.

  **Imports + harness pattern** (lines 42-99 verbatim):
  ```rust
  use std::fs;
  use std::sync::Mutex;
  static ENV_LOCK: Mutex<()> = Mutex::new(());

  struct SptHomeGuard { prior: Option<String> }
  impl SptHomeGuard {
      fn set(path: &std::path::Path) -> Self {
          let prior = std::env::var("SPT_HOME").ok();
          std::env::set_var("SPT_HOME", path);
          Self { prior }
      }
  }
  impl Drop for SptHomeGuard {
      fn drop(&mut self) {
          match &self.prior {
              Some(v) => std::env::set_var("SPT_HOME", v),
              None => std::env::remove_var("SPT_HOME"),
          }
      }
  }
  ```

  **Hook invocation pattern** (lines 81-99 — `assert_cmd::Command::cargo_bin("owl")`):
  ```rust
  let mut cmd = assert_cmd::Command::cargo_bin("owl").expect("owl binary must be built");
  cmd.arg("hook-idle")
      .env("SPT_HOME", tmp)
      .env("HOME", tmp)
      .env("USERPROFILE", tmp)
      .env_remove("OWL_ECHO_COMMUNE");
  // stdin pipe via .write_stdin(json) per assert_cmd
  ```

  **Perch-fixture pattern** (lines 105-120 verbatim — `write_live_perch`):
  - create `{spt_home}/owlery/{id}/ready`
  - write `info.json` with `parent_pid = std::process::id() as u64`
  - so `find_perch_by_parent_pid` resolves to this perch when the subprocess inherits the test PID as its ppid

  **Test cases to cover (per CONTEXT Claude's Discretion → Tests bullet):**
  1. version-mismatch single-step → emits block
  2. version-mismatch multi-step → emits block with `step_count > 1`
  3. first-install (no sentinel) → silent write, no emit
  4. sentinel == compile-time version → no emit
  5. `OWL_ECHO_COMMUNE=1` → no emit (recursion guard)
  6. no perch resolved → no emit
  7. `version-remind 1.10.9` → sentinel rewritten round-trip
  8. CHANGELOG.md parser: happy path, `[Unreleased]` reject, malformed H2 graceful, link-reference lines ignored

  ---

  ### MOD `src/owl/hook_idle.rs` (controller, event-driven)

  **This is a self-reference — append a new block at the TOP of `run()` between perch resolution (line 32) and `set_idle_ready` (line 39).**

  **Recursion guard (mirror lines 64-69 EXACTLY — MUST be first):**
  ```rust
  if std::env::var("OWL_ECHO_COMMUNE")
      .map(|v| !v.is_empty())
      .unwrap_or(false)
  {
      // skip version-change check in haiku echo-commune subprocess
  } else {
      // version-change emission block here
  }
  ```
  **OR** reorder so OWL_ECHO_COMMUNE guard runs at the very top of `run()` itself (cleaner — the existing guard at line 64-69 then becomes a no-op or is removed since `spawn_echo_commune_if_live` is called after). Planner's choice; CONTEXT D-03 accepts either.

  **Block emission (D-04 — Stop hook output schema):**
  ```rust
  // Stop hooks have no hookSpecificOutput.additionalContext channel
  // (per the hook_idle.rs:1-5 file-doc comment). Phase 34 uses
  // decision:"block" + reason instead. Research-confirmed: reason
  // text propagates to Claude as re-engagement context.
  let response = serde_json::json!({
      "decision": "block",
      "reason": format!(
          "<spt-version-changelog>\n{}\n</spt-version-changelog>",
          payload_xml
      ),
  });
  println!("{}", serde_json::to_string(&response).unwrap());
  ```
  **NOTE:** This is the channel Phase 34 picks — NOT the Pattern S2 `hookSpecificOutput` envelope from `plugin_session_start.rs:139-155` (`emit_auto_pick`). The S2 pattern is for SessionStart/PreToolUse/UserPromptSubmit hooks. Stop's only context-injection channel is `decision:"block"` + `reason`.

  **Sentinel write site (D-05):**
  ```rust
  // Write the NEW version BEFORE emitting the block. The `Remind me later`
  // AUQ option calls `$OWL version-remind <old>` to roll back; all other
  // branches (Yes-full / Yes-highlights / Skip) leave the sentinel at NEW.
  let _ = owlery::atomic_write_string(
      &owlery::spt_home().join("last-seen-version.json"),
      &serde_json::to_string(&SentinelV1 { version: current.to_string() }).unwrap(),
  );
  ```

  **SessionStart's `emit_auto_pick` envelope for comparison** (`plugin_session_start.rs:139-155`):
  ```rust
  pub(crate) fn emit_auto_pick(pick_spec_json: &str) {
      let context = format!(
          "<spt-live-auto-pick>\n{}\n</spt-live-auto-pick>",
          pick_spec_json
      );
      let response = serde_json::json!({
          "hookSpecificOutput": {
              "hookEventName": "SessionStart",
              "additionalContext": context,
          }
      });
      let out = serde_json::to_string(&response).unwrap_or_else(|_| {
          r#"{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":""}}"#.to_string()
      });
      println!("{}", out);
  }
  ```
  **Phase 34 does NOT mirror this shape.** Stop uses `decision:"block"` per the file-doc comment at `hook_idle.rs:1-5`. Confirm semantics during research per CONTEXT D-04 / Open Question.

  ---

  ### MOD `docs/DEPLOY.ps1` (utility, batch file rewrite)

  **Analog A — stub-injection (regex-replace-then-verify)** at `docs/DEPLOY.ps1:246-274`:
  ```powershell
  # Regex-rewrite preserves existing formatting (indent, key order, trailing newline)
  $pattern = '"version"\s*:\s*"' + [regex]::Escape($CurrentVersion) + '"'
  $replacement = '"version": "' + $NewVersion + '"'
  $newRaw = [regex]::Replace($PluginRaw, $pattern, $replacement)
  if ($newRaw -eq $PluginRaw) {
      throw "Failed to locate ""version"": ""$CurrentVersion"" in plugin.json -- aborting bump"
  }
  [System.IO.File]::WriteAllText($PluginJson, $newRaw)

  # Verify the write took
  $verify = (Get-Content $PluginJson -Raw | ConvertFrom-Json).version
  if ($verify -ne $NewVersion) {
      throw "plugin.json post-write reads version='$verify', expected '$NewVersion'"
  }
  ```

  **Pattern to copy for CHANGELOG.md stub-append (D-10 — slots in after line 274, before commit at line 279):**
  ```powershell
  # Phase 34 D-10: append stub H2 to repo-root CHANGELOG.md (no commit)
  $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  $ChangelogRaw  = Get-Content $ChangelogPath -Raw
  $todoMarker    = "TODO: changelog entry"
  # Abort if PRIOR -Bump's stub for current version is still un-filled
  if ($ChangelogRaw -match [regex]::Escape("## [$CurrentVersion]") -and `
      $ChangelogRaw -match [regex]::Escape($todoMarker)) {
      Write-Host "ERROR: CHANGELOG.md contains '$todoMarker' for v$CurrentVersion." -ForegroundColor Red
      Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
      exit 1
  }
  $today = Get-Date -Format 'yyyy-MM-dd'
  $stub = "`n## [$NewVersion] - $today`n`n- $todoMarker`n"
  $newChangelog = $ChangelogRaw.TrimEnd("`r","`n") + $stub
  [System.IO.File]::WriteAllText($ChangelogPath, $newChangelog)
  Write-SubStep "Appended CHANGELOG.md stub for v$NewVersion (fill in + commit before next -Bump)"
  # Intentionally NOT staged/committed — gates next -Bump on author filling
  ```

  **Analog B — sync to marketplace (Copy-Item)** at `docs/DEPLOY.ps1:359-370`:
  ```powershell
  Invoke-Mutation "Copy owl.exe -> $MarketSpt" {
      Copy-Item -Path $OwlBinary -Destination (Join-Path $MarketSpt 'owl.exe') -Force
  }
  Invoke-Mutation "Copy plugin.json -> $MarketPluginMeta" {
      Copy-Item -Path $PluginJson -Destination (Join-Path $MarketPluginMeta 'plugin.json') -Force
  }
  ```

  **Pattern to copy for CHANGELOG.md sync (D-11 — `{Marketplace}/plugin/spt/CHANGELOG.md`, sibling to `plugin.json`, NOT inside `.claude-plugin/`):**
  ```powershell
  # Phase 34 D-11: sync repo-root CHANGELOG.md to marketplace plugin root
  Invoke-Mutation "Copy CHANGELOG.md -> $MarketSpt" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $MarketSpt 'CHANGELOG.md') -Force
  }
  ```
  **Insert site:** at line 367 (after `Copy hooks/* -> $MarketHooks`, before `Copy plugin.json`).

  **Cache-tier sync** (mirror at `docs/DEPLOY.ps1:489-500` — same `Copy-Item -Force` shape):
  ```powershell
  Invoke-Mutation "Copy CHANGELOG.md -> $CacheVer" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $CacheVer 'CHANGELOG.md') -Force
  }
  ```
  **Insert site:** at line 497 (after `Copy hooks/*`, before `Copy plugin.json -> $CacheVer/.claude-plugin`).

  ---

  ### MOD `.planning/REQUIREMENTS.md` + `.planning/ROADMAP.md` (config, spec doc)

  **Analog:** Phase 31 D-11 + Phase 33 D-03/D-04 amendment-commit precedent. Plan 01 step 0 is a single commit that rewrites VERS-02/04/06/08/09 per CONTEXT D-12 and ROADMAP SC1/SC2/SC5 per CONTEXT D-12 — BEFORE any Rust code lands. Commit message: `docs(34): amend VERS-02..09 + SC1/2/5 for Stop-hook pivot`.

  ---

  ## Shared Patterns

  ### Recursion Guard (OWL_ECHO_COMMUNE)
  **Source:** `src/owl/hook_idle.rs:64-69`
  **Apply to:** version-change emission block + sentinel write (must precede both)
  ```rust
  if std::env::var("OWL_ECHO_COMMUNE")
      .map(|v| !v.is_empty())
      .unwrap_or(false)
  {
      return;
  }
  ```

  ### Atomic File Write
  **Source:** `src/common/owlery.rs:248-255`
  **Apply to:** sentinel write (Stop-hook path AND `version-remind` handler)
  ```rust
  owlery::atomic_write_string(&path, &body)
  ```

  ### Sentinel Read (graceful degrade)
  **Source:** `src/owl/hook_idle.rs:71-83` pattern
  **Apply to:** Stop-hook sentinel read
  - Missing file → treat as first-install (write NEW, no emit)
  - Malformed JSON → treat as missing (best-effort, no panic)

  ### Compile-Time Version
  **Source:** `src/owl/poll.rs:75`, `setup.rs:35`, `live/stop.rs:199`
  **Apply to:** all version comparisons in Phase 34 code
  ```rust
  env!("CARGO_PKG_VERSION")
  ```
  **NEVER read runtime `plugin.json`** (per VERS-02 amended wording).

  ### Perch-Resolution Gate
  **Source:** `src/owl/hook_idle.rs:24-32` (already in place in `hook_idle.rs::run()`)
  **Apply to:** version-change emission — only fires AFTER `owl_id = ... or return`. No new predicate code per D-02.

  ### Hook Stdin Parsing
  **Source:** `src/common/hook_output.rs:26-29` (`parse_hook_stdin`)
  **Apply to:** any new hook-handler stdin reads (version-change code doesn't need it — it reads no stdin fields beyond what perch-resolution already consumes).

  ### Test Env-Mutation Hygiene
  **Source:** `src/owl/hook_idle.rs:108-135` (in-file unit tests) + `tests/auto_pick_integration.rs:46-65` (integration tests)
  **Apply to:** all unit + integration tests touching `SPT_HOME`
  - `static ENV_LOCK: Mutex<()> = Mutex::new(())`
  - `EnvSnapshot::capture()` RAII (Drop restores)
  - `cmd.env_remove(...)` for all gate-listed env vars before subprocess invocation

  ### DEPLOY.ps1 Mutation Wrapping
  **Source:** `docs/DEPLOY.ps1:359-370` (`Invoke-Mutation "...desc..." { Copy-Item ... }`)
  **Apply to:** every new file write in DEPLOY.ps1 — wrap in `Invoke-Mutation` so `-DryRun` is auto-respected and step descriptions print uniformly.

  ---

  ## Risks: Deviations from Established Patterns

  1. **Stop-hook output channel — `decision:"block"` + `reason` is unprecedented in this codebase.** Every other hook (`hook_check`, `hook_prompt`, `plugin_session_start`) uses `hookSpecificOutput.additionalContext`. The file-doc comment at `hook_idle.rs:1-5` explicitly says Stop has no such schema. Phase 34 picks `decision:"block"` per CONTEXT D-04 but this is a research blocker — confirm with `~/.claude/reference_docs/claude-code-hooks.md` that `reason` text actually propagates to Claude on re-engagement. **Fallback if broken:** PreToolUse (next tool call) or revisit UserPromptSubmit. Flagged in CONTEXT Deferred.

  2. **Sentinel lives at `$SPT_HOME/last-seen-version.json`, NOT under `$SPT_HOME/owlery/`.** Per VERS-01 the sentinel is a SIBLING of `owlery/`, breaking the "everything spt-related lives under `owlery_dir()`" convention. There is no existing precedent for files at `spt_home()` root. Other dirs (`bin/`, `psyches/`, `pulses/`, `status/`, `logs_latest/`) are all subdirs. CONTEXT locks this; the sentinel is intentionally OUT of owlery to avoid coupling with perch lifecycle (e.g., `owl cleanup`). Add helper `pub fn sentinel_path() -> PathBuf { spt_home().join("last-seen-version.json") }` to `owlery.rs` to keep the convention encapsulated.

  3. **`version-remind` is the first subcommand whose only purpose is to mutate a single sentinel file.** Most subcommands wrap richer logic (`Setup`, `Poll`, `Live`, `Send`). The closest precedent is `NewAlarm` (one-shot file write) but `NewAlarm` also threads through resolve + pulse-dir registration. `version-remind` is ~10 lines and a single fs write — keep it tiny; don't over-engineer.

  4. **No `regex` crate currently in `Cargo.toml`** (verify during planning). The CHANGELOG.md H2 parser regex is small enough to hand-roll (5-10 lines, mirror the parser style at `tests/skill_hints.rs:29-56` for zero-dep parsing). Adding `regex` for this one use would be a heavy dep; planner should default to hand-rolled.

  5. **DEPLOY.ps1 stub-injection does not commit.** Every other DEPLOY.ps1 mutation either commits immediately (`-Bump` at line 281) or is part of the marketplace push (line 403). The Phase 34 CHANGELOG.md stub-append is deliberately uncommitted to gate the next `-Bump` on author action — this is a NEW interaction shape for DEPLOY.ps1. Surface it clearly in the post-bump notice so users aren't surprised by `git status` showing dirty CHANGELOG.md after `-Bump`.

  6. **CHANGELOG.md is read at runtime from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` rather than embedded via `include_str!`** (D-11). `psyche.md` is the established precedent and IS embedded. CONTEXT calls out the rationale: changelog grows monotonically, binary should not bloat. Tradeoff: runtime file read can fail (missing file, IO error) → must degrade gracefully (skip emission, log silently — mirror the `hook_idle.rs:71-83` read-then-bail pattern).

  7. **AskUserQuestion + `$OWL version-remind <old>` round-trip relies on Claude faithfully passing the literal `old` value from the block payload.** No structural guarantee — purely instructional. Block-body MUST surface the exact invocation string verbatim with `<old>` substituted (CONTEXT D-08). The only enforcement is the `version-remind` subcommand rejecting a missing arg.

  ---

  ## Metadata

  **Analog search scope:** `src/owl/`, `src/common/`, `src/live/`, `tests/`, `docs/DEPLOY.ps1`, `src/cli.rs`, `src/main.rs`
  **Files scanned:** 12 read in full or in targeted ranges
  **Pattern extraction date:** 2026-05-17

  ### Plans to Review
  #### 34-01-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 01
  type: execute
  wave: 1
  depends_on: []
  files_modified:
    - .planning/REQUIREMENTS.md
    - .planning/ROADMAP.md
    - CHANGELOG.md
    - docs/DEPLOY.ps1
  autonomous: true
  requirements:
    - VERS-02
    - VERS-04
    - VERS-06
    - VERS-07
    - VERS-08
    - VERS-09
  user_setup: []

  must_haves:
    truths:
      - "REQUIREMENTS.md VERS-02/04/06/08/09 reflect the Stop-hook pivot + 4-option AUQ + AUTO-03-coexistence-by-hook-separation wording per CONTEXT D-12"
      - "ROADMAP.md Phase 34 Success Criteria 1, 2, 5 reflect the Stop-hook pivot wording per CONTEXT D-12"
      - "Repo-root CHANGELOG.md exists in Keep-a-Changelog 1.1.0 format with an H2 section per historical version bump (38 candidate versions surfaced by `git log --grep='chore: bump'`)"
      - "Running `docs/DEPLOY.ps1 -Bump patch -DryRun` (after this plan) shows the new TODO-marker abort gate and CHANGELOG.md stub-append substeps in the dry-run trace"
      - "Running `docs/DEPLOY.ps1` (deploy without bump) shows a new substep that copies repo-root CHANGELOG.md into `{Marketplace}/plugin/spt/CHANGELOG.md` AND into `{CacheVer}/CHANGELOG.md`"
    artifacts:
      - path: "CHANGELOG.md"
        provides: "Keep-a-Changelog 1.1.0 root document; consumed at runtime by Plan 02's parser via $CLAUDE_PLUGIN_ROOT/CHANGELOG.md"
        contains: "## ["
      - path: ".planning/REQUIREMENTS.md"
        provides: "Amended VERS-02/04/06/08/09 per D-12"
        contains: "Stop hook"
      - path: ".planning/ROADMAP.md"
        provides: "Amended Phase 34 SC1/SC2/SC5 per D-12"
        contains: "Stop hook"
      - path: "docs/DEPLOY.ps1"
        provides: "(a) -Bump TODO-marker abort gate, (b) -Bump CHANGELOG.md stub-append (uncommitted, after the bump commit), (c) CHANGELOG.md sync to MarketSpt + CacheVer"
        contains: "CHANGELOG.md"
    key_links:
      - from: "docs/DEPLOY.ps1 -Bump flow"
        to: "repo-root CHANGELOG.md"
        via: "stub-append after `git commit` at line 282, before any further work"
        pattern: "TODO: changelog entry"
      - from: "docs/DEPLOY.ps1 deploy/sync flow"
        to: "{Marketplace}/plugin/spt/CHANGELOG.md AND {CacheVer}/CHANGELOG.md"
        via: "two new Invoke-Mutation `Copy-Item` blocks"
        pattern: "Copy CHANGELOG.md"
  ---

  <objective>
  Phase 34 doc-amendment-first plan + CHANGELOG.md genesis + DEPLOY.ps1 extension.
  PURE doc/script work — NO Rust code in this plan.

  Purpose: Land three preconditions Plan 02 + Plan 03 depend on:
    1. REQUIREMENTS.md + ROADMAP.md no longer assert SessionStart wording (CONTEXT D-12; mirrors Phase 31 D-11 / Phase 33 D-03/D-04 amendment-first pattern).
    2. Repo-root CHANGELOG.md exists in Keep-a-Changelog 1.1.0 format so Plan 02's parser has real content to consume in unit + integration tests.
    3. DEPLOY.ps1 (a) gates future bumps on a filled CHANGELOG entry and (b) syncs the file to the marketplace + cache so the hook's runtime path (`$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`) resolves.

  Output: 3 commits (amendment, CHANGELOG genesis, DEPLOY.ps1 extension). No Rust touched.
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @docs/DEPLOY.ps1

  <interfaces>
  <!-- Locked invariants this plan must preserve / not contradict. -->

  REQUIREMENTS.md VERS-02 — REPLACEMENT WORDING (CONTEXT D-12):
    "On Stop hook (in `src/owl/hook_idle.rs`), gated on active perch resolution via existing `find_perch_by_session` / `find_perch_by_parent_pid`. Hook compares `env!(\"CARGO_PKG_VERSION\")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`."

  REQUIREMENTS.md VERS-04 — REPLACEMENT WORDING (CONTEXT D-12):
    "AskUserQuestion with four options: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Question text includes `step_count` when > 1."

  REQUIREMENTS.md VERS-06 — REPLACEMENT WORDING (CONTEXT D-12):
    "Multi-version skip surfaces the same 4-option AskUserQuestion as single-step; user-chosen `Yes, full changelog` renders all interim H2 sections verbatim, `Yes, highlights only` defers to agent discretion. The CHANGELOG.md pointer is appended to the highlights body."

  REQUIREMENTS.md VERS-08 — REPLACEMENT WORDING (CONTEXT D-12):
    "Version-change prompt (VERS-04) fires from Stop hook and SessionStart auto-pick (AUTO-03 from Phase 33) fires from SessionStart hook. The two never share a hook invocation; no allow-list reconciliation required."

  REQUIREMENTS.md VERS-09 — REPLACEMENT WORDING (CONTEXT D-12):
    "Single write site at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard. Atomic via existing `atomic_write_string`. Binary handoff (Phase 18.4/18.5) does not race because Stop hooks do not fire during in-flight binary swap (Stop fires between turns, handoff happens between processes)."

  ROADMAP.md Phase 34 SC1 — REPLACEMENT WORDING (CONTEXT D-12):
    "On a fresh install (no `$SPT_HOME/last-seen-version.json` present), on first Stop hook fire in any spt-bearing session, the sentinel silently writes the current `CARGO_PKG_VERSION` and emits NO changelog prompt to the user."

  ROADMAP.md Phase 34 SC2 — REPLACEMENT WORDING (CONTEXT D-12):
    "After the plugin version changes (compiled-in `CARGO_PKG_VERSION` differs from sentinel contents), the next Stop hook fire in an spt-bearing session emits a `<spt-version-changelog>` block via `decision:\"block\"` + `reason`; Claude renders an AskUserQuestion with four options (`Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`); the sentinel is atomically rewritten with the current version at hook entry (before the block emission), so the prompt does not re-fire next session unless the user picks `Remind me later`, which invokes `$OWL version-remind <old>` to roll the sentinel back."

  ROADMAP.md Phase 34 SC5 — REPLACEMENT WORDING (CONTEXT D-12):
    "VERS-04 fires from Stop hook (`hook_idle.rs`); AUTO-03 (from Phase 33) fires from SessionStart hook (`plugin_session_start.rs`); collision is impossible by hook separation. The sentinel write site lives at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard (atomic via existing `atomic_write_string`); Stop hooks do not fire mid-binary-handoff, so a race cannot corrupt it."

  ROADMAP.md Phase 34 SC3, SC4, SC6 — UNCHANGED (CONTEXT D-12).

  CHANGELOG.md format (Keep-a-Changelog 1.1.0, RESEARCH Q10):
    H2 line shape: `## [X.Y.Z] - YYYY-MM-DD`
    Parser regex (Plan 02 will enforce): `^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$`
    NO `[Unreleased]` section (parser rejects it; we author retroactively from git log).
    H3 subsections / bullets / prose paragraphs all acceptable in body (parser is body-content-agnostic).
    Link-reference lines at file bottom are valid per spec but unused; do not add them.

  DEPLOY.ps1 stub format (CONTEXT specifics):
  ```
  ## [{NEW_VERSION}] - {YYYY-MM-DD}

  - TODO: changelog entry
  ```
  Literal `TODO: changelog entry` is the scan token for the abort gate.
  Date computed at bump-time via `Get-Date -Format 'yyyy-MM-dd'`.

  DEPLOY.ps1 insertion sites (verified via Read against current docs/DEPLOY.ps1):
  - Abort gate: insert AFTER existing Cargo.toml dirty-state check (current line 222), BEFORE the `# Read Cargo.toml ...` comment (current line 224). The check runs only when `$Bump` is truthy.
  - Stub append: insert AFTER the `Push-Location $RepoRoot ... } finally { Pop-Location } Write-SubStep "Committed bump: $bumpSha (not pushed)"` block (current line 287), BEFORE `$Version = $NewVersion` (current line 290). CRITICAL: must run AFTER `git commit` so the stub stays in working tree only — Pitfall 5 in RESEARCH.md.
  - Sync to MarketSpt: insert AFTER `Copy hooks/* -> $MarketHooks` block (current lines 365-367), BEFORE `Copy plugin.json -> $MarketPluginMeta` (current lines 368-370).
  - Sync to CacheVer: insert AFTER `Copy hooks/* -> $CacheVer/hooks` block (current lines 495-497), BEFORE `Copy plugin.json -> $CacheVer/.claude-plugin` (current lines 498-500).

  CHANGELOG.md author-time scope (RESEARCH Appendix A): 38 versions surfaced by `git log --oneline --grep='chore: bump'`, ranging from v1.5.4 to current (v1.10.10 or later — verify at author time). Skipped versions (1.9.11, 1.9.6, 1.9.0/1/3/4) get no entry. NOT a literal log dump — human-readable curated prose per version. One commit.
  </interfaces>
  </context>

  <tasks>

  <task type="auto">
    <name>Task 1: Amend REQUIREMENTS.md VERS-02/04/06/08/09 + ROADMAP.md Phase 34 SC1/SC2/SC5 per D-12</name>
    <files>.planning/REQUIREMENTS.md, .planning/ROADMAP.md</files>
    <action>
  Mirror Phase 31 D-11 + Phase 33 D-03/D-04 amendment-commit precedent (CONTEXT canonical_refs).

  In `.planning/REQUIREMENTS.md`, edit the five VERS-* bullets currently at lines 72-79 (verify line numbers by reading the file first — they may have drifted; the canonical anchors are the bullet IDs `**VERS-02**`, `**VERS-04**`, `**VERS-06**`, `**VERS-08**`, `**VERS-09**`):
    - Replace VERS-02 body with the verbatim REPLACEMENT WORDING block in `<interfaces>` above (CONTEXT D-12).
    - Replace VERS-04 body with its REPLACEMENT WORDING.
    - Replace VERS-06 body with its REPLACEMENT WORDING.
    - Replace VERS-08 body with its REPLACEMENT WORDING.
    - Replace VERS-09 body with its REPLACEMENT WORDING.
    - DO NOT touch VERS-01, VERS-03, VERS-05, VERS-07. They remain valid as-written.
    - DO NOT touch the bullet IDs themselves (`- [ ] **VERS-02**:` etc.) — only the prose after the colon.
    - Keep the `*Updated YYYY-MM-DD — ...*` footer convention: append a new line directly under the existing `*Updated 2026-05-16 — FRESH-04 and FRESH-05 struck per Phase 33 CONTEXT D-03; FRESH-06 reworded per D-04.*` line saying: `*Updated 2026-05-17 — VERS-02/04/06/08/09 amended per Phase 34 CONTEXT D-12 (Stop-hook pivot, 4-option AUQ, AUTO-03-coexistence-by-hook-separation).*`

  In `.planning/ROADMAP.md`, edit the Phase 34 Success Criteria block (currently at lines 643-655, verify by reading first; anchor is `### Phase 34: Version-Change Changelog`):
    - Replace SC1 body with the verbatim REPLACEMENT WORDING (CONTEXT D-12).
    - Replace SC2 body with its REPLACEMENT WORDING.
    - Replace SC5 body with its REPLACEMENT WORDING.
    - DO NOT touch SC3, SC4, SC6 — they remain unchanged.
    - DO NOT touch the **Goal**, **Milestone**, **Depends on**, **Requirements**, **Plans:** lines.

  After both edits, run:
  ```
  git diff --stat .planning/REQUIREMENTS.md .planning/ROADMAP.md
  ```
  Verify exactly two files changed.

  Commit (in repo root) with message:
  ```
  docs(34): amend VERS-02/04/06/08/09 + ROADMAP SC1/2/5 per Phase 34 D-12

  Pivots from the original SessionStart wording to Stop-hook + 4-option AUQ
  + AUTO-03-coexistence-by-hook-separation. Mirrors Phase 31 D-11 and
  Phase 33 D-03/D-04 doc-amendment-first commit pattern.

  No code touched in this commit (Plan 01 step 1).
  ```

  DO NOT stage other files; use `git add .planning/REQUIREMENTS.md .planning/ROADMAP.md` explicitly.
    </action>
    <verify>
      <automated>git log -1 --format=%s | grep -q "amend VERS-02/04/06/08/09" &amp;&amp; grep -q "Stop hook" .planning/REQUIREMENTS.md &amp;&amp; grep -q "VERS-04 fires from Stop" .planning/ROADMAP.md</automated>
    </verify>
    <done>Commit landed on HEAD; REQUIREMENTS.md contains the five amended VERS bodies (search confirms `Stop hook` substring); ROADMAP.md SC5 contains the literal "VERS-04 fires from Stop" phrase; no other files in the commit.</done>
  </task>

  <task type="auto">
    <name>Task 2: Author repo-root CHANGELOG.md from git-log version-bump skeleton</name>
    <files>CHANGELOG.md</files>
    <action>
  Per CONTEXT D-09: author by hand. NO `owl changelog backfill` subcommand (deferred). One commit.

  Step A: Generate version skeleton from git log:
  ```
  git log --oneline --grep='chore: bump' > /tmp/version-bumps.txt
  ```
  Expect ~38 entries (RESEARCH Appendix A surfaced this count). Read the output; map each `chore: bump spt plugin to vX.Y.Z` line to a target H2 section. Skipped versions (1.9.11, 1.9.6, 1.9.0/1/3/4) get NO entry — they were never released.

  Step B: For each released version, derive curated prose by inspecting the commit RANGE between consecutive bumps:
  ```
  git log --oneline <previous_bump_sha>..<this_bump_sha>
  ```
  Summarize the user-facing changes (feature adds, fixes, breaking changes if any) in 1-5 bullet points per version. Format guidance:
    - Top-level title: `# Changelog` followed by a one-line description and a link to Keep-a-Changelog 1.1.0 + Semantic Versioning per spec convention.
    - Each version: H2 line `## [X.Y.Z] - YYYY-MM-DD` (date = the bump commit's author date in ISO-8601, get via `git show -s --format=%ad --date=short <sha>`).
    - Body: prose paragraphs OR bullet lists OR H3 subsections (`### Added` / `### Fixed` / etc. per Keep-a-Changelog 1.1.0) — author's discretion; the Plan 02 parser does NOT care about body shape.
    - NEWEST version first, OLDEST last (Keep-a-Changelog convention).
    - NO `[Unreleased]` section — we author retroactively.
    - NO link-reference lines (`[1.10.10]: <url>`) — unused by our parser.

  Step C: Validate format by hand-running the parser regex against each H2 line:
  ```
  grep -E '^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$' CHANGELOG.md | wc -l
  ```
  The match count MUST equal the number of H2 sections you authored. If any H2 line fails this regex, the Plan 02 parser will skip that version → re-author the offending line to match.

  Step D: Read back the file once with `wc -l CHANGELOG.md` and confirm it is non-empty and "reasonable size" (expect 100-400 lines for ~30+ versions of curated prose).

  Step E: Commit:
  ```
  git add CHANGELOG.md
  git commit -m "docs(34): create repo-root CHANGELOG.md (Keep-a-Changelog 1.1.0)"
  ```

  CRITICAL CONSTRAINTS:
  - The CHANGELOG content itself is curated prose — you are authoring history, not auto-generating. Quality bar: each entry should read as a useful release note for a future user.
  - Date format MUST be ISO-8601 `YYYY-MM-DD` with hyphens. Slashes will fail the parser regex.
  - Patch-level semver only (`X.Y.Z`). NO pre-release suffixes (`-rc1`) — parser rejects them per RESEARCH Q10.
  - No commit-message-style prefixes inside the body (no `feat:` / `fix:` prefixes — those go in the git log, not the CHANGELOG).
    </action>
    <verify>
      <!-- N2 fix: research surfaced 38 historical bumps (RESEARCH Appendix A). Raise the
           floor from 20 to 30 to better track reality, AND cross-check against the live
           `git log --grep='chore: bump'` count so the floor self-calibrates if more bumps
           have landed since RESEARCH was written. Skipped versions (1.9.11/6/0/1/3/4) mean
           the H2 count can legitimately be a few less than the raw git-log count — the
           tolerance is `>= (bump_count - 8)` to absorb skips without false negatives. -->
      <automated>test -f CHANGELOG.md &amp;&amp; H2_COUNT=$(grep -cE '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$' CHANGELOG.md) &amp;&amp; test "$H2_COUNT" -ge 30 &amp;&amp; BUMP_COUNT=$(git log --grep='chore: bump' --oneline | wc -l | tr -d ' ') &amp;&amp; test "$H2_COUNT" -ge "$((BUMP_COUNT - 8))" &amp;&amp; ! grep -qE '^## \[Unreleased\]' CHANGELOG.md &amp;&amp; ! grep -qE '^## \[[0-9]+\.[0-9]+\.[0-9]+-' CHANGELOG.md</automated>
    </verify>
    <done>CHANGELOG.md exists at repo root; ≥30 valid Keep-a-Changelog H2 sections (and within 8 of the live `git log --grep='chore: bump'` count); no `[Unreleased]`; no pre-release suffix versions; committed.</done>
  </task>

  <task type="auto">
    <name>Task 3: Extend docs/DEPLOY.ps1 — TODO-marker abort gate + stub-append + CHANGELOG.md sync (MarketSpt + CacheVer)</name>
    <files>docs/DEPLOY.ps1</files>
    <action>
  Four discrete insertions per the verified line citations in `<interfaces>`. Read the file FIRST and confirm the anchor lines before editing — DEPLOY.ps1 evolves between phases.

  === Insertion 1: TODO-marker abort gate (inside -Bump branch) ===

  Pattern source: existing dirty-state checks at docs/DEPLOY.ps1:213-222 (CONTEXT canonical_refs).
  Insert AFTER the existing Cargo.toml dirty-state `if` block (currently ending at line 222 `exit 1 }`), BEFORE the `# Read Cargo.toml and locate its current [package] version` comment (currently line 224).

  Insert this block verbatim:
  ```powershell
      # Phase 34 D-10 abort gate: refuse to bump if the previous -Bump's CHANGELOG.md
      # stub is still un-filled. Scans for the literal TODO marker the stub injects.
      $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
      if (Test-Path $ChangelogPath) {
          $ChangelogContent = Get-Content $ChangelogPath -Raw
          if ($ChangelogContent -match 'TODO: changelog entry') {
              Write-Host "ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump." -ForegroundColor Red
              Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
              exit 1
          }
      } else {
          Write-Host "ERROR: CHANGELOG.md not found at $ChangelogPath" -ForegroundColor Red
          Write-Host "       Phase 34 requires a repo-root CHANGELOG.md; create one before -Bump." -ForegroundColor Red
          exit 1
      }
  ```

  === Insertion 2: CHANGELOG.md stub-append (uncommitted, after `git commit`) ===

  Pattern source: existing Cargo.toml regex-rewrite-then-verify at docs/DEPLOY.ps1:246-274 (CONTEXT canonical_refs).
  CRITICAL ORDERING per Pitfall 5 in RESEARCH.md: stub append MUST run AFTER `git commit` (current line 281) so CHANGELOG.md stays in working tree only — NEVER staged into the bump commit.

  Insert AFTER `Write-SubStep "Committed bump: $bumpSha (not pushed)"` (current line 287), BEFORE the closing `}` of the `-not $DryRun` else branch (current line 288). Wrap in an `if ($DryRun)` / `else` to honor the existing dry-run discipline.

  Actually — to keep the indent and ordering clean, insert AFTER the `} else {` ... `Write-SubStep "Committed bump: $bumpSha (not pushed)"` ... `}` closing brace of the `if ($DryRun) { ... } else { ... }` block (current line 288), and BEFORE `$Version = $NewVersion` (current line 290).

  Insert this block:
  ```powershell
      # Phase 34 D-10 stub-append: drop a TODO H2 into repo-root CHANGELOG.md
      # WITHOUT staging or committing. The next -Bump's abort gate (insertion 1)
      # will refuse to proceed until the author fills the entry and commits it.
      $today = Get-Date -Format 'yyyy-MM-dd'
      $todoMarker = "TODO: changelog entry"
      $stub = "`n## [$NewVersion] - $today`n`n- $todoMarker`n"
      if ($DryRun) {
          Write-Host "    [DRY-RUN] append stub to CHANGELOG.md: ## [$NewVersion] - $today (uncommitted)" -ForegroundColor DarkYellow
      } else {
          $ChangelogRaw = Get-Content $ChangelogPath -Raw
          $newChangelog = $ChangelogRaw.TrimEnd("`r","`n") + $stub
          [System.IO.File]::WriteAllText($ChangelogPath, $newChangelog)
          Write-SubStep "Appended CHANGELOG.md stub for v$NewVersion (fill in + commit before next -Bump)"
          Write-SubStep "  NOTE: CHANGELOG.md is intentionally unstaged. Edit it, then 'git add CHANGELOG.md && git commit'."
      }
  ```

  NOTE: `$ChangelogPath` is in scope from insertion 1's variable declaration. If the variable is not assigned outside the `if ($Bump)` block (insertion 1 lives inside `if ($Bump) { ... }`), redeclare it here:
  ```powershell
      $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  ```

  === Insertion 3: CHANGELOG.md sync -> MarketSpt ===

  Pattern source: existing `Copy-Item -Force` wrapped in `Invoke-Mutation` at docs/DEPLOY.ps1:359-370 (CONTEXT canonical_refs).
  Insert AFTER the `Copy hooks/* -> $MarketHooks` Invoke-Mutation block (current lines 365-367), BEFORE the `Copy plugin.json -> $MarketPluginMeta` block (current lines 368-370).

  Insert this block:
  ```powershell
      Invoke-Mutation "Copy CHANGELOG.md -> $MarketSpt" {
          Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $MarketSpt 'CHANGELOG.md') -Force
      }
  ```

  Per CONTEXT D-11: target is `{Marketplace}/plugin/spt/CHANGELOG.md` — sibling to plugin.json, NOT inside `.claude-plugin/`. `$MarketSpt` already resolves to that directory (verify by reading the variable assignment earlier in the script; do not re-derive).

  === Insertion 4: CHANGELOG.md sync -> CacheVer ===

  Pattern source: existing CacheVer `Copy-Item -Force` blocks at docs/DEPLOY.ps1:489-500 (CONTEXT canonical_refs).
  Insert AFTER the `Copy hooks/* -> $CacheVer/hooks` block (current lines 495-497), BEFORE the `Copy plugin.json -> $CacheVer/.claude-plugin` block (current lines 498-500).

  Insert this block:
  ```powershell
  Invoke-Mutation "Copy CHANGELOG.md -> $CacheVer" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $CacheVer 'CHANGELOG.md') -Force
  }
  ```

  NOTE: this is the cache-tier sync; target is `$CacheVer/CHANGELOG.md` (plugin root, NOT `$CacheVer/.claude-plugin/CHANGELOG.md`). Hook resolves at runtime via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`, and `$CLAUDE_PLUGIN_ROOT` points to `$CacheVer` for plugin-bundled hooks.

  === Verification dry-run ===

  After all four insertions, run:
  ```
  powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun
  ```
  Expect to see (in some order):
    - `[DRY-RUN] Copy CHANGELOG.md -> {MarketSpt path}` (insertion 3)
    - `[DRY-RUN] Copy CHANGELOG.md -> {CacheVer path}` (insertion 4)

  Then run:
  ```
  powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun
  ```
  Expect to see (in order):
    - The new `ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump.` is NOT triggered (CHANGELOG just authored has no TODO marker).
    - `[DRY-RUN] append stub to CHANGELOG.md: ## [X.Y.Z+1] - YYYY-MM-DD (uncommitted)` (insertion 2 dry-run line).
    - The two sync substeps above also surface.

  === Commit ===

  ```
  git add docs/DEPLOY.ps1
  git commit -m "feat(34): DEPLOY.ps1 CHANGELOG.md sync + bump stub/abort gate"
  ```

  Body suggestion:
  ```
  - New abort gate refuses -Bump when prior CHANGELOG stub still has 'TODO: changelog entry' marker.
  - New stub-append drops `## [NEW] - YYYY-MM-DD\n\n- TODO: changelog entry` into CHANGELOG.md AFTER the bump commit, intentionally unstaged so the next -Bump's gate fires until the author fills + commits.
  - New sync substeps copy repo-root CHANGELOG.md to {Marketplace}/plugin/spt/CHANGELOG.md AND to {CacheVer}/CHANGELOG.md (sibling to plugin.json, NOT inside .claude-plugin/) per CONTEXT D-11.
  ```
    </action>
    <verify>
      <!-- PRECONDITION: working tree MUST be clean OR this task MUST run in a tempdir-cloned repo.
           DEPLOY.ps1 -Bump (and even bare -DryRun in some branches) aborts on a dirty Cargo.toml
           dirty-state check; we cannot reliably exercise the -Bump dry-run mid-plan-execution.
           The blocking verify therefore runs the no-bump dry-run only (which surfaces insertions 3+4)
           and uses comment-stripped grep counts (per grep-gate hygiene: bare `grep -c` counts
           self-referential comment lines like the canonical_refs prose, inflating the count).
           Insertion 1's abort gate (literal TODO scan) and insertion 2's stub-append dry-run line
           are observed in the non-blocking <done> below. -->
      <automated>TODO_COUNT=$(grep -v '^[[:space:]]*#' docs/DEPLOY.ps1 | grep -c "TODO: changelog entry") &amp;&amp; test "$TODO_COUNT" -ge 2 &amp;&amp; COPY_COUNT=$(grep -v '^[[:space:]]*#' docs/DEPLOY.ps1 | grep -c "Copy CHANGELOG.md") &amp;&amp; test "$COPY_COUNT" -ge 2 &amp;&amp; powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun 2>&amp;1 | tee /tmp/d.log &amp;&amp; grep -q "Copy CHANGELOG.md -> .*MarketSpt\|Copy CHANGELOG.md -> .*spt" /tmp/d.log &amp;&amp; grep -c "Copy CHANGELOG.md" /tmp/d.log | grep -qE '^[2-9]'</automated>
    </verify>
    <done>docs/DEPLOY.ps1 contains both `TODO: changelog entry` references (gate scan + stub literal) and both `Copy CHANGELOG.md` Invoke-Mutation blocks (MarketSpt + CacheVer); the no-bump `-DryRun` surfaces the two new sync substeps; commit landed.

  NON-BLOCKING OBSERVATIONS (manually verify in a clean-tree environment OR a tempdir clone — do not gate the task on these, since `-Bump patch -DryRun` may abort on the live working tree's dirty state):
  - `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun` (against a clean tree) emits `[DRY-RUN] append stub to CHANGELOG.md: ## [X.Y.Z+1] - YYYY-MM-DD (uncommitted)` (insertion 2).
  - The abort gate (insertion 1) is reachable: temporarily inject `TODO: changelog entry` into CHANGELOG.md, run `-Bump patch -DryRun`, and observe the `ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump.` exit-1 path. Remove the injection after observation.</done>
  </task>

  </tasks>

  <verification>
  After all 3 tasks:

  1. `git log --oneline -5` shows three new commits in order:
     - `docs(34): amend VERS-02/04/06/08/09 + ROADMAP SC1/2/5 per Phase 34 D-12`
     - `docs(34): create repo-root CHANGELOG.md (Keep-a-Changelog 1.1.0)`
     - `feat(34): DEPLOY.ps1 CHANGELOG.md sync + bump stub/abort gate`

  2. `git diff --name-only HEAD~3` shows exactly 4 paths:
     - `.planning/REQUIREMENTS.md`
     - `.planning/ROADMAP.md`
     - `CHANGELOG.md`
     - `docs/DEPLOY.ps1`

  3. `grep -cE '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$' CHANGELOG.md` ≥ 20 (curated history).

  4. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun` exits 0 AND emits both new `Copy CHANGELOG.md` substeps.

  5. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun` exits 0 AND emits the new `[DRY-RUN] append stub to CHANGELOG.md` substep.

  6. Cargo tests untouched: `cargo test --no-run` builds (smoke check that nothing accidentally introduced a Rust syntax break — Plan 01 is pure doc/script, no Rust expected).
  </verification>

  <success_criteria>
  - Plan 01 maps to (post-amendment) VERS-02 (REQUIREMENTS.md wording landed), VERS-04 (wording landed), VERS-06 (wording landed), VERS-07 (CHANGELOG.md exists + sync syncs it), VERS-08 (wording landed), VERS-09 (wording landed).
  - Plan 01 unblocks Plan 02 by establishing CHANGELOG.md with real content for parser unit + integration tests.
  - Plan 01 unblocks future end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6) by installing the maintenance gate.
  - ROADMAP.md Phase 34 SC1, SC2, SC5 read coherently against the implementation Plan 02/03 will deliver.
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | author -> repo | hand-authored CHANGELOG.md content reaches plugin users via deploy; trusted because author IS the user |
  | deploy script -> filesystem | DEPLOY.ps1 writes to marketplace + cache; local-only, no network surface |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-01 | Tampering | CHANGELOG.md author content | accept | Hand-authored prose under git review; no automated injection path |
  | T-34-02 | Tampering | DEPLOY.ps1 abort gate bypass | accept | Local script, single-user trust boundary; the gate is UX-not-security |
  | T-34-03 | Repudiation | Bump commit not including CHANGELOG.md | mitigate | Stub-append runs AFTER `git commit` (insertion 2 ordering, Pitfall 5) so stub stays unstaged and the bump commit is provably clean |
  | T-34-04 | DoS | Abort gate infinite-loop (author cannot escape) | accept | Gate prints clear instructions: "Fill in the entry and commit before re-running -Bump." Author has a direct remedy. |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-01-SUMMARY.md` documenting:
  - Three commit SHAs
  - CHANGELOG.md final H2 section count
  - Any deviations from the line-citation anchors in `<interfaces>` (DEPLOY.ps1 may have drifted; record the actual insertion line numbers used)
  - Confirmation that `-Bump patch -DryRun` surfaces the new substeps in expected order
  </output>

  #### 34-02-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 02
  type: execute
  wave: 2
  depends_on: [34-01]
  files_modified:
    - src/owl/version_changelog.rs
    - src/owl/mod.rs
    - src/owl/hook_idle.rs
    - src/cli.rs
    - src/common/owlery.rs
  autonomous: true
  requirements:
    - VERS-01
    - VERS-02
    - VERS-03
    - VERS-05
    - VERS-09
  user_setup: []

  must_haves:
    truths:
      - "A new `src/owl/version_changelog.rs` module owns sentinel I/O, the Keep-a-Changelog parser, and the payload-XML builder"
      - "`hook_idle.rs::run()` calls the version-change check AFTER perch resolution (lines 25-32) AND after an OWL_ECHO_COMMUNE guard, BEFORE `set_idle_ready` (line 39)"
      - "On version mismatch the hook emits `{\"decision\":\"block\",\"reason\":\"<spt-version-changelog>...</spt-version-changelog>\"}` to stdout"
      - "Sentinel is pre-written to NEW version atomically via `owlery::atomic_write_string` BEFORE the block is emitted; rollback ONLY via `$OWL version-remind <old>`"
      - "First-install path (sentinel missing) silently writes the current version and emits NO block (VERS-03)"
      - "`stop_hook_active=true` from stdin short-circuits the check (RESEARCH Pitfall 4 — defensive infinite-loop guard)"
      - "A new clap subcommand `owl version-remind <old>` writes `{\"version\":\"<old>\"}` to the sentinel and exits 0; missing-arg or non-semver arg exits 1 (RESEARCH security V5)"
      - "A test-only env var `SPT_OVERRIDE_PKG_VERSION` lets unit + integration tests simulate any version without rebuilding (RESEARCH Q9)"
      - "Co-located unit tests in `src/owl/version_changelog.rs::tests` cover the 18-row parser corpus + sentinel read/write round-trips"
      - "`tests/version_changelog.rs` integration tests cover: first-install silent, single-step mismatch, multi-step mismatch, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard, no-perch guard, stop_hook_active guard, version-remind round-trip, version-remind missing-arg, version-remind non-semver-arg"
    artifacts:
      - path: "src/owl/version_changelog.rs"
        provides: "Sentinel + parser + payload builder + `run_version_remind` handler"
        min_lines: 200
      - path: "src/owl/hook_idle.rs"
        provides: "Version-change emission call site at top of `run()` after perch resolution + OWL_ECHO_COMMUNE guard"
        contains: "version_changelog::"
      - path: "src/cli.rs"
        provides: "`VersionRemind { old: String }` clap variant + clap parse-test"
        contains: "VersionRemind"
      - path: "src/owl/mod.rs"
        provides: "Module declaration `pub mod version_changelog;` + dispatch arm `Some(Commands::VersionRemind { old }) => ...`"
        contains: "VersionRemind"
      - path: "src/common/owlery.rs"
        provides: "New helper `pub fn sentinel_path() -> PathBuf` to encapsulate the spt_home().join(\"last-seen-version.json\") expression (RESEARCH Pitfall 2 / Risk #2)"
        contains: "sentinel_path"
      - path: "tests/version_changelog.rs"
        provides: "Integration tests covering all 10 cases above; mirrors `tests/auto_pick_integration.rs` SPT_HOME sandbox + perch-fixture pattern"
        min_lines: 300
    key_links:
      - from: "src/owl/hook_idle.rs::run()"
        to: "src/owl/version_changelog.rs::maybe_emit_version_change_block"
        via: "function call after perch resolution + OWL_ECHO_COMMUNE guard"
        pattern: "version_changelog::maybe_emit"
      - from: "src/cli.rs Commands::VersionRemind"
        to: "src/owl/version_changelog.rs::run_version_remind"
        via: "dispatch arm in src/owl/mod.rs::handle_command"
        pattern: "Commands::VersionRemind"
      - from: "Stop hook block emission"
        to: "sentinel pre-write"
        via: "atomic_write_string is called BEFORE the `println!` that emits the block"
        pattern: "atomic_write_string"
  ---

  <objective>
  Phase 34 Rust internals: sentinel + parser + payload builder + Stop-hook wiring + `version-remind` subcommand.

  Purpose: Land all the deterministic, testable code surface for Phase 34. No user-facing wording yet — the block-body XML scaffold is emitted but the natural-language instructions inside `<instructions>` get authored in Plan 03.

  Output: One new module, three modified files, one new integration-test file, ~30+ tests (parser corpus + sentinel + hook + subcommand + integration). Cargo tests green.
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @.planning/phases/34-version-change-changelog/34-01-SUMMARY.md
  @CHANGELOG.md
  @src/owl/hook_idle.rs
  @src/cli.rs
  @src/owl/mod.rs
  @src/common/owlery.rs

  <interfaces>
  <!-- Verified line citations from Read tool — confirm by re-reading before editing. -->

  src/owl/hook_idle.rs::run() (verified lines 14-43, file may evolve):
    Line 14: `pub fn run() {`
    Lines 17-22: stdin parse + session_id resolve
    Lines 25-32: perch resolution -> early return on None
    Line 34-39: comment + `inbox::set_idle_ready(&owl_id);`
    Line 42: `spawn_echo_commune_if_live(&owl_id);`

  src/owl/hook_idle.rs::spawn_echo_commune_if_live() (verified lines 57-98):
    Lines 64-69: OWL_ECHO_COMMUNE recursion guard pattern (COPY THIS SHAPE EXACTLY)

  src/common/owlery.rs::atomic_write_string (verified line 248):
    Signature: `pub fn atomic_write_string(path: &std::path::Path, body: &str) -> std::io::Result<()>`
    Tmp + rename; NTFS+POSIX same-volume atomic.

  src/common/owlery.rs::spt_home (verified lines 18-32):
    Signature: `pub fn spt_home() -> PathBuf`
    Resolves `$SPT_HOME` env override or platform default; create-if-missing.

  src/cli.rs Commands::NewAlarm (verified lines 139-153) — POSITIONAL-ARG ANALOG:
  ```rust
  #[command(name = "new-alarm")]
  NewAlarm {
      self_id: String,
      time_spec: String,
      ...
  },
  ```

  src/cli.rs Commands::HookIdle (verified line 197):
    `HookIdle,`  // variant only, no fields; dispatch handled in src/owl/mod.rs

  src/owl/mod.rs::handle_command (verified line 37) — DISPATCH ARMS:
    Line 116: `Some(Commands::NewAlarm { self_id, time_spec, message_file, message }) => { ... }`
    Line 143: `Some(Commands::HookIdle) => { ... }`

  src/main.rs (verified): `let cli = Cli::parse(); handle_command(cli.command);` (line 40-41)
    No edits needed to main.rs.

  NO `regex` crate in Cargo.toml — parser MUST be hand-rolled.

  Sentinel JSON shape (LOCKED — CONTEXT specifics):
  ```json
  {"version":"X.Y.Z"}
  ```
  Single field. No timestamp. No plugin name.

  Sentinel path (LOCKED — VERS-01 + CONTEXT):
    `spt_home().join("last-seen-version.json")` — SIBLING to `owlery/`, NOT inside it.

  Compile-time version source (LOCKED — VERS-02, RESEARCH Q9):
  ```rust
  fn current_version() -> String {
      // Test seam: SPT_OVERRIDE_PKG_VERSION lets tests simulate any version
      // without rebuilding. Production paths see env!("CARGO_PKG_VERSION").
      if let Ok(v) = std::env::var("SPT_OVERRIDE_PKG_VERSION") {
          if !v.is_empty() {
              return v;
          }
      }
      env!("CARGO_PKG_VERSION").to_string()
  }
  ```
  Return `String` (not `&'static str`) to avoid the `Box::leak` discussed in RESEARCH Q9. The allocation cost is paid once per hook invocation; immaterial.

  Parser regex (LOCKED — RESEARCH Q10 corpus):
    Regex equivalent: `^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$`
    Hand-rolled MUST:
      - Require literal prefix `## [` at column 0
      - Capture three `\d+` semver components separated by `.`
      - Require literal `] - ` between version and date
      - Capture `\d{4}-\d{2}-\d{2}` ISO-8601 date
      - Allow trailing whitespace, reject any other trailing content
      - Reject `[Unreleased]`, `[1.10]`, `[1.10.10-rc1]`, `# [1.10.10] - ...`, `### [1.10.10] - ...`, `[1.10.10]: <url>`, `## [1.10.10] - 2026/05/16`
    See RESEARCH "CHANGELOG.md Parser — Test Corpus" for the full 18-row case set.

  ChangelogEntry struct (RESEARCH parser API):
  ```rust
  pub struct ChangelogEntry {
      pub version: (u32, u32, u32),
      pub date: String,        // "YYYY-MM-DD" verbatim
      pub body: String,        // content between this H2 and next H2 / EOF, verbatim
      pub line_no: usize,      // 1-indexed line where the H2 lives
  }
  ```

  Public API surface (RESEARCH):
  ```rust
  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry>;
  pub fn sections_between(entries: &[ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&ChangelogEntry>;
  ```
  `sections_between` returns entries E where `old < E.version <= new`, ordered ascending by version. Used by Plan 03 wording and by the payload's `step_count`.

  Block payload shape (LOCKED — CONTEXT D-07 + RESEARCH Q11):
  ```xml
  <spt-version-changelog>
    <old>X.Y.Z</old>
    <new>X.Y.Z</new>
    <old_date>YYYY-MM-DD</old_date>
    <new_date>YYYY-MM-DD</new_date>
    <step_count>N</step_count>
    <changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>
    <invocation_for_remind>$OWL version-remind X.Y.Z</invocation_for_remind>
    <instructions>
      [PLAN 03 OWNS THIS WORDING; Plan 02 emits a placeholder string here]
    </instructions>
  </spt-version-changelog>
  ```
  `<old_date>` / `<new_date>` may be absent if CHANGELOG.md is unresolvable (Pitfall 2 graceful-degrade); emit empty elements `<old_date></old_date>` rather than skipping the tag.

  Plan 02 emits literal placeholder for `<instructions>` block (Plan 03 replaces). Use this exact placeholder so Plan 03 can grep+replace deterministically:
  ```
  [PLAN_03_INSTRUCTIONS_PLACEHOLDER]
  ```

  Recursion guard ordering (LOCKED — CONTEXT D-03 / RESEARCH Pitfall 4):
    OWL_ECHO_COMMUNE guard MUST run BEFORE the version-change check. Stop_hook_active guard MUST also run before the check. Both short-circuit the check; perch resolution is the only predicate that allows the check to proceed.

  Block emission MUST use `serde_json::json!` + `println!` (RESEARCH code examples):
  ```rust
  let response = serde_json::json!({
      "decision": "block",
      "reason": reason_xml,
  });
  println!("{}", serde_json::to_string(&response).unwrap());
  ```
  This writes to stdout; Claude Code reads the JSON envelope and propagates `reason` to Claude on re-engagement (RESEARCH Q1 GREEN).

  CRITICAL — `hook_idle.rs::run()` currently DOES NOT print anything to stdout (it's a side-effect hook). Adding the version-change emission changes that. Document this in the new function's doc-comment.

  Sentinel write ordering (LOCKED — CONTEXT D-05):
    1. Write NEW version to sentinel atomically.
    2. Emit block.
    Order matters: if step 2 fails to print (impossible barring OOM), the sentinel still reflects truth. If step 1 fails, RESEARCH Pitfall 1 says: skip emission AND do not panic. The user sees no prompt this turn; next Stop will re-check.

  `$CLAUDE_PLUGIN_ROOT` resolution (RESEARCH Q4 + Pitfall 2):
  ```rust
  fn resolve_changelog_path() -> Option<std::path::PathBuf> {
      let root = std::env::var("CLAUDE_PLUGIN_ROOT").ok()?;
      let path = std::path::PathBuf::from(root).join("CHANGELOG.md");
      if path.is_file() { Some(path) } else { None }
  }
  ```
  Hook may emit even when CHANGELOG.md is unresolvable — the block then carries empty `<old_date>` / `<new_date>` / `<changelog_path>NOT_RESOLVED</changelog_path>` and `<step_count>` derived from raw version diff (e.g., minor-version delta as a rough proxy). Researcher's recommendation per Pitfall 2.

  Stop_hook_active parsing:
    Extend `hook_output::parse_hook_stdin` (in src/common/hook_output.rs) to capture `stop_hook_active: bool` (default false). If the existing parser doesn't already surface it, add the field. Reuse if present.

  `version-remind` security validation (LOCKED — RESEARCH security V5):
    Validate `<old>` against semver regex `^\d+\.\d+\.\d+$` BEFORE writing the sentinel. Hand-rolled validator (no `regex` crate):
  ```rust
  fn is_plain_semver(s: &str) -> bool {
      let parts: Vec<&str> = s.split('.').collect();
      parts.len() == 3 && parts.iter().all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
  }
  ```
  On non-match: stderr `error: <old version> must match X.Y.Z semver`, exit 1.
  On missing arg: clap handles natively (positional arg required).

  Test-helper for SPT_HOME sandbox + perch fixture (PATTERN MAP — `tests/auto_pick_integration.rs:1-120`):
    Replicate verbatim. Key fields:
      - `static ENV_LOCK: Mutex<()> = Mutex::new(())`
      - `SptHomeGuard` RAII for SPT_HOME mutation
      - `write_live_perch(spt_home, id)` helper writing `{spt_home}/owlery/{id}/ready` + `info.json` with `parent_pid = std::process::id() as u64`
      - `assert_cmd::Command::cargo_bin("owl")` to invoke the subprocess
      - `.env("SPT_HOME", tmp).env("HOME", tmp).env("USERPROFILE", tmp).env_remove("OWL_ECHO_COMMUNE")` env hygiene

  `tests/version_changelog.rs` integration cases (LOCKED — RESEARCH Integration Test Corpus + CONTEXT):
    1. `first_install_silent_writes_sentinel` — empty SPT_HOME, SPT_OVERRIDE_PKG_VERSION=1.10.10; assert sentinel written, no `decision:"block"` in stdout
    2. `single_step_mismatch_emits_block` — sentinel `{"version":"1.10.9"}`, OVERRIDE=1.10.10; CHANGELOG fixture has both versions; assert block emitted with `<old>1.10.9</old>`, `<new>1.10.10</new>`, `<step_count>1</step_count>`; sentinel now 1.10.10
    3. `multi_step_mismatch_emits_block_with_step_count` — sentinel 1.10.5, OVERRIDE=1.10.10, CHANGELOG has 1.10.5..1.10.10; step_count=5
    4. `sentinel_equal_no_emit` — sentinel 1.10.10, OVERRIDE=1.10.10; no block; sentinel unchanged
    5. `owl_echo_commune_guard_skips_emission` — mismatch + OWL_ECHO_COMMUNE=1; no block; sentinel unchanged
    6. `no_perch_skips_emission` — mismatch but no perch fixture written; no block; sentinel unchanged
    7. `stop_hook_active_skips_emission` — mismatch + stdin `{"stop_hook_active":true}`; no block; sentinel unchanged
    8. `version_remind_rollback_round_trip` — set sentinel to 1.10.10, invoke `owl version-remind 1.10.9`, assert sentinel reads `{"version":"1.10.9"}`
    9. `version_remind_missing_arg_exits_1` — `owl version-remind` no arg; exit code != 0 (clap-emitted)
    10. `version_remind_non_semver_exits_1` — `owl version-remind not-a-version`; exit code 1; stderr matches "semver"

    Test fixtures: write a synthetic `CHANGELOG.md` into the SPT_HOME tempdir under a path the test sets `CLAUDE_PLUGIN_ROOT` to. Suggested fixture: a string constant inside the test module with 6-10 Keep-a-Changelog entries spanning 1.10.5..1.10.10.

  Co-located unit tests in `src/owl/version_changelog.rs::tests` (LOCKED — RESEARCH parser corpus):
    Cover the full 18-row test corpus from RESEARCH Q10. One test per row OR table-driven test. Each row asserts parse-or-reject + extracted version+date if parsed.
  </interfaces>
  </context>

  <tasks>

  <task type="auto" tdd="true">
    <name>Task 1: Create src/owl/version_changelog.rs (sentinel + parser + payload + version-remind handler) with unit tests</name>
    <files>src/owl/version_changelog.rs, src/owl/mod.rs, src/common/owlery.rs, src/cli.rs</files>
    <behavior>
  Unit tests (co-located `#[cfg(test)] mod tests`):
  - Parser corpus (RESEARCH Q10, 18 rows):
    - Accept `## [1.10.10] - 2026-05-16` -> version=(1,10,10), date="2026-05-16"
    - Accept `## [0.9.3] - 2025-12-01` (pre-1.0)
    - Reject `## [Unreleased]`
    - Reject `## [1.10.10]` (no date)
    - Reject `## [1.10] - 2026-05-16` (no patch)
    - Reject `## [1] - 2026-05-16` (no minor)
    - Reject `[1.10.10]: https://...` (link reference)
    - Reject `# [1.10.10] - 2026-05-16` (H1)
    - Reject `### [1.10.10] - 2026-05-16` (H3)
    - Accept `## [1.10.10] - 2026-05-16  ` (trailing whitespace tolerance)
    - Accept BOM-prefixed line (strip BOM at file-read time)
    - Reject `## [1.10.10] - 2026-05-16 (yanked)` (strict trailing-text rejection)
    - Reject `## [1.10.10-rc1] - 2026-05-16` (pre-release suffix)
    - Reject `## ` (empty H2)
    - Reject `## [ 1.10.10 ] - 2026-05-16` (whitespace in brackets)
    - Reject `## [1.10.10] - 2026/05/16` (slash date)
  - `sections_between` corpus:
    - Range query 1.10.5 (exclusive) -> 1.10.7 (inclusive) over entries [1.10.5, 1.10.6, 1.10.7, 1.10.8] returns [1.10.6, 1.10.7]
    - Body content preserved verbatim (no parser mutation)
  - Sentinel I/O:
    - `read_sentinel()` returns None when file missing
    - `read_sentinel()` returns None when JSON malformed
    - `read_sentinel()` returns Some("X.Y.Z") on happy path
    - `write_sentinel("1.10.10")` round-trips through `read_sentinel()` correctly
    - `write_sentinel` uses `atomic_write_string` (no .tmp residue after success)
  - `current_version()`:
    - With SPT_OVERRIDE_PKG_VERSION unset -> returns `env!("CARGO_PKG_VERSION")` value
    - With SPT_OVERRIDE_PKG_VERSION="1.99.99" -> returns "1.99.99"
    - With SPT_OVERRIDE_PKG_VERSION="" -> falls through to env! (empty treated as unset)
  - `is_plain_semver` validator:
    - Accept "1.10.10", "0.0.0", "999.999.999"
    - Reject "", "1.10", "1.10.10.10", "1.10.x", "1.10.10-rc1", "abc.def.ghi", " 1.10.10"
  - `run_version_remind`:
    - Valid semver arg writes sentinel + returns success
    - Non-semver arg returns ExitCode::from(1) without writing sentinel
  - `build_block_reason` builder:
    - Given old=(1,10,9), new=(1,10,10), step_count=1, optional old_date/new_date -> returns a string containing all required XML elements + literal `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]`
    - Element ordering: `<old>` before `<new>` before `<old_date>` before `<new_date>` before `<step_count>` before `<changelog_path>` before `<invocation_for_remind>` before `<instructions>`
    - When CHANGELOG path unresolvable: `<changelog_path>NOT_RESOLVED</changelog_path>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>` is verbatim (no escaping; literal `$OWL` substring)
    </behavior>
    <action>
  PRE-EDIT ANCHOR GUARD (C1 — line-citations in `<interfaces>` drift between phases). Run these greps FIRST; if either fails, ABORT the task and re-read the cited files to resync anchors before proceeding:
  ```
  grep -q "Some(Commands::NewAlarm" src/owl/mod.rs
  grep -q "NewAlarm" src/cli.rs
  ```
  The first confirms the dispatch arm pattern for `Some(Commands::NewAlarm { ... })` is still present in `src/owl/mod.rs` (anchor for inserting the new `Some(Commands::VersionRemind { old })` arm). The second confirms the clap `NewAlarm` variant still exists in `src/cli.rs` (anchor for inserting `VersionRemind` after it). If either grep fails (exit != 0), STOP — the task's line citations are stale and the insertions risk landing in the wrong place. Resync by re-reading the files, then update the per-step line numbers below, then re-run the greps as a final sanity check.

  After the guard passes, proceed:

  Create the new module file `src/owl/version_changelog.rs`. Hand-roll all parsing (NO `regex` crate per Cargo.toml verification).

  Structure the file as:
  ```rust
  // Phase 34 (D-01..D-12): version-change detection for Stop hook.
  // Public surface:
  //   - read_sentinel / write_sentinel — atomic sentinel I/O at $SPT_HOME/last-seen-version.json
  //   - current_version — env!("CARGO_PKG_VERSION") with SPT_OVERRIDE_PKG_VERSION test seam (RESEARCH Q9)
  //   - parse_changelog / sections_between — Keep-a-Changelog 1.1.0 H2 parser
  //   - build_block_reason — emit the <spt-version-changelog> payload XML for hook_idle.rs
  //   - maybe_emit_version_change_block — Stop-hook entry point (called from hook_idle.rs::run)
  //   - run_version_remind — clap-dispatched handler for `owl version-remind <old>`
  //
  // NO regex crate dependency — all parsing is hand-rolled. Sentinel JSON shape
  // is `{"version":"X.Y.Z"}` and the writer formats it directly (no serde_json
  // round-trip needed for the write path; the reader uses serde_json::Value
  // to gracefully degrade on malformed input per RESEARCH Pitfall 1).
  ```

  Implement these functions (signatures verbatim):
  ```rust
  pub fn current_version() -> String { ... }                                     // see <interfaces>
  pub fn sentinel_path() -> std::path::PathBuf { ... }                           // delegates to owlery::sentinel_path
  pub fn read_sentinel() -> Option<String> { ... }                               // graceful None on missing/malformed
  pub fn write_sentinel(version: &str) -> std::io::Result<()> { ... }            // atomic_write_string

  pub struct ChangelogEntry { ... }                                              // see <interfaces>
  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry> { ... }           // hand-rolled
  pub fn parse_h2_line(line: &str) -> Option<((u32,u32,u32), String)> { ... }    // single-line helper
  pub fn sections_between<'a>(entries: &'a [ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&'a ChangelogEntry> { ... }

  pub fn is_plain_semver(s: &str) -> bool { ... }                                // V5 input validation
  pub fn parse_semver_tuple(s: &str) -> Option<(u32,u32,u32)> { ... }            // for hook side

  pub fn build_block_reason(
      old: &str,
      new: &str,
      old_date: Option<&str>,
      new_date: Option<&str>,
      step_count: usize,
      changelog_path_resolved: bool,
  ) -> String { ... }

  pub fn resolve_changelog_path() -> Option<std::path::PathBuf> { ... }          // reads $CLAUDE_PLUGIN_ROOT
  pub fn read_changelog() -> Option<String> { ... }                              // reads file at resolve_changelog_path
  pub fn maybe_emit_version_change_block() { ... }                               // hook entry point

  pub fn run_version_remind(old: &str) -> std::process::ExitCode { ... }         // subcommand handler
  ```

  `maybe_emit_version_change_block` body algorithm (CONTEXT D-04..D-06 + RESEARCH Pitfall 1/2/4):
  1. If `current_version()` == `read_sentinel()` -> return (no mismatch).
  2. If `read_sentinel()` is None (first install) -> write sentinel = current; return (silent per VERS-03).
  3. Mismatch path:
     a. Capture `old = sentinel`, `new = current`.
     b. Try `read_changelog()` -> parse_changelog -> sections_between(old, new) -> compute `step_count` + `old_date` + `new_date`.
     c. If CHANGELOG unresolvable: step_count = 1 (rough proxy), dates = None, changelog_path_resolved = false.
     d. Write sentinel = new (atomic). On Err: log stderr, return (no emit — Pitfall 1).
     e. Build reason XML via `build_block_reason(...)`.
     f. Emit `serde_json::json!({"decision":"block","reason":reason})` to stdout via println!.

  Add `pub mod version_changelog;` to `src/owl/mod.rs`. The module declarations live alphabetically near other `pub mod` lines (find the existing `pub mod hook_idle;` or `pub mod poll;` — insert alongside).

  In `src/owl/mod.rs::handle_command` (line 37 entry, NewAlarm dispatch arm at line 116, HookIdle arm at line 143): add a new arm after `Some(Commands::NewAlarm { ... }) => { ... }` (around line 130) and BEFORE the HookIdle arm:
  ```rust
  Some(Commands::VersionRemind { old }) => {
      std::process::exit(u8::from(version_changelog::run_version_remind(&old)) as i32);
  }
  ```
  (Match the existing dispatch idiom — some arms call into functions that themselves call `std::process::exit`; mirror whichever convention the surrounding NewAlarm arm uses. The `version-remind` handler returns `ExitCode` per the public signature; convert to process exit code.)

  In `src/cli.rs`: add a new variant to the `Commands` enum AFTER `NewAlarm` (line 153) and BEFORE `HookSubagentStart` (line 156):
  ```rust
  /// Roll the last-seen version sentinel back to <old> so the next Stop
  /// hook re-fires the version-change prompt. Invoked by Claude when the
  /// user picks `Remind me later` from the AskUserQuestion. Phase 34 D-08.
  #[command(name = "version-remind")]
  VersionRemind {
      /// The previous version to roll the sentinel back to (e.g. `1.10.9`).
      /// MUST match the `old` value from the <spt-version-changelog> block
      /// Claude received — the subcommand has no way to infer it otherwise.
      old: String,
  },
  ```

  Also add to the `#[cfg(test)] mod tests` block at the bottom of `src/cli.rs` (line 351+), a clap parse test:
  ```rust
  #[test]
  fn parses_version_remind_positional() {
      let cli = Cli::try_parse_from(["owl", "version-remind", "1.10.9"]).unwrap();
      match cli.command {
          Some(Commands::VersionRemind { old }) => assert_eq!(old, "1.10.9"),
          other => panic!("expected VersionRemind, got {:?}", other),
      }
  }

  #[test]
  fn version_remind_requires_positional() {
      let result = Cli::try_parse_from(["owl", "version-remind"]);
      assert!(result.is_err());
  }
  ```

  In `src/common/owlery.rs`: add the encapsulating helper (RESEARCH Risk #2):
  ```rust
  /// Phase 34: sentinel path for the version-change detector. Sibling to `owlery/`,
  /// NOT inside it (per VERS-01). Centralized here so the convention is encapsulated
  /// rather than scattered across hook + subcommand call sites.
  pub fn sentinel_path() -> std::path::PathBuf {
      spt_home().join("last-seen-version.json")
  }
  ```
  Insert alongside the existing `pub fn spt_home()` / `pub fn owlery_dir()` definitions (around lines 18-32 / 65). Add doc comment per the existing style in the file.

  Author the co-located `#[cfg(test)] mod tests` block in `src/owl/version_changelog.rs` covering ALL behaviors listed in `<behavior>`. Reuse the `ENV_LOCK` + `EnvSnapshot` pattern from `src/owl/hook_idle.rs:100-264` (lines 108-135 are the canonical shape). For SPT_HOME mutations, use tempfile::tempdir() per test.

  Final step: run `cargo test --lib version_changelog` and confirm all tests green. Then `cargo test --lib` (full library suite) to confirm no regressions elsewhere. Then `cargo test --test version_changelog` will fail (file not yet created — that's Task 3).

  Commit:
  ```
  feat(34): add version_changelog module + version-remind subcommand

  - New src/owl/version_changelog.rs: sentinel I/O, hand-rolled Keep-a-Changelog
    parser, payload XML builder, hook entry point, version-remind handler.
  - src/cli.rs: new VersionRemind { old } variant + clap parse tests.
  - src/owl/mod.rs: dispatch arm + module declaration.
  - src/common/owlery.rs: pub fn sentinel_path() encapsulation helper.

  NO regex crate — all parsing hand-rolled. Block-body <instructions>
  element emits placeholder [PLAN_03_INSTRUCTIONS_PLACEHOLDER]; Plan 03
  will replace with the AUQ + render wording.
  ```
    </action>
    <verify>
      <automated>cargo test --lib version_changelog 2>&amp;1 | tee /tmp/t.log &amp;&amp; grep -qE "test result: ok\. [0-9]+ passed" /tmp/t.log &amp;&amp; cargo test --lib 2>&amp;1 | grep -qE "test result: ok"</automated>
    </verify>
    <done>Module file exists; all co-located unit tests pass; clap parse tests pass; full lib test suite green; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 2: Wire version-change check into hook_idle.rs::run() (after perch resolution, before set_idle_ready)</name>
    <files>src/owl/hook_idle.rs, src/common/hook_output.rs</files>
    <behavior>
  - `hook_idle.rs::run()` continues to early-return on missing perch (existing behavior, lines 25-32 unchanged).
  - After perch resolution succeeds, `OWL_ECHO_COMMUNE` is checked at the top of the version-change code path; if set, skip the version-change check entirely.
  - If `stop_hook_active` is true in stdin JSON, skip the version-change check entirely (RESEARCH Pitfall 4 defensive guard).
  - Otherwise call `version_changelog::maybe_emit_version_change_block()` which handles its own internal mismatch logic + sentinel write + block emission.
  - After the version-change call, `inbox::set_idle_ready(&owl_id)` continues to fire (unchanged behavior — sentinel + version-change is additive, not a short-circuit).
  - After `set_idle_ready`, `spawn_echo_commune_if_live(&owl_id)` continues to fire (unchanged).

  Behavior tests live in `tests/version_changelog.rs` (Task 3). Co-located unit tests in hook_idle.rs are extended only if existing tests depend on `run()` not printing to stdout — verify and adjust as needed.
    </behavior>
    <action>
  Step A: confirm `parse_hook_stdin` surface in `src/common/hook_output.rs` includes a `stop_hook_active: bool` field. Read the file; if absent, add it:
  ```rust
  #[derive(Deserialize, Default)]
  pub struct ParsedHookStdin {
      ...existing fields...,
      #[serde(default)]
      pub stop_hook_active: bool,
  }
  ```
  If the struct shape already covers it via a different name, use that name. Keep changes minimal; this is the smallest possible additive change.

  Step B: edit `src/owl/hook_idle.rs::run()`:

  Current shape (lines 14-43):
  ```rust
  pub fn run() {
      let stdin = hook_output::parse_hook_stdin();
      let session_id = std::env::var("OWL_SESSION_ID")
          .ok()
          .filter(|s| !s.is_empty())
          .or(stdin.session_id);
      let owl_id = match &session_id {
          Some(sid) => hook_output::find_perch_cached(sid),
          None => hook_output::find_perch_by_parent_pid(),
      };
      let owl_id = match owl_id {
          Some(id) => id,
          None => return,
      };
      inbox::set_idle_ready(&owl_id);
      spawn_echo_commune_if_live(&owl_id);
  }
  ```

  Modify to:
  ```rust
  pub fn run() {
      // 1. Parse stdin JSON — Stop has common fields only.
      let stdin = hook_output::parse_hook_stdin();

      let session_id = std::env::var("OWL_SESSION_ID")
          .ok()
          .filter(|s| !s.is_empty())
          .or(stdin.session_id.clone());

      // 2. Find perch — perch resolution is the active-spt-session predicate
      //    for both legacy set_idle_ready / spawn_echo_commune AND the Phase 34
      //    version-change emission (CONTEXT D-02).
      let owl_id = match &session_id {
          Some(sid) => hook_output::find_perch_cached(sid),
          None => hook_output::find_perch_by_parent_pid(),
      };
      let owl_id = match owl_id {
          Some(id) => id,
          None => return,
      };

      // Phase 34 (D-01..D-06): version-change detection + block emission.
      // MUST run BEFORE set_idle_ready / spawn_echo_commune_if_live so the
      // block emission (println! to stdout) is the only thing on stdout this
      // turn — set_idle_ready and spawn_echo_commune are side-effect calls
      // and emit nothing to stdout.
      //
      // OWL_ECHO_COMMUNE recursion guard: haiku echo-commune subprocesses
      // MUST NOT see a version-change block. Mirrors lines 64-69 below.
      // stop_hook_active defensive guard: prevent infinite-loop pathologies
      // (RESEARCH Pitfall 4).
      let skip_version_change =
          std::env::var("OWL_ECHO_COMMUNE")
              .map(|v| !v.is_empty())
              .unwrap_or(false)
          || stdin.stop_hook_active;
      if !skip_version_change {
          crate::owl::version_changelog::maybe_emit_version_change_block();
      }

      // 3. Set .idle-ready so poll can deliver (per 18.1 D-02, D-04).
      inbox::set_idle_ready(&owl_id);

      // 4. Phase 18.3 D-01..D-03: write .more-done sentinel if live+psyche-ready.
      spawn_echo_commune_if_live(&owl_id);
  }
  ```

  Step C: update the file-doc comment at lines 1-5:
  ```rust
  // hook-idle subcommand -- Stop hook handler
  // Sets .idle-ready so poll can deliver messages.
  // Phase 34 (D-04): also emits a `<spt-version-changelog>` block via
  // `decision:"block"` + `reason` on version mismatch (compile-time
  // CARGO_PKG_VERSION vs $SPT_HOME/last-seen-version.json sentinel).
  // Stop hook output schema supports `decision`/`reason` only — no
  // hookSpecificOutput envelope. The block IS the only stdout this turn.
  // On no-mismatch / first-install / echo-commune guard hit: silent (no stdout).
  //
  // Phase 18.3 (D-01..D-04): no longer spawns `_echo-commune` directly.
  // (rest of existing comment unchanged)
  ```

  Step D: existing unit tests in `src/owl/hook_idle.rs::tests` (lines 100-264) MAY rely on `run()` producing no stdout. Read those tests; if any assert no stdout, they will need adjustment because the version-change path now CAN write to stdout when there's a mismatch. Strategy:
    - Add an `OWL_ECHO_COMMUNE=1` env var to those tests (or `stop_hook_active=true` stdin) to bypass the version-change path. This keeps existing tests focused on the `.more-done` sentinel behavior.
    - Alternatively, set `SPT_OVERRIDE_PKG_VERSION` to match the sentinel value (or set sentinel to current version via `write_sentinel(env!("CARGO_PKG_VERSION"))`).
    - Preserve existing test semantics — Phase 18.3's `.more-done` behavior MUST keep working.

  Run `cargo test --lib hook_idle` and confirm green. Run `cargo test --lib` and confirm full suite green.

  Commit:
  ```
  feat(34): wire version-change emission into hook_idle.rs::run()

  Adds version_changelog::maybe_emit_version_change_block call after
  perch resolution, gated by OWL_ECHO_COMMUNE (D-03) and stop_hook_active
  (RESEARCH Pitfall 4). Block emission precedes set_idle_ready so stdout
  carries only the JSON envelope when a mismatch fires.

  Updates hook_idle file-doc comment to reflect the new stdout channel
  (Stop hooks now CAN emit on this hook path; previously side-effect-only).
  ```
    </action>
    <verify>
      <automated>cargo test --lib hook_idle 2>&amp;1 | grep -qE "test result: ok" &amp;&amp; cargo test --lib 2>&amp;1 | grep -qE "test result: ok" &amp;&amp; grep -q "version_changelog::maybe_emit_version_change_block" src/owl/hook_idle.rs &amp;&amp; grep -q "stop_hook_active" src/owl/hook_idle.rs</automated>
    </verify>
    <done>hook_idle.rs::run() invokes maybe_emit_version_change_block before set_idle_ready; OWL_ECHO_COMMUNE + stop_hook_active guards in place; all library tests green; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 3: Create tests/version_changelog.rs integration suite (10 cases) using SPT_HOME sandbox + perch fixture pattern</name>
    <files>tests/version_changelog.rs</files>
    <behavior>
  Integration tests (subprocess-driven via `assert_cmd::Command::cargo_bin("owl")`):

  1. `first_install_silent_writes_sentinel`
     - Setup: empty SPT_HOME, OVERRIDE=1.10.10, no sentinel, perch fixture written, synthetic CHANGELOG.md present.
     - Invoke: `owl hook-idle` with stdin `{}`.
     - Assert: exit 0; stdout does NOT contain `"decision":"block"`; sentinel file now contains `{"version":"1.10.10"}`.

  2. `single_step_mismatch_emits_block`
     - Setup: sentinel `{"version":"1.10.9"}`, OVERRIDE=1.10.10, perch fixture, CHANGELOG with both versions.
     - Invoke: `owl hook-idle` with stdin `{}`.
     - Assert: exit 0; stdout JSON has `decision=block`; reason contains `<old>1.10.9</old>`, `<new>1.10.10</new>`, `<step_count>1</step_count>`; sentinel post-call is `1.10.10`.

  3. `multi_step_mismatch_emits_block_with_step_count`
     - Setup: sentinel `1.10.5`, OVERRIDE=1.10.10, CHANGELOG with H2 sections for 1.10.5, 1.10.6, 1.10.7, 1.10.8, 1.10.9, 1.10.10.
     - Assert: step_count == 5; reason has `<step_count>5</step_count>`.

  4. `sentinel_equal_no_emit`
     - Setup: sentinel `1.10.10`, OVERRIDE=1.10.10.
     - Assert: no `decision:"block"` in stdout; sentinel unchanged.

  5. `owl_echo_commune_guard_skips_emission`
     - Setup: mismatch present; cmd.env("OWL_ECHO_COMMUNE", "1").
     - Assert: no block emitted; sentinel UNCHANGED (guard runs before mismatch check + sentinel write).

  6. `no_perch_skips_emission`
     - Setup: SPT_HOME tempdir BUT no perch fixture written.
     - Assert: no block; sentinel unchanged.

  7. `stop_hook_active_skips_emission`
     - Setup: mismatch present; stdin `{"stop_hook_active":true}`.
     - Assert: no block; sentinel unchanged.

  8. `version_remind_rollback_round_trip`
     - Setup: sentinel = 1.10.10.
     - Invoke: `owl version-remind 1.10.9`.
     - Assert: exit 0; sentinel reads `{"version":"1.10.9"}`.

  9. `version_remind_missing_arg_exits_nonzero`
     - Invoke: `owl version-remind` (no positional).
     - Assert: exit code != 0; stderr non-empty (clap default).

  10. `version_remind_non_semver_exits_1`
      - Invoke: `owl version-remind not-a-version`.
      - Assert: exit code == 1; stderr matches `semver` substring; sentinel unchanged (or doesn't exist).
    </behavior>
    <action>
  Create `tests/version_changelog.rs` mirroring `tests/auto_pick_integration.rs:1-120` SPT_HOME sandbox pattern verbatim.

  File outline:
  ```rust
  //! Phase 34 integration tests: version-change detection + sentinel + version-remind.
  //! Mirrors tests/auto_pick_integration.rs SPT_HOME sandbox pattern.

  use std::fs;
  use std::path::Path;
  use std::sync::Mutex;

  static ENV_LOCK: Mutex<()> = Mutex::new(());

  const CHANGELOG_FIXTURE: &str = r#"# Changelog

  ## [1.10.10] - 2026-05-16

  - Multi-step test entry for 1.10.10.

  ## [1.10.9] - 2026-05-15

  - Entry for 1.10.9.

  ## [1.10.8] - 2026-05-14

  - Entry for 1.10.8.

  ## [1.10.7] - 2026-05-13

  - Entry for 1.10.7.

  ## [1.10.6] - 2026-05-12

  - Entry for 1.10.6.

  ## [1.10.5] - 2026-05-11

  - Entry for 1.10.5.
  "#;

  struct SptHomeGuard {
      prior_spt: Option<String>,
      prior_plugin_root: Option<String>,
      prior_override: Option<String>,
  }

  impl SptHomeGuard {
      fn set(spt_home: &Path, plugin_root: &Path, override_version: Option<&str>) -> Self {
          let prior_spt = std::env::var("SPT_HOME").ok();
          let prior_plugin_root = std::env::var("CLAUDE_PLUGIN_ROOT").ok();
          let prior_override = std::env::var("SPT_OVERRIDE_PKG_VERSION").ok();
          std::env::set_var("SPT_HOME", spt_home);
          std::env::set_var("CLAUDE_PLUGIN_ROOT", plugin_root);
          match override_version {
              Some(v) => std::env::set_var("SPT_OVERRIDE_PKG_VERSION", v),
              None => std::env::remove_var("SPT_OVERRIDE_PKG_VERSION"),
          }
          Self { prior_spt, prior_plugin_root, prior_override }
      }
  }

  impl Drop for SptHomeGuard {
      fn drop(&mut self) {
          match &self.prior_spt {
              Some(v) => std::env::set_var("SPT_HOME", v),
              None => std::env::remove_var("SPT_HOME"),
          }
          match &self.prior_plugin_root {
              Some(v) => std::env::set_var("CLAUDE_PLUGIN_ROOT", v),
              None => std::env::remove_var("CLAUDE_PLUGIN_ROOT"),
          }
          match &self.prior_override {
              Some(v) => std::env::set_var("SPT_OVERRIDE_PKG_VERSION", v),
              None => std::env::remove_var("SPT_OVERRIDE_PKG_VERSION"),
          }
      }
  }

  fn write_live_perch(spt_home: &Path, id: &str) {
      let perch_dir = spt_home.join("owlery").join(id);
      fs::create_dir_all(&perch_dir).unwrap();
      fs::write(perch_dir.join("ready"), "").unwrap();
      let info = serde_json::json!({
          "owl_id": id,
          "pid": std::process::id() as u64,
          // N1: parent_pid = test-process PID so hook_output::find_perch_by_parent_pid()
          // resolves THIS test's perch when the owl subprocess (spawned by assert_cmd
          // under the test process) walks up the parent chain. Mirrors the
          // auto_pick_integration.rs fixture convention.
          "parent_pid": std::process::id() as u64,
          "mode": "listen",
          "session_id": "test-session",
          "state": "Ready",
          "project_history": [],
      });
      fs::write(perch_dir.join("info.json"), serde_json::to_string(&info).unwrap()).unwrap();
  }

  fn write_changelog_fixture(plugin_root: &Path) {
      fs::write(plugin_root.join("CHANGELOG.md"), CHANGELOG_FIXTURE).unwrap();
  }

  fn write_sentinel_fixture(spt_home: &Path, version: &str) {
      let body = format!("{{\"version\":\"{}\"}}", version);
      fs::write(spt_home.join("last-seen-version.json"), body).unwrap();
  }

  // ... 10 #[test] fns covering the behaviors above ...
  ```

  For each test:
    - `let _lock = ENV_LOCK.lock().unwrap();`
    - `let tmp = tempfile::tempdir().unwrap();`
    - Both SPT_HOME and CLAUDE_PLUGIN_ROOT point at `tmp.path()` for simplicity (CHANGELOG.md sits at `tmp.path()/CHANGELOG.md`; sentinel at `tmp.path()/last-seen-version.json`; perch at `tmp.path()/owlery/{id}/`).
    - `let _guard = SptHomeGuard::set(tmp.path(), tmp.path(), Some("1.10.10"));`
    - Write fixtures: `write_changelog_fixture`, `write_live_perch("test-owl")`, `write_sentinel_fixture(..)` as required by the test case.
    - Invoke the subprocess:
      ```rust
      let mut cmd = assert_cmd::Command::cargo_bin("owl").expect("owl binary must be built");
      let output = cmd
          .arg("hook-idle")
          .env("SPT_HOME", tmp.path())
          .env("CLAUDE_PLUGIN_ROOT", tmp.path())
          .env("SPT_OVERRIDE_PKG_VERSION", "1.10.10")
          .env("HOME", tmp.path())
          .env("USERPROFILE", tmp.path())
          .env_remove("OWL_ECHO_COMMUNE")    // per-test may re-add
          .write_stdin("{}")
          .output()
          .expect("owl subprocess");
      ```
    - For the `version-remind` tests: replace the args with `["version-remind", "<arg>"]` and skip stdin/perch fixtures.
    - Assert via `output.status.success()`, `String::from_utf8_lossy(&output.stdout)`, and reading the sentinel file post-call.

  For test 5 (`owl_echo_commune_guard_skips_emission`):
    ```rust
    .env("OWL_ECHO_COMMUNE", "1")
    ```

  For test 7 (`stop_hook_active_skips_emission`):
    ```rust
    .write_stdin(r#"{"stop_hook_active":true}"#)
    ```

  For tests 9 + 10 (subcommand error cases): use `output.status.code()` and `String::from_utf8_lossy(&output.stderr)`.

  CRITICAL: `assert_cmd` requires the binary to be built. Add a `cargo build` step in the test setup OR rely on `cargo test`'s implicit build. The `cargo_bin` helper points at the test binary's `target/debug/owl` — `cargo test` builds it automatically.

  After authoring, run `cargo test --test version_changelog` and confirm all 10 tests pass.

  Commit:
  ```
  test(34): integration tests for version-change hook + version-remind subcommand

  10 cases covering first-install silent, single + multi-step mismatch,
  sentinel-equal no-emit, OWL_ECHO_COMMUNE + stop_hook_active + no-perch
  guards, version-remind round-trip + missing-arg + non-semver-arg.

  Uses SPT_OVERRIDE_PKG_VERSION test seam (no rebuild required per case).
  Fixture CHANGELOG.md spans 1.10.5..1.10.10 to exercise step_count math.
  ```
    </action>
    <verify>
      <automated>cargo test --test version_changelog 2>&amp;1 | tee /tmp/it.log &amp;&amp; grep -qE "test result: ok\. 10 passed" /tmp/it.log</automated>
    </verify>
    <done>tests/version_changelog.rs exists with 10 #[test] fns; all pass; commit landed.</done>
  </task>

  </tasks>

  <verification>
  After all 3 tasks:

  1. Full test suite green:
     ```
     cargo test
     ```
     Expect `test result: ok` for both lib and integration test runs.

  2. New module wired correctly:
     ```
     grep -c "version_changelog" src/owl/mod.rs src/owl/hook_idle.rs src/cli.rs
     ```
     Expect at least 4 hits (mod declaration + dispatch arm + hook call + Plan 03 placeholder reference if Plan 03 hasn't started).

  3. Single sentinel write site (VERS-09 audit):
     ```
     grep -rn "last-seen-version" src/
     ```
     Expect exactly 2 hits: one in `src/common/owlery.rs::sentinel_path()` and one in tests OR one in `src/owl/version_changelog.rs` (the consumer of `sentinel_path`).
     No `last-seen-version` literal in `hook_idle.rs` (must go through the module API).

  4. No regex crate added (Cargo.toml audit):
     ```
     grep -c "^regex" Cargo.toml
     ```
     Expect 0.

  5. Version-change emission ordering preserved:
     ```
     grep -n "version_changelog\|set_idle_ready\|spawn_echo_commune" src/owl/hook_idle.rs
     ```
     Manual check: `maybe_emit_version_change_block` line number is LESS than `set_idle_ready` line number, which is LESS than `spawn_echo_commune_if_live` line number.

  6. Three commits landed:
     ```
     git log --oneline -3
     ```
     Expect:
     - `test(34): integration tests for version-change hook + version-remind subcommand`
     - `feat(34): wire version-change emission into hook_idle.rs::run()`
     - `feat(34): add version_changelog module + version-remind subcommand`
  </verification>

  <success_criteria>
  - Plan 02 maps to VERS-01 (sentinel path encapsulated in owlery::sentinel_path), VERS-02 (CARGO_PKG_VERSION via current_version() with test seam; NEVER reads plugin.json at runtime), VERS-03 (first-install silent path tested), VERS-05 (atomic_write_string used at sentinel write site), VERS-09 (single write site at hook entry; grep audit confirms 2 callers via sentinel_path).
  - Block-body XML scaffold complete with placeholder for Plan 03's wording.
  - All 30+ unit tests + 10 integration tests pass.
  - No new crate dependencies.
  - hook_idle.rs change preserves Phase 18.3 `.more-done` sentinel behavior (existing tests still pass).
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | Claude Code -> hook subprocess | stdin JSON (untrusted shape but trust-boundary-internal — Claude Code is local) |
  | Claude -> $OWL bash invocation | `version-remind <old>` argv crosses Claude's tool-call boundary to the owl subprocess |
  | disk -> hook | CHANGELOG.md content at $CLAUDE_PLUGIN_ROOT (author-controlled) is read and inlined into the block reason |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-05 | Tampering | `version-remind <old>` argv injection | mitigate | `is_plain_semver` validator rejects any non-`X.Y.Z` arg BEFORE sentinel write (RESEARCH security V5); test 10 covers this |
  | T-34-06 | Tampering | Sentinel path traversal | mitigate | Path constructed via `spt_home().join("last-seen-version.json")` — no user input in the path; encapsulated in `owlery::sentinel_path()` |
  | T-34-07 | DoS | Infinite Stop-hook block loop | mitigate | `stop_hook_active` guard short-circuits regardless of mismatch (RESEARCH Pitfall 4); test 7 covers this; belt-and-suspenders with sentinel pre-write per CONTEXT D-05 |
  | T-34-08 | DoS | OWL_ECHO_COMMUNE recursion (haiku subprocesses) | mitigate | Guard runs BEFORE the version-change call (lines 64-69 pattern mirrored); test 5 covers this |
  | T-34-09 | Information disclosure | CHANGELOG.md content injection | accept | Author-controlled file; content reaches Claude only (no XSS / code-exec surface); plaintext markdown |
  | T-34-10 | Tampering | Sentinel write failure leaves stale value | mitigate | Best-effort pattern per RESEARCH Pitfall 1: on Err, skip emission (no false notification); next Stop re-checks; stop_hook_active is the bounded loop guard |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-02-SUMMARY.md` documenting:
  - Three commit SHAs
  - Final test counts (unit + integration)
  - Confirmation that the `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` literal appears exactly once in `src/owl/version_changelog.rs`
  - Any deviations from the line-citation anchors in `<interfaces>` (hook_idle.rs / cli.rs / owlery.rs may have drifted; record actual line numbers)
  - Confirmation `cargo test` ends green with no warnings introduced by Plan 02 code
  </output>

  #### 34-03-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 03
  type: execute
  wave: 3
  depends_on: [34-01, 34-02]
  files_modified:
    - src/owl/version_changelog.rs
    - tests/version_changelog.rs
    - tests/golden/version_changelog_block.txt
  autonomous: false
  requirements:
    - VERS-04
    - VERS-06
  user_setup: []

  must_haves:
    truths:
      - "The `<instructions>` element inside the `<spt-version-changelog>` block carries the verbatim 4-option AUQ wording locked in CONTEXT D-08: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`"
      - "The `Yes, full changelog` instruction tells Claude to read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and print every H2 section where `{old} < version <= {new}`, verbatim markdown (RESEARCH Pitfall 6)"
      - "The `Yes, highlights only` instruction tells Claude to extract 2-5 most impactful feature adds and fixes across the range, preferring user-facing behavior changes, skipping internal refactors and test-only changes; appends pointer to $CLAUDE_PLUGIN_ROOT/CHANGELOG.md (CONTEXT D-08 / specifics)"
      - "The `Remind me later` instruction surfaces the literal invocation string `$OWL version-remind <old_version_substituted_verbatim>` so Claude passes the `old` value with no ambiguity (CONTEXT D-08 + follow-up clarification)"
      - "The question text uses the `step_count` field: single-step `SPT plugin updated v{old} → v{new}. Print changelog?`; multi-step `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`"
      - "Dates surface in Claude's pre-question prose, NOT in the AUQ question line (RESEARCH open question Q3)"
      - "A golden fixture at `tests/golden/version_changelog_block.txt` snapshots the canonical block reason string for a fixed 1.10.9->1.10.10 transition; an integration test diffs the live output against it"
      - "End-to-end UAT documentation captures the manual test protocol for confirming the AUQ surfaces correctly in a real Claude Code session"
    artifacts:
      - path: "src/owl/version_changelog.rs"
        provides: "Replaced `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` with the final wording inside `build_block_reason`"
        contains: "Yes, full changelog"
      - path: "tests/version_changelog.rs"
        provides: "New `block_reason_matches_golden_snapshot` test covering the 1.10.9->1.10.10 canonical case"
        contains: "version_changelog_block.txt"
      - path: "tests/golden/version_changelog_block.txt"
        provides: "Canonical snapshot of the `reason` string for the 1.10.9->1.10.10 transition fixture"
        min_lines: 25
    key_links:
      - from: "build_block_reason output"
        to: "tests/golden/version_changelog_block.txt"
        via: "snapshot equality assertion (with template substitution)"
        pattern: "version_changelog_block.txt"
      - from: "<instructions> element text"
        to: "Claude's AUQ rendering at re-engagement"
        via: "Stop hook reason -> Claude continuation -> AskUserQuestion tool call (RESEARCH Q1/Q2 GREEN)"
        pattern: "AskUserQuestion"
  ---

  <objective>
  Phase 34 user-facing wording + golden snapshot + UAT.

  Purpose: Replace Plan 02's placeholder with the production AUQ instruction text. This is the only file in the repo whose exact prose determines whether Claude reliably invokes AskUserQuestion with the four locked options on hook re-engagement. Wording quality matters more than code volume.

  Output: One module edit (replace placeholder), one new test (golden snapshot), one new fixture file. End-to-end UAT documented for `cargo build --release` + `DEPLOY.ps1` + manual Claude Code session verification.

  This plan has ONE checkpoint at the end (manual UAT against a real Claude Code session — `checkpoint:human-verify`).
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @.planning/phases/34-version-change-changelog/34-01-SUMMARY.md
  @.planning/phases/34-version-change-changelog/34-02-SUMMARY.md
  @src/owl/version_changelog.rs
  @tests/version_changelog.rs

  <interfaces>
  <!-- Locked invariants from CONTEXT D-08 + RESEARCH Q11 -->

  AUQ 4-option labels (LOCKED — CONTEXT D-08, verbatim):
    1. `Yes, full changelog`
    2. `Yes, highlights only`
    3. `Remind me later`
    4. `Skip`

  `Yes, full changelog` behavior (LOCKED — CONTEXT D-08 + RESEARCH Pitfall 6):
    Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and prints EVERY H2 section where `{old} < version <= {new}`, VERBATIM markdown. Single-step renders one section; multi-step renders all interim sections in order (newest first or oldest first — author chooses ONE convention and locks it in the instructions text).

  `Yes, highlights only` framing (LOCKED — CONTEXT specifics, suggested wording):
    "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes. Append `See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail.`"

  `Remind me later` invocation (LOCKED — CONTEXT D-08 + clarification):
    Claude invokes `$OWL version-remind <old>` via Bash, where `<old>` is the LITERAL value from the `<old>` element in the block. Block MUST surface the exact invocation string verbatim with the old value substituted — Claude cannot infer the rollback target from any other source.
    Example for 1.10.9->1.10.10 transition: `$OWL version-remind 1.10.9`

  `Skip` behavior (LOCKED):
    No action. Sentinel was already updated by the hook at run-entry; no re-fire. Claude acknowledges briefly and proceeds.

  Question text template (LOCKED — CONTEXT specifics):
    Single-step (`step_count == 1`): `SPT plugin updated v{old} → v{new}. Print changelog?`
    Multi-step (`step_count > 1`): `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`
    (Note: `versions` is always plural in the multi-step variant; pluralization is unconditional because step_count > 1 means at least 2.)

  Pre-question prose (Claude's discretion, but the instructions SHOULD recommend a shape per RESEARCH open question Q3):
    Before invoking AskUserQuestion, Claude states a one-line context line such as:
    `SPT plugin updated v1.10.9 (released 2026-05-15) → v1.10.10 (released 2026-05-16). Print changelog?`
    Dates are taken from `<old_date>` / `<new_date>` elements; omit the parenthetical when the element is empty (CHANGELOG.md unresolvable case).

  XML element ordering (LOCKED — Plan 02's `build_block_reason`):
    `<old>` -> `<new>` -> `<old_date>` -> `<new_date>` -> `<step_count>` -> `<changelog_path>` -> `<invocation_for_remind>` -> `<instructions>`

  NO ESCAPING of `$OWL` / `$CLAUDE_PLUGIN_ROOT` in the XML body — these are LITERAL substrings Claude reads and passes to Bash. The Bash tool expands them at invocation time. If we escape `$` to `\$`, Claude will pass the escaped form to Bash and the invocation will fail.

  `<instructions>` element wording — DRAFT (planner-authored, locked in this plan; refine inside the action below):
  ```
  Render an AskUserQuestion to confirm whether the user wants to see the changelog for this SPT plugin update.

  Before invoking AskUserQuestion, write one short pre-question line that names the version transition (and dates if both <old_date> and <new_date> are non-empty), e.g. "SPT plugin updated v{old} (released {old_date}) → v{new} (released {new_date}). Print changelog?". Omit the dates if either element is empty.

  Then invoke AskUserQuestion with EXACTLY these four options (labels verbatim, do not paraphrase):

    1. "Yes, full changelog" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version <= {new}, verbatim markdown (preserve the original H2 lines, bullet structure, and any H3 subsections). Render newest-first (the order entries appear in the file). Do not summarize, do not paraphrase.

    2. "Yes, highlights only" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and synthesize 2-5 highlight bullets across the H2 sections where {old} < version <= {new}. Prefer items that change user-facing behavior. Skip purely-internal refactors and test-only changes. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."

    3. "Remind me later" — On selection: invoke Bash with EXACTLY this command (substitute the {old} value verbatim from the <old> element above, do NOT paraphrase or compute):
       $OWL version-remind {old}
       This rolls the sentinel back so the next Stop hook re-fires this prompt. Do not run any other commands as part of this option.

    4. "Skip" — On selection: take no action. Acknowledge briefly ("OK, skipping.") and continue with the user's prior intent.

  If the user provides freeform input via AskUserQuestion's "Other" option:
    - "not now" / "later" / "skip for now" → treat as "Remind me later" (invoke the Bash command above).
    - "ignore" / "stop asking" / "no" → treat as "Skip".

  When the question text mentions {step_count} versions (i.e. step_count > 1), the multi-version skip rendering applies: "Yes, full changelog" prints ALL interim H2 sections, NOT just the endpoints.

  After completing the user's choice, return control to whatever the user was working on before this prompt.
  ```

  CRITICAL — `{old}` and `{new}` and `{old_date}` and `{new_date}` and `{step_count}` are TEMPLATE TOKENS in the instructions element that `build_block_reason` substitutes at emit time. The actual emitted text MUST have the real values inline (not `{old}` placeholders). E.g., for a 1.10.9->1.10.10 transition the `<instructions>` block reads `$OWL version-remind 1.10.9` LITERALLY — with `1.10.9` baked in.

  Golden snapshot strategy (RESEARCH open question Q1):
    Snapshot just the parts of the reason string that are stable: the XML element shape + the literal AUQ option labels + the literal invocation string. Body content of the highlights instructions is fragile (any wording tweak invalidates the snapshot), so the test asserts SUBSTRING matches for stable invariants rather than full-string equality:
      - `<old>1.10.9</old>`
      - `<new>1.10.10</new>`
      - `<step_count>1</step_count>`
      - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
      - Literal labels: `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"`
      - `Pick 2-5 most impactful` (highlights rubric anchor)
      - `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md` (instructions for both Yes options)
      - `$OWL version-remind 1.10.9` (instructions text for Remind, with substitution)

    The golden FILE stores the full expected reason string for reference / human review, but the TEST uses substring assertions for resilience.

  UAT protocol (CONTEXT canonical_refs + RESEARCH plan-boundary Q8 Plan 03):
    1. `cargo build --release` — clean build with all Phase 34 code.
    2. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1` — sync to marketplace + cache.
    3. `claude plugin install spt@cplugs` (DEPLOY.ps1 does this; verify it ran).
    4. Run `/reload-plugins` manually inside Claude Code.
    5. Synthesize a version mismatch: `node -e 'fs.writeFileSync(process.env.LOCALAPPDATA + "/spt/last-seen-version.json", JSON.stringify({version:"0.0.1"}))'` (Windows) or equivalent Unix path. Now the sentinel claims v0.0.1 and the installed plugin claims its real current version.
    6. Inside Claude Code: start an spt session (e.g., run `/spt:listen` or `/spt:live`).
    7. End the agent's turn naturally. Stop hook fires.
    8. EXPECTED: Claude responds with the pre-question line + AskUserQuestion showing the four locked options.
    9. Test each branch:
       - Pick `Yes, full changelog`: Claude prints CHANGELOG.md sections.
       - Pick `Yes, highlights only`: Claude renders 2-5 bullets + pointer line.
       - Pick `Remind me later`: Claude invokes `$OWL version-remind 0.0.1` via Bash; sentinel rolls back; next Stop re-fires.
       - Pick `Skip`: Claude says "OK, skipping" and proceeds; sentinel stays at current version; next Stop does NOT re-fire.
    10. Confirm sentinel state at each step via `cat $SPT_HOME/last-seen-version.json` (or `type %LOCALAPPDATA%\spt\last-seen-version.json` on Windows).
  </interfaces>
  </context>

  <tasks>

  <task type="auto" tdd="true">
    <name>Task 1: Replace [PLAN_03_INSTRUCTIONS_PLACEHOLDER] in build_block_reason with the locked 4-option AUQ wording (with template substitution)</name>
    <files>src/owl/version_changelog.rs</files>
    <behavior>
  - `build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true)` returns a string containing:
    - `<old>1.10.9</old>`
    - `<new>1.10.10</new>`
    - `<old_date>2026-05-15</old_date>`
    - `<new_date>2026-05-16</new_date>`
    - `<step_count>1</step_count>`
    - `<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
    - Inside `<instructions>`: literal substrings `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"`, `Pick 2-5 most impactful`, `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md`, `$OWL version-remind 1.10.9` (with the 1.10.9 substituted verbatim).
  - The literal token `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` is REMOVED from the file (grep -n returns no results).
  - For the multi-step case `build_block_reason("1.10.5", "1.10.10", ..., step_count=5, ...)`:
    - `<step_count>5</step_count>`
    - The pre-question hint inside instructions surfaces the multi-step form: `SPT plugin updated v1.10.5 → v1.10.10 (5 versions). Print changelog?`
    - The invocation_for_remind substitutes `1.10.5`: `$OWL version-remind 1.10.5`
  - For the unresolvable-CHANGELOG case `build_block_reason("1.10.9", "1.10.10", None, None, 1, false)`:
    - `<old_date></old_date>` (empty element)
    - `<new_date></new_date>` (empty element)
    - `<changelog_path>NOT_RESOLVED</changelog_path>`
    - Instructions still surface the four options; the Yes-full / Yes-highlights branches degrade gracefully (Claude sees NOT_RESOLVED and skips the CHANGELOG read, just states a generic "no changelog detail available" message). The instructions text MUST handle this branch explicitly so Claude doesn't try to read a nonexistent file.
    </behavior>
    <action>
  Step A: Read `src/owl/version_changelog.rs` to confirm the current shape of `build_block_reason` (Plan 02 left a placeholder `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` inside the `<instructions>` element).

  Step B: Replace the placeholder with the verbatim instructions wording in `<interfaces>` above, applying string templating for `{old}`, `{new}`, `{old_date}`, `{new_date}`, `{step_count}`. The `format!` macro is the natural fit:

  ```rust
  let question_text = if step_count == 1 {
      format!("SPT plugin updated v{old} → v{new}. Print changelog?")
  } else {
      format!("SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?")
  };

  let date_hint = match (old_date, new_date) {
      (Some(od), Some(nd)) if !od.is_empty() && !nd.is_empty() => {
          format!(" (released {od} → released {nd})")
      }
      _ => String::new(),
  };

  let changelog_path_element = if changelog_path_resolved {
      "$CLAUDE_PLUGIN_ROOT/CHANGELOG.md".to_string()
  } else {
      "NOT_RESOLVED".to_string()
  };

  let resolved_branch_extra = if changelog_path_resolved {
      String::new()  // Yes-full / Yes-highlights work normally
  } else {
      "  NOTE: <changelog_path> is NOT_RESOLVED — $CLAUDE_PLUGIN_ROOT/CHANGELOG.md was not readable at hook time. For 'Yes, full changelog' and 'Yes, highlights only', state briefly that the changelog file was not available and offer to retry after the user verifies the plugin install. Do not attempt to read the file.\n".to_string()
  };

  let instructions = format!(r#"Render an AskUserQuestion to confirm whether the user wants to see the changelog for this SPT plugin update.

  Before invoking AskUserQuestion, write one short pre-question line that names the version transition (and dates if both <old_date> and <new_date> are non-empty), e.g. "{question_text}"{date_hint}.

  Then invoke AskUserQuestion with EXACTLY these four options (labels verbatim, do not paraphrase):

    1. "Yes, full changelog" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version <= {new}, verbatim markdown (preserve the original H2 lines, bullet structure, and any H3 subsections). Render in the order they appear in the file (newest first). Do not summarize, do not paraphrase.

    2. "Yes, highlights only" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and synthesize 2-5 highlight bullets across the H2 sections where {old} < version <= {new}. Prefer items that change user-facing behavior. Skip purely-internal refactors and test-only changes. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."

    3. "Remind me later" — On selection: invoke Bash with EXACTLY this command (the {old} value is already substituted; pass it verbatim, do not paraphrase or recompute):
       $OWL version-remind {old}
       This rolls the sentinel back so the next Stop hook re-fires this prompt. Do not run any other commands as part of this option.

    4. "Skip" — On selection: take no action. Acknowledge briefly ("OK, skipping.") and continue with the user's prior intent.

  If the user provides freeform input via AskUserQuestion's "Other" option:
    - "not now" / "later" / "skip for now" → treat as "Remind me later" (invoke the Bash command in option 3).
    - "ignore" / "stop asking" / "no" → treat as "Skip".

  When the question text mentions multiple versions (step_count > 1), the multi-version skip rendering applies: "Yes, full changelog" prints ALL interim H2 sections (not just the endpoints).
  {resolved_branch_extra}
  After completing the user's choice, return control to whatever the user was working on before this prompt."#);
  ```

  CRITICAL: do NOT escape `$` in the `$OWL` or `$CLAUDE_PLUGIN_ROOT` substrings. These literally need to reach Claude as `$OWL` and `$CLAUDE_PLUGIN_ROOT` for Bash expansion. Rust's `format!` macro does NOT touch `$` characters — they pass through verbatim. Sanity-check: print the output of `build_block_reason("1.10.9", "1.10.10", None, None, 1, true)` once locally during development and confirm `$OWL version-remind 1.10.9` appears as-is.

  CRITICAL: do NOT include `{old}`/`{new}`/etc. AS LITERAL TEXT in the final emitted string — `format!` substitutes them. If you need the LITERAL characters `{old}` in the instructions (for some reason), escape with `{{old}}`. Audit the output: a successful build_block_reason call should have NO unsubstituted `{name}` patterns in the output (a regex `\{[a-z_]+\}` should match zero times).

  Step C: extend the co-located `#[cfg(test)] mod tests` block with three additional tests covering the substitution invariants in `<behavior>`:
  ```rust
  #[test]
  fn build_block_reason_single_step_with_dates_substitutes_correctly() {
      let out = build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true);
      assert!(out.contains("<old>1.10.9</old>"));
      assert!(out.contains("<new>1.10.10</new>"));
      assert!(out.contains("<old_date>2026-05-15</old_date>"));
      assert!(out.contains("<new_date>2026-05-16</new_date>"));
      assert!(out.contains("<step_count>1</step_count>"));
      assert!(out.contains("<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>"));
      assert!(out.contains("<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>"));
      assert!(out.contains("\"Yes, full changelog\""));
      assert!(out.contains("\"Yes, highlights only\""));
      assert!(out.contains("\"Remind me later\""));
      assert!(out.contains("\"Skip\""));
      assert!(out.contains("Pick 2-5 most impactful") || out.contains("2-5 highlight bullets"));
      assert!(out.contains("$OWL version-remind 1.10.9"));
      assert!(!out.contains("[PLAN_03_INSTRUCTIONS_PLACEHOLDER]"));
      // No unsubstituted template tokens:
      let token_re_hits = out.matches('{').filter(|_| true).count();
      // A handful of literal `{` are acceptable (none in our wording though)
      // — assert no `{old}` / `{new}` / `{step_count}` literal remains:
      assert!(!out.contains("{old}"));
      assert!(!out.contains("{new}"));
      assert!(!out.contains("{step_count}"));
      assert!(!out.contains("{old_date}"));
      assert!(!out.contains("{new_date}"));
  }

  #[test]
  fn build_block_reason_multi_step_substitutes_step_count() {
      let out = build_block_reason("1.10.5", "1.10.10", Some("2026-05-11"), Some("2026-05-16"), 5, true);
      assert!(out.contains("<step_count>5</step_count>"));
      assert!(out.contains("(5 versions)"));
      assert!(out.contains("$OWL version-remind 1.10.5"));
  }

  #[test]
  fn build_block_reason_unresolved_changelog_degrades_gracefully() {
      let out = build_block_reason("1.10.9", "1.10.10", None, None, 1, false);
      assert!(out.contains("<old_date></old_date>"));
      assert!(out.contains("<new_date></new_date>"));
      assert!(out.contains("<changelog_path>NOT_RESOLVED</changelog_path>"));
      assert!(out.contains("NOT_RESOLVED") || out.contains("not available"));
      // Four options still present even when CHANGELOG unresolvable:
      assert!(out.contains("\"Yes, full changelog\""));
      assert!(out.contains("\"Skip\""));
  }
  ```

  Step D: run `cargo test --lib version_changelog` and confirm all tests pass (the existing Plan 02 tests + the three new ones).

  Step E: Audit for unsubstituted placeholders across the entire file:
  ```
  grep -c "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/owl/version_changelog.rs
  ```
  MUST return 0. (Plan 02 left exactly one occurrence; Plan 03 must remove it.)

  Step F: Commit:
  ```
  feat(34): author 4-option AUQ instructions for version-change block

  Replaces Plan 02's [PLAN_03_INSTRUCTIONS_PLACEHOLDER] with the locked
  AUQ wording per CONTEXT D-08:
  - "Yes, full changelog" / "Yes, highlights only" / "Remind me later" / "Skip"
  - Substitutes {old}/{new}/{step_count}/{old_date}/{new_date} verbatim
  - Surfaces $OWL version-remind {old} invocation literally (Claude has no
    way to infer the rollback target otherwise — CONTEXT D-08 clarification)
  - Highlights rubric: 2-5 most impactful items, user-facing-behavior bias
  - Multi-step path renders ALL interim H2 sections (RESEARCH Pitfall 6)
  - NOT_RESOLVED branch degrades gracefully when CHANGELOG.md unreadable

  Three new unit tests assert substitution invariants + no unsubstituted
  tokens remain.
  ```
    </action>
    <verify>
      <automated>! grep -q "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/owl/version_changelog.rs &amp;&amp; cargo test --lib version_changelog 2>&amp;1 | grep -qE "test result: ok"</automated>
    </verify>
    <done>Placeholder removed; three new substitution tests pass; all prior Plan 02 tests still pass; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 2: Add golden snapshot fixture + integration test covering the end-to-end block reason string</name>
    <files>tests/golden/version_changelog_block.txt, tests/version_changelog.rs</files>
    <behavior>
  - `tests/golden/version_changelog_block.txt` exists with the full expected reason string for the canonical 1.10.9 -> 1.10.10 transition (step_count=1, both dates present, CHANGELOG resolved).
  - New integration test `block_reason_matches_golden_snapshot` invokes `owl hook-idle` with a mismatch fixture mirroring the golden's input parameters; captures stdout; parses the JSON envelope; extracts the `reason` field; asserts it equals the golden file content (after stripping leading/trailing whitespace) OR asserts a curated set of substring invariants per the RESEARCH open-question Q1 recommendation (snapshot just the stable parts).
  - Choose the SUBSTRING approach for resilience: any wording tweak in Task 1 invalidates a full-string snapshot but should NOT break the test. Substring invariants:
    - `<old>1.10.9</old>`
    - `<new>1.10.10</new>`
    - `<step_count>1</step_count>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
    - `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"` (literal labels)
    - `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md`
    - `$OWL version-remind 1.10.9` (with substituted old version)
  - The golden FILE serves as human-reviewable reference but the test's PASS/FAIL is on the substring set.
    </behavior>
    <action>
  Step A: Generate the canonical reason string by running locally:
  ```
  cargo test --lib version_changelog::tests::build_block_reason_single_step_with_dates_substitutes_correctly -- --nocapture 2>&1 | grep -A 200 "^---"
  ```
  OR write a tiny helper test that prints the canonical output:
  ```rust
  #[test]
  #[ignore]  // run with `cargo test -- --ignored` to regenerate golden
  fn print_canonical_block_reason_for_golden() {
      let out = build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true);
      println!("---GOLDEN-BEGIN---");
      println!("{}", out);
      println!("---GOLDEN-END---");
  }
  ```
  Run with `cargo test --lib print_canonical_block_reason_for_golden -- --ignored --nocapture`, capture the section between the markers, write to `tests/golden/version_changelog_block.txt`.

  Step B: Add the integration test to `tests/version_changelog.rs`:
  ```rust
  #[test]
  fn block_reason_matches_golden_snapshot() {
      let _lock = ENV_LOCK.lock().unwrap();
      let tmp = tempfile::tempdir().unwrap();

      // Fixture: sentinel at 1.10.9, OVERRIDE to 1.10.10, CHANGELOG covering both.
      let _guard = SptHomeGuard::set(tmp.path(), tmp.path(), Some("1.10.10"));
      write_changelog_fixture(tmp.path());
      write_live_perch(tmp.path(), "golden-perch");
      write_sentinel_fixture(tmp.path(), "1.10.9");

      let output = assert_cmd::Command::cargo_bin("owl")
          .expect("owl binary must be built")
          .arg("hook-idle")
          .env("SPT_HOME", tmp.path())
          .env("CLAUDE_PLUGIN_ROOT", tmp.path())
          .env("SPT_OVERRIDE_PKG_VERSION", "1.10.10")
          .env("HOME", tmp.path())
          .env("USERPROFILE", tmp.path())
          .env_remove("OWL_ECHO_COMMUNE")
          .write_stdin("{}")
          .output()
          .expect("owl subprocess");

      assert!(output.status.success(), "owl exited non-zero: {:?}", output);
      let stdout = String::from_utf8_lossy(&output.stdout);

      // Parse the JSON envelope and extract `reason`.
      let envelope: serde_json::Value = serde_json::from_str(&stdout.trim())
          .expect(&format!("stdout is not valid JSON: {}", stdout));
      assert_eq!(envelope["decision"], "block");
      let reason = envelope["reason"]
          .as_str()
          .expect("reason field missing or not a string");

      // Substring invariants per RESEARCH Q1 recommendation. Any one failure
      // means a wording regression vs the locked CONTEXT D-08 surface.
      let invariants: &[&str] = &[
          "<spt-version-changelog>",
          "</spt-version-changelog>",
          "<old>1.10.9</old>",
          "<new>1.10.10</new>",
          "<step_count>1</step_count>",
          "<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>",
          "<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>",
          "\"Yes, full changelog\"",
          "\"Yes, highlights only\"",
          "\"Remind me later\"",
          "\"Skip\"",
          "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md",
          "$OWL version-remind 1.10.9",
      ];
      for inv in invariants {
          assert!(
              reason.contains(inv),
              "reason missing required substring: {:?}\nfull reason:\n{}",
              inv,
              reason
          );
      }

      // Golden file is a human-readable reference. Diff-check it for completeness,
      // but allow drift (the test passes on substring invariants alone).
      let golden_path = std::path::Path::new("tests/golden/version_changelog_block.txt");
      if let Ok(golden) = std::fs::read_to_string(golden_path) {
          if golden.trim() != reason.trim() {
              eprintln!("NOTE: golden file at {} differs from live output. Regenerate via `cargo test -- --ignored print_canonical_block_reason_for_golden`.", golden_path.display());
          }
      } else {
          eprintln!("NOTE: golden file at {} missing. Regenerate via `cargo test -- --ignored print_canonical_block_reason_for_golden`.", golden_path.display());
      }
  }
  ```

  Step C: ensure `tests/golden/` directory exists; commit both the fixture and the test together.

  Step D: run `cargo test --test version_changelog` and confirm all 11 integration tests pass (10 from Plan 02 + this new one).

  Step E: commit:
  ```
  test(34): golden snapshot for version-change block reason

  Adds tests/golden/version_changelog_block.txt as human-reviewable
  reference for the canonical 1.10.9 -> 1.10.10 transition reason
  string. Integration test asserts a curated set of substring
  invariants (XML elements + literal AUQ labels + $OWL invocation)
  rather than full-string equality, per RESEARCH open question Q1
  recommendation: snapshot the stable parts, allow prose drift.
  ```
    </action>
    <verify>
      <!-- C2 fix: original regex `test result: ok\. (11|12) passed` over-constrained the
       count, coupling the gate to a fragile arithmetic invariant. Relax to `[0-9]+ passed`
       for the count assertion AND add a SEPARATE substring assertion that the new test
       name `block_reason_matches_golden_snapshot` actually appears in the output. This
       decouples "tests pass" from "the specific new behavior was tested" and survives
       future test additions in this file. -->
  <automated>test -f tests/golden/version_changelog_block.txt &amp;&amp; cargo test --test version_changelog 2>&amp;1 | tee /tmp/it2.log &amp;&amp; grep -qE "test result: ok\. [0-9]+ passed" /tmp/it2.log &amp;&amp; grep -q "block_reason_matches_golden_snapshot" /tmp/it2.log</automated>
    </verify>
    <done>Golden fixture committed; new integration test passes; full integration suite (now 11 tests) green; commit landed.</done>
  </task>

  <task type="checkpoint:human-verify" gate="blocking">
    <name>Task 3: UAT — manually verify the AUQ surfaces correctly in a real Claude Code session</name>
    <what-built>
  The full Phase 34 stack:
  - Stop-hook version-change detection with sentinel at `$SPT_HOME/last-seen-version.json`
  - `<spt-version-changelog>` block emission via `decision:"block"` + `reason`
  - 4-option AUQ flow rendered by Claude on re-engagement
  - `$OWL version-remind <old>` rollback subcommand
  - DEPLOY.ps1 syncs CHANGELOG.md alongside the binary
    </what-built>
    <how-to-verify>
  1. **Clean build + deploy:**
     ```
     cargo build --release
     powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1
     ```
     Verify the deploy log shows the two new `Copy CHANGELOG.md ->` substeps. After deploy, run `/reload-plugins` in Claude Code.

  2. **Verify CHANGELOG.md landed in plugin cache:**
     Windows:
     ```
     type %LOCALAPPDATA%\..\..\.claude\plugins\cache\cplugs\spt\<version>\CHANGELOG.md
     ```
     (Path varies; verify against `installed_plugins.json`.) The file should contain the curated Keep-a-Changelog content authored in Plan 01.

  3. **Synthesize a version mismatch by rewriting the sentinel to a fake old version:**
     Windows PowerShell:
     ```
     Set-Content -Path "$env:LOCALAPPDATA\spt\last-seen-version.json" -Value '{"version":"0.0.1"}'
     ```
     Unix:
     ```
     echo '{"version":"0.0.1"}' > ~/.spt/last-seen-version.json
     ```

  4. **Trigger a Stop hook:** open a Claude Code session in any spt-aware repo (this one works). Run a no-op task that ends a turn (e.g., "what files are in this directory?"). When Claude finishes, the Stop hook fires.

  5. **EXPECTED behavior on re-engagement:**
     a. Claude writes a pre-question line naming the version transition (and dates, if visible in the block payload).
     b. Claude invokes AskUserQuestion with EXACTLY four options (labels verbatim):
        - `Yes, full changelog`
        - `Yes, highlights only`
        - `Remind me later`
        - `Skip`
     c. Note: the question text mentions "X versions" if step_count > 1 (when 0.0.1 -> current, step_count will be large because CHANGELOG.md has many entries between).

  6. **Test each branch (in 4 separate Claude Code sessions or by re-synthesizing the mismatch between each):**
     - **Yes, full changelog:** Claude prints H2 sections from CHANGELOG.md verbatim. Confirm formatting preserved (bullets, H3 subsections, etc.). Sentinel after: current version.
     - **Yes, highlights only:** Claude renders 2-5 highlight bullets across the range + literal pointer line "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail." Sentinel after: current version.
     - **Remind me later:** Claude invokes `$OWL version-remind 0.0.1` via Bash. Sentinel after: `{"version":"0.0.1"}` (rolled back). The next Stop hook re-fires the block.
     - **Skip:** Claude says "OK, skipping." or similar; sentinel after: current version; next Stop does NOT re-fire.

  7. **Verify sentinel state after each branch** by running `type %LOCALAPPDATA%\spt\last-seen-version.json` (Windows) or `cat ~/.spt/last-seen-version.json` (Unix).

  8. **Test the "Other" freeform fallback** (one of the branches): when the AUQ surfaces, type something like "not now" into Other. Verify Claude maps it to `Remind me later` and invokes `$OWL version-remind 0.0.1`.

  9. **Regression check** — confirm the existing Phase 18.3 `.more-done` sentinel still works:
     - Start a `/spt:live` session with a psyche.
     - End a turn.
     - Verify `$SPT_HOME/owlery/<psyche_id>/.more-done` exists after Stop fires.
     - Verify the version-change block did NOT cancel out this behavior.

  10. **OWL_ECHO_COMMUNE regression check** — manually invoke `OWL_ECHO_COMMUNE=1 owl hook-idle <<< '{}'` after re-synthesizing a mismatch. Verify stdout is EMPTY (no block emitted; guard fires) and sentinel is UNCHANGED.

  If all 10 checks pass: type `approved` to gate-pass this checkpoint.

  If any check fails: describe the failure precisely (which step, what was expected, what actually happened, any stderr/stdout captured). The orchestrator will route the failure back through `/gsd-plan-phase --gaps` for closure.
    </how-to-verify>
    <resume-signal>Type "approved" if all 10 UAT checks pass, or describe the failure for gap closure routing.</resume-signal>
  </task>

  </tasks>

  <verification>
  After all 3 tasks (Tasks 1+2 automated, Task 3 manual UAT):

  1. Placeholder fully removed:
     ```
     grep -r "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/ tests/
     ```
     Expect 0 hits.

  2. Full test suite green:
     ```
     cargo test
     ```
     Expect ~30+ unit tests + 11 integration tests, all passing.

  3. Golden fixture present and non-empty:
     ```
     test -s tests/golden/version_changelog_block.txt
     ```
     Plus a manual eyeball: does the golden content read as the AUQ wording you authored?

  4. Two new commits in addition to Plan 01 + Plan 02:
     ```
     git log --oneline -2
     ```
     Expect:
     - `test(34): golden snapshot for version-change block reason`
     - `feat(34): author 4-option AUQ instructions for version-change block`

  5. UAT approved by human via Task 3 checkpoint.

  6. End-to-end stack: `cargo build --release && powershell docs/DEPLOY.ps1 -Bump patch -DryRun` exits 0 and surfaces the new CHANGELOG.md stub-append substep (regression check against Plan 01 work).
  </verification>

  <success_criteria>
  - Plan 03 maps to VERS-04 (4-option AUQ wording authored verbatim; AskUserQuestion surface verified in UAT) and VERS-06 (multi-version path uses the same 4-option AUQ; "Yes, full changelog" renders all interim H2 sections per Pitfall 6).
  - The `<instructions>` element in the block reason is the only file where wording lives — change-control is centralized.
  - Golden snapshot resilience strategy (substring invariants, not full-string) accommodates future wording tweaks without breaking the test.
  - UAT confirms the entire Phase 34 surface works in a real Claude Code session.
  - After approval, Phase 34 is shippable in the next end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6).
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | hook reason -> Claude continuation | the `<instructions>` element text drives Claude's next turn; wording reliability determines whether the AUQ fires correctly |
  | Claude -> Bash | `$OWL version-remind <old>` invocation crosses Claude's Bash tool boundary; `<old>` is substituted by the hook, not by Claude |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-11 | Tampering | Wording drift in `<instructions>` causes Claude to invoke wrong tool / wrong args | mitigate | Substring-invariant integration test (Task 2) asserts the 4 option labels + literal invocation string survive any prose tweak; UAT (Task 3) catches semantic regressions |
  | T-34-12 | Spoofing | Claude paraphrases `$OWL version-remind <old>` to a different value | mitigate | Instructions text explicitly says "substitute the {old} value verbatim, do NOT paraphrase or recompute"; the `<old>` value is pre-substituted by the hook so Claude only needs to copy-paste; the `is_plain_semver` validator in version-remind rejects garbage arg |
  | T-34-13 | Information disclosure | Pre-question prose leaks unintended detail | accept | Author-controlled wording; only surfaces version numbers + dates from CHANGELOG.md (already public artifact) |
  | T-34-14 | DoS | Highlights branch reads CHANGELOG.md for every turn | accept | Read only happens on user-explicit "Yes, highlights only" pick, bounded by user action; no automated re-read |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-03-SUMMARY.md` documenting:
  - Two commit SHAs (Task 1 wording, Task 2 golden)
  - UAT result (approved / failed-with-detail)
  - Final substring invariant count in the integration test
  - Snapshot file SHA-256 (for future drift detection)
  - Confirmation `grep -r PLAN_03_INSTRUCTIONS_PLACEHOLDER src/ tests/` returns 0 hits
  - Any wording tweaks made between Task 1 draft and the final committed text (preserve the audit trail)
  - Confirmation Phase 34 is ready for the next end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6)
  </output>

  ## Review Instructions

  Analyze each plan and provide:
  1. Summary — one-paragraph assessment
  2. Strengths (bullets)
  3. Concerns (bullets, severity HIGH/MEDIUM/LOW)
  4. Suggestions (specific bullets)
  5. Risk Assessment (LOW/MEDIUM/HIGH + justification)

  Focus on: missing edge cases, dependency ordering, scope creep, security, performance, whether plans achieve phase goals.
  Output in markdown.

preview:
  # Cross-AI Plan Review Request

  You are reviewing implementation plans for software project phase 34.
  Provide structured feedback on plan quality, completeness, and risks.

  ## Project Context
  # Owl Skill (SPT — Spacetime / Sentience Pocket Transacter)

  ## What This Is

  A zero-dependency inter-agent messaging and live-agent lifecycle system for Claude Code, shipped as the `spt` plugin via the `cplugs` marketplace. Native Rust binary (`owl.exe`) uses TCP-primary delivery with SQLite spool fallback. Live agents run a detached Psyche wrapper (periodic `claude -p --resume` sessions) that absorbs `COMMUNE` updates from Self and reacts to scheduled pulses, file-drops, and SessionStart boundaries. Listener now streams `<EVENT>` envelopes via Claude Code's Monitor tool. Used by multiple people — Windows-native portability with no runtime dependencies is a hard constraint.

  ## Core Value

  Reliable, zero-dependency agent-to-agent messaging that just works on any machine, with a live-agent layer that survives `/clear`, `/compact`, binary handoffs, and orphan boundaries without losing context.

  ## Current Milestone: v1.7.1 Seamlessification II

  **Goal:** Polish round on live-agent friction — listing/picker correctness, skill hint surface, fresh-start commune prompt, auto-resume affordance, plugin-update changelog notification.

  **Target features:**
  - `list` fn overhaul (`--all` / `--offline` / `--here`; default = online-only) for `$OWL list` + `$LIVE list`
  - `argument-hint` frontmatter coverage on every `/spt:*` skill
  - Fresh-start live-agent commune flow — when agent identity is known-new (via pick-spec OR psyche-download empty), surface first-commune summary and prompt user before init
  - `$LIVE pick-spec` correctness — return live agents in repo even when all are online; calling flow enforces "new name or cancel"
  - `/spt:live --auto` flag — SessionStart auto-surfaces pick-spec JSON for non-live/non-listening agents; `--auto` picks most-recent + immediately resumes any clear next body of work; activates on casual prompts ("pick up where we left off", "keep going", "resume work")
  - Version-change changelog notification — sentinel + `%LOCALAPPDATA%` current-version file + SessionStart/PreToolUse hook prompts user with old→new diff

  ## Current State

  **Shipped: v1.7 Seamlessification** (2026-05-16; deployed v1.10.10)

  Five phases (26-30) focused on friction removal across the live-agent lifecycle:
  - **Phase 26** — `/spt:live` auto-pick + AskUserQuestion fork + free-text resolve
  - **Phase 27** — Listener poll migrated from Bash to Monitor tool (streaming `<EVENT>` envelopes)
  - **Phase 28** — SessionStart injects `<psyche-context>` on live perches (`/clear` & `/compact`)
  - **Phase 29** — Auto-fire echo-commune at boundaries + typed `<EVENT type="echo_commune">` envelope cutover
  - **Phase 30** — Commune/signoff file-drop protocol (`.claude/{id}-{commune,signoff}.md`) replaces stdin piping

  26 plans, 211 commits, 7-day cycle. Off-roadmap sub-iterations 18.x continued through v1.10.x defense-in-depth deploys (stale-signoff drain, init_signoff envelope-shape predicate, FileDropOutcome::BreakLoop).

  **In flight: v1.7.1 Seamlessification II** — Phase 31 complete (2026-05-17): `$LIVE pick-spec` now emits `kind:"all-live"` when every known agent in the repo is online (covers PICK-01/02/04/05/06; PICK-03 deferred per D-10/D-11). Phases 32 (list overhaul + skill hint audit), 33 (fresh-start commune + auto-resume, depends on 31), and 34 (version-change changelog) remain.

  **Next milestone:** v1.8 Psyche Restructure — Phases 23/24/25 (commune/signoff project-root + HEAD SHA stamping, tracked-dir per-agent layout + sessions audit log, psyche perch nesting + general/project context split).

  ## Requirements

  ### Validated

  - ✓ Harden owl.sh against race conditions and edge cases — v1.0
  - ✓ Unified output formatting: compact symbols, ANSI color codes, no emoji — v1.0
  - ✓ Auto-setup env vars and session cleanup hook — v1.0
  - ✓ Live Agent start + Psyche subagent + periodic pulse + COMMUNE format — v1.0
  - ✓ Native Rust binary with P2P TCP messaging + SQLite offline spool — v1.1
  - ✓ Golden test suite + cross-platform CI (4 targets) — v1.1
  - ✓ Timed pulses + Spine supervisor + single-binary drop-in — v1.1
  - ✓ Plugin migration to `/spt` namespace — v1.5
  - ✓ Spacetime filesystem (`%LOCALAPPDATA%\spt` / `~/.spt`) with `SPT_HOME` override — v1.5
  - ✓ Persistent offline perches + indefinite queueing + reconnection — v1.5
  - ✓ Memformat-driven Psyche brainstorming + INSIGHT messages — v1.5
  - ✓ PerchState enum + owl doctor + session detection + optional agent_id — v1.5
  - ✓ Hand-rolled MCP server (JSON-RPC over stdio) — v1.6
  - ✓ Spool unification (peek_all + mark_delivered) + Result variant refactor — v1.6
  - ✓ 13 MCP tools + 5 MCP resources + check-on-request notifications — v1.6
  - ✓ Seamless binary handoff for long-running spt processes — v1.6 (Phase 18.4/18.5 sub-iterations)
  - ✓ Echo-commune fresh-session jsonl excerpt + 15-min cadence gate — v1.6 (Phase 18.8 sub-iteration)
  - ✓ `/spt:live` auto-pick + AskUserQuestion fork + `$LIVE pick-spec`/`$LIVE fork` — v1.7
  - ✓ Listener poll: Monitor tool primary + `<EVENT>` envelopes + Bash `--once` fallback — v1.7
  - ✓ SessionStart `<psyche-context>` injection on live perches — v1.7
  - ✓ Auto-fire echo-commune on `/clear`+`/compact`+orphan; typed envelope cutover — v1.7
  - ✓ Commune/signoff file-drop protocol (`.claude/{id}-{commune,signoff}.md`) — v1.7

  ### Active

  - Phase 23 — Commune/signoff payload includes `project_root` + `HEAD` SHA (v1.8 candidate)
  - Phase 24 — `psyches/tracked/<self_id>/` per-agent layout + sessions audit log (v1.8)
  - Phase 25 — Psyche perch nesting + general.md / proj-{project}.md context split (v1.8)
  - Phase 19-22 — Capsule (psmux-based sendkeys pipeline, abrupt messages, GSD proceed via sendkeys) (v1.9)
  - Resolve `psyche-stale-after-clear` debug session (root cause known, fix pending)
  - 2 Phase 28 raw-stdout /clear-cycle Human-UAT tests (deferred, non-blocking)

  ### Out of Scope

  - macOS/Linux-specific features — cross-platform CI covers builds but not platform-specific optimizations
  - External dependencies beyond the single binary — copy-one-file install is core constraint
  - MCP transport prioritization — deprioritized post-v1.6 in favor of CLI-first model (see `feedback_mcp_performance.md`); MCP layer remains but Phase 18.1+ sub-iterations refactored away from MCP-centric flows

  ## Phase 34: Version-Change Changelog
  ### Roadmap Section
  ### Phase 34: Version-Change Changelog
  **Goal**: When a user's installed `spt` plugin version changes underneath them, the next SessionStart surfaces an old→new changelog prompt (via AskUserQuestion) sourced from compiled-in `CARGO_PKG_VERSION` against an `$SPT_HOME/last-seen-version.json` sentinel — without firing on first-install, without colliding with the SessionStart auto-pick (AUTO-03), and without racing the binary handoff (Phase 18.4/18.5)
  **Milestone**: v1.7.1 Seamlessification II
  **Depends on**: Nothing in v1.7.1 (independent at the Rust level); sequenced AFTER Phase 33 to keep the two SessionStart-hook extensions (AUTO-03 + VERS-04) in separate phases
  **Requirements**: VERS-01, VERS-02, VERS-03, VERS-04, VERS-05, VERS-06, VERS-07, VERS-08, VERS-09
  **Success Criteria** (what must be TRUE):
    1. On a fresh install (no `$SPT_HOME/last-seen-version.json` present), SessionStart silently writes the current `CARGO_PKG_VERSION` and emits NO changelog prompt to the user
    2. After the plugin version changes (compiled-in `CARGO_PKG_VERSION` differs from sentinel contents), the next SessionStart emits a single `SPT plugin updated v{old} → v{new}. Print changelog?` AskUserQuestion (yes/no/dismiss); the sentinel is atomically rewritten with the current version regardless of user response, so the prompt does not re-fire next session
    3. A multi-version skip (e.g. v1.10.5 → v1.12.0) surfaces a version-list summary plus a pointer to `CHANGELOG.md` — never full prose for each interim version
    4. Repo-root `CHANGELOG.md` exists with H2-per-version sections (created from git log if absent during phase kickoff); `docs/DEPLOY.ps1` syncs it into the plugin meta dir alongside `plugin.json`
    5. The version-change prompt (VERS-04) and SessionStart auto-pick (AUTO-03 from Phase 33) coexist deterministically under non-overlapping allow-list conditions; the sentinel write site lives at the top-level SessionStart entry (atomic write, no in-handoff trampoline writes) so a mid-handoff race cannot corrupt it
    6. End-of-milestone DEPLOY.ps1 `-Bump patch` ships all four phases together (no per-phase deploys during v1.7.1)
  **Plans:** TBD

  Plans:
  - [ ] TBD — discuss/plan-phase to break down (research flag: phase must begin with CHANGELOG.md existence check + create-from-git-log if absent)

  ## Progress

  ### Requirements Addressed (pre-amendment baseline; Plan 01 will amend VERS-02/04/06/08/09)

  - [ ] **VERS-01**: A new file `$SPT_HOME/last-seen-version.json` (sibling to `owlery/`, NOT inside it) stores the most recently seen plugin version per the running owl.exe.
  - [ ] **VERS-02**: On SessionStart (in `plugin_session_start.rs`), owl.exe compares `env!("CARGO_PKG_VERSION")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`.
  - [ ] **VERS-03**: First-install case (sentinel missing) silently writes the current version — NO prompt is emitted to the user.
  - [ ] **VERS-04**: Version-mismatch case emits a single SessionStart prompt: `SPT plugin updated v{old} → v{new}. Print changelog?` via AskUserQuestion (yes / no / dismiss).
  - [ ] **VERS-05**: Regardless of user response, the sentinel is atomically rewritten with the current version using existing `atomic_write_string` helper (tmp + rename) so the prompt does not re-fire next session.
  - [ ] **VERS-06**: Multi-version skip (e.g. v1.10.5 → v1.12.0) surfaces a version-list summary plus a pointer to `CHANGELOG.md`, not full prose for each interim version.
  - [ ] **VERS-07**: A repo-root `CHANGELOG.md` is created (or existing one is verified) with H2-per-version sections so excerpt parsing works; deploy script (`docs/DEPLOY.ps1`) syncs it into the plugin meta dir alongside `plugin.json`.
  - [ ] **VERS-08**: SessionStart auto-pick (AUTO-03) and version-change prompt (VERS-04) coexist without colliding — they fire under non-overlapping allow-list conditions and in deterministic order.
  - [ ] **VERS-09**: Version-sentinel write does not race against binary handoff (Phase 18.4/18.5) — single write site at the top-level SessionStart entry, after user sees the prompt; atomic; no in-handoff trampoline writes.


  ### User Decisions (CONTEXT.md)
  # Phase 34: Version-Change Changelog - Context

  **Gathered:** 2026-05-17
  **Status:** Ready for planning

  <domain>
  ## Phase Boundary

  A single end-of-turn `Stop` hook prompt that surfaces an old→new changelog excerpt to the user the first time the agent finishes a turn after the plugin version has changed underneath them. Scope:

  1. **Sentinel + version-mismatch detection** — `$SPT_HOME/last-seen-version.json` (sibling to `owlery/`, NOT inside it) stores the most recently seen version. `hook_idle.rs` (Stop hook) compares `env!("CARGO_PKG_VERSION")` against the sentinel after perch resolution; on mismatch, emits a `<spt-version-changelog>` block via `decision:"block"` + `reason:"..."` and atomically rewrites the sentinel at the same site.
  2. **Repo-root `CHANGELOG.md`** — created from git-log history at Plan 01 start in Keep-a-Changelog format (`## [X.Y.Z] - YYYY-MM-DD`). DEPLOY.ps1 `-Bump` appends a stub H2 without committing and refuses to deploy until author fills + commits.
  3. **DEPLOY.ps1 sync** — copies repo-root `CHANGELOG.md` into `{Marketplace}/plugin/spt/CHANGELOG.md` alongside the existing binary/skills/hooks/plugin.json sync step. Hook resolves at runtime via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.
  4. **Block payload + AskUserQuestion shape** — Rust hook parses CHANGELOG.md once, builds `{old, new, old_date, new_date, step_count}` payload, embeds in block. Claude renders a 4-option AskUserQuestion (`Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`); on `Remind me later` invokes new `$OWL version-remind` subcommand that rolls the sentinel back to `old`, re-arming the next-Stop emission.
  5. **Hook-pivot triggers REQUIREMENTS.md + ROADMAP.md amendment** (Plan 01 first commit, mirrors Phase 31 D-11 / Phase 33 D-03/D-04 pattern) — strikes the SessionStart-entry assumption baked into VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md success criteria 1, 2, 5.

  Touches:
  - `src/owl/hook_idle.rs` — version-mismatch detection + emission + sentinel atomic write at top of `run()`, after perch resolution and before `set_idle_ready` / `spawn_echo_commune_if_live`. Recursion guard: skip when `OWL_ECHO_COMMUNE` set (mirror line 64-69).
  - New `src/owl/version_changelog.rs` (or co-located in `hook_idle.rs`) — sentinel read/write helpers, CHANGELOG.md parser (regex `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`), payload builder.
  - New bin subcommand `owl version-remind <old>` — atomically writes `{"version":"<old>"}` back to the sentinel using existing `atomic_write_string`. ~10-line implementation.
  - New repo-root `CHANGELOG.md` — Keep-a-Changelog format, authored by hand at Plan 01 from `git log --oneline --grep='chore: bump'`.
  - `docs/DEPLOY.ps1` — (a) new sync step copying `CHANGELOG.md` to marketplace, (b) `-Bump` extension that appends stub H2 to repo-root CHANGELOG.md uncommitted and aborts on unchanged-but-required content.
  - `.planning/REQUIREMENTS.md` — amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 per decisions D-01..D-09.
  - `.planning/ROADMAP.md` Phase 34 — revise success criteria 1, 2, 5 in lockstep with REQUIREMENTS.md.

  No new on-disk artifacts beyond the sentinel + CHANGELOG.md. No new info.json fields. No skill surface (no `/spt:changelog` skill — block-body inline instructions drive Claude through the AUQ + remind flow).

  </domain>

  <decisions>
  ## Implementation Decisions

  ### Hook Pivot (architectural foundation)

  - **D-01: Stop hook (`hook_idle.rs`), not SessionStart.** Surfacing the version-change prompt at end-of-turn lands the AskUserQuestion exactly when the agent is between tasks. Rationale: a SessionStart or UserPromptSubmit prompt risks the user issuing "go do X" → walking away → AskUserQuestion blocking the agent → returning many minutes later to find zero progress. Stop hook guarantees the question fires after the agent's current work is complete and before any new turn starts. This invalidates VERS-02's "On SessionStart (in `plugin_session_start.rs`)" wording — amendment scope below.

  - **D-02: Predicate = `owl_id` resolves in `hook_idle.rs::run()`.** Piggybacks on the existing perch-resolution gate at hook_idle.rs:25-32. If no perch matches `OWL_SESSION_ID` or `parent_pid`, hook early-returns and the version-change check never runs — exactly the "spt is in use" semantic the user requested (`spt listen` / `spt live` / `spt revive` all create perches; live-agent sessions are perches). No new predicate code; no new info.json field.

  - **D-03: Recursion guard reuses `OWL_ECHO_COMMUNE` env-var skip** (hook_idle.rs:64-69 pattern). Haiku echo-commune subprocesses MUST NOT see a version-change block. New version-changelog code MUST run after the OWL_ECHO_COMMUNE check or be itself guarded equivalently.

  ### Block Emission + Sentinel Write Site

  - **D-04: Block emission via `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"`** at the top of `hook_idle.rs::run()`, immediately after perch resolution and the OWL_ECHO_COMMUNE guard, before `set_idle_ready` / `spawn_echo_commune_if_live`. Block content is the rendered XML payload (D-05). Stop hook's `decision:"block"` + `reason` schema is the natural injection channel for Stop (which has no `hookSpecificOutput.additionalContext` per the existing hook_idle.rs:1-5 comment) — research flag: confirm Stop's `reason` text reaches Claude as injected context on re-engagement, not as a silent abort. See Research Flags below.

  - **D-05: Sentinel write site = same `run()` top, BEFORE returning the block.** Atomic via existing `atomic_write_string` (src/common/owlery.rs:248). Default behavior: writes the NEW version. Re-fire on next Stop only happens if the user picks `Remind me later`, which calls `$OWL version-remind <old>` to roll the sentinel back. This inverts the "ack on yes/skip" model: the hook commits sentinel pre-emission; only the explicit remind path rolls back. Rationale (per user): most-used paths (Yes-full / Yes-highlights / Skip) have zero UX bloat; the less-common Remind path pays the round-trip.

  - **D-06: Fire-once cadence = once per version transition.** With D-05 sentinel pre-write in place, the next Stop in the same session sees `sentinel == CARGO_PKG_VERSION` and emits nothing. Matches VERS-05's "does not re-fire next session" intent — also doesn't re-fire next Stop in same session.

  ### Block Payload + AskUserQuestion Shape

  - **D-07: Payload shape = `{old, new, old_date, new_date, step_count}`.** Embedded JSON-ish inside the `<spt-version-changelog>` block. Dates parsed from CHANGELOG.md H2 lines (Keep-a-Changelog `## [X.Y.Z] - YYYY-MM-DD` regex). `step_count` = number of H2 sections between `old` (exclusive) and `new` (inclusive). Surfaces in the AUQ question text (`SPT plugin updated v{old} → v{new} ({step_count} version{s}). Print changelog?`); does NOT change the option set.

  - **D-08: AskUserQuestion 4-option shape:** `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`.
    - `Yes, full changelog` — Claude prints ALL H2 sections from CHANGELOG.md where `old < v ≤ new`, verbatim markdown, concatenated. Single-step and multi-step rendered identically (overrides VERS-06's "never full prose for each interim version" constraint — see amendment scope D-09).
    - `Yes, highlights only` — Claude's discretion: extracts the most impactful feature adds + fixes from the same range, prose-style. No fixed rubric; agent uses judgment. Pointer to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` appended for users who want full detail.
    - `Remind me later` — Claude invokes `$OWL version-remind <old version>` (e.g. `$OWL version-remind 1.10.9`), passing the `old` value from the block payload as a literal positional arg. `$OWL` is the env-var-injected owl-binary path (matches existing `$OWL deliver` / `$LIVE pick-spec` conventions across `/spt:*` skills). The subcommand rewrites `$SPT_HOME/last-seen-version.json` to `{"version":"<old version>"}` so the next Stop hook re-detects mismatch and re-fires the block. No body rendered. **The block body MUST surface the exact `$OWL version-remind <old>` invocation string verbatim** so Claude has no ambiguity about which arg to pass — Claude cannot infer `<old>` from anything other than the block payload it just received.
    - `Skip` — no action. Sentinel already updated by hook at run-entry; no re-fire.

  ### CHANGELOG.md Genesis + Maintenance

  - **D-09: Repo-root `CHANGELOG.md` authored one-shot at Plan 01 start.** Plan 01 step 1 (after the REQUIREMENTS.md/ROADMAP.md amendment commit): author CHANGELOG.md by hand using `git log --oneline --grep='chore: bump'` as the version-bump skeleton. Curated prose per version, Keep-a-Changelog format. One commit; never auto-regenerated. No new `owl changelog backfill` subcommand — out of scope.

  - **D-10: Forward maintenance = DEPLOY.ps1 `-Bump` stub injection + commit gate.** When `-Bump patch|minor|major` runs, DEPLOY.ps1 appends a stub `## [NEW_VERSION] - YYYY-MM-DD\n\n- TODO: changelog entry\n` to repo-root CHANGELOG.md **without committing**. Outputs an explicit notice that the user must fill + commit before deploy will succeed. On the next `-Bump` invocation: if CHANGELOG.md still contains the TODO marker for `NEW_VERSION` (or is otherwise unchanged from prior commit), DEPLOY aborts with the same notice. Combines stub-injection (option 1) with the "abort if unchanged" gate (option 2) — guaranteed CHANGELOG.md update before any bump can ship.

  - **D-11: DEPLOY.ps1 sync target = `{Marketplace}/plugin/spt/CHANGELOG.md`** (sibling to `plugin.json`, NOT inside `.claude-plugin/`). Resolves at hook time as `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`. Add to DEPLOY.ps1's sync step alongside `owl.exe` + `skills/` + `hooks/` + `plugin.json`. Mirrors `psyche.md` placement convention (plugin root, not subdir).

  ### REQUIREMENTS.md + ROADMAP.md Amendment Scope (Plan 01 first commit)

  - **D-12: Amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md success criteria 1, 2, 5.** Mirrors Phase 31 D-11 and Phase 33 D-03/D-04 amendment-commit pattern.
    - **VERS-02:** "On SessionStart (in `plugin_session_start.rs`)" → "On Stop hook (in `src/owl/hook_idle.rs`), gated on active perch resolution via existing `find_perch_by_session` / `find_perch_by_parent_pid`. Hook compares `env!("CARGO_PKG_VERSION")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`." (compiled-in source clause preserved)
    - **VERS-04:** yes/no/dismiss → "AskUserQuestion with four options: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Question text includes `step_count` when > 1."
    - **VERS-06:** drop "never full prose for each interim version" constraint. New wording: "Multi-version skip surfaces the same 4-option AskUserQuestion as single-step; user-chosen `Yes, full changelog` renders all interim H2 sections verbatim, `Yes, highlights only` defers to agent discretion. The CHANGELOG.md pointer is appended to the highlights body."
    - **VERS-08:** AUTO-03 (SessionStart) and VERS-04 (Stop) fire on different hooks; coexistence is enforced by hook-separation, not allow-list overlap. New wording: "Version-change prompt (VERS-04) fires from Stop hook and SessionStart auto-pick (AUTO-03 from Phase 33) fires from SessionStart hook. The two never share a hook invocation; no allow-list reconciliation required."
    - **VERS-09:** "single write site at the top-level SessionStart entry... no in-handoff trampoline writes" → "Single write site at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard. Atomic via existing `atomic_write_string`. Binary handoff (Phase 18.4/18.5) does not race because Stop hooks do not fire during in-flight binary swap (Stop fires between turns, handoff happens between processes)."
    - **ROADMAP.md SC1:** "On a fresh install, SessionStart silently writes..." → "...on first Stop hook fire in any spt-bearing session, sentinel silently writes..."
    - **ROADMAP.md SC2:** "next SessionStart emits..." → "next Stop hook fire in an spt-bearing session emits..."
    - **ROADMAP.md SC5:** drop coexistence language → "VERS-04 fires from Stop; AUTO-03 fires from SessionStart; collision impossible by hook separation."
    - Plan 01 begins with this amendment commit before any Rust code lands.

  ### Claude's Discretion

  - **Stop hook `decision:"block"` + `reason` injection semantics** — Confirm via Claude Code hooks reference (`~/.claude/reference_docs/claude-code-hooks.md`) that the `reason` text in a blocked Stop response reaches Claude as injected context on the next turn re-engagement. The hook_idle.rs:1-5 comment notes Stop "has no hookSpecificOutput schema" — but `decision:"block"` is a separate output shape that may carry user-visible context. Flag for research phase. If `decision:"block"` does NOT propagate text to Claude usefully, fall back: emit via PreToolUse (next tool call) or revisit UserPromptSubmit despite the earlier "walk-away" concern.
  - **`<spt-version-changelog>` block exact XML shape** — child elements vs JSON-as-text payload vs hybrid. Locked decisions: must carry `old`, `new`, `old_date`, `new_date`, `step_count`. Open: also embed full CHANGELOG.md excerpt for `old < v ≤ new`, or have Claude read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` at AUQ-yes time? D-05 sentinel-pre-write removes the "block must be self-contained" pressure; either is workable. Planner / researcher decides — leaning toward Claude reads at yes-time so block payload stays small.
  - **`version-remind` subcommand exact signature** — Locked: `$OWL version-remind <old version>` (bare positional arg, e.g. `$OWL version-remind 1.10.9`). Block-body MUST surface this exact invocation string with the `old` value substituted so Claude passes it verbatim; the subcommand cannot infer the rollback target from any other source.
  - **CHANGELOG.md parser regex tightness** — Keep-a-Changelog allows `## [Unreleased]` and link references (`[1.10.10]: <url>`). The phase 34 parser SHOULD reject `[Unreleased]` (no date, not a real release) and ignore link reference lines (not H2 markers — they're `[ref]:` form). Add to RESEARCH.md test corpus.
  - **Highlights extraction policy** — D-08 says "Claude's discretion". Skill-body wording (or block-body wording, since no skill is planned) should give Claude enough framing: "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes". Final wording in Plan 02 or wherever the block-body authoring lives.
  - **Plan split** — likely 3 plans: (1) REQUIREMENTS.md/ROADMAP.md amendment + CHANGELOG.md genesis + DEPLOY.ps1 sync/stub, (2) Rust hook + sentinel + parser + `version-remind` subcommand + tests, (3) block-body Claude instructions + integration test on a synthesized version transition. Final boundary at planner's discretion.
  - **Tests** — version-mismatch detection (single-step, multi-step, first-install silent, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard, no-perch guard); CHANGELOG.md parser (Keep-a-Changelog happy path, `[Unreleased]` reject, malformed H2 graceful); `version-remind` rollback round-trip; DEPLOY.ps1 stub injection + abort-on-unchanged.

  </decisions>

  <canonical_refs>
  ## Canonical References

  **Downstream agents MUST read these before planning or implementing.**

  ### Phase requirements + planning artifacts
  - `.planning/REQUIREMENTS.md` VERS-01..VERS-09 — 9 requirements. **VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 to be amended per D-12.** Plan 01 begins with the amendment commit.
  - `.planning/ROADMAP.md` Phase 34 — goal + 6 success criteria. **Success criteria 1, 2, 5 to be revised in lockstep with REQUIREMENTS.md amendment.** Success criteria 3, 4, 6 unchanged.

  ### Cross-references to prior phase decisions
  - `.planning/phases/33-fresh-start-commune-auto-resume/33-CONTEXT.md` — Phase 33 D-11 amendment-commit pattern (Plan 01 mirrors this for D-12). AUTO-03 SessionStart emission pattern (`emit_auto_pick` at plugin_session_start.rs:139-155) — informational; Phase 34 does NOT emit from SessionStart but the Pattern S2 envelope shape is reusable for any future SessionStart additions. Phase 33 D-04 / FRESH-06 rewording precedent for VERS-08's coexistence-by-hook-separation rewording.
  - `.planning/phases/31-picker-correctness/31-CONTEXT.md` D-11 — doc-amendment-first plan structure (Plan 01 amends REQUIREMENTS.md + ROADMAP.md before any code).
  - `.planning/phases/32-list-overhaul-skill-hint-audit/32-CONTEXT.md` — DEPLOY.ps1 multi-file sync pattern (informational; Phase 34 extends the sync step).

  ### Stop hook surface (D-01..D-06)
  - `src/owl/hook_idle.rs:14-43` — `run()` entry. Version-change emission site is AFTER perch resolution (lines 25-32) and AFTER the OWL_ECHO_COMMUNE guard (mirror lines 64-69 — guard MUST run before any version-change work), BEFORE `set_idle_ready` (line 39) and `spawn_echo_commune_if_live` (line 42).
  - `src/owl/hook_idle.rs:1-5` — comment asserts "Stop hooks have no hookSpecificOutput schema". This is research-relevant: Phase 34 uses `decision:"block"` + `reason` instead, which is a different output channel. Confirm semantics against `~/.claude/reference_docs/claude-code-hooks.md` during research.
  - `src/owl/hook_idle.rs:64-69` — `OWL_ECHO_COMMUNE` recursion guard. Reuse pattern verbatim for the version-change code path.

  ### Claude Code hooks reference (D-04 research target)
  - `~/.claude/reference_docs/claude-code-hooks.md` — Stop hook output schema. Confirm `decision:"block"` + `reason` propagates `reason` text to Claude as re-engagement context (not as a silent abort).

  ### Atomic write + sentinel path (VERS-01, D-05)
  - `src/common/owlery.rs:248` — `atomic_write_string(path, body)` (tmp + rename). Use verbatim for both Stop-hook sentinel write (D-05) and `version-remind` rollback write.
  - `src/common/owlery.rs:18-32` — `spt_home()` resolves `$SPT_HOME` env override or platform default. Sentinel lives at `spt_home().join("last-seen-version.json")` — sibling to `spt_home().join("owlery")` per VERS-01.

  ### Compiled-in version source (VERS-02)
  - `src/owl/poll.rs:75`, `src/owl/setup.rs:35`, `src/live/stop.rs:199` — existing call sites for `env!("CARGO_PKG_VERSION")`. Pattern for the Phase 34 hook: same `env!` macro at compile time; no runtime `plugin.json` read.

  ### CHANGELOG.md format + parser surface (D-08, D-09, D-11)
  - `https://keepachangelog.com/en/1.1.0/` — Keep-a-Changelog v1.1.0 spec. Reference for `## [X.Y.Z] - YYYY-MM-DD` H2 shape. Phase 34 parser regex: `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`. Reject `[Unreleased]`; ignore link reference lines.
  - `psyche.md` (repo root, embedded via `include_str!`) — placement precedent for repo-root markdown files synced to plugin meta dir. Phase 34's CHANGELOG.md follows this placement convention but is NOT embedded (D-11: runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`).

  ### DEPLOY.ps1 surfaces (D-10, D-11)
  - `docs/DEPLOY.ps1:154-176` — plugin.json version resolution + `-Bump` entry. Stub-injection logic (D-10) hooks in alongside the existing Cargo.toml sync at lines 224-269.
  - `docs/DEPLOY.ps1:171-292` — `-Bump` block. New CHANGELOG.md stub-append + unchanged-abort gate slots in after the Cargo.toml/plugin.json bump but before commit (lines 277-287).
  - `docs/DEPLOY.ps1` sync step (post-build) — exact lines vary; CHANGELOG.md sync target is `{Marketplace}/plugin/spt/CHANGELOG.md` per D-11.

  ### Existing version-text precedent
  - `src/owl/poll.rs:75` — `READY:{id} (spt v{})` format. Informational only; Phase 34 prompt text uses `v{old} → v{new}` shape from D-07/D-08.

  ### AskUserQuestion native "Other" field
  - AskUserQuestion automatically provides an "Other" free-text option. Phase 34 D-08's 4-option set explicitly chooses NOT to use "Other" because Remind / Skip already cover the non-print branches. If a user provides freeform input, Claude should map continuation phrases ("not now", "later", "skip for now") to `Remind me later` and dismissal phrases ("ignore", "stop asking") to `Skip` — matches Phase 33 D-08 / advisor mode pattern.

  </canonical_refs>

  <code_context>
  ## Existing Code Insights

  ### Reusable Assets
  - **`atomic_write_string`** (src/common/owlery.rs:248) — tmp + rename atomic write. Used verbatim for sentinel writes from both Stop hook (D-05) and `version-remind` subcommand.
  - **`spt_home()` + `owlery_dir()`** (src/common/owlery.rs:18-32, 65) — already resolve `$SPT_HOME` / platform default. Sentinel path = `spt_home().join("last-seen-version.json")`. No new path helper needed.
  - **`hook_output::parse_hook_stdin` + `find_perch_by_session` + `find_perch_by_parent_pid` + `find_perch_cached`** — perch-resolution path already proven across `hook_idle.rs`, `hook_prompt.rs`, `hook_check.rs`. Phase 34 reuses verbatim.
  - **`env!("CARGO_PKG_VERSION")`** — compile-time version macro already used in 3 existing call sites (poll.rs, setup.rs, stop.rs). Identical usage for Phase 34's hook check.
  - **`OWL_ECHO_COMMUNE` recursion guard** (hook_idle.rs:64-69) — copy/paste pattern for the version-change code path. Haiku subprocesses MUST NOT see a version-change block.
  - **AskUserQuestion native "Other"** — Phase 34 deliberately ignores; 4-option set is complete.
  - **DEPLOY.ps1 `-Bump` Cargo.toml/plugin.json sync** (DEPLOY.ps1:224-269) — atomic-rewrite-then-verify pattern. CHANGELOG.md stub-injection (D-10) follows the same regex-replace-then-verify shape.

  ### Established Patterns
  - **Doc-amendment-first plan structure** (Phase 31 Plan 01, Phase 32 Plan 01, Phase 33 Plan 01): when a phase's discovered design contradicts a requirement-as-written, Plan 01's first commit amends REQUIREMENTS.md + ROADMAP.md before any code. Phase 34 follows this for D-12.
  - **Pattern S2: hookSpecificOutput envelope** (plugin_session_start.rs:139-155 `emit_auto_pick`) — informational only for Phase 34; Stop uses `decision:"block"` + `reason` instead. Confirmation needed during research.
  - **`include_str!` precedent for repo-root markdown** (psyche.md): Phase 34 chooses NOT to follow this for CHANGELOG.md (per D-11: runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` instead). Rationale: changelog grows monotonically; binary should not bloat with history.
  - **Sync-then-verify in DEPLOY.ps1** (DEPLOY.ps1:256-273): each file write verifies via re-read. CHANGELOG.md sync follows the same pattern.
  - **Schema additivity** (Phase 26 PLAN-OVERVIEW Q4, Phase 31 D-12, Phase 33): no new info.json fields; no new on-disk artifacts beyond the sentinel; the `version-remind` subcommand is purely additive.

  ### Integration Points
  - **`src/owl/hook_idle.rs::run()`** — version-change emission lands between perch resolution (lines 25-32) and `set_idle_ready` (line 39). OWL_ECHO_COMMUNE guard MUST precede any new logic.
  - **`docs/DEPLOY.ps1` `-Bump` flow** — CHANGELOG.md stub-injection + unchanged-abort gate inserts after Cargo.toml verification (line 238), before the regex-replace block (line 247).
  - **`docs/DEPLOY.ps1` sync step** — `{Marketplace}/plugin/spt/CHANGELOG.md` copy slots alongside the existing binary/skills/hooks/plugin.json sync.
  - **`Cargo.toml` `[[bin]]` table** — new `version-remind` subcommand attaches as a `clap` subcommand on the existing `owl` binary; no new binary target.
  - **`tests/`** — version-change detection tests live in a new `tests/version_changelog.rs` integration test file (mirrors `tests/skill_hints.rs` from Phase 32 regression-guard pattern). Unit tests for the CHANGELOG.md parser co-locate with `src/owl/version_changelog.rs` (or wherever the parser lands).

  </code_context>

  <specifics>
  ## Specific Ideas

  - **Sentinel JSON shape (locked):** `{"version":"X.Y.Z"}`. Single field, no timestamp, no plugin name. Minimal for `serde_json::from_str` round-trip. Path: `$SPT_HOME/last-seen-version.json` (sibling to `owlery/` per VERS-01).
  - **CHANGELOG.md H2 regex (locked):** `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$`. Case-sensitive. Reject `## [Unreleased]` (no date matched). Ignore link reference lines (start with `[` not `## [`).
  - **Prompt question text template:**
    - Single-step: `SPT plugin updated v{old} → v{new}. Print changelog?`
    - Multi-step: `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`
    - Dates surface in the AUQ description / Claude's pre-question framing, not in the question line itself.
  - **4-option labels (locked, verbatim per D-08):**
    - `Yes, full changelog`
    - `Yes, highlights only`
    - `Remind me later`
    - `Skip`
  - **Highlights body framing (suggested wording for block-body or skill-body):** "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes. Append `See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail.`"
  - **`version-remind` subcommand signature (locked):** `$OWL version-remind <old version>` — bare positional arg (e.g. `$OWL version-remind 1.10.9`). Writes `{"version":"<old version>"}` to `$SPT_HOME/last-seen-version.json` via `atomic_write_string`. Exits 0 on success, 1 on missing arg or write failure. The `<old version>` arg is REQUIRED — the subcommand has no way to infer it; the block-body MUST instruct Claude to pass the literal `old` value from the block payload it received. `$OWL` is the env-var-injected owl-binary path per existing `/spt:*` skill conventions; subcommand also callable directly via `owl version-remind <old version>` for tests / scripted invocation.
  - **DEPLOY.ps1 stub format (locked):** Appended block at end of repo-root CHANGELOG.md:
    ```markdown
    ## [{NEW_VERSION}] - {YYYY-MM-DD}

    - TODO: changelog entry
    ```
    Date = bump-time `Get-Date -Format 'yyyy-MM-dd'`. The literal `TODO: changelog entry` marker is what the unchanged-abort gate scans for on the next `-Bump` invocation.
  - **DEPLOY.ps1 unchanged-abort message (suggested):** `ERROR: CHANGELOG.md contains 'TODO: changelog entry' for v{NEW_VERSION}. Fill in the entry and commit before re-running -Bump.`

  </specifics>

  <deferred>
  ## Deferred Ideas

  - **`owl changelog backfill` subcommand** — would auto-generate CHANGELOG.md from `git log --grep='chore: bump'`. Rejected for Phase 34 per D-09 (one-shot manual author at Plan 01). Re-evaluate if a future fresh-fork bootstrap is needed.
  - **`/spt:changelog` skill** — would own AUQ + render + remind flow as a skill body. Rejected for Phase 34 per overall scope (no new skill surface). Block-body inline instructions to Claude are sufficient. Re-evaluate if the inline-instructions pattern becomes brittle or if other phases want to compose with the changelog flow.
  - **Sentinel with reminder flag** — alternative to `version-remind`-rolls-back: sentinel could carry `{"version":"X.Y.Z","reminder":bool}` and version-remind sets `reminder:true` while keeping the new version. Rejected for Phase 34 to keep sentinel shape minimal (D-07). Could reconsider if a future requirement needs richer sentinel metadata.
  - **Telemetry for AUQ option distribution** — log how often each of the 4 options is picked. Useful for tuning highlights framing. Out of scope; no telemetry infrastructure in spt today.
  - **`Yes, highlights only` rubric formalization** — current decision is "Claude's discretion". A future phase could specify an exact rubric (e.g., "include all `## Added`, `## Fixed` H3 subsections; exclude `## Changed (internal)`"). Requires CHANGELOG.md to use H3 subsections per Keep-a-Changelog v1.1.0 — out of scope for Phase 34's minimal H2-per-version format.
  - **Block payload embedded full excerpt vs Claude-reads-at-yes-time** — D-04 / Claude's Discretion flagged this as planner choice. If planner picks embed-in-block, Phase 34 hook also parses the H2 sections during the hook run. Defer to RESEARCH.md.
  - **PreToolUse fallback if Stop's `decision:"block"` + `reason` doesn't propagate text to Claude** — flagged as research blocker in D-04 / Claude's Discretion. If confirmed broken, escalate during planning.
  - **UserPromptSubmit revisit** — the "walk-away mid-task" rationale (D-01) is the reason we pivoted from UserPromptSubmit to Stop. If a future requirement needs the prompt to land at session-start time (e.g., a critical security update notice), UserPromptSubmit becomes viable again with a different UX framing. Not in Phase 34.

  </deferred>

  ---

  *Phase: 34-version-change-changelog*
  *Context gathered: 2026-05-17*

  ### Research Findings
  # Phase 34: Version-Change Changelog - Research

  **Researched:** 2026-05-17
  **Domain:** Claude Code Stop hook semantics, CHANGELOG.md parsing, Windows atomic-write races, DEPLOY.ps1 stub-injection
  **Confidence:** HIGH (primary research blocker D-04 RESOLVED with multi-source agreement)

  ---

  ## Summary

  - **Q1 / D-04 GREEN:** Stop hook `decision:"block"` + `reason` propagates `reason` text to Claude as continuation instruction — Claude generates a new assistant turn responding to it. No silent abort. Multi-source agreement (Anthropic schema text "`reason` tells Claude how to proceed", disler hooks-mastery, claude.com blog, claudefa.st). The architectural choice in D-04 is sound; no fallback to PreToolUse / UserPromptSubmit is required.
  - **Q2 nuance:** Claude CAN invoke AskUserQuestion immediately on re-engagement (the block IS the new-turn trigger; no user prompt needed first). The `reason` text drives Claude's response, and Claude is free to call any tool — exactly the pattern that "stop hook forces task completion" examples in the ecosystem rely on.
  - **Stop hook has NO `hookSpecificOutput` schema** — confirmed. Only top-level `decision` + `reason` + universal fields (`continue`, `stopReason`, `suppressOutput`, `systemMessage`). The existing `hook_idle.rs:1-5` comment is accurate. Block-body content MUST live inside `reason` verbatim. Payload size budget: `reason` is a single string; no documented hard cap but practical limit aligns with the 10,000-char JSON-output cap. Our payload is tiny (a few hundred chars).
  - **`stop_hook_active` infinite-loop guard:** Industry-standard `Stop` input flag. Set to `true` on Claude's re-engagement after a previous block in the same continuation chain. Our hook MUST short-circuit on `stop_hook_active==true` regardless of version mismatch, as a defensive guard. With D-05 sentinel pre-write the version-mismatch path naturally won't re-fire, but the guard prevents pathological cases (e.g., sentinel write failure → mismatch persists → infinite block loop).
  - **`$CLAUDE_PLUGIN_ROOT` confirmed available** to plugin-bundled hooks at invocation. Documented in Claude Code hooks reference.
  - **Plan boundary recommendation:** 3 plans, refined boundary below (Q8). Plan 01 = amendment-commit + CHANGELOG.md genesis + DEPLOY.ps1 (doc/script only, no Rust). Plan 02 = Rust hook + sentinel + parser + `version-remind` subcommand + unit tests. Plan 03 = block-body authoring + integration test on synthesized version transition.

  **Primary recommendation:** Proceed with the design exactly as captured in CONTEXT.md D-01..D-12. The single research blocker (D-04) resolves favorably; no architectural pivot required.

  ---

  ## User Constraints (from CONTEXT.md)

  ### Locked Decisions (D-01..D-12 — verbatim authority)

  - **D-01:** Stop hook (`hook_idle.rs`), not SessionStart.
  - **D-02:** Predicate = `owl_id` resolves in `hook_idle.rs::run()` (piggyback existing perch resolution).
  - **D-03:** Recursion guard reuses `OWL_ECHO_COMMUNE` env-var skip (hook_idle.rs:64-69 pattern).
  - **D-04:** Block emission via top-level `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"`. RESEARCH-CONFIRMED.
  - **D-05:** Sentinel write site = same `run()` top, BEFORE returning the block. Atomic via `atomic_write_string`. Default behavior: writes the NEW version. Rollback only via explicit `version-remind`.
  - **D-06:** Fire-once cadence = once per version transition (sentinel pre-write enforces this).
  - **D-07:** Payload shape = `{old, new, old_date, new_date, step_count}`.
  - **D-08:** AskUserQuestion 4-option shape: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Block body MUST surface the exact `$OWL version-remind <old>` invocation string verbatim.
  - **D-09:** Repo-root `CHANGELOG.md` authored one-shot at Plan 01 start from `git log --grep='chore: bump'`. No `owl changelog backfill` subcommand.
  - **D-10:** Forward maintenance = DEPLOY.ps1 `-Bump` stub injection + commit gate (combined: append `TODO: changelog entry` stub uncommitted; next `-Bump` aborts if marker still present).
  - **D-11:** DEPLOY.ps1 sync target = `{Marketplace}/plugin/spt/CHANGELOG.md` (sibling to plugin.json). Resolves at hook time via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.
  - **D-12:** Amend VERS-02, VERS-04, VERS-06, VERS-08, VERS-09 + ROADMAP.md SC1/SC2/SC5 per the rewordings in CONTEXT.md.

  ### Claude's Discretion (planner picks)

  - `<spt-version-changelog>` block exact XML shape (planner picks JSON-as-text vs child elements).
  - Block payload: embed full excerpt vs Claude-reads-at-yes-time (researcher leans toward Claude-reads, see Q11 below).
  - CHANGELOG.md parser regex tightness (`[Unreleased]` reject, link reference lines ignored) — locked in test corpus below.
  - `Yes, highlights only` extraction policy framing — final wording in Plan 02 or 03.
  - Plan split (3 plans) — final boundary at planner's discretion; researcher recommendation in Q8.

  ### Deferred Ideas (OUT OF SCOPE)

  - `owl changelog backfill` subcommand
  - `/spt:changelog` skill
  - Sentinel with reminder flag (richer schema)
  - Telemetry for AUQ option distribution
  - `Yes, highlights only` rubric formalization (H3 subsections)
  - PreToolUse fallback (not needed — D-04 confirmed)
  - UserPromptSubmit revisit
  - Block payload embedded full excerpt — researcher recommends NOT embedding (see Q11)

  ---

  ## Phase Requirements

  | ID | Description | Research Support |
  |----|-------------|------------------|
  | VERS-01 | `$SPT_HOME/last-seen-version.json` sentinel sibling to `owlery/` | `src/common/owlery.rs:18-32` spt_home resolution verified; sentinel path = `spt_home().join("last-seen-version.json")` |
  | VERS-02 | Stop hook compares `env!("CARGO_PKG_VERSION")` against sentinel (AMENDED per D-12) | hook_idle.rs:14-43 emission site verified; CARGO_PKG_VERSION precedent at poll.rs:75, setup.rs:35, stop.rs:199 |
  | VERS-03 | First-install silent write | Sentinel-missing path: read returns None/Err → atomic_write_string current version, return without block emission |
  | VERS-04 | AskUserQuestion 4-option shape (AMENDED per D-12) | Block-body inline instructions to Claude; AUQ invoked by Claude on re-engagement |
  | VERS-05 | Atomic write via `atomic_write_string` | Verified at src/common/owlery.rs:248; tmp + rename; NTFS same-volume safe |
  | VERS-06 | Multi-version skip (AMENDED per D-12) — 4-option AUQ applies uniformly | `step_count` field in payload; Yes-full renders all H2 sections verbatim |
  | VERS-07 | Repo-root `CHANGELOG.md` synced via DEPLOY.ps1 to plugin meta dir | DEPLOY.ps1 sync step extended; resolves to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` per D-11 |
  | VERS-08 | AUTO-03 vs VERS-04 coexistence (AMENDED per D-12) — separate hooks | AUTO-03 is SessionStart (plugin_session_start.rs); VERS-04 is Stop (hook_idle.rs). No collision possible. |
  | VERS-09 | Single write site, no in-handoff race (AMENDED per D-12) | hook_idle.rs::run() top after perch resolution + OWL_ECHO_COMMUNE guard. Stop hooks do not fire mid-handoff. |

  ---

  ## Architectural Responsibility Map

  | Capability | Primary Tier | Secondary Tier | Rationale |
  |------------|-------------|----------------|-----------|
  | Version-mismatch detection | Rust hook (`hook_idle.rs`) | — | Compile-time `env!("CARGO_PKG_VERSION")` is the only handoff-safe source per Out-of-Scope §3 |
  | Sentinel read/write | Rust hook + `version-remind` subcommand | `atomic_write_string` (owlery.rs:248) | Both write sites share the same helper; rollback is a discrete subcommand for explicit user intent |
  | CHANGELOG.md parse | Rust hook | — | "Rust hook does it once" chosen explicitly in Discussion Log Round 2 |
  | Block payload composition | Rust hook | — | Hook builds `{old, new, old_date, new_date, step_count}` from parser output; embeds verbatim into `reason` |
  | AUQ render + branching | Claude (block-body instructions) | — | Stop hook has no AUQ-emission channel; Claude invokes AUQ on re-engagement |
  | `Yes, full changelog` render | Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` | — | See Q11 recommendation — block payload stays small |
  | `Yes, highlights only` synthesis | Claude (discretion + framing) | — | D-08; no fixed rubric |
  | `Remind me later` rollback | `$OWL version-remind <old>` subcommand | `atomic_write_string` | Bare positional arg; ~10-line implementation |
  | CHANGELOG.md authorship (genesis) | Human (Plan 01 step 1) | git log skeleton | One-shot, never auto-regenerated |
  | Forward CHANGELOG.md maintenance | DEPLOY.ps1 `-Bump` stub + commit gate | Human fills | Combined enforcement: stub-append + unchanged-abort |

  ---

  ## Standard Stack

  ### Core

  | Library | Version | Purpose | Why Standard |
  |---------|---------|---------|--------------|
  | `serde_json` | (existing, bundled with `preserve_order`) | Sentinel JSON read/write | Already in tree; minimal `{"version":"X.Y.Z"}` shape needs only string round-trip |
  | `clap` | (existing) | `version-remind` subcommand registration | Already in tree; bare positional arg slots in alongside existing owl subcommands |

  ### Supporting

  | Library | Purpose | When to Use |
  |---------|---------|-------------|
  | `std::fs::read_to_string` | CHANGELOG.md slurp at hook time | One-time read; file ~kB scale |
  | `std::env::var` | `$CLAUDE_PLUGIN_ROOT` resolution | Hook fallback path: walk up from hook script if env var unset (defensive) |

  ### Alternatives Considered

  | Instead of | Could Use | Tradeoff |
  |------------|-----------|----------|
  | Manual H2 regex parser | `pulldown-cmark` markdown crate | Adds dep weight; Phase 34 H2-only parsing is ~10 LOC of regex — overkill |
  | `include_str!` embed CHANGELOG.md | Runtime read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` | Explicitly rejected in Discussion Round 1: binary bloat with history |
  | `owl changelog backfill` subcommand | One-shot manual author | Discussion Round 1: deferred (D-09) |

  **Verified versions:** No new crates required. All work uses already-installed `serde_json`, `clap`, and `std` — no version drift risk.

  ---

  ## Per-Question Findings

  ### Q1 (CRITICAL): Stop hook `decision:"block"` + `reason` propagation

  **Status:** GREEN — RESOLVED.

  **Finding:** When a Stop hook emits `{"decision":"block","reason":"..."}`, the `reason` text is propagated to Claude as continuation instruction. Claude does NOT silently abort — Claude generates a new assistant turn responding to the reason text, exactly as if the reason had been delivered as a system message instructing Claude to continue working.

  **Sources:**
  - Anthropic's hooks reference `~/.claude/reference_docs/claude-code-hooks.md` (lines 389-399): Stop hook output is `{"decision":"block","reason":"Reason to block completion"}` (note the schema text positions `reason` as actionable instruction).
  - [Claude Code Hooks Mastery (disler)](https://github.com/disler/claude-code-hooks-mastery): "Yes, when `decision: "block"` is returned with a reason field in the Stop hook, the reason text is fed back to Claude as a continuation instruction." `[CITED]`
  - [Anthropic blog: How to configure hooks](https://claude.com/blog/how-to-configure-hooks): documents `decision:"block"` with `reason` as "shown to Claude" `[CITED]`
  - [Claudefa.st Stop Hook guide](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement): "When you use `decision: block` in a Stop hook, it automatically prompts Claude with the reason provided, which is crucial because the reason field is fed back to Claude" `[CITED]`
  - [Steve Kinney AI Tools course](https://stevekinney.com/courses/ai-development/claude-code-hook-control-flow): confirms same mechanic
  - Practical evidence: the ecosystem "force task completion" pattern (e.g. "must pass tests before stopping") relies on this exact propagation. If `reason` did not reach Claude, that entire pattern category would be inoperative — and it is documented as working across multiple guides.

  **Rendered shape on Claude's side:** Claude sees the reason text as the new continuation context. The block IS the new-turn trigger. No user prompt is required. Claude's response then renders the block body inline, then proceeds to call AskUserQuestion (matching the pattern other Stop-hook-driven flows use).

  **Counter-evidence considered:** One WebFetch response claimed the reason is "shown to the user only, not fed back to Claude" — this was anomalous and contradicted by 4+ other sources including the official hooks reference. Discounted as parsing artifact.

  **No fallback required.** PreToolUse and UserPromptSubmit alternatives in Deferred Ideas remain dormant.

  ### Q2: AskUserQuestion invocation from blocked Stop context

  **Status:** GREEN.

  **Finding:** Once Claude re-engages with the `reason` text in context, Claude can immediately invoke AskUserQuestion in the same turn — without waiting for a new user prompt. The block IS the new-turn trigger; the new turn is bounded by Claude's response, which can include any tool call.

  **Sources:**
  - Inference from Q1: if reason drives Claude's new turn, Claude is free to invoke tools per the normal assistant-turn model.
  - Ecosystem pattern: "stop hook → forced continuation → tool call → new stop hook" loop is well-attested in the disler/claudefa.st examples. The `stop_hook_active` flag exists precisely because Claude can and will fire tools in the forced-continuation turn.

  **Implication for D-08:** The block body's inline instructions to "render the AUQ now with these 4 options" will be honored on the same turn — AUQ fires immediately, before any new user message.

  **Caveat:** One Anthropic-docs WebFetch snippet suggested "Claude would need a new user prompt OR the hook to provide additionalContext" — but since the Stop hook does NOT support `additionalContext` (Q3) and the ecosystem clearly demonstrates Claude responding to `reason`, the practical conclusion is that `reason` IS the new context channel for Stop hooks. The disler/claudefa.st consensus wins.

  ### Q3: Stop hook output schema — `hookSpecificOutput.additionalContext`?

  **Status:** Confirmed NEGATIVE.

  **Finding:** Stop hook output supports ONLY top-level `decision` + `reason` + universal fields (`continue`, `stopReason`, `suppressOutput`, `systemMessage`, `terminalSequence`). NO `hookSpecificOutput` envelope. NO `additionalContext` field.

  **Sources:**
  - `~/.claude/reference_docs/claude-code-hooks.md` lines 389-399 (Stop section) — only `decision` + `reason` documented.
  - Anthropic hooks reference page (WebFetch verified): "Stop is explicitly listed as supporting only `decision` and `reason` at the top level—not `hookSpecificOutput`."
  - Existing `src/owl/hook_idle.rs:1-5` comment is accurate and predates our research.

  **Implication:** ALL block-body content (the `<spt-version-changelog>` XML, the AUQ instructions, the `$OWL version-remind <old>` invocation string) MUST live inside the single `reason` string. This is fine — payload is tiny, well under any practical limit.

  ### Q4: `$CLAUDE_PLUGIN_ROOT` availability inside Stop hook process

  **Status:** GREEN.

  **Finding:** `$CLAUDE_PLUGIN_ROOT` is set by Claude Code on the spawned hook process for ALL plugin-bundled hooks. Documented in the hooks reference (line 607) and verified via authoritative Anthropic docs.

  **Sources:**
  - `~/.claude/reference_docs/claude-code-hooks.md` line 607: "`$CLAUDE_PLUGIN_ROOT` -- Plugin directory" listed under "Environment Variables Available in Hooks".
  - Anthropic hooks page (WebFetch verified): "Both forms support the same path placeholders, and both export them as the environment variables `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, and `CLAUDE_PLUGIN_DATA` on the spawned process."

  **Implication:** Hook resolves CHANGELOG.md path as:
  ```rust
  let plugin_root = std::env::var("CLAUDE_PLUGIN_ROOT")
      .map_err(|_| "CLAUDE_PLUGIN_ROOT must be set by Claude Code")?;
  let changelog_path = std::path::Path::new(&plugin_root).join("CHANGELOG.md");
  ```

  **Defensive fallback:** If `$CLAUDE_PLUGIN_ROOT` is unset (e.g., direct invocation in tests, or theoretical CC bug), the hook should fail soft — skip emission, do not panic. Tests cover both branches.

  ### Q5: `$OWL` env var availability inside AUQ-renderer Claude turn

  **Status:** GREEN.

  **Finding:** `$OWL` is injected into `~/.claude/settings.json` by SPT's SessionStart hook (per CLAUDE.md §"Env vars: `$OWL` and `$LIVE` are auto-injected into `~/.claude/settings.json` at SessionStart"). Settings env vars persist for the full session, across all turns — Bash tool invocations resolve `$OWL` to the absolute owl.exe path.

  **Sources:**
  - `CLAUDE.md` §Technology Stack: "Env vars: `$OWL` and `$LIVE` are auto-injected into `~/.claude/settings.json` at SessionStart."
  - Existing skill convention: every `/spt:*` skill (e.g., `commune/SKILL.md`, `signoff/SKILL.md`) uses `$OWL deliver` / `$LIVE pick-spec` without first having to resolve the path — confirms the env var is honored at tool-invocation time.

  **Implication:** Claude can call `$OWL version-remind 1.10.9` in a Bash tool invocation directly. The block body should surface the exact invocation string verbatim per D-08.

  ### Q6: `atomic_write_string` Windows semantics under concurrent writers

  **Status:** GREEN — acceptable for our use case.

  **Finding:** Rust's `std::fs::rename` on Windows uses `MoveFileEx` with `MOVEFILE_REPLACE_EXISTING` semantics. On NTFS same-volume renames (our case — both source `.tmp` and destination live under `$SPT_HOME`), the rename is atomic with respect to the file pointer: readers either see the old file or the new file, never a torn read.

  **Concurrent writers behavior:** Last-writer-wins. If two Stop hooks race to write `last-seen-version.json` (e.g., two concurrent spt-bearing agents both transitioning v1.10.9 → v1.10.10 simultaneously), both write the same value `{"version":"1.10.10"}` — the final on-disk content is identical regardless of which rename "won". No torn reads possible.

  **Race-condition window:** Between the two `fs::write(&tmp, body)?` calls and the two `fs::rename` calls, there is a window where both `.tmp` files exist. The temp filename is shared (`last-seen-version.json.tmp` — see `atomic_write_string` source at owlery.rs:249) which CAN cause the second writer to fail with `ERROR_SHARING_VIOLATION` or overwrite the first's `.tmp`. Per existing call-site pattern (`write_last_commune_epoch` in owlery.rs:264) this is treated as best-effort: errors are swallowed at the caller layer. Our Phase 34 hook should match this — wrap `atomic_write_string` in a best-effort pattern; on failure, log to stderr and continue. Worst case: sentinel doesn't update this cycle → block re-fires on next Stop in same session (degraded but not broken).

  **Sources:**
  - `src/common/owlery.rs:238-247` — existing comment asserts NTFS+POSIX same-volume atomic rename.
  - [Rust std::fs::rename docs](https://doc.rust-lang.org/std/fs/fn.rename.html) — Windows uses MoveFileEx.
  - [Microsoft Learn: MoveFileEx + MOVEFILE_REPLACE_EXISTING + NTFS + same volume = atomic](https://learn.microsoft.com/en-us/archive/msdn-technet-forums/449bb49d-8acc-48dc-a46f-0760ceddbfc3) — confirmed atomic for our use case.
  - [rust-lang/rust PR#131072](https://github.com/rust-lang/rust/pull/131072) — Recent rust now uses POSIX rename semantics on Windows 1601+ via `FileRenameInfoEx`.

  **Test coverage required:** Unit test that simulates double-write and asserts final content is correct. Not a race test (impossible to deterministically reproduce on a unit-test timeline), just a sequential equivalence check.

  ### Q7: DEPLOY.ps1 stub-injection idempotency

  **Status:** GREEN with abort-gate design locked.

  **Finding:** Per D-10 the design is "stub + commit gate": first `-Bump` appends the stub uncommitted; next `-Bump` aborts if the unchanged TODO marker is still present. This is the correct semantics. The DEPLOY.ps1 existing patterns at lines 199-238 (refuse-on-dirty-plugin.json and refuse-on-dirty-Cargo.toml) provide the precedent template.

  **Mapping to DEPLOY.ps1 structure:**

  Insertion site:
  - **Bump abort gate:** Insert at lines 199-222, after the dirty-plugin.json / dirty-Cargo.toml refuse-on-dirty gates. New check: `if (CHANGELOG.md contains "TODO: changelog entry") { Write-Host "ERROR:..."; exit 1 }`. The scan is a substring or regex match for the literal `TODO: changelog entry`.
  - **Stub injection:** Insert at lines 277-287, after Cargo.toml/plugin.json regex-rewrites (line 274) but BEFORE the `git add` + `git commit` (lines 279-282). The new stub MUST be appended to repo-root `CHANGELOG.md` AFTER the bump verification (lines 257-274) succeeds. Critically: `git add` MUST NOT stage the CHANGELOG.md — the stub stays in working-tree only. The commit at line 281 should explicitly list `plugin.json` and `Cargo.toml` (already does — line 279) and NOT add CHANGELOG.md to the path list.

  **Idempotency property:** A user running `-Bump` twice without filling the CHANGELOG entry:
  1. First run: bumps Cargo.toml/plugin.json, commits, appends stub to CHANGELOG.md (unstaged, uncommitted).
  2. Second run: hits the abort gate at the dirty-state check OR the new TODO-marker check — depending on whether the user committed Cargo.toml/plugin.json from run 1 (they did; we commit it ourselves), so the dirty-state check passes; the TODO-marker check then fires and aborts.

  **Wait — there's a gap:** Between run 1 and run 2, the user may have edited CHANGELOG.md AND committed it. If they committed it without filling in real prose, the TODO marker would still be present. Our check would still fire (correct). If they committed it WITH real prose, the TODO marker is gone, the check passes, and the next `-Bump` proceeds (correct). The gate works in all branches.

  **Edge case:** If the user manually deletes the stub WITHOUT committing real content, the next `-Bump` sees no TODO marker but ALSO no entry for `NewVersion`. Should we check for both? Recommendation: also check that an H2 line matching `## [<NewVersion>] - YYYY-MM-DD` exists in CHANGELOG.md. If neither the stub nor a real entry for the bumped version is present, abort. This catches the "user deleted the stub but forgot to write a real entry" case.

  **Recommended check shape** (insert at ~line 215):
  ```powershell
  # Check that the previous bump's CHANGELOG entry was filled in.
  # A "TODO: changelog entry" marker means the previous bump's stub is still pending.
  $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  if (Test-Path $ChangelogPath) {
      $ChangelogContent = Get-Content $ChangelogPath -Raw
      if ($ChangelogContent -match 'TODO: changelog entry') {
          Write-Host "ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump." -ForegroundColor Red
          Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
          exit 1
      }
  }
  ```

  ### Q8: Plan boundary recommendation

  **Status:** RECOMMEND 3 plans, with refined boundary below.

  **Recommendation:**

  **Plan 01 — Doc Amendment + CHANGELOG.md Genesis + DEPLOY.ps1 (NO Rust)**
  - Step 1 (FIRST COMMIT): REQUIREMENTS.md + ROADMAP.md amendment per D-12 (mirrors Phase 31 / Phase 33 doc-amendment-first pattern).
  - Step 2: Author `CHANGELOG.md` at repo root by hand from `git log --grep='chore: bump'` skeleton (38 versions surfaced; see Appendix A below). Single commit with curated prose per version.
  - Step 3: Extend `docs/DEPLOY.ps1`:
    - Sync step: copy repo-root `CHANGELOG.md` to `{Marketplace}/plugin/spt/CHANGELOG.md` alongside the existing binary/skills/hooks/plugin.json sync (~line 368, new `Invoke-Mutation` block).
    - `-Bump` flow: insert TODO-marker abort gate at ~line 215 (after dirty-state checks); insert stub append at ~line 287 (after Cargo.toml regex-rewrite verification, before `git add`). Critically: the stub append MUST NOT stage CHANGELOG.md.
  - Verification: dry-run `-Bump patch` on a test branch; confirm stub appended, not committed; confirm `-Bump patch` again aborts with the new error.

  **Plan 02 — Rust Hook + Sentinel + Parser + `version-remind` Subcommand (NO Claude-side block-body)**
  - Author `src/owl/version_changelog.rs` (new module): sentinel read/write helpers, CHANGELOG.md parser, payload builder. Co-located unit tests for parser corpus (Q3 below).
  - Wire into `src/owl/hook_idle.rs::run()`: after perch resolution (lines 25-32), after `OWL_ECHO_COMMUNE` guard (mirrored from spawn_echo_commune_if_live:64-69), BEFORE `set_idle_ready` (line 39). On version-mismatch: emit `decision:"block"` + `reason:"<spt-version-changelog>...</spt-version-changelog>"` via stdout JSON, then exit 0. Sentinel write happens at hook-entry top, BEFORE the block emission (per D-05).
  - `stop_hook_active` guard: parse from stdin JSON; short-circuit if true.
  - Add `version-remind <old>` subcommand to clap subcommand dispatch in `src/main.rs` (or wherever owl's bin entry is wired). ~10-line implementation: parse positional `<old>`, atomic_write_string `{"version":"<old>"}` to sentinel path, exit 0.
  - Unit tests in `src/owl/version_changelog.rs::tests`: parser happy path, parser reject `[Unreleased]`, parser ignore link references, sentinel read missing → None, sentinel read present → Some, sentinel write atomic correctness.
  - Hook-emission integration tests in `tests/version_changelog.rs` (NEW file): version-mismatch detection (single-step, multi-step, first-install silent, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard skip, no-perch guard skip, stop_hook_active guard skip); `version-remind` rollback round-trip.

  **Plan 03 — Block Body Authoring + End-to-End Integration Test**
  - Author the block-body text (the content inside `reason`'s `<spt-version-changelog>` XML). Includes: the 4-option AUQ instructions verbatim, the highlights-extraction framing wording (D-08), the exact `$OWL version-remind <old>` invocation string with `{old}` slot, the pointer to `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` for `Yes, highlights only` and `Yes, full changelog` paths.
  - This text lives in the Rust payload-builder helper as a `format!` template. Plan 03 owns the wording; Plan 02 owns the scaffold.
  - End-to-end integration test in `tests/version_changelog.rs`: synthesized version transition (sentinel set to v1.10.9, CARGO_PKG_VERSION mock'd or env-var-overridden to v1.10.10), assert stdout JSON has `decision:"block"` and `reason` matches expected XML envelope (snapshot-style assertion via golden fixture in `tests/golden/`).
  - UAT: manually run `cargo build --release`, deploy via DEPLOY.ps1, manually trigger a version mismatch, verify the AUQ surfaces in a real Claude Code session.

  **Rationale for boundary:**
  - Plan 01 is doc/script only — no Rust, no test framework dependency. Can ship independently.
  - Plan 02 is Rust internals — testable in isolation. Subcommand pairs with the hook because both share the parser+sentinel module; splitting would force the subcommand to duplicate the path-helper.
  - Plan 03 is the user-facing surface — wording iterations don't churn Plan 02's scaffold. Integration test lives here because it depends on the block-body text Plan 03 owns.

  **Dependency graph:** Plan 01 → Plan 02 → Plan 03 (linear). Plan 02 cannot start until CHANGELOG.md exists (parser needs sample data for unit tests). Plan 03 cannot start until Plan 02's payload builder exists.

  **Alternative considered: 4 plans.** Splitting CHANGELOG.md genesis from amendment + DEPLOY.ps1 (separate Plan 0/Plan 1). Rejected — genesis is a single commit of one file; it doesn't warrant a separate plan boundary.

  ### Q9: Test fixture for synthesized version transition

  **Status:** RECOMMEND env-var override seam.

  **Recommendation:** Introduce a test-only env var `SPT_OVERRIDE_PKG_VERSION` (cfg-gated under `#[cfg(test)]` OR runtime-read). When set and non-empty, the version-changelog module reads it INSTEAD of `env!("CARGO_PKG_VERSION")`. Otherwise falls through to `env!`.

  **Why this approach:**
  - **Least invasive:** No rebuild required per test case. A single binary can simulate any version transition.
  - **Matches existing patterns:** `SPT_HOME` env override (owlery.rs:18-32) is the established seam pattern in this codebase. `SPT_OVERRIDE_PKG_VERSION` is symmetric.
  - **No production risk:** Document explicitly as "test-only; never set in production". Optionally gate behind a runtime check that emits a stderr warning if set in non-test contexts.

  **Alternatives rejected:**
  - **Mock seam (trait + impl injection):** Overkill for a single `env!` call site. Rust mocking ergonomics are poor for compile-time macros.
  - **Helper that writes sentinel to temp `$SPT_HOME`:** Already needed for sentinel-side tests, but doesn't solve the CARGO_PKG_VERSION side — the binary version is baked in at compile time.
  - **Per-test custom binary build:** Slow, flaky, increases test-runtime cost dramatically.

  **Implementation seam (in `version_changelog.rs`):**
  ```rust
  fn current_version() -> &'static str {
      // Test seam: SPT_OVERRIDE_PKG_VERSION lets tests simulate any version
      // without rebuilding. Production code paths see env!("CARGO_PKG_VERSION").
      if let Ok(v) = std::env::var("SPT_OVERRIDE_PKG_VERSION") {
          if !v.is_empty() {
              // SAFETY: leak is bounded by test count; acceptable for test-only seam.
              return Box::leak(v.into_boxed_str());
          }
      }
      env!("CARGO_PKG_VERSION")
  }
  ```

  Note: the `Box::leak` is intentional for the test-only path; in practice tests setting this env var run a finite number of times per binary lifecycle. If the leak offends, the function can return `String` and accept the allocation cost on every call.

  ### Q10: Keep-a-Changelog 1.1.0 — `[Unreleased]` and format details

  **Status:** GREEN with `[Unreleased]` omitted from our format.

  **Findings:**

  1. **`## [X.Y.Z] - YYYY-MM-DD` format is canonical.** Verified at [keepachangelog.com/en/1.1.0](https://keepachangelog.com/en/1.1.0/).
  2. **`[Unreleased]` section is conventional but not strictly required.** The spec recommends it for projects with active development; we explicitly omit it (D-09 says we author from `git log` retroactively — no current "unreleased" state at genesis).
  3. **Spec recommends categories `Added/Changed/Deprecated/Removed/Fixed/Security`.** Phase 34's parser doesn't care about H3 subsections — it only locates H2 version headers. The body content between H2 boundaries is preserved verbatim for the `Yes, full changelog` path. Our author MAY use H3 subsections in prose, MAY use plain bullet lists, MAY use prose paragraphs — parser is body-content-agnostic.
  4. **Link references at file bottom (`[1.10.10]: https://...`)** are valid per spec but irrelevant for our use case (we don't render comparison links). Parser MUST ignore them by virtue of the `^## \[` anchor (link refs start with `[`, not `## [`).

  **Parser implications:** The regex `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$` rejects `[Unreleased]` (no version match) and ignores link references (no `## ` prefix). No additional spec compliance needed.

  ### Q11: Block payload — embed full excerpt vs Claude-reads-at-yes-time

  **Status:** RECOMMENDATION: Claude-reads-at-yes-time.

  **Rationale:**

  | Approach | Block size | Hook complexity | Token cost | Multi-version impact |
  |----------|-----------|-----------------|------------|---------------------|
  | Embed full excerpt in block | Large (every visible session pays for all transition prose) | Rust hook parses + concatenates H2 sections | High on every transition, even if user picks Skip | Worst with multi-version skip (e.g. 10 versions = 10x prose) |
  | Claude-reads-at-yes-time | Small (just `{old,new,old_date,new_date,step_count}` + invocation string) | Rust hook parses only H2 headers (cheap) | Low on Skip / Remind paths; cost paid only on Yes paths | Multi-version still cheap (one CHANGELOG.md read regardless of step count) |

  Block payload stays compact. Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` only when user picks `Yes, full changelog` or `Yes, highlights only`. Cost is bounded by user choice.

  **Block-body XML shape (recommendation):**
  ```xml
  <spt-version-changelog>
    <old>1.10.9</old>
    <new>1.10.10</new>
    <old_date>2026-05-15</old_date>
    <new_date>2026-05-16</new_date>
    <step_count>1</step_count>
    <changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>
    <invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>
    <instructions>
      Render an AskUserQuestion with question: "SPT plugin updated v1.10.9 → v1.10.10. Print changelog?"
      Options (exactly 4):
      - "Yes, full changelog": read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section
        where 1.10.9 &lt; version &lt;= 1.10.10, verbatim markdown.
      - "Yes, highlights only": read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md, extract 2-5 most impactful
        feature adds and fixes from the same range; prefer user-facing behavior changes;
        skip purely-internal refactors and test-only changes. Append: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."
      - "Remind me later": invoke Bash: `$OWL version-remind 1.10.9` — pass the literal old version.
      - "Skip": no action.
    </instructions>
  </spt-version-changelog>
  ```

  Child elements (not JSON-as-text) recommended. Rationale: Claude parses XML reliably; child elements are unambiguous; readable in transcripts; matches the existing Phase 33 `<spt-live-auto-pick>` pattern convention.

  `step_count` surfaces in the question text when `> 1` per D-07: `SPT plugin updated v1.10.5 → v1.12.0 (12 versions). Print changelog?`

  ---

  ## CHANGELOG.md Parser — Test Corpus

  | Test Case | Input | Expected |
  |-----------|-------|----------|
  | **Happy path: single-version H2** | `## [1.10.10] - 2026-05-16` | Match: version=1.10.10, date=2026-05-16 |
  | **Happy path: pre-1.0** | `## [0.9.3] - 2025-12-01` | Match: version=0.9.3 |
  | **Reject: Unreleased** | `## [Unreleased]` | No match |
  | **Reject: no date** | `## [1.10.10]` | No match |
  | **Reject: incomplete semver (no patch)** | `## [1.10] - 2026-05-16` | No match |
  | **Reject: incomplete semver (no minor)** | `## [1] - 2026-05-16` | No match |
  | **Reject: link reference line** | `[1.10.10]: https://github.com/org/repo/compare/v1.10.9...v1.10.10` | No match (no `## ` prefix) |
  | **Reject: H1 with same body** | `# [1.10.10] - 2026-05-16` | No match (single `#`) |
  | **Reject: H3 with same body** | `### [1.10.10] - 2026-05-16` | No match (three `#`) |
  | **Trailing whitespace tolerance** | `## [1.10.10] - 2026-05-16  ` (2 trailing spaces) | Researcher recommends: **accept** (loose anchor: `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})\s*$`). Reject only if extra non-whitespace content. |
  | **BOM tolerance** | `\u{feff}## [1.10.10] - 2026-05-16` | Match — parser strips BOM at file-read time |
  | **Edge: extra text after date** | `## [1.10.10] - 2026-05-16 (yanked)` | Researcher recommends: **reject** (strict-trailing-text rejection keeps parser deterministic; if a release is yanked, author should rewrite the H2 line or use a separate convention) |
  | **Edge: lowercase version** | `## [1.10.10] - 2026-05-16` | Match (case-sensitive comparison) |
  | **Edge: pre-release suffix** | `## [1.10.10-rc1] - 2026-05-16` | No match (our format is plain semver; pre-release out of scope) |
  | **Edge: empty H2** | `## ` | No match |
  | **Whitespace inside brackets** | `## [ 1.10.10 ] - 2026-05-16` | No match (strict bracket-hugging) |
  | **Date format variation** | `## [1.10.10] - 2026/05/16` | No match (ISO-8601 hyphen-only) |
  | **Range query (parser-driver level)** | Parser asked for all H2 sections between 1.10.5 (exclusive) and 1.10.7 (inclusive). File contains 1.10.5, 1.10.6, 1.10.7, 1.10.8. | Return body content of 1.10.6 and 1.10.7 only, concatenated. |
  | **Body content preservation** | H2 section body includes nested H3 headers, code fences, lists | Body returned verbatim (parser DOES NOT modify body content) |

  **Recommended regex:**
  ```rust
  const H2_RE: &str = r"^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$";
  ```

  Three capture groups for semver components plus three for date components. Multiline mode; case-sensitive. Strict ISO-8601 date format.

  **Parser API (in `src/owl/version_changelog.rs`):**
  ```rust
  pub struct ChangelogEntry {
      pub version: (u32, u32, u32),
      pub date: String, // "YYYY-MM-DD" verbatim
      pub body: String, // content between this H2 and next H2 / EOF, verbatim
      pub line_no: usize, // for error messages
  }

  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry>;
  pub fn sections_between(entries: &[ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&ChangelogEntry>;
  ```

  ---

  ## Integration Test Corpus (tests/version_changelog.rs)

  | Test | Setup | Assertion |
  |------|-------|-----------|
  | `first_install_silent_writes_sentinel` | Empty `$SPT_HOME`; bake `SPT_OVERRIDE_PKG_VERSION=1.10.10`; run hook | Sentinel written `{"version":"1.10.10"}`; stdout JSON contains no `decision:"block"` |
  | `single_step_mismatch_emits_block` | Sentinel `{"version":"1.10.9"}`; `SPT_OVERRIDE_PKG_VERSION=1.10.10`; CHANGELOG.md has both versions | stdout JSON `decision:"block"`, `reason` contains `<spt-version-changelog>` with `<old>1.10.9</old>` and `<new>1.10.10</new>` and `<step_count>1</step_count>`; sentinel now `1.10.10` |
  | `multi_step_mismatch_emits_block_with_step_count` | Sentinel `1.10.5`; PKG_VERSION `1.10.10`; CHANGELOG has 1.10.5-1.10.10 | `step_count` = 5 in payload; question text mentions "5 versions" |
  | `sentinel_equal_no_emit` | Sentinel `1.10.10`; PKG_VERSION `1.10.10` | No `decision:"block"`; sentinel unchanged (no write at all — write only on mismatch path) |
  | `owl_echo_commune_guard_skips_emission` | Mismatch present; `OWL_ECHO_COMMUNE=1` set | No `decision:"block"`; sentinel unchanged |
  | `no_perch_skips_emission` | Mismatch present; NO perch found (perch resolution returns None) | No `decision:"block"`; sentinel unchanged (per D-02 predicate) |
  | `stop_hook_active_skips_emission` | Mismatch present; stdin JSON has `stop_hook_active: true` | No `decision:"block"`; sentinel unchanged (defensive infinite-loop guard) |
  | `version_remind_rollback_round_trip` | Set sentinel to 1.10.10; invoke `owl version-remind 1.10.9`; assert sentinel now 1.10.9 | Sentinel content `{"version":"1.10.9"}` |
  | `version_remind_missing_arg_exits_1` | Invoke `owl version-remind` with no arg | exit code 1; stderr non-empty |
  | `version_remind_atomic_write` | Invoke `owl version-remind` twice in succession | No `.tmp` sibling left behind |
  | `changelog_parser_unit_corpus` | Feed parser the 18 test-corpus rows above | All pass per expected behavior |

  ---

  ## Don't Hand-Roll

  | Problem | Don't Build | Use Instead | Why |
  |---------|-------------|-------------|-----|
  | Atomic file write | Custom tmp+rename | `atomic_write_string` (owlery.rs:248) | Already battle-tested in 8+ call sites; NTFS+POSIX semantics encoded |
  | Spt-home path resolution | New env helper | `spt_home()` (owlery.rs:18-32) | Handles SPT_HOME override + platform default + create-if-missing |
  | Perch resolution | New session-id lookup | `find_perch_by_session` / `find_perch_by_parent_pid` (existing) | Same path proven across hook_idle.rs, hook_prompt.rs, hook_check.rs |
  | Markdown parsing | Full markdown crate | Regex H2 matcher (~10 LOC) | We only need H2 headers; body content is preserved verbatim |
  | Version-equality comparison | Semver crate | Tuple `(u32, u32, u32)` comparison | Plain semver only; no pre-release / build-metadata in our format |
  | JSON sentinel | Hand-roll serializer | `serde_json::to_string` / `from_str` | Single field; round-trip trivial |
  | Recursion guard | New env var | `OWL_ECHO_COMMUNE` (hook_idle.rs:64-69) | Reuse existing — same haiku-subprocess gate applies |
  | AUQ rendering in Rust | Try to emit AUQ JSON | Claude renders AUQ via block-body instructions | Stop hook has no AUQ-emission channel; Claude is the only AUQ surface |

  ---

  ## Common Pitfalls

  ### Pitfall 1: Sentinel write before block emission must NOT fail silently in ways that hide the mismatch

  **What goes wrong:** If `atomic_write_string` returns `Err` (e.g., disk full, perm denied) and the code swallows the error and proceeds to emit the block, the user sees the block, but the sentinel stays at the OLD value → next Stop fires again → infinite block loop (mitigated by `stop_hook_active` but still alarming).

  **Why it happens:** Existing best-effort pattern in `write_last_commune_epoch` (owlery.rs:264) swallows errors.

  **How to avoid:** Either (a) swallow the error AND skip emission if sentinel write fails (degraded UX: no notification this session; correct next session), OR (b) emit anyway AND rely on `stop_hook_active` to break the loop. Recommendation: (a) — fail silent is the established codebase idiom and matches the "best-effort sentinel" philosophy from Phase 18.8.

  **Warning signs:** Tests should include a "sentinel write fails (mock IO error)" case → assert no block emitted.

  ### Pitfall 2: `$CLAUDE_PLUGIN_ROOT` unset in non-plugin invocations

  **What goes wrong:** During tests or direct CLI invocation (`owl hook-idle`), `$CLAUDE_PLUGIN_ROOT` is not set. If the parser fails to find CHANGELOG.md, the block emission either crashes or emits a degraded payload.

  **Why it happens:** Tests bypass the Claude Code plugin invocation path.

  **How to avoid:** Block emission should ALSO require `$CLAUDE_PLUGIN_ROOT` to resolve to a real file. If the env var is unset OR the file is missing, treat as "no changelog available" → still emit the block but with a degraded `<changelog_path>NOT_RESOLVED</changelog_path>` AND skip the `step_count` / `old_date` / `new_date` fields. Claude's AUQ surface still works (with just version numbers, no dates). Better than crashing.

  **Warning signs:** Tests should explicitly include the "CLAUDE_PLUGIN_ROOT unset" branch.

  ### Pitfall 3: Parser ignores body content with `## ` at column 0 inside fenced code blocks

  **What goes wrong:** A CHANGELOG.md entry's body contains a markdown code block with `## [something]` inside (e.g., showing a doc example) → parser incorrectly matches it as an H2.

  **Why it happens:** Regex matches line-by-line without code-fence awareness.

  **How to avoid:** Either (a) accept this edge case as author responsibility (don't include `## [` at line start in code blocks), OR (b) track code-fence state across lines. Recommendation: (a) — Phase 34 CHANGELOG.md will be hand-authored prose; if we ever auto-generate, revisit. Document in code comment so future maintainers know.

  ### Pitfall 4: `stop_hook_active` semantics — we should respect it even though D-05 sentinel pre-write prevents normal re-fire

  **What goes wrong:** Edge case where sentinel write fails (Pitfall 1) — without `stop_hook_active` guard, hook would re-fire infinitely if write keeps failing.

  **Why it happens:** Sentinel write is fallible; `stop_hook_active` is Claude Code's documented infinite-loop guard.

  **How to avoid:** Parse `stop_hook_active` from stdin JSON. If true, short-circuit — exit 0 with no block emission, regardless of version mismatch. Belt-and-suspenders with the sentinel pre-write.

  ### Pitfall 5: DEPLOY.ps1 stub append at WRONG point causes git add to stage CHANGELOG.md

  **What goes wrong:** If stub append runs BEFORE `git add 'plugin/spt/.claude-plugin/plugin.json' 'Cargo.toml'` (DEPLOY.ps1 line 279), and a user later runs `git add -A` reflexively, CHANGELOG.md gets committed in a different commit and the unchanged-abort gate breaks (TODO marker now committed, not in working tree).

  **Why it happens:** Author confusion about where in the `-Bump` sequence to insert the stub.

  **How to avoid:** Stub append MUST happen AFTER `git commit` (line 281) — the bump commit excludes CHANGELOG.md. The stub then lives in working-tree only. Document this constraint with a code comment.

  ### Pitfall 6: Block payload's `step_count` is for SURFACE TEXT only — full-changelog body still reads ALL intermediate versions

  **What goes wrong:** Claude sees `step_count=5` and prints only 5 version headers without reading the full content from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`.

  **Why it happens:** Claude is being asked to surface a count, not a body.

  **How to avoid:** Block-body instructions for `Yes, full changelog` MUST explicitly say "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version ≤ {new}". The `step_count` is for the AUQ question text only, not the body-rendering surface.

  ---

  ## Runtime State Inventory

  | Category | Items Found | Action Required |
  |----------|-------------|------------------|
  | Stored data | New sentinel `$SPT_HOME/last-seen-version.json` — single field `{"version":"X.Y.Z"}`. **Pre-existing data:** none (new artifact in Phase 34). | Code edit: create read/write helpers in `version_changelog.rs`. No data migration needed (first-install silent path handles missing sentinel per VERS-03). |
  | Live service config | `$CLAUDE_PLUGIN_ROOT` resolves to `~/.claude/plugins/cache/cplugs/spt/<version>/` at runtime; CHANGELOG.md must be present at that path. | DEPLOY.ps1 sync step copies CHANGELOG.md alongside other plugin files (per D-11). Hook reads at invocation time. |
  | OS-registered state | None — no Task Scheduler, pm2, systemd, or launchd registrations involve Phase 34 artifacts. | None. |
  | Secrets/env vars | New test-only env var `SPT_OVERRIDE_PKG_VERSION` (test seam per Q9). `$OWL` env var dependency confirmed (per Q5). `$CLAUDE_PLUGIN_ROOT` confirmed (per Q4). | None — test seam is additive. |
  | Build artifacts | `target/release/owl.exe` rebuilds with new `version_changelog.rs` module. Marketplace copy via DEPLOY.ps1 (existing pattern). | Standard `cargo build --release` + DEPLOY.ps1 sync. |

  **The canonical question:** *After every file in the repo is updated, what runtime systems still have the old string cached, stored, or registered?*

  Phase 34 is greenfield (new artifact), not a rename. No legacy cached state needs migration. The only runtime-state interaction is the sentinel itself, which is created on first hook fire after deploy.

  ---

  ## Environment Availability

  | Dependency | Required By | Available | Version | Fallback |
  |------------|------------|-----------|---------|----------|
  | Rust toolchain (cargo) | Plan 02, Plan 03 build | ✓ | (existing) | — |
  | PowerShell 5.1+ | Plan 01 DEPLOY.ps1 changes | ✓ | (existing) | — |
  | `git` CLI | Plan 01 CHANGELOG genesis (`git log --grep`) | ✓ | (existing) | — |
  | `serde_json` crate | Sentinel JSON | ✓ | (existing in Cargo.toml) | — |
  | `clap` crate | `version-remind` subcommand | ✓ | (existing in Cargo.toml) | — |
  | Claude Code (`claude` CLI) | UAT verification of AUQ surface | ✓ | (existing) | Manual test via direct hook invocation possible but lower-fidelity |

  No missing dependencies. No blocking gaps.

  ---

  ## Validation Architecture

  ### Test Framework

  | Property | Value |
  |----------|-------|
  | Framework | Rust `cargo test` (built-in, no extra crate) |
  | Config file | `Cargo.toml` (existing) |
  | Quick run command | `cargo test --lib version_changelog` |
  | Full suite command | `cargo test` |

  ### Phase Requirements → Test Map

  | Req ID | Behavior | Test Type | Automated Command | File Exists? |
  |--------|----------|-----------|-------------------|-------------|
  | VERS-01 | Sentinel at `$SPT_HOME/last-seen-version.json` | unit | `cargo test sentinel_path` | ❌ Wave 2 (new module) |
  | VERS-02 | Compile-time CARGO_PKG_VERSION read; never reads plugin.json at runtime | unit + crate-wide audit | `cargo test current_version_uses_env_macro` + grep audit | ❌ Wave 2 |
  | VERS-03 | First-install silent write | integration | `cargo test --test version_changelog first_install_silent_writes_sentinel` | ❌ Wave 2 (new integration file) |
  | VERS-04 | 4-option AUQ shape (per D-12 amendment) | integration | `cargo test --test version_changelog single_step_mismatch_emits_block` | ❌ Wave 2 |
  | VERS-05 | Atomic write via `atomic_write_string` | unit | `cargo test sentinel_write_atomic` | ❌ Wave 2 |
  | VERS-06 | Multi-version uses same 4-option (per D-12) | integration | `cargo test --test version_changelog multi_step_mismatch_emits_block_with_step_count` | ❌ Wave 2 |
  | VERS-07 | DEPLOY.ps1 syncs CHANGELOG.md to plugin meta dir | manual (DEPLOY.ps1 dry-run) | `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun` | n/a — script |
  | VERS-08 | AUTO-03 + VERS-04 coexistence by hook separation | integration | manual UAT — both hooks present in fixture, neither collides | n/a — UAT |
  | VERS-09 | Single write site at hook-entry top | manual code review + grep audit | `grep -r "last-seen-version" src/` should yield 2 hits (hook + subcommand) | n/a — review |

  ### Sampling Rate
  - **Per task commit:** `cargo test --lib version_changelog` (parser unit tests; fast)
  - **Per wave merge:** `cargo test` (full suite including integration)
  - **Phase gate:** Full suite green + UAT against real Claude Code session

  ### Wave 0 Gaps
  - `src/owl/version_changelog.rs` — new module file with parser + sentinel helpers
  - `tests/version_changelog.rs` — new integration test file (mirrors `tests/skill_hints.rs` from Phase 32)
  - `tests/golden/version_changelog_block.txt` — golden fixture for block-body snapshot assertion (Plan 03)

  ---

  ## Security Domain

  | ASVS Category | Applies | Standard Control |
  |---------------|---------|-----------------|
  | V2 Authentication | no | n/a — local-only sentinel; no auth boundary |
  | V3 Session Management | no | n/a |
  | V4 Access Control | no | n/a — sentinel lives under user's own `$SPT_HOME` |
  | V5 Input Validation | yes | Parse `<old version>` arg in `version-remind`: validate against semver regex BEFORE writing sentinel. If not matching `^\d+\.\d+\.\d+$`, exit 1. Prevents arbitrary content injection into the sentinel JSON. |
  | V6 Cryptography | no | n/a — no secrets, no signing |

  ### Known Threat Patterns

  | Pattern | STRIDE | Standard Mitigation |
  |---------|--------|---------------------|
  | `version-remind` arg injection | Tampering | Validate arg matches `^\d+\.\d+\.\d+$` regex; reject any other input |
  | Sentinel path traversal | Tampering | `$SPT_HOME` resolution is authoritative; path is constructed via `spt_home().join("last-seen-version.json")` — no user input in the path |
  | CHANGELOG.md content injection | Tampering | Author-controlled file; content goes into block `reason` text → reaches Claude, but Claude is the only consumer and the format is markdown. No XSS / code-exec surface. |
  | `OWL_ECHO_COMMUNE` recursion guard bypass | DoS (loop) | Guard MUST come BEFORE version-changelog logic (D-03); mirror hook_idle.rs:64-69 exactly |

  **Argument validation MUST happen in the `version-remind` subcommand entry, BEFORE any filesystem write.**

  ---

  ## Code Examples (Verified Patterns)

  ### Atomic write of sentinel (Plan 02)

  ```rust
  // Source: src/common/owlery.rs:248 (existing helper)
  use crate::common::owlery::{atomic_write_string, spt_home};

  fn write_sentinel(version: &str) -> std::io::Result<()> {
      let path = spt_home().join("last-seen-version.json");
      let body = format!("{{\"version\":\"{}\"}}", version);
      atomic_write_string(&path, &body)
  }
  ```

  ### Sentinel read with first-install handling (Plan 02)

  ```rust
  // First-install: read returns None; caller writes current version silently.
  fn read_sentinel() -> Option<String> {
      let path = spt_home().join("last-seen-version.json");
      let content = std::fs::read_to_string(&path).ok()?;
      let v: serde_json::Value = serde_json::from_str(&content).ok()?;
      v.get("version")?.as_str().map(String::from)
  }
  ```

  ### Hook emission via stdout JSON (Plan 02)

  ```rust
  // Source: existing pattern in src/common/hook_output.rs
  fn emit_block(reason: &str) {
      let output = serde_json::json!({
          "decision": "block",
          "reason": reason,
      });
      println!("{}", output);
  }
  ```

  ### Stop hook stdin parsing (Plan 02 — extend existing parse_hook_stdin)

  ```rust
  // Stop hook input includes stop_hook_active per ecosystem convention
  #[derive(Deserialize)]
  struct StopHookInput {
      #[serde(default)]
      session_id: Option<String>,
      #[serde(default)]
      stop_hook_active: bool,
  }
  ```

  ### `version-remind` subcommand body (Plan 02)

  ```rust
  // New subcommand. Bare positional arg per D-08.
  pub fn version_remind(old: &str) -> std::process::ExitCode {
      // V5 input validation: reject non-semver
      let semver_re = regex::Regex::new(r"^\d+\.\d+\.\d+$").unwrap();
      if !semver_re.is_match(old) {
          eprintln!("error: <old version> must match X.Y.Z semver");
          return std::process::ExitCode::from(1);
      }
      let path = spt_home().join("last-seen-version.json");
      let body = format!("{{\"version\":\"{}\"}}", old);
      match atomic_write_string(&path, &body) {
          Ok(_) => std::process::ExitCode::SUCCESS,
          Err(e) => {
              eprintln!("error: sentinel write failed: {}", e);
              std::process::ExitCode::from(1)
          }
      }
  }
  ```

  Note: `regex` crate may not be in Cargo.toml today. If not, use a hand-rolled validator (`old.split('.').count() == 3 && all parts parse as u32`).

  ---

  ## Assumptions Log

  | # | Claim | Section | Risk if Wrong |
  |---|-------|---------|---------------|
  | A1 | Stop hook `reason` text reaches Claude as continuation context strong enough for Claude to invoke AUQ in same turn | Q1 / Q2 | If wrong: AUQ never fires; user never sees changelog prompt. Mitigation: cross-source confirmation (4+ sources agree); ecosystem-wide "stop hook task completion" pattern relies on this exactly. Confidence: HIGH. |
  | A2 | `regex` crate not yet in Cargo.toml | Q9 / code example | If wrong: minor — just use it. Researcher did not verify Cargo.toml deps. Planner should check before Plan 02 implementation. |
  | A3 | `step_count` calculation: count of H2 sections where `old < version <= new` (exclusive of old, inclusive of new) | Q11 / Q3 corpus | If wrong: off-by-one in question text. Locked in CONTEXT.md D-07; aligns with Discussion Log Round 2. Confidence: HIGH (locked in CONTEXT). |
  | A4 | Test seam env var `SPT_OVERRIDE_PKG_VERSION` is acceptable codebase pattern | Q9 | If user/maintainer rejects test-only env var approach, fall back to a `cfg(test)` module replacement (slightly more invasive). Confidence: MEDIUM — pattern matches `SPT_HOME` precedent but is novel for VERSION specifically. |
  | A5 | DEPLOY.ps1 `-Bump` insertion sites at lines 215 (abort gate) and 287 (stub append) | Q7 | If wrong: stub appended at wrong sequence point may either be staged into the bump commit (bad) or fail to detect dirty state (bad). Mitigation: Plan 01 must dry-run DEPLOY.ps1 and verify the sequence holds. Confidence: MEDIUM — based on reading the script, not on test execution. |
  | A6 | `$OWL` env var persists across Claude Code turns via `~/.claude/settings.json` injection | Q5 | If wrong: Claude's `Bash($OWL version-remind 1.10.9)` invocation would fail with "$OWL: command not found". Mitigation: every existing `/spt:*` skill uses `$OWL` and works — strong empirical evidence. Confidence: HIGH. |

  ---

  ## Open Questions

  1. **Should the integration test golden-fixture include a multi-version scenario?**
     - What we know: single-step and multi-step are both required test cases (Q3, integration corpus).
     - What's unclear: how many H2 sections to include in the test fixture CHANGELOG.md, and whether to snapshot the full `reason` string or just key fields.
     - Recommendation: Snapshot just `<old>`, `<new>`, `<step_count>` and the invocation string. Body content varies with CHANGELOG.md edits and would make tests fragile.

  2. **Should `version-remind` log its action to stderr for debugging?**
     - What we know: success exit 0, failure exit 1.
     - What's unclear: whether a single-line stderr "rolled back sentinel to 1.10.9" is helpful or noise.
     - Recommendation: emit a single line on success (matches `READY:id` / `SENT:id` status-tag convention from CLAUDE.md §Conventions). E.g., `REMINDED:1.10.9`. Final wording in Plan 02.

  3. **Should the block body include the dates in the AUQ question text, or only in the AUQ description / pre-question framing?**
     - What we know: D-07 says dates surface in "AUQ description / Claude's pre-question framing, not in the question line itself".
     - What's unclear: AskUserQuestion's API may or may not have a description slot per option vs a global header.
     - Recommendation: Claude renders dates in the prose surface BEFORE invoking AUQ (e.g., "v1.10.9 (released 2026-05-15) → v1.10.10 (released 2026-05-16). Print changelog?"). AUQ itself uses just the version-number question.

  ---

  ## State of the Art

  | Old Approach (in REQUIREMENTS.md as written) | Current Approach (per CONTEXT.md D-01..D-12) | When Changed | Impact |
  |--------------|------------------|--------------|--------|
  | SessionStart hook emission | Stop hook emission | Phase 34 discuss-phase | Pivots VERS-02 from `plugin_session_start.rs` to `hook_idle.rs`; user is between turns when prompted, not at session boot |
  | 3-option AUQ (yes/no/dismiss) | 4-option AUQ (yes-full / yes-highlights / remind / skip) | Phase 34 discuss-phase | VERS-04 amendment; richer UX, splits "yes" into "all detail" vs "discretion summary" |
  | Multi-version = version-list + pointer (never full prose) | Multi-version = same 4-option as single; `Yes, full` renders all interim H2 sections verbatim | Phase 34 discuss-phase | VERS-06 amendment; user-driven choice rather than format-driven constraint |
  | Sentinel write coupled to user response | Sentinel pre-written at hook entry; `version-remind` rolls back on `Remind me later` | Phase 34 discuss-phase Round 5 | Inverts ack/dismiss flow — most-used paths (Yes-full / Yes-highlights / Skip) have zero round-trip; less-common Remind pays the cost |
  | `last-seen-version.json` inside `owlery/` | `last-seen-version.json` SIBLING to `owlery/` | VERS-01 (original requirement) | Per VERS-01 wording; respected in D-05 |

  **No deprecated items.** Phase 34 is greenfield.

  ---

  ## Plan-Boundary Recommendation Summary

  **3 plans** in linear sequence:

  | Plan | Title | Touches | Tests |
  |------|-------|---------|-------|
  | Plan 01 | Doc amendment + CHANGELOG.md genesis + DEPLOY.ps1 stub/sync | REQUIREMENTS.md, ROADMAP.md, CHANGELOG.md (new), docs/DEPLOY.ps1 | DEPLOY.ps1 dry-run; manual review |
  | Plan 02 | Rust hook + sentinel + parser + `version-remind` subcommand | src/owl/version_changelog.rs (new), src/owl/hook_idle.rs, src/main.rs (subcommand wiring), tests/version_changelog.rs | Unit tests for parser corpus; integration tests for hook emission |
  | Plan 03 | Block body authoring + end-to-end integration test + UAT | src/owl/version_changelog.rs (block-body template), tests/version_changelog.rs (snapshot), tests/golden/ | Golden snapshot; manual UAT against real Claude Code |

  **Dependencies:** 01 → 02 → 03 (linear). Plan 02 needs CHANGELOG.md to exist (for parser tests against real content). Plan 03 needs Plan 02's payload-builder helper.

  ---

  ## Open Risks / Blockers for Planner

  1. **(LOW)** DEPLOY.ps1 stub-append site exact insertion may need adjustment after Plan 01 dry-run. Recommend: dry-run before committing the script change.
  2. **(LOW)** `regex` crate availability — verify before Plan 02 implementation. Easy fix if missing.
  3. **(LOW)** Yanked-version handling deferred (parser rejects `## [1.10.10] - 2026-05-16 (yanked)`). If a real yank ever happens, author edits CHANGELOG.md to remove the H2 line entirely. Document as a constraint in CHANGELOG.md authoring guidelines.
  4. **(MEDIUM)** AUQ-instructions wording in block body (Plan 03) needs careful authoring to ensure Claude reliably invokes the right tools in the right order. Recommend: explicit numbered steps inside `<instructions>` element.
  5. **(LOW)** The block-body XML shape (Q11 recommendation) is researcher's pick — planner is empowered to revise. Constraint: must carry `old`, `new`, `old_date`, `new_date`, `step_count`, and the exact `$OWL version-remind <old>` invocation string verbatim.

  ---

  ## Appendix A: CHANGELOG.md Genesis Scope (Plan 01 Step 2)

  Versions identified from `git log --grep='chore: bump'` (38 entries):

  ```
  v1.10.10  (most recent — Phase 33 completion era)
  v1.10.9
  v1.10.8
  v1.10.7
  v1.10.6
  v1.10.5
  v1.10.4
  v1.10.3
  v1.10.2
  v1.10.1
  v1.10.0
  v1.9.15
  v1.9.14
  v1.9.13
  v1.9.12
  v1.9.10  (note: 1.9.11 missing — skip in CHANGELOG)
  v1.9.9
  v1.9.8
  v1.9.7
  v1.9.5   (note: 1.9.6 missing — skip)
  v1.9.2   (note: 1.9.0/1/3/4 missing — skip)
  v1.8.11
  v1.8.10
  v1.8.9
  v1.8.8
  v1.8.7
  v1.8.6
  v1.8.5
  v1.8.4
  v1.8.3
  v1.8.2
  v1.8.1
  v1.8.0
  v1.7.5
  v1.7.4
  v1.7.3
  v1.7.2
  v1.5.4   (older format: "bump plugin version to 1.5.4 and add test artifacts")
  ```

  **Author note for Plan 01:** Per D-09, author curated prose per version using `git log <range>` to scope the changes between each bump. The CHANGELOG.md is NOT a literal log dump — it is a human-readable summary. Skipped versions (1.9.11, 1.9.6, etc.) need no entry. Format: Keep-a-Changelog 1.1.0; H2 `## [X.Y.Z] - YYYY-MM-DD`; body uses bullet lists or prose paragraphs (author's discretion — parser doesn't care about body structure).

  ---

  ## Sources

  ### Primary (HIGH confidence)
  - `~/.claude/reference_docs/claude-code-hooks.md` — Stop hook schema, environment variables, exit codes
  - `src/owl/hook_idle.rs` — Stop hook emission site, OWL_ECHO_COMMUNE guard pattern
  - `src/common/owlery.rs:18-32, 65, 248` — spt_home, owlery_dir, atomic_write_string
  - `docs/DEPLOY.ps1:154-292` — `-Bump` flow, sync step
  - `.planning/phases/34-version-change-changelog/34-CONTEXT.md` — 12 locked decisions (D-01..D-12)
  - `.planning/phases/34-version-change-changelog/34-DISCUSSION-LOG.md` — alternatives + rationale
  - `.planning/REQUIREMENTS.md` — VERS-01..VERS-09 + Out-of-Scope clauses

  ### Secondary (MEDIUM-HIGH confidence — multi-source verified)
  - [Anthropic Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) — Stop hook output schema, environment variables
  - [disler/claude-code-hooks-mastery](https://github.com/disler/claude-code-hooks-mastery) — Stop hook `decision:"block"` semantics, ecosystem patterns
  - [Anthropic blog: How to configure hooks](https://claude.com/blog/how-to-configure-hooks) — Stop hook reason propagation
  - [Claudefa.st Stop Hook task enforcement](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement) — reason field "fed back to Claude"
  - [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — format specification

  ### Tertiary (LOW confidence — single-source / training data)
  - Rust `std::fs::rename` Windows behavior — verified via [doc.rust-lang.org](https://doc.rust-lang.org/std/fs/fn.rename.html) and [Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-technet-forums/449bb49d-8acc-48dc-a46f-0760ceddbfc3); HIGH confidence on same-volume NTFS atomicity, LOWER on concurrent-write edge cases

  ---

  ## Metadata

  **Confidence breakdown:**
  - Stop hook `decision:"block"` semantics (Q1/Q2): HIGH (4+ sources agree; ecosystem-wide pattern; CONTEXT.md design is sound)
  - Standard stack: HIGH (no new deps; all existing crates)
  - Architecture (3-plan boundary): HIGH (mirrors Phase 31/32/33 amendment-first pattern)
  - Parser corpus: HIGH (test cases derived from Keep-a-Changelog spec + common edge cases)
  - DEPLOY.ps1 stub injection sites: MEDIUM (based on script reading, not dry-run execution)
  - Test seam (SPT_OVERRIDE_PKG_VERSION): MEDIUM (novel pattern; need user confirm before Plan 02)
  - Pitfalls: HIGH (derived from established codebase patterns: best-effort IO, recursion guard, atomic write)

  **Research date:** 2026-05-17
  **Valid until:** 2026-06-16 (30 days — Claude Code hooks API is stable-ish but new event types added periodically)

  ### Patterns Map
  # Phase 34: Version-Change Changelog - Pattern Map

  **Mapped:** 2026-05-17
  **Files analyzed:** 8 (4 new, 4 modified)
  **Analogs found:** 8 / 8

  ## File Classification

  | New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
  |-------------------|------|-----------|----------------|---------------|
  | NEW `src/owl/version_changelog.rs` | utility (sentinel I/O + parser + payload builder) | file-I/O + transform | `src/common/owlery.rs:248-255` (atomic_write_string) + `src/owl/hook_idle.rs:71-98` (sentinel read pattern) | exact (composite) |
  | NEW `owl version-remind <old>` subcommand | controller (one-shot CLI handler) | request-response (argv → file write) | `src/cli.rs:142-153` + `src/owl/mod.rs:116-130` (`NewAlarm`) | exact |
  | NEW repo-root `CHANGELOG.md` | config (static asset) | n/a | `psyche.md` (repo root) + no `include_str!` (D-11) | placement-only analog |
  | NEW `tests/version_changelog.rs` | test (integration) | event-driven (subprocess + stdin) | `tests/auto_pick_integration.rs:1-120` (SPT_HOME sandbox + assert_cmd) | exact |
  | MOD `src/owl/hook_idle.rs` | controller (Stop hook) | event-driven (hook stdin → JSON stdout) | self — append at run() top mirroring own `OWL_ECHO_COMMUNE` guard (lines 64-69) | self-reference |
  | MOD `docs/DEPLOY.ps1` | utility (build script) | batch (file rewrite + verify) | `docs/DEPLOY.ps1:246-274` (regex-replace-then-verify) + `:359-370` (Copy-Item to marketplace) | exact |
  | MOD `.planning/REQUIREMENTS.md` | config (spec doc) | n/a | Phase 31 D-11 / Phase 33 D-03/D-04 amendment commit | structural |
  | MOD `.planning/ROADMAP.md` | config (spec doc) | n/a | Phase 31 D-11 / Phase 33 D-03/D-04 amendment commit | structural |

  ---

  ## Pattern Assignments

  ### NEW `src/owl/version_changelog.rs` (utility, file-I/O + transform)

  **Analog A — sentinel atomic write** (`src/common/owlery.rs:248-255`):
  ```rust
  pub fn atomic_write_string(path: &std::path::Path, body: &str) -> std::io::Result<()> {
      let tmp = path.with_file_name(format!(
          "{}.tmp",
          path.file_name().and_then(|s| s.to_str()).unwrap_or("epoch")
      ));
      fs::write(&tmp, body)?;
      fs::rename(&tmp, path)
  }
  ```
  **Reuse verbatim.** Sentinel path: `owlery::spt_home().join("last-seen-version.json")`. Body: `serde_json::to_string(&SentinelV1 { version: new })?`.

  **Analog B — sentinel read with graceful degrade** (`src/owl/hook_idle.rs:71-83`):
  ```rust
  let info_path = owlery::info_file(owl_id);
  let content = match std::fs::read_to_string(&info_path) {
      Ok(c) => c,
      Err(_) => return,
  };
  let info: types::InfoJson = match serde_json::from_str(&content) {
      Ok(i) => i,
      Err(_) => return,
  };
  ```
  **Copy shape.** For sentinel: missing-file (first-install) → silently write NEW + skip emission; malformed JSON → log nothing, treat as missing.

  **Analog C — compile-time version source** (`src/owl/poll.rs:75`, `setup.rs:35`, `live/stop.rs:199`):
  ```rust
  env!("CARGO_PKG_VERSION")
  ```
  **Use identically.** Stored result for sentinel comparison; NEVER read runtime `plugin.json`.

  **Sentinel struct (locked per CONTEXT specifics):**
  ```rust
  #[derive(serde::Serialize, serde::Deserialize)]
  struct SentinelV1 { version: String }
  ```

  **CHANGELOG parser regex (locked):** `^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})$` — case-sensitive, rejects `[Unreleased]`, ignores link-reference lines (`[1.2.3]: <url>` form). No external `regex` crate dep — use the `regex` crate already in `Cargo.toml` if present; otherwise hand-roll (mirror `tests/skill_hints.rs:29-56` zero-dep YAML parser style).

  **Unit-test pattern** (co-located `#[cfg(test)]`, mirror `src/owl/hook_idle.rs:100-264`):
  - `ENV_LOCK: Mutex<()>` static + `EnvSnapshot` RAII for `SPT_HOME` mutation (lines 108-135)
  - `setup_spt_home(&tmp)` helper (lines 162-165)
  - Per-test tempdir via `tempfile::tempdir()`

  ---

  ### NEW `owl version-remind <old>` subcommand (controller, request-response)

  **Clap registration analog** (`src/cli.rs:142-153` — `NewAlarm`, the most recent positional-arg subcommand):
  ```rust
  /// Schedule a one-shot timed reminder ...
  #[command(name = "new-alarm")]
  NewAlarm {
      self_id: String,
      time_spec: String,
      ...
  },
  ```

  **Pattern to copy for Phase 34** — add to `src/cli.rs` `Commands` enum (no `hide = true`; visible like `NewAlarm`):
  ```rust
  /// Roll the last-seen version sentinel back to <old> so the next Stop
  /// hook re-fires the version-change prompt. Invoked by Claude when the
  /// user picks `Remind me later` from the AUQ.
  #[command(name = "version-remind")]
  VersionRemind {
      /// The previous version to roll the sentinel back to (e.g. `1.10.9`).
      /// MUST match the `old` value from the <spt-version-changelog> block
      /// Claude received — the subcommand cannot infer this otherwise.
      old: String,
  },
  ```

  **Dispatch arm** (`src/owl/mod.rs:37-160` `handle_command` match — add alongside `Commands::EchoCommune` at ~line 153):
  ```rust
  Some(Commands::VersionRemind { old }) => {
      version_changelog::run_version_remind(&old);
  }
  ```

  **Handler body** (~10 lines, mirrors `src/common/owlery.rs:264-269` `write_last_commune_epoch`):
  ```rust
  pub fn run_version_remind(old: &str) {
      let path = owlery::spt_home().join("last-seen-version.json");
      let body = serde_json::to_string(&SentinelV1 { version: old.to_string() })
          .expect("SentinelV1 serialization is infallible");
      match owlery::atomic_write_string(&path, &body) {
          Ok(_) => std::process::exit(0),
          Err(e) => {
              eprintln!("[owl] version-remind: failed to rewrite {}: {}", path.display(), e);
              std::process::exit(1);
          }
      }
  }
  ```

  **Clap parse-test pattern** (`src/cli.rs:351-411` — co-locate in `#[cfg(test)] mod tests`):
  ```rust
  #[test]
  fn parses_version_remind_positional() {
      let cli = Cli::try_parse_from(["owl", "version-remind", "1.10.9"]).unwrap();
      match cli.command {
          Some(Commands::VersionRemind { old }) => assert_eq!(old, "1.10.9"),
          other => panic!("expected VersionRemind, got {:?}", other),
      }
  }
  ```

  ---

  ### NEW repo-root `CHANGELOG.md` (config, static asset)

  **Placement analog:** `psyche.md` (repo root, copied into plugin meta dir). Phase 34 explicitly diverges from `psyche.md` in ONE respect: NOT embedded via `include_str!` (D-11 — runtime-read from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`).

  **Format (locked, Keep-a-Changelog 1.1.0):**
  ```markdown
  # Changelog

  ## [1.11.0] - 2026-05-17

  - Added: ...
  - Fixed: ...

  ## [1.10.10] - 2026-05-13

  - ...
  ```

  **Genesis** (Plan 01 step 1, after REQUIREMENTS/ROADMAP amendment commit): author by hand using `git log --oneline --grep='chore: bump'` as the version skeleton. One commit. No auto-regeneration.

  ---

  ### NEW `tests/version_changelog.rs` (test, event-driven integration)

  **Analog:** `tests/auto_pick_integration.rs:1-120` — the closest existing subprocess-driven hook test.

  **Imports + harness pattern** (lines 42-99 verbatim):
  ```rust
  use std::fs;
  use std::sync::Mutex;
  static ENV_LOCK: Mutex<()> = Mutex::new(());

  struct SptHomeGuard { prior: Option<String> }
  impl SptHomeGuard {
      fn set(path: &std::path::Path) -> Self {
          let prior = std::env::var("SPT_HOME").ok();
          std::env::set_var("SPT_HOME", path);
          Self { prior }
      }
  }
  impl Drop for SptHomeGuard {
      fn drop(&mut self) {
          match &self.prior {
              Some(v) => std::env::set_var("SPT_HOME", v),
              None => std::env::remove_var("SPT_HOME"),
          }
      }
  }
  ```

  **Hook invocation pattern** (lines 81-99 — `assert_cmd::Command::cargo_bin("owl")`):
  ```rust
  let mut cmd = assert_cmd::Command::cargo_bin("owl").expect("owl binary must be built");
  cmd.arg("hook-idle")
      .env("SPT_HOME", tmp)
      .env("HOME", tmp)
      .env("USERPROFILE", tmp)
      .env_remove("OWL_ECHO_COMMUNE");
  // stdin pipe via .write_stdin(json) per assert_cmd
  ```

  **Perch-fixture pattern** (lines 105-120 verbatim — `write_live_perch`):
  - create `{spt_home}/owlery/{id}/ready`
  - write `info.json` with `parent_pid = std::process::id() as u64`
  - so `find_perch_by_parent_pid` resolves to this perch when the subprocess inherits the test PID as its ppid

  **Test cases to cover (per CONTEXT Claude's Discretion → Tests bullet):**
  1. version-mismatch single-step → emits block
  2. version-mismatch multi-step → emits block with `step_count > 1`
  3. first-install (no sentinel) → silent write, no emit
  4. sentinel == compile-time version → no emit
  5. `OWL_ECHO_COMMUNE=1` → no emit (recursion guard)
  6. no perch resolved → no emit
  7. `version-remind 1.10.9` → sentinel rewritten round-trip
  8. CHANGELOG.md parser: happy path, `[Unreleased]` reject, malformed H2 graceful, link-reference lines ignored

  ---

  ### MOD `src/owl/hook_idle.rs` (controller, event-driven)

  **This is a self-reference — append a new block at the TOP of `run()` between perch resolution (line 32) and `set_idle_ready` (line 39).**

  **Recursion guard (mirror lines 64-69 EXACTLY — MUST be first):**
  ```rust
  if std::env::var("OWL_ECHO_COMMUNE")
      .map(|v| !v.is_empty())
      .unwrap_or(false)
  {
      // skip version-change check in haiku echo-commune subprocess
  } else {
      // version-change emission block here
  }
  ```
  **OR** reorder so OWL_ECHO_COMMUNE guard runs at the very top of `run()` itself (cleaner — the existing guard at line 64-69 then becomes a no-op or is removed since `spawn_echo_commune_if_live` is called after). Planner's choice; CONTEXT D-03 accepts either.

  **Block emission (D-04 — Stop hook output schema):**
  ```rust
  // Stop hooks have no hookSpecificOutput.additionalContext channel
  // (per the hook_idle.rs:1-5 file-doc comment). Phase 34 uses
  // decision:"block" + reason instead. Research-confirmed: reason
  // text propagates to Claude as re-engagement context.
  let response = serde_json::json!({
      "decision": "block",
      "reason": format!(
          "<spt-version-changelog>\n{}\n</spt-version-changelog>",
          payload_xml
      ),
  });
  println!("{}", serde_json::to_string(&response).unwrap());
  ```
  **NOTE:** This is the channel Phase 34 picks — NOT the Pattern S2 `hookSpecificOutput` envelope from `plugin_session_start.rs:139-155` (`emit_auto_pick`). The S2 pattern is for SessionStart/PreToolUse/UserPromptSubmit hooks. Stop's only context-injection channel is `decision:"block"` + `reason`.

  **Sentinel write site (D-05):**
  ```rust
  // Write the NEW version BEFORE emitting the block. The `Remind me later`
  // AUQ option calls `$OWL version-remind <old>` to roll back; all other
  // branches (Yes-full / Yes-highlights / Skip) leave the sentinel at NEW.
  let _ = owlery::atomic_write_string(
      &owlery::spt_home().join("last-seen-version.json"),
      &serde_json::to_string(&SentinelV1 { version: current.to_string() }).unwrap(),
  );
  ```

  **SessionStart's `emit_auto_pick` envelope for comparison** (`plugin_session_start.rs:139-155`):
  ```rust
  pub(crate) fn emit_auto_pick(pick_spec_json: &str) {
      let context = format!(
          "<spt-live-auto-pick>\n{}\n</spt-live-auto-pick>",
          pick_spec_json
      );
      let response = serde_json::json!({
          "hookSpecificOutput": {
              "hookEventName": "SessionStart",
              "additionalContext": context,
          }
      });
      let out = serde_json::to_string(&response).unwrap_or_else(|_| {
          r#"{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":""}}"#.to_string()
      });
      println!("{}", out);
  }
  ```
  **Phase 34 does NOT mirror this shape.** Stop uses `decision:"block"` per the file-doc comment at `hook_idle.rs:1-5`. Confirm semantics during research per CONTEXT D-04 / Open Question.

  ---

  ### MOD `docs/DEPLOY.ps1` (utility, batch file rewrite)

  **Analog A — stub-injection (regex-replace-then-verify)** at `docs/DEPLOY.ps1:246-274`:
  ```powershell
  # Regex-rewrite preserves existing formatting (indent, key order, trailing newline)
  $pattern = '"version"\s*:\s*"' + [regex]::Escape($CurrentVersion) + '"'
  $replacement = '"version": "' + $NewVersion + '"'
  $newRaw = [regex]::Replace($PluginRaw, $pattern, $replacement)
  if ($newRaw -eq $PluginRaw) {
      throw "Failed to locate ""version"": ""$CurrentVersion"" in plugin.json -- aborting bump"
  }
  [System.IO.File]::WriteAllText($PluginJson, $newRaw)

  # Verify the write took
  $verify = (Get-Content $PluginJson -Raw | ConvertFrom-Json).version
  if ($verify -ne $NewVersion) {
      throw "plugin.json post-write reads version='$verify', expected '$NewVersion'"
  }
  ```

  **Pattern to copy for CHANGELOG.md stub-append (D-10 — slots in after line 274, before commit at line 279):**
  ```powershell
  # Phase 34 D-10: append stub H2 to repo-root CHANGELOG.md (no commit)
  $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  $ChangelogRaw  = Get-Content $ChangelogPath -Raw
  $todoMarker    = "TODO: changelog entry"
  # Abort if PRIOR -Bump's stub for current version is still un-filled
  if ($ChangelogRaw -match [regex]::Escape("## [$CurrentVersion]") -and `
      $ChangelogRaw -match [regex]::Escape($todoMarker)) {
      Write-Host "ERROR: CHANGELOG.md contains '$todoMarker' for v$CurrentVersion." -ForegroundColor Red
      Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
      exit 1
  }
  $today = Get-Date -Format 'yyyy-MM-dd'
  $stub = "`n## [$NewVersion] - $today`n`n- $todoMarker`n"
  $newChangelog = $ChangelogRaw.TrimEnd("`r","`n") + $stub
  [System.IO.File]::WriteAllText($ChangelogPath, $newChangelog)
  Write-SubStep "Appended CHANGELOG.md stub for v$NewVersion (fill in + commit before next -Bump)"
  # Intentionally NOT staged/committed — gates next -Bump on author filling
  ```

  **Analog B — sync to marketplace (Copy-Item)** at `docs/DEPLOY.ps1:359-370`:
  ```powershell
  Invoke-Mutation "Copy owl.exe -> $MarketSpt" {
      Copy-Item -Path $OwlBinary -Destination (Join-Path $MarketSpt 'owl.exe') -Force
  }
  Invoke-Mutation "Copy plugin.json -> $MarketPluginMeta" {
      Copy-Item -Path $PluginJson -Destination (Join-Path $MarketPluginMeta 'plugin.json') -Force
  }
  ```

  **Pattern to copy for CHANGELOG.md sync (D-11 — `{Marketplace}/plugin/spt/CHANGELOG.md`, sibling to `plugin.json`, NOT inside `.claude-plugin/`):**
  ```powershell
  # Phase 34 D-11: sync repo-root CHANGELOG.md to marketplace plugin root
  Invoke-Mutation "Copy CHANGELOG.md -> $MarketSpt" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $MarketSpt 'CHANGELOG.md') -Force
  }
  ```
  **Insert site:** at line 367 (after `Copy hooks/* -> $MarketHooks`, before `Copy plugin.json`).

  **Cache-tier sync** (mirror at `docs/DEPLOY.ps1:489-500` — same `Copy-Item -Force` shape):
  ```powershell
  Invoke-Mutation "Copy CHANGELOG.md -> $CacheVer" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $CacheVer 'CHANGELOG.md') -Force
  }
  ```
  **Insert site:** at line 497 (after `Copy hooks/*`, before `Copy plugin.json -> $CacheVer/.claude-plugin`).

  ---

  ### MOD `.planning/REQUIREMENTS.md` + `.planning/ROADMAP.md` (config, spec doc)

  **Analog:** Phase 31 D-11 + Phase 33 D-03/D-04 amendment-commit precedent. Plan 01 step 0 is a single commit that rewrites VERS-02/04/06/08/09 per CONTEXT D-12 and ROADMAP SC1/SC2/SC5 per CONTEXT D-12 — BEFORE any Rust code lands. Commit message: `docs(34): amend VERS-02..09 + SC1/2/5 for Stop-hook pivot`.

  ---

  ## Shared Patterns

  ### Recursion Guard (OWL_ECHO_COMMUNE)
  **Source:** `src/owl/hook_idle.rs:64-69`
  **Apply to:** version-change emission block + sentinel write (must precede both)
  ```rust
  if std::env::var("OWL_ECHO_COMMUNE")
      .map(|v| !v.is_empty())
      .unwrap_or(false)
  {
      return;
  }
  ```

  ### Atomic File Write
  **Source:** `src/common/owlery.rs:248-255`
  **Apply to:** sentinel write (Stop-hook path AND `version-remind` handler)
  ```rust
  owlery::atomic_write_string(&path, &body)
  ```

  ### Sentinel Read (graceful degrade)
  **Source:** `src/owl/hook_idle.rs:71-83` pattern
  **Apply to:** Stop-hook sentinel read
  - Missing file → treat as first-install (write NEW, no emit)
  - Malformed JSON → treat as missing (best-effort, no panic)

  ### Compile-Time Version
  **Source:** `src/owl/poll.rs:75`, `setup.rs:35`, `live/stop.rs:199`
  **Apply to:** all version comparisons in Phase 34 code
  ```rust
  env!("CARGO_PKG_VERSION")
  ```
  **NEVER read runtime `plugin.json`** (per VERS-02 amended wording).

  ### Perch-Resolution Gate
  **Source:** `src/owl/hook_idle.rs:24-32` (already in place in `hook_idle.rs::run()`)
  **Apply to:** version-change emission — only fires AFTER `owl_id = ... or return`. No new predicate code per D-02.

  ### Hook Stdin Parsing
  **Source:** `src/common/hook_output.rs:26-29` (`parse_hook_stdin`)
  **Apply to:** any new hook-handler stdin reads (version-change code doesn't need it — it reads no stdin fields beyond what perch-resolution already consumes).

  ### Test Env-Mutation Hygiene
  **Source:** `src/owl/hook_idle.rs:108-135` (in-file unit tests) + `tests/auto_pick_integration.rs:46-65` (integration tests)
  **Apply to:** all unit + integration tests touching `SPT_HOME`
  - `static ENV_LOCK: Mutex<()> = Mutex::new(())`
  - `EnvSnapshot::capture()` RAII (Drop restores)
  - `cmd.env_remove(...)` for all gate-listed env vars before subprocess invocation

  ### DEPLOY.ps1 Mutation Wrapping
  **Source:** `docs/DEPLOY.ps1:359-370` (`Invoke-Mutation "...desc..." { Copy-Item ... }`)
  **Apply to:** every new file write in DEPLOY.ps1 — wrap in `Invoke-Mutation` so `-DryRun` is auto-respected and step descriptions print uniformly.

  ---

  ## Risks: Deviations from Established Patterns

  1. **Stop-hook output channel — `decision:"block"` + `reason` is unprecedented in this codebase.** Every other hook (`hook_check`, `hook_prompt`, `plugin_session_start`) uses `hookSpecificOutput.additionalContext`. The file-doc comment at `hook_idle.rs:1-5` explicitly says Stop has no such schema. Phase 34 picks `decision:"block"` per CONTEXT D-04 but this is a research blocker — confirm with `~/.claude/reference_docs/claude-code-hooks.md` that `reason` text actually propagates to Claude on re-engagement. **Fallback if broken:** PreToolUse (next tool call) or revisit UserPromptSubmit. Flagged in CONTEXT Deferred.

  2. **Sentinel lives at `$SPT_HOME/last-seen-version.json`, NOT under `$SPT_HOME/owlery/`.** Per VERS-01 the sentinel is a SIBLING of `owlery/`, breaking the "everything spt-related lives under `owlery_dir()`" convention. There is no existing precedent for files at `spt_home()` root. Other dirs (`bin/`, `psyches/`, `pulses/`, `status/`, `logs_latest/`) are all subdirs. CONTEXT locks this; the sentinel is intentionally OUT of owlery to avoid coupling with perch lifecycle (e.g., `owl cleanup`). Add helper `pub fn sentinel_path() -> PathBuf { spt_home().join("last-seen-version.json") }` to `owlery.rs` to keep the convention encapsulated.

  3. **`version-remind` is the first subcommand whose only purpose is to mutate a single sentinel file.** Most subcommands wrap richer logic (`Setup`, `Poll`, `Live`, `Send`). The closest precedent is `NewAlarm` (one-shot file write) but `NewAlarm` also threads through resolve + pulse-dir registration. `version-remind` is ~10 lines and a single fs write — keep it tiny; don't over-engineer.

  4. **No `regex` crate currently in `Cargo.toml`** (verify during planning). The CHANGELOG.md H2 parser regex is small enough to hand-roll (5-10 lines, mirror the parser style at `tests/skill_hints.rs:29-56` for zero-dep parsing). Adding `regex` for this one use would be a heavy dep; planner should default to hand-rolled.

  5. **DEPLOY.ps1 stub-injection does not commit.** Every other DEPLOY.ps1 mutation either commits immediately (`-Bump` at line 281) or is part of the marketplace push (line 403). The Phase 34 CHANGELOG.md stub-append is deliberately uncommitted to gate the next `-Bump` on author action — this is a NEW interaction shape for DEPLOY.ps1. Surface it clearly in the post-bump notice so users aren't surprised by `git status` showing dirty CHANGELOG.md after `-Bump`.

  6. **CHANGELOG.md is read at runtime from `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` rather than embedded via `include_str!`** (D-11). `psyche.md` is the established precedent and IS embedded. CONTEXT calls out the rationale: changelog grows monotonically, binary should not bloat. Tradeoff: runtime file read can fail (missing file, IO error) → must degrade gracefully (skip emission, log silently — mirror the `hook_idle.rs:71-83` read-then-bail pattern).

  7. **AskUserQuestion + `$OWL version-remind <old>` round-trip relies on Claude faithfully passing the literal `old` value from the block payload.** No structural guarantee — purely instructional. Block-body MUST surface the exact invocation string verbatim with `<old>` substituted (CONTEXT D-08). The only enforcement is the `version-remind` subcommand rejecting a missing arg.

  ---

  ## Metadata

  **Analog search scope:** `src/owl/`, `src/common/`, `src/live/`, `tests/`, `docs/DEPLOY.ps1`, `src/cli.rs`, `src/main.rs`
  **Files scanned:** 12 read in full or in targeted ranges
  **Pattern extraction date:** 2026-05-17

  ### Plans to Review
  #### 34-01-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 01
  type: execute
  wave: 1
  depends_on: []
  files_modified:
    - .planning/REQUIREMENTS.md
    - .planning/ROADMAP.md
    - CHANGELOG.md
    - docs/DEPLOY.ps1
  autonomous: true
  requirements:
    - VERS-02
    - VERS-04
    - VERS-06
    - VERS-07
    - VERS-08
    - VERS-09
  user_setup: []

  must_haves:
    truths:
      - "REQUIREMENTS.md VERS-02/04/06/08/09 reflect the Stop-hook pivot + 4-option AUQ + AUTO-03-coexistence-by-hook-separation wording per CONTEXT D-12"
      - "ROADMAP.md Phase 34 Success Criteria 1, 2, 5 reflect the Stop-hook pivot wording per CONTEXT D-12"
      - "Repo-root CHANGELOG.md exists in Keep-a-Changelog 1.1.0 format with an H2 section per historical version bump (38 candidate versions surfaced by `git log --grep='chore: bump'`)"
      - "Running `docs/DEPLOY.ps1 -Bump patch -DryRun` (after this plan) shows the new TODO-marker abort gate and CHANGELOG.md stub-append substeps in the dry-run trace"
      - "Running `docs/DEPLOY.ps1` (deploy without bump) shows a new substep that copies repo-root CHANGELOG.md into `{Marketplace}/plugin/spt/CHANGELOG.md` AND into `{CacheVer}/CHANGELOG.md`"
    artifacts:
      - path: "CHANGELOG.md"
        provides: "Keep-a-Changelog 1.1.0 root document; consumed at runtime by Plan 02's parser via $CLAUDE_PLUGIN_ROOT/CHANGELOG.md"
        contains: "## ["
      - path: ".planning/REQUIREMENTS.md"
        provides: "Amended VERS-02/04/06/08/09 per D-12"
        contains: "Stop hook"
      - path: ".planning/ROADMAP.md"
        provides: "Amended Phase 34 SC1/SC2/SC5 per D-12"
        contains: "Stop hook"
      - path: "docs/DEPLOY.ps1"
        provides: "(a) -Bump TODO-marker abort gate, (b) -Bump CHANGELOG.md stub-append (uncommitted, after the bump commit), (c) CHANGELOG.md sync to MarketSpt + CacheVer"
        contains: "CHANGELOG.md"
    key_links:
      - from: "docs/DEPLOY.ps1 -Bump flow"
        to: "repo-root CHANGELOG.md"
        via: "stub-append after `git commit` at line 282, before any further work"
        pattern: "TODO: changelog entry"
      - from: "docs/DEPLOY.ps1 deploy/sync flow"
        to: "{Marketplace}/plugin/spt/CHANGELOG.md AND {CacheVer}/CHANGELOG.md"
        via: "two new Invoke-Mutation `Copy-Item` blocks"
        pattern: "Copy CHANGELOG.md"
  ---

  <objective>
  Phase 34 doc-amendment-first plan + CHANGELOG.md genesis + DEPLOY.ps1 extension.
  PURE doc/script work — NO Rust code in this plan.

  Purpose: Land three preconditions Plan 02 + Plan 03 depend on:
    1. REQUIREMENTS.md + ROADMAP.md no longer assert SessionStart wording (CONTEXT D-12; mirrors Phase 31 D-11 / Phase 33 D-03/D-04 amendment-first pattern).
    2. Repo-root CHANGELOG.md exists in Keep-a-Changelog 1.1.0 format so Plan 02's parser has real content to consume in unit + integration tests.
    3. DEPLOY.ps1 (a) gates future bumps on a filled CHANGELOG entry and (b) syncs the file to the marketplace + cache so the hook's runtime path (`$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`) resolves.

  Output: 3 commits (amendment, CHANGELOG genesis, DEPLOY.ps1 extension). No Rust touched.
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @docs/DEPLOY.ps1

  <interfaces>
  <!-- Locked invariants this plan must preserve / not contradict. -->

  REQUIREMENTS.md VERS-02 — REPLACEMENT WORDING (CONTEXT D-12):
    "On Stop hook (in `src/owl/hook_idle.rs`), gated on active perch resolution via existing `find_perch_by_session` / `find_perch_by_parent_pid`. Hook compares `env!(\"CARGO_PKG_VERSION\")` (compiled-in) against `last-seen-version.json`. NEVER reads runtime `plugin.json`."

  REQUIREMENTS.md VERS-04 — REPLACEMENT WORDING (CONTEXT D-12):
    "AskUserQuestion with four options: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`. Question text includes `step_count` when > 1."

  REQUIREMENTS.md VERS-06 — REPLACEMENT WORDING (CONTEXT D-12):
    "Multi-version skip surfaces the same 4-option AskUserQuestion as single-step; user-chosen `Yes, full changelog` renders all interim H2 sections verbatim, `Yes, highlights only` defers to agent discretion. The CHANGELOG.md pointer is appended to the highlights body."

  REQUIREMENTS.md VERS-08 — REPLACEMENT WORDING (CONTEXT D-12):
    "Version-change prompt (VERS-04) fires from Stop hook and SessionStart auto-pick (AUTO-03 from Phase 33) fires from SessionStart hook. The two never share a hook invocation; no allow-list reconciliation required."

  REQUIREMENTS.md VERS-09 — REPLACEMENT WORDING (CONTEXT D-12):
    "Single write site at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard. Atomic via existing `atomic_write_string`. Binary handoff (Phase 18.4/18.5) does not race because Stop hooks do not fire during in-flight binary swap (Stop fires between turns, handoff happens between processes)."

  ROADMAP.md Phase 34 SC1 — REPLACEMENT WORDING (CONTEXT D-12):
    "On a fresh install (no `$SPT_HOME/last-seen-version.json` present), on first Stop hook fire in any spt-bearing session, the sentinel silently writes the current `CARGO_PKG_VERSION` and emits NO changelog prompt to the user."

  ROADMAP.md Phase 34 SC2 — REPLACEMENT WORDING (CONTEXT D-12):
    "After the plugin version changes (compiled-in `CARGO_PKG_VERSION` differs from sentinel contents), the next Stop hook fire in an spt-bearing session emits a `<spt-version-changelog>` block via `decision:\"block\"` + `reason`; Claude renders an AskUserQuestion with four options (`Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`); the sentinel is atomically rewritten with the current version at hook entry (before the block emission), so the prompt does not re-fire next session unless the user picks `Remind me later`, which invokes `$OWL version-remind <old>` to roll the sentinel back."

  ROADMAP.md Phase 34 SC5 — REPLACEMENT WORDING (CONTEXT D-12):
    "VERS-04 fires from Stop hook (`hook_idle.rs`); AUTO-03 (from Phase 33) fires from SessionStart hook (`plugin_session_start.rs`); collision is impossible by hook separation. The sentinel write site lives at the top of `hook_idle.rs::run()` after perch resolution + OWL_ECHO_COMMUNE guard (atomic via existing `atomic_write_string`); Stop hooks do not fire mid-binary-handoff, so a race cannot corrupt it."

  ROADMAP.md Phase 34 SC3, SC4, SC6 — UNCHANGED (CONTEXT D-12).

  CHANGELOG.md format (Keep-a-Changelog 1.1.0, RESEARCH Q10):
    H2 line shape: `## [X.Y.Z] - YYYY-MM-DD`
    Parser regex (Plan 02 will enforce): `^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$`
    NO `[Unreleased]` section (parser rejects it; we author retroactively from git log).
    H3 subsections / bullets / prose paragraphs all acceptable in body (parser is body-content-agnostic).
    Link-reference lines at file bottom are valid per spec but unused; do not add them.

  DEPLOY.ps1 stub format (CONTEXT specifics):
  ```
  ## [{NEW_VERSION}] - {YYYY-MM-DD}

  - TODO: changelog entry
  ```
  Literal `TODO: changelog entry` is the scan token for the abort gate.
  Date computed at bump-time via `Get-Date -Format 'yyyy-MM-dd'`.

  DEPLOY.ps1 insertion sites (verified via Read against current docs/DEPLOY.ps1):
  - Abort gate: insert AFTER existing Cargo.toml dirty-state check (current line 222), BEFORE the `# Read Cargo.toml ...` comment (current line 224). The check runs only when `$Bump` is truthy.
  - Stub append: insert AFTER the `Push-Location $RepoRoot ... } finally { Pop-Location } Write-SubStep "Committed bump: $bumpSha (not pushed)"` block (current line 287), BEFORE `$Version = $NewVersion` (current line 290). CRITICAL: must run AFTER `git commit` so the stub stays in working tree only — Pitfall 5 in RESEARCH.md.
  - Sync to MarketSpt: insert AFTER `Copy hooks/* -> $MarketHooks` block (current lines 365-367), BEFORE `Copy plugin.json -> $MarketPluginMeta` (current lines 368-370).
  - Sync to CacheVer: insert AFTER `Copy hooks/* -> $CacheVer/hooks` block (current lines 495-497), BEFORE `Copy plugin.json -> $CacheVer/.claude-plugin` (current lines 498-500).

  CHANGELOG.md author-time scope (RESEARCH Appendix A): 38 versions surfaced by `git log --oneline --grep='chore: bump'`, ranging from v1.5.4 to current (v1.10.10 or later — verify at author time). Skipped versions (1.9.11, 1.9.6, 1.9.0/1/3/4) get no entry. NOT a literal log dump — human-readable curated prose per version. One commit.
  </interfaces>
  </context>

  <tasks>

  <task type="auto">
    <name>Task 1: Amend REQUIREMENTS.md VERS-02/04/06/08/09 + ROADMAP.md Phase 34 SC1/SC2/SC5 per D-12</name>
    <files>.planning/REQUIREMENTS.md, .planning/ROADMAP.md</files>
    <action>
  Mirror Phase 31 D-11 + Phase 33 D-03/D-04 amendment-commit precedent (CONTEXT canonical_refs).

  In `.planning/REQUIREMENTS.md`, edit the five VERS-* bullets currently at lines 72-79 (verify line numbers by reading the file first — they may have drifted; the canonical anchors are the bullet IDs `**VERS-02**`, `**VERS-04**`, `**VERS-06**`, `**VERS-08**`, `**VERS-09**`):
    - Replace VERS-02 body with the verbatim REPLACEMENT WORDING block in `<interfaces>` above (CONTEXT D-12).
    - Replace VERS-04 body with its REPLACEMENT WORDING.
    - Replace VERS-06 body with its REPLACEMENT WORDING.
    - Replace VERS-08 body with its REPLACEMENT WORDING.
    - Replace VERS-09 body with its REPLACEMENT WORDING.
    - DO NOT touch VERS-01, VERS-03, VERS-05, VERS-07. They remain valid as-written.
    - DO NOT touch the bullet IDs themselves (`- [ ] **VERS-02**:` etc.) — only the prose after the colon.
    - Keep the `*Updated YYYY-MM-DD — ...*` footer convention: append a new line directly under the existing `*Updated 2026-05-16 — FRESH-04 and FRESH-05 struck per Phase 33 CONTEXT D-03; FRESH-06 reworded per D-04.*` line saying: `*Updated 2026-05-17 — VERS-02/04/06/08/09 amended per Phase 34 CONTEXT D-12 (Stop-hook pivot, 4-option AUQ, AUTO-03-coexistence-by-hook-separation).*`

  In `.planning/ROADMAP.md`, edit the Phase 34 Success Criteria block (currently at lines 643-655, verify by reading first; anchor is `### Phase 34: Version-Change Changelog`):
    - Replace SC1 body with the verbatim REPLACEMENT WORDING (CONTEXT D-12).
    - Replace SC2 body with its REPLACEMENT WORDING.
    - Replace SC5 body with its REPLACEMENT WORDING.
    - DO NOT touch SC3, SC4, SC6 — they remain unchanged.
    - DO NOT touch the **Goal**, **Milestone**, **Depends on**, **Requirements**, **Plans:** lines.

  After both edits, run:
  ```
  git diff --stat .planning/REQUIREMENTS.md .planning/ROADMAP.md
  ```
  Verify exactly two files changed.

  Commit (in repo root) with message:
  ```
  docs(34): amend VERS-02/04/06/08/09 + ROADMAP SC1/2/5 per Phase 34 D-12

  Pivots from the original SessionStart wording to Stop-hook + 4-option AUQ
  + AUTO-03-coexistence-by-hook-separation. Mirrors Phase 31 D-11 and
  Phase 33 D-03/D-04 doc-amendment-first commit pattern.

  No code touched in this commit (Plan 01 step 1).
  ```

  DO NOT stage other files; use `git add .planning/REQUIREMENTS.md .planning/ROADMAP.md` explicitly.
    </action>
    <verify>
      <automated>git log -1 --format=%s | grep -q "amend VERS-02/04/06/08/09" &amp;&amp; grep -q "Stop hook" .planning/REQUIREMENTS.md &amp;&amp; grep -q "VERS-04 fires from Stop" .planning/ROADMAP.md</automated>
    </verify>
    <done>Commit landed on HEAD; REQUIREMENTS.md contains the five amended VERS bodies (search confirms `Stop hook` substring); ROADMAP.md SC5 contains the literal "VERS-04 fires from Stop" phrase; no other files in the commit.</done>
  </task>

  <task type="auto">
    <name>Task 2: Author repo-root CHANGELOG.md from git-log version-bump skeleton</name>
    <files>CHANGELOG.md</files>
    <action>
  Per CONTEXT D-09: author by hand. NO `owl changelog backfill` subcommand (deferred). One commit.

  Step A: Generate version skeleton from git log:
  ```
  git log --oneline --grep='chore: bump' > /tmp/version-bumps.txt
  ```
  Expect ~38 entries (RESEARCH Appendix A surfaced this count). Read the output; map each `chore: bump spt plugin to vX.Y.Z` line to a target H2 section. Skipped versions (1.9.11, 1.9.6, 1.9.0/1/3/4) get NO entry — they were never released.

  Step B: For each released version, derive curated prose by inspecting the commit RANGE between consecutive bumps:
  ```
  git log --oneline <previous_bump_sha>..<this_bump_sha>
  ```
  Summarize the user-facing changes (feature adds, fixes, breaking changes if any) in 1-5 bullet points per version. Format guidance:
    - Top-level title: `# Changelog` followed by a one-line description and a link to Keep-a-Changelog 1.1.0 + Semantic Versioning per spec convention.
    - Each version: H2 line `## [X.Y.Z] - YYYY-MM-DD` (date = the bump commit's author date in ISO-8601, get via `git show -s --format=%ad --date=short <sha>`).
    - Body: prose paragraphs OR bullet lists OR H3 subsections (`### Added` / `### Fixed` / etc. per Keep-a-Changelog 1.1.0) — author's discretion; the Plan 02 parser does NOT care about body shape.
    - NEWEST version first, OLDEST last (Keep-a-Changelog convention).
    - NO `[Unreleased]` section — we author retroactively.
    - NO link-reference lines (`[1.10.10]: <url>`) — unused by our parser.

  Step C: Validate format by hand-running the parser regex against each H2 line:
  ```
  grep -E '^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$' CHANGELOG.md | wc -l
  ```
  The match count MUST equal the number of H2 sections you authored. If any H2 line fails this regex, the Plan 02 parser will skip that version → re-author the offending line to match.

  Step D: Read back the file once with `wc -l CHANGELOG.md` and confirm it is non-empty and "reasonable size" (expect 100-400 lines for ~30+ versions of curated prose).

  Step E: Commit:
  ```
  git add CHANGELOG.md
  git commit -m "docs(34): create repo-root CHANGELOG.md (Keep-a-Changelog 1.1.0)"
  ```

  CRITICAL CONSTRAINTS:
  - The CHANGELOG content itself is curated prose — you are authoring history, not auto-generating. Quality bar: each entry should read as a useful release note for a future user.
  - Date format MUST be ISO-8601 `YYYY-MM-DD` with hyphens. Slashes will fail the parser regex.
  - Patch-level semver only (`X.Y.Z`). NO pre-release suffixes (`-rc1`) — parser rejects them per RESEARCH Q10.
  - No commit-message-style prefixes inside the body (no `feat:` / `fix:` prefixes — those go in the git log, not the CHANGELOG).
    </action>
    <verify>
      <!-- N2 fix: research surfaced 38 historical bumps (RESEARCH Appendix A). Raise the
           floor from 20 to 30 to better track reality, AND cross-check against the live
           `git log --grep='chore: bump'` count so the floor self-calibrates if more bumps
           have landed since RESEARCH was written. Skipped versions (1.9.11/6/0/1/3/4) mean
           the H2 count can legitimately be a few less than the raw git-log count — the
           tolerance is `>= (bump_count - 8)` to absorb skips without false negatives. -->
      <automated>test -f CHANGELOG.md &amp;&amp; H2_COUNT=$(grep -cE '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$' CHANGELOG.md) &amp;&amp; test "$H2_COUNT" -ge 30 &amp;&amp; BUMP_COUNT=$(git log --grep='chore: bump' --oneline | wc -l | tr -d ' ') &amp;&amp; test "$H2_COUNT" -ge "$((BUMP_COUNT - 8))" &amp;&amp; ! grep -qE '^## \[Unreleased\]' CHANGELOG.md &amp;&amp; ! grep -qE '^## \[[0-9]+\.[0-9]+\.[0-9]+-' CHANGELOG.md</automated>
    </verify>
    <done>CHANGELOG.md exists at repo root; ≥30 valid Keep-a-Changelog H2 sections (and within 8 of the live `git log --grep='chore: bump'` count); no `[Unreleased]`; no pre-release suffix versions; committed.</done>
  </task>

  <task type="auto">
    <name>Task 3: Extend docs/DEPLOY.ps1 — TODO-marker abort gate + stub-append + CHANGELOG.md sync (MarketSpt + CacheVer)</name>
    <files>docs/DEPLOY.ps1</files>
    <action>
  Four discrete insertions per the verified line citations in `<interfaces>`. Read the file FIRST and confirm the anchor lines before editing — DEPLOY.ps1 evolves between phases.

  === Insertion 1: TODO-marker abort gate (inside -Bump branch) ===

  Pattern source: existing dirty-state checks at docs/DEPLOY.ps1:213-222 (CONTEXT canonical_refs).
  Insert AFTER the existing Cargo.toml dirty-state `if` block (currently ending at line 222 `exit 1 }`), BEFORE the `# Read Cargo.toml and locate its current [package] version` comment (currently line 224).

  Insert this block verbatim:
  ```powershell
      # Phase 34 D-10 abort gate: refuse to bump if the previous -Bump's CHANGELOG.md
      # stub is still un-filled. Scans for the literal TODO marker the stub injects.
      $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
      if (Test-Path $ChangelogPath) {
          $ChangelogContent = Get-Content $ChangelogPath -Raw
          if ($ChangelogContent -match 'TODO: changelog entry') {
              Write-Host "ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump." -ForegroundColor Red
              Write-Host "       Fill in the entry and commit before re-running -Bump." -ForegroundColor Red
              exit 1
          }
      } else {
          Write-Host "ERROR: CHANGELOG.md not found at $ChangelogPath" -ForegroundColor Red
          Write-Host "       Phase 34 requires a repo-root CHANGELOG.md; create one before -Bump." -ForegroundColor Red
          exit 1
      }
  ```

  === Insertion 2: CHANGELOG.md stub-append (uncommitted, after `git commit`) ===

  Pattern source: existing Cargo.toml regex-rewrite-then-verify at docs/DEPLOY.ps1:246-274 (CONTEXT canonical_refs).
  CRITICAL ORDERING per Pitfall 5 in RESEARCH.md: stub append MUST run AFTER `git commit` (current line 281) so CHANGELOG.md stays in working tree only — NEVER staged into the bump commit.

  Insert AFTER `Write-SubStep "Committed bump: $bumpSha (not pushed)"` (current line 287), BEFORE the closing `}` of the `-not $DryRun` else branch (current line 288). Wrap in an `if ($DryRun)` / `else` to honor the existing dry-run discipline.

  Actually — to keep the indent and ordering clean, insert AFTER the `} else {` ... `Write-SubStep "Committed bump: $bumpSha (not pushed)"` ... `}` closing brace of the `if ($DryRun) { ... } else { ... }` block (current line 288), and BEFORE `$Version = $NewVersion` (current line 290).

  Insert this block:
  ```powershell
      # Phase 34 D-10 stub-append: drop a TODO H2 into repo-root CHANGELOG.md
      # WITHOUT staging or committing. The next -Bump's abort gate (insertion 1)
      # will refuse to proceed until the author fills the entry and commits it.
      $today = Get-Date -Format 'yyyy-MM-dd'
      $todoMarker = "TODO: changelog entry"
      $stub = "`n## [$NewVersion] - $today`n`n- $todoMarker`n"
      if ($DryRun) {
          Write-Host "    [DRY-RUN] append stub to CHANGELOG.md: ## [$NewVersion] - $today (uncommitted)" -ForegroundColor DarkYellow
      } else {
          $ChangelogRaw = Get-Content $ChangelogPath -Raw
          $newChangelog = $ChangelogRaw.TrimEnd("`r","`n") + $stub
          [System.IO.File]::WriteAllText($ChangelogPath, $newChangelog)
          Write-SubStep "Appended CHANGELOG.md stub for v$NewVersion (fill in + commit before next -Bump)"
          Write-SubStep "  NOTE: CHANGELOG.md is intentionally unstaged. Edit it, then 'git add CHANGELOG.md && git commit'."
      }
  ```

  NOTE: `$ChangelogPath` is in scope from insertion 1's variable declaration. If the variable is not assigned outside the `if ($Bump)` block (insertion 1 lives inside `if ($Bump) { ... }`), redeclare it here:
  ```powershell
      $ChangelogPath = Join-Path $RepoRoot 'CHANGELOG.md'
  ```

  === Insertion 3: CHANGELOG.md sync -> MarketSpt ===

  Pattern source: existing `Copy-Item -Force` wrapped in `Invoke-Mutation` at docs/DEPLOY.ps1:359-370 (CONTEXT canonical_refs).
  Insert AFTER the `Copy hooks/* -> $MarketHooks` Invoke-Mutation block (current lines 365-367), BEFORE the `Copy plugin.json -> $MarketPluginMeta` block (current lines 368-370).

  Insert this block:
  ```powershell
      Invoke-Mutation "Copy CHANGELOG.md -> $MarketSpt" {
          Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $MarketSpt 'CHANGELOG.md') -Force
      }
  ```

  Per CONTEXT D-11: target is `{Marketplace}/plugin/spt/CHANGELOG.md` — sibling to plugin.json, NOT inside `.claude-plugin/`. `$MarketSpt` already resolves to that directory (verify by reading the variable assignment earlier in the script; do not re-derive).

  === Insertion 4: CHANGELOG.md sync -> CacheVer ===

  Pattern source: existing CacheVer `Copy-Item -Force` blocks at docs/DEPLOY.ps1:489-500 (CONTEXT canonical_refs).
  Insert AFTER the `Copy hooks/* -> $CacheVer/hooks` block (current lines 495-497), BEFORE the `Copy plugin.json -> $CacheVer/.claude-plugin` block (current lines 498-500).

  Insert this block:
  ```powershell
  Invoke-Mutation "Copy CHANGELOG.md -> $CacheVer" {
      Copy-Item -Path (Join-Path $RepoRoot 'CHANGELOG.md') -Destination (Join-Path $CacheVer 'CHANGELOG.md') -Force
  }
  ```

  NOTE: this is the cache-tier sync; target is `$CacheVer/CHANGELOG.md` (plugin root, NOT `$CacheVer/.claude-plugin/CHANGELOG.md`). Hook resolves at runtime via `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md`, and `$CLAUDE_PLUGIN_ROOT` points to `$CacheVer` for plugin-bundled hooks.

  === Verification dry-run ===

  After all four insertions, run:
  ```
  powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun
  ```
  Expect to see (in some order):
    - `[DRY-RUN] Copy CHANGELOG.md -> {MarketSpt path}` (insertion 3)
    - `[DRY-RUN] Copy CHANGELOG.md -> {CacheVer path}` (insertion 4)

  Then run:
  ```
  powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun
  ```
  Expect to see (in order):
    - The new `ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump.` is NOT triggered (CHANGELOG just authored has no TODO marker).
    - `[DRY-RUN] append stub to CHANGELOG.md: ## [X.Y.Z+1] - YYYY-MM-DD (uncommitted)` (insertion 2 dry-run line).
    - The two sync substeps above also surface.

  === Commit ===

  ```
  git add docs/DEPLOY.ps1
  git commit -m "feat(34): DEPLOY.ps1 CHANGELOG.md sync + bump stub/abort gate"
  ```

  Body suggestion:
  ```
  - New abort gate refuses -Bump when prior CHANGELOG stub still has 'TODO: changelog entry' marker.
  - New stub-append drops `## [NEW] - YYYY-MM-DD\n\n- TODO: changelog entry` into CHANGELOG.md AFTER the bump commit, intentionally unstaged so the next -Bump's gate fires until the author fills + commits.
  - New sync substeps copy repo-root CHANGELOG.md to {Marketplace}/plugin/spt/CHANGELOG.md AND to {CacheVer}/CHANGELOG.md (sibling to plugin.json, NOT inside .claude-plugin/) per CONTEXT D-11.
  ```
    </action>
    <verify>
      <!-- PRECONDITION: working tree MUST be clean OR this task MUST run in a tempdir-cloned repo.
           DEPLOY.ps1 -Bump (and even bare -DryRun in some branches) aborts on a dirty Cargo.toml
           dirty-state check; we cannot reliably exercise the -Bump dry-run mid-plan-execution.
           The blocking verify therefore runs the no-bump dry-run only (which surfaces insertions 3+4)
           and uses comment-stripped grep counts (per grep-gate hygiene: bare `grep -c` counts
           self-referential comment lines like the canonical_refs prose, inflating the count).
           Insertion 1's abort gate (literal TODO scan) and insertion 2's stub-append dry-run line
           are observed in the non-blocking <done> below. -->
      <automated>TODO_COUNT=$(grep -v '^[[:space:]]*#' docs/DEPLOY.ps1 | grep -c "TODO: changelog entry") &amp;&amp; test "$TODO_COUNT" -ge 2 &amp;&amp; COPY_COUNT=$(grep -v '^[[:space:]]*#' docs/DEPLOY.ps1 | grep -c "Copy CHANGELOG.md") &amp;&amp; test "$COPY_COUNT" -ge 2 &amp;&amp; powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun 2>&amp;1 | tee /tmp/d.log &amp;&amp; grep -q "Copy CHANGELOG.md -> .*MarketSpt\|Copy CHANGELOG.md -> .*spt" /tmp/d.log &amp;&amp; grep -c "Copy CHANGELOG.md" /tmp/d.log | grep -qE '^[2-9]'</automated>
    </verify>
    <done>docs/DEPLOY.ps1 contains both `TODO: changelog entry` references (gate scan + stub literal) and both `Copy CHANGELOG.md` Invoke-Mutation blocks (MarketSpt + CacheVer); the no-bump `-DryRun` surfaces the two new sync substeps; commit landed.

  NON-BLOCKING OBSERVATIONS (manually verify in a clean-tree environment OR a tempdir clone — do not gate the task on these, since `-Bump patch -DryRun` may abort on the live working tree's dirty state):
  - `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun` (against a clean tree) emits `[DRY-RUN] append stub to CHANGELOG.md: ## [X.Y.Z+1] - YYYY-MM-DD (uncommitted)` (insertion 2).
  - The abort gate (insertion 1) is reachable: temporarily inject `TODO: changelog entry` into CHANGELOG.md, run `-Bump patch -DryRun`, and observe the `ERROR: CHANGELOG.md contains 'TODO: changelog entry' from a prior -Bump.` exit-1 path. Remove the injection after observation.</done>
  </task>

  </tasks>

  <verification>
  After all 3 tasks:

  1. `git log --oneline -5` shows three new commits in order:
     - `docs(34): amend VERS-02/04/06/08/09 + ROADMAP SC1/2/5 per Phase 34 D-12`
     - `docs(34): create repo-root CHANGELOG.md (Keep-a-Changelog 1.1.0)`
     - `feat(34): DEPLOY.ps1 CHANGELOG.md sync + bump stub/abort gate`

  2. `git diff --name-only HEAD~3` shows exactly 4 paths:
     - `.planning/REQUIREMENTS.md`
     - `.planning/ROADMAP.md`
     - `CHANGELOG.md`
     - `docs/DEPLOY.ps1`

  3. `grep -cE '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}\s*$' CHANGELOG.md` ≥ 20 (curated history).

  4. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -DryRun` exits 0 AND emits both new `Copy CHANGELOG.md` substeps.

  5. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch -DryRun` exits 0 AND emits the new `[DRY-RUN] append stub to CHANGELOG.md` substep.

  6. Cargo tests untouched: `cargo test --no-run` builds (smoke check that nothing accidentally introduced a Rust syntax break — Plan 01 is pure doc/script, no Rust expected).
  </verification>

  <success_criteria>
  - Plan 01 maps to (post-amendment) VERS-02 (REQUIREMENTS.md wording landed), VERS-04 (wording landed), VERS-06 (wording landed), VERS-07 (CHANGELOG.md exists + sync syncs it), VERS-08 (wording landed), VERS-09 (wording landed).
  - Plan 01 unblocks Plan 02 by establishing CHANGELOG.md with real content for parser unit + integration tests.
  - Plan 01 unblocks future end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6) by installing the maintenance gate.
  - ROADMAP.md Phase 34 SC1, SC2, SC5 read coherently against the implementation Plan 02/03 will deliver.
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | author -> repo | hand-authored CHANGELOG.md content reaches plugin users via deploy; trusted because author IS the user |
  | deploy script -> filesystem | DEPLOY.ps1 writes to marketplace + cache; local-only, no network surface |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-01 | Tampering | CHANGELOG.md author content | accept | Hand-authored prose under git review; no automated injection path |
  | T-34-02 | Tampering | DEPLOY.ps1 abort gate bypass | accept | Local script, single-user trust boundary; the gate is UX-not-security |
  | T-34-03 | Repudiation | Bump commit not including CHANGELOG.md | mitigate | Stub-append runs AFTER `git commit` (insertion 2 ordering, Pitfall 5) so stub stays unstaged and the bump commit is provably clean |
  | T-34-04 | DoS | Abort gate infinite-loop (author cannot escape) | accept | Gate prints clear instructions: "Fill in the entry and commit before re-running -Bump." Author has a direct remedy. |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-01-SUMMARY.md` documenting:
  - Three commit SHAs
  - CHANGELOG.md final H2 section count
  - Any deviations from the line-citation anchors in `<interfaces>` (DEPLOY.ps1 may have drifted; record the actual insertion line numbers used)
  - Confirmation that `-Bump patch -DryRun` surfaces the new substeps in expected order
  </output>

  #### 34-02-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 02
  type: execute
  wave: 2
  depends_on: [34-01]
  files_modified:
    - src/owl/version_changelog.rs
    - src/owl/mod.rs
    - src/owl/hook_idle.rs
    - src/cli.rs
    - src/common/owlery.rs
  autonomous: true
  requirements:
    - VERS-01
    - VERS-02
    - VERS-03
    - VERS-05
    - VERS-09
  user_setup: []

  must_haves:
    truths:
      - "A new `src/owl/version_changelog.rs` module owns sentinel I/O, the Keep-a-Changelog parser, and the payload-XML builder"
      - "`hook_idle.rs::run()` calls the version-change check AFTER perch resolution (lines 25-32) AND after an OWL_ECHO_COMMUNE guard, BEFORE `set_idle_ready` (line 39)"
      - "On version mismatch the hook emits `{\"decision\":\"block\",\"reason\":\"<spt-version-changelog>...</spt-version-changelog>\"}` to stdout"
      - "Sentinel is pre-written to NEW version atomically via `owlery::atomic_write_string` BEFORE the block is emitted; rollback ONLY via `$OWL version-remind <old>`"
      - "First-install path (sentinel missing) silently writes the current version and emits NO block (VERS-03)"
      - "`stop_hook_active=true` from stdin short-circuits the check (RESEARCH Pitfall 4 — defensive infinite-loop guard)"
      - "A new clap subcommand `owl version-remind <old>` writes `{\"version\":\"<old>\"}` to the sentinel and exits 0; missing-arg or non-semver arg exits 1 (RESEARCH security V5)"
      - "A test-only env var `SPT_OVERRIDE_PKG_VERSION` lets unit + integration tests simulate any version without rebuilding (RESEARCH Q9)"
      - "Co-located unit tests in `src/owl/version_changelog.rs::tests` cover the 18-row parser corpus + sentinel read/write round-trips"
      - "`tests/version_changelog.rs` integration tests cover: first-install silent, single-step mismatch, multi-step mismatch, sentinel-equal-no-emit, OWL_ECHO_COMMUNE guard, no-perch guard, stop_hook_active guard, version-remind round-trip, version-remind missing-arg, version-remind non-semver-arg"
    artifacts:
      - path: "src/owl/version_changelog.rs"
        provides: "Sentinel + parser + payload builder + `run_version_remind` handler"
        min_lines: 200
      - path: "src/owl/hook_idle.rs"
        provides: "Version-change emission call site at top of `run()` after perch resolution + OWL_ECHO_COMMUNE guard"
        contains: "version_changelog::"
      - path: "src/cli.rs"
        provides: "`VersionRemind { old: String }` clap variant + clap parse-test"
        contains: "VersionRemind"
      - path: "src/owl/mod.rs"
        provides: "Module declaration `pub mod version_changelog;` + dispatch arm `Some(Commands::VersionRemind { old }) => ...`"
        contains: "VersionRemind"
      - path: "src/common/owlery.rs"
        provides: "New helper `pub fn sentinel_path() -> PathBuf` to encapsulate the spt_home().join(\"last-seen-version.json\") expression (RESEARCH Pitfall 2 / Risk #2)"
        contains: "sentinel_path"
      - path: "tests/version_changelog.rs"
        provides: "Integration tests covering all 10 cases above; mirrors `tests/auto_pick_integration.rs` SPT_HOME sandbox + perch-fixture pattern"
        min_lines: 300
    key_links:
      - from: "src/owl/hook_idle.rs::run()"
        to: "src/owl/version_changelog.rs::maybe_emit_version_change_block"
        via: "function call after perch resolution + OWL_ECHO_COMMUNE guard"
        pattern: "version_changelog::maybe_emit"
      - from: "src/cli.rs Commands::VersionRemind"
        to: "src/owl/version_changelog.rs::run_version_remind"
        via: "dispatch arm in src/owl/mod.rs::handle_command"
        pattern: "Commands::VersionRemind"
      - from: "Stop hook block emission"
        to: "sentinel pre-write"
        via: "atomic_write_string is called BEFORE the `println!` that emits the block"
        pattern: "atomic_write_string"
  ---

  <objective>
  Phase 34 Rust internals: sentinel + parser + payload builder + Stop-hook wiring + `version-remind` subcommand.

  Purpose: Land all the deterministic, testable code surface for Phase 34. No user-facing wording yet — the block-body XML scaffold is emitted but the natural-language instructions inside `<instructions>` get authored in Plan 03.

  Output: One new module, three modified files, one new integration-test file, ~30+ tests (parser corpus + sentinel + hook + subcommand + integration). Cargo tests green.
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @.planning/phases/34-version-change-changelog/34-01-SUMMARY.md
  @CHANGELOG.md
  @src/owl/hook_idle.rs
  @src/cli.rs
  @src/owl/mod.rs
  @src/common/owlery.rs

  <interfaces>
  <!-- Verified line citations from Read tool — confirm by re-reading before editing. -->

  src/owl/hook_idle.rs::run() (verified lines 14-43, file may evolve):
    Line 14: `pub fn run() {`
    Lines 17-22: stdin parse + session_id resolve
    Lines 25-32: perch resolution -> early return on None
    Line 34-39: comment + `inbox::set_idle_ready(&owl_id);`
    Line 42: `spawn_echo_commune_if_live(&owl_id);`

  src/owl/hook_idle.rs::spawn_echo_commune_if_live() (verified lines 57-98):
    Lines 64-69: OWL_ECHO_COMMUNE recursion guard pattern (COPY THIS SHAPE EXACTLY)

  src/common/owlery.rs::atomic_write_string (verified line 248):
    Signature: `pub fn atomic_write_string(path: &std::path::Path, body: &str) -> std::io::Result<()>`
    Tmp + rename; NTFS+POSIX same-volume atomic.

  src/common/owlery.rs::spt_home (verified lines 18-32):
    Signature: `pub fn spt_home() -> PathBuf`
    Resolves `$SPT_HOME` env override or platform default; create-if-missing.

  src/cli.rs Commands::NewAlarm (verified lines 139-153) — POSITIONAL-ARG ANALOG:
  ```rust
  #[command(name = "new-alarm")]
  NewAlarm {
      self_id: String,
      time_spec: String,
      ...
  },
  ```

  src/cli.rs Commands::HookIdle (verified line 197):
    `HookIdle,`  // variant only, no fields; dispatch handled in src/owl/mod.rs

  src/owl/mod.rs::handle_command (verified line 37) — DISPATCH ARMS:
    Line 116: `Some(Commands::NewAlarm { self_id, time_spec, message_file, message }) => { ... }`
    Line 143: `Some(Commands::HookIdle) => { ... }`

  src/main.rs (verified): `let cli = Cli::parse(); handle_command(cli.command);` (line 40-41)
    No edits needed to main.rs.

  NO `regex` crate in Cargo.toml — parser MUST be hand-rolled.

  Sentinel JSON shape (LOCKED — CONTEXT specifics):
  ```json
  {"version":"X.Y.Z"}
  ```
  Single field. No timestamp. No plugin name.

  Sentinel path (LOCKED — VERS-01 + CONTEXT):
    `spt_home().join("last-seen-version.json")` — SIBLING to `owlery/`, NOT inside it.

  Compile-time version source (LOCKED — VERS-02, RESEARCH Q9):
  ```rust
  fn current_version() -> String {
      // Test seam: SPT_OVERRIDE_PKG_VERSION lets tests simulate any version
      // without rebuilding. Production paths see env!("CARGO_PKG_VERSION").
      if let Ok(v) = std::env::var("SPT_OVERRIDE_PKG_VERSION") {
          if !v.is_empty() {
              return v;
          }
      }
      env!("CARGO_PKG_VERSION").to_string()
  }
  ```
  Return `String` (not `&'static str`) to avoid the `Box::leak` discussed in RESEARCH Q9. The allocation cost is paid once per hook invocation; immaterial.

  Parser regex (LOCKED — RESEARCH Q10 corpus):
    Regex equivalent: `^## \[(\d+)\.(\d+)\.(\d+)\] - (\d{4})-(\d{2})-(\d{2})\s*$`
    Hand-rolled MUST:
      - Require literal prefix `## [` at column 0
      - Capture three `\d+` semver components separated by `.`
      - Require literal `] - ` between version and date
      - Capture `\d{4}-\d{2}-\d{2}` ISO-8601 date
      - Allow trailing whitespace, reject any other trailing content
      - Reject `[Unreleased]`, `[1.10]`, `[1.10.10-rc1]`, `# [1.10.10] - ...`, `### [1.10.10] - ...`, `[1.10.10]: <url>`, `## [1.10.10] - 2026/05/16`
    See RESEARCH "CHANGELOG.md Parser — Test Corpus" for the full 18-row case set.

  ChangelogEntry struct (RESEARCH parser API):
  ```rust
  pub struct ChangelogEntry {
      pub version: (u32, u32, u32),
      pub date: String,        // "YYYY-MM-DD" verbatim
      pub body: String,        // content between this H2 and next H2 / EOF, verbatim
      pub line_no: usize,      // 1-indexed line where the H2 lives
  }
  ```

  Public API surface (RESEARCH):
  ```rust
  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry>;
  pub fn sections_between(entries: &[ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&ChangelogEntry>;
  ```
  `sections_between` returns entries E where `old < E.version <= new`, ordered ascending by version. Used by Plan 03 wording and by the payload's `step_count`.

  Block payload shape (LOCKED — CONTEXT D-07 + RESEARCH Q11):
  ```xml
  <spt-version-changelog>
    <old>X.Y.Z</old>
    <new>X.Y.Z</new>
    <old_date>YYYY-MM-DD</old_date>
    <new_date>YYYY-MM-DD</new_date>
    <step_count>N</step_count>
    <changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>
    <invocation_for_remind>$OWL version-remind X.Y.Z</invocation_for_remind>
    <instructions>
      [PLAN 03 OWNS THIS WORDING; Plan 02 emits a placeholder string here]
    </instructions>
  </spt-version-changelog>
  ```
  `<old_date>` / `<new_date>` may be absent if CHANGELOG.md is unresolvable (Pitfall 2 graceful-degrade); emit empty elements `<old_date></old_date>` rather than skipping the tag.

  Plan 02 emits literal placeholder for `<instructions>` block (Plan 03 replaces). Use this exact placeholder so Plan 03 can grep+replace deterministically:
  ```
  [PLAN_03_INSTRUCTIONS_PLACEHOLDER]
  ```

  Recursion guard ordering (LOCKED — CONTEXT D-03 / RESEARCH Pitfall 4):
    OWL_ECHO_COMMUNE guard MUST run BEFORE the version-change check. Stop_hook_active guard MUST also run before the check. Both short-circuit the check; perch resolution is the only predicate that allows the check to proceed.

  Block emission MUST use `serde_json::json!` + `println!` (RESEARCH code examples):
  ```rust
  let response = serde_json::json!({
      "decision": "block",
      "reason": reason_xml,
  });
  println!("{}", serde_json::to_string(&response).unwrap());
  ```
  This writes to stdout; Claude Code reads the JSON envelope and propagates `reason` to Claude on re-engagement (RESEARCH Q1 GREEN).

  CRITICAL — `hook_idle.rs::run()` currently DOES NOT print anything to stdout (it's a side-effect hook). Adding the version-change emission changes that. Document this in the new function's doc-comment.

  Sentinel write ordering (LOCKED — CONTEXT D-05):
    1. Write NEW version to sentinel atomically.
    2. Emit block.
    Order matters: if step 2 fails to print (impossible barring OOM), the sentinel still reflects truth. If step 1 fails, RESEARCH Pitfall 1 says: skip emission AND do not panic. The user sees no prompt this turn; next Stop will re-check.

  `$CLAUDE_PLUGIN_ROOT` resolution (RESEARCH Q4 + Pitfall 2):
  ```rust
  fn resolve_changelog_path() -> Option<std::path::PathBuf> {
      let root = std::env::var("CLAUDE_PLUGIN_ROOT").ok()?;
      let path = std::path::PathBuf::from(root).join("CHANGELOG.md");
      if path.is_file() { Some(path) } else { None }
  }
  ```
  Hook may emit even when CHANGELOG.md is unresolvable — the block then carries empty `<old_date>` / `<new_date>` / `<changelog_path>NOT_RESOLVED</changelog_path>` and `<step_count>` derived from raw version diff (e.g., minor-version delta as a rough proxy). Researcher's recommendation per Pitfall 2.

  Stop_hook_active parsing:
    Extend `hook_output::parse_hook_stdin` (in src/common/hook_output.rs) to capture `stop_hook_active: bool` (default false). If the existing parser doesn't already surface it, add the field. Reuse if present.

  `version-remind` security validation (LOCKED — RESEARCH security V5):
    Validate `<old>` against semver regex `^\d+\.\d+\.\d+$` BEFORE writing the sentinel. Hand-rolled validator (no `regex` crate):
  ```rust
  fn is_plain_semver(s: &str) -> bool {
      let parts: Vec<&str> = s.split('.').collect();
      parts.len() == 3 && parts.iter().all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
  }
  ```
  On non-match: stderr `error: <old version> must match X.Y.Z semver`, exit 1.
  On missing arg: clap handles natively (positional arg required).

  Test-helper for SPT_HOME sandbox + perch fixture (PATTERN MAP — `tests/auto_pick_integration.rs:1-120`):
    Replicate verbatim. Key fields:
      - `static ENV_LOCK: Mutex<()> = Mutex::new(())`
      - `SptHomeGuard` RAII for SPT_HOME mutation
      - `write_live_perch(spt_home, id)` helper writing `{spt_home}/owlery/{id}/ready` + `info.json` with `parent_pid = std::process::id() as u64`
      - `assert_cmd::Command::cargo_bin("owl")` to invoke the subprocess
      - `.env("SPT_HOME", tmp).env("HOME", tmp).env("USERPROFILE", tmp).env_remove("OWL_ECHO_COMMUNE")` env hygiene

  `tests/version_changelog.rs` integration cases (LOCKED — RESEARCH Integration Test Corpus + CONTEXT):
    1. `first_install_silent_writes_sentinel` — empty SPT_HOME, SPT_OVERRIDE_PKG_VERSION=1.10.10; assert sentinel written, no `decision:"block"` in stdout
    2. `single_step_mismatch_emits_block` — sentinel `{"version":"1.10.9"}`, OVERRIDE=1.10.10; CHANGELOG fixture has both versions; assert block emitted with `<old>1.10.9</old>`, `<new>1.10.10</new>`, `<step_count>1</step_count>`; sentinel now 1.10.10
    3. `multi_step_mismatch_emits_block_with_step_count` — sentinel 1.10.5, OVERRIDE=1.10.10, CHANGELOG has 1.10.5..1.10.10; step_count=5
    4. `sentinel_equal_no_emit` — sentinel 1.10.10, OVERRIDE=1.10.10; no block; sentinel unchanged
    5. `owl_echo_commune_guard_skips_emission` — mismatch + OWL_ECHO_COMMUNE=1; no block; sentinel unchanged
    6. `no_perch_skips_emission` — mismatch but no perch fixture written; no block; sentinel unchanged
    7. `stop_hook_active_skips_emission` — mismatch + stdin `{"stop_hook_active":true}`; no block; sentinel unchanged
    8. `version_remind_rollback_round_trip` — set sentinel to 1.10.10, invoke `owl version-remind 1.10.9`, assert sentinel reads `{"version":"1.10.9"}`
    9. `version_remind_missing_arg_exits_1` — `owl version-remind` no arg; exit code != 0 (clap-emitted)
    10. `version_remind_non_semver_exits_1` — `owl version-remind not-a-version`; exit code 1; stderr matches "semver"

    Test fixtures: write a synthetic `CHANGELOG.md` into the SPT_HOME tempdir under a path the test sets `CLAUDE_PLUGIN_ROOT` to. Suggested fixture: a string constant inside the test module with 6-10 Keep-a-Changelog entries spanning 1.10.5..1.10.10.

  Co-located unit tests in `src/owl/version_changelog.rs::tests` (LOCKED — RESEARCH parser corpus):
    Cover the full 18-row test corpus from RESEARCH Q10. One test per row OR table-driven test. Each row asserts parse-or-reject + extracted version+date if parsed.
  </interfaces>
  </context>

  <tasks>

  <task type="auto" tdd="true">
    <name>Task 1: Create src/owl/version_changelog.rs (sentinel + parser + payload + version-remind handler) with unit tests</name>
    <files>src/owl/version_changelog.rs, src/owl/mod.rs, src/common/owlery.rs, src/cli.rs</files>
    <behavior>
  Unit tests (co-located `#[cfg(test)] mod tests`):
  - Parser corpus (RESEARCH Q10, 18 rows):
    - Accept `## [1.10.10] - 2026-05-16` -> version=(1,10,10), date="2026-05-16"
    - Accept `## [0.9.3] - 2025-12-01` (pre-1.0)
    - Reject `## [Unreleased]`
    - Reject `## [1.10.10]` (no date)
    - Reject `## [1.10] - 2026-05-16` (no patch)
    - Reject `## [1] - 2026-05-16` (no minor)
    - Reject `[1.10.10]: https://...` (link reference)
    - Reject `# [1.10.10] - 2026-05-16` (H1)
    - Reject `### [1.10.10] - 2026-05-16` (H3)
    - Accept `## [1.10.10] - 2026-05-16  ` (trailing whitespace tolerance)
    - Accept BOM-prefixed line (strip BOM at file-read time)
    - Reject `## [1.10.10] - 2026-05-16 (yanked)` (strict trailing-text rejection)
    - Reject `## [1.10.10-rc1] - 2026-05-16` (pre-release suffix)
    - Reject `## ` (empty H2)
    - Reject `## [ 1.10.10 ] - 2026-05-16` (whitespace in brackets)
    - Reject `## [1.10.10] - 2026/05/16` (slash date)
  - `sections_between` corpus:
    - Range query 1.10.5 (exclusive) -> 1.10.7 (inclusive) over entries [1.10.5, 1.10.6, 1.10.7, 1.10.8] returns [1.10.6, 1.10.7]
    - Body content preserved verbatim (no parser mutation)
  - Sentinel I/O:
    - `read_sentinel()` returns None when file missing
    - `read_sentinel()` returns None when JSON malformed
    - `read_sentinel()` returns Some("X.Y.Z") on happy path
    - `write_sentinel("1.10.10")` round-trips through `read_sentinel()` correctly
    - `write_sentinel` uses `atomic_write_string` (no .tmp residue after success)
  - `current_version()`:
    - With SPT_OVERRIDE_PKG_VERSION unset -> returns `env!("CARGO_PKG_VERSION")` value
    - With SPT_OVERRIDE_PKG_VERSION="1.99.99" -> returns "1.99.99"
    - With SPT_OVERRIDE_PKG_VERSION="" -> falls through to env! (empty treated as unset)
  - `is_plain_semver` validator:
    - Accept "1.10.10", "0.0.0", "999.999.999"
    - Reject "", "1.10", "1.10.10.10", "1.10.x", "1.10.10-rc1", "abc.def.ghi", " 1.10.10"
  - `run_version_remind`:
    - Valid semver arg writes sentinel + returns success
    - Non-semver arg returns ExitCode::from(1) without writing sentinel
  - `build_block_reason` builder:
    - Given old=(1,10,9), new=(1,10,10), step_count=1, optional old_date/new_date -> returns a string containing all required XML elements + literal `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]`
    - Element ordering: `<old>` before `<new>` before `<old_date>` before `<new_date>` before `<step_count>` before `<changelog_path>` before `<invocation_for_remind>` before `<instructions>`
    - When CHANGELOG path unresolvable: `<changelog_path>NOT_RESOLVED</changelog_path>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>` is verbatim (no escaping; literal `$OWL` substring)
    </behavior>
    <action>
  PRE-EDIT ANCHOR GUARD (C1 — line-citations in `<interfaces>` drift between phases). Run these greps FIRST; if either fails, ABORT the task and re-read the cited files to resync anchors before proceeding:
  ```
  grep -q "Some(Commands::NewAlarm" src/owl/mod.rs
  grep -q "NewAlarm" src/cli.rs
  ```
  The first confirms the dispatch arm pattern for `Some(Commands::NewAlarm { ... })` is still present in `src/owl/mod.rs` (anchor for inserting the new `Some(Commands::VersionRemind { old })` arm). The second confirms the clap `NewAlarm` variant still exists in `src/cli.rs` (anchor for inserting `VersionRemind` after it). If either grep fails (exit != 0), STOP — the task's line citations are stale and the insertions risk landing in the wrong place. Resync by re-reading the files, then update the per-step line numbers below, then re-run the greps as a final sanity check.

  After the guard passes, proceed:

  Create the new module file `src/owl/version_changelog.rs`. Hand-roll all parsing (NO `regex` crate per Cargo.toml verification).

  Structure the file as:
  ```rust
  // Phase 34 (D-01..D-12): version-change detection for Stop hook.
  // Public surface:
  //   - read_sentinel / write_sentinel — atomic sentinel I/O at $SPT_HOME/last-seen-version.json
  //   - current_version — env!("CARGO_PKG_VERSION") with SPT_OVERRIDE_PKG_VERSION test seam (RESEARCH Q9)
  //   - parse_changelog / sections_between — Keep-a-Changelog 1.1.0 H2 parser
  //   - build_block_reason — emit the <spt-version-changelog> payload XML for hook_idle.rs
  //   - maybe_emit_version_change_block — Stop-hook entry point (called from hook_idle.rs::run)
  //   - run_version_remind — clap-dispatched handler for `owl version-remind <old>`
  //
  // NO regex crate dependency — all parsing is hand-rolled. Sentinel JSON shape
  // is `{"version":"X.Y.Z"}` and the writer formats it directly (no serde_json
  // round-trip needed for the write path; the reader uses serde_json::Value
  // to gracefully degrade on malformed input per RESEARCH Pitfall 1).
  ```

  Implement these functions (signatures verbatim):
  ```rust
  pub fn current_version() -> String { ... }                                     // see <interfaces>
  pub fn sentinel_path() -> std::path::PathBuf { ... }                           // delegates to owlery::sentinel_path
  pub fn read_sentinel() -> Option<String> { ... }                               // graceful None on missing/malformed
  pub fn write_sentinel(version: &str) -> std::io::Result<()> { ... }            // atomic_write_string

  pub struct ChangelogEntry { ... }                                              // see <interfaces>
  pub fn parse_changelog(content: &str) -> Vec<ChangelogEntry> { ... }           // hand-rolled
  pub fn parse_h2_line(line: &str) -> Option<((u32,u32,u32), String)> { ... }    // single-line helper
  pub fn sections_between<'a>(entries: &'a [ChangelogEntry], old: (u32,u32,u32), new: (u32,u32,u32)) -> Vec<&'a ChangelogEntry> { ... }

  pub fn is_plain_semver(s: &str) -> bool { ... }                                // V5 input validation
  pub fn parse_semver_tuple(s: &str) -> Option<(u32,u32,u32)> { ... }            // for hook side

  pub fn build_block_reason(
      old: &str,
      new: &str,
      old_date: Option<&str>,
      new_date: Option<&str>,
      step_count: usize,
      changelog_path_resolved: bool,
  ) -> String { ... }

  pub fn resolve_changelog_path() -> Option<std::path::PathBuf> { ... }          // reads $CLAUDE_PLUGIN_ROOT
  pub fn read_changelog() -> Option<String> { ... }                              // reads file at resolve_changelog_path
  pub fn maybe_emit_version_change_block() { ... }                               // hook entry point

  pub fn run_version_remind(old: &str) -> std::process::ExitCode { ... }         // subcommand handler
  ```

  `maybe_emit_version_change_block` body algorithm (CONTEXT D-04..D-06 + RESEARCH Pitfall 1/2/4):
  1. If `current_version()` == `read_sentinel()` -> return (no mismatch).
  2. If `read_sentinel()` is None (first install) -> write sentinel = current; return (silent per VERS-03).
  3. Mismatch path:
     a. Capture `old = sentinel`, `new = current`.
     b. Try `read_changelog()` -> parse_changelog -> sections_between(old, new) -> compute `step_count` + `old_date` + `new_date`.
     c. If CHANGELOG unresolvable: step_count = 1 (rough proxy), dates = None, changelog_path_resolved = false.
     d. Write sentinel = new (atomic). On Err: log stderr, return (no emit — Pitfall 1).
     e. Build reason XML via `build_block_reason(...)`.
     f. Emit `serde_json::json!({"decision":"block","reason":reason})` to stdout via println!.

  Add `pub mod version_changelog;` to `src/owl/mod.rs`. The module declarations live alphabetically near other `pub mod` lines (find the existing `pub mod hook_idle;` or `pub mod poll;` — insert alongside).

  In `src/owl/mod.rs::handle_command` (line 37 entry, NewAlarm dispatch arm at line 116, HookIdle arm at line 143): add a new arm after `Some(Commands::NewAlarm { ... }) => { ... }` (around line 130) and BEFORE the HookIdle arm:
  ```rust
  Some(Commands::VersionRemind { old }) => {
      std::process::exit(u8::from(version_changelog::run_version_remind(&old)) as i32);
  }
  ```
  (Match the existing dispatch idiom — some arms call into functions that themselves call `std::process::exit`; mirror whichever convention the surrounding NewAlarm arm uses. The `version-remind` handler returns `ExitCode` per the public signature; convert to process exit code.)

  In `src/cli.rs`: add a new variant to the `Commands` enum AFTER `NewAlarm` (line 153) and BEFORE `HookSubagentStart` (line 156):
  ```rust
  /// Roll the last-seen version sentinel back to <old> so the next Stop
  /// hook re-fires the version-change prompt. Invoked by Claude when the
  /// user picks `Remind me later` from the AskUserQuestion. Phase 34 D-08.
  #[command(name = "version-remind")]
  VersionRemind {
      /// The previous version to roll the sentinel back to (e.g. `1.10.9`).
      /// MUST match the `old` value from the <spt-version-changelog> block
      /// Claude received — the subcommand has no way to infer it otherwise.
      old: String,
  },
  ```

  Also add to the `#[cfg(test)] mod tests` block at the bottom of `src/cli.rs` (line 351+), a clap parse test:
  ```rust
  #[test]
  fn parses_version_remind_positional() {
      let cli = Cli::try_parse_from(["owl", "version-remind", "1.10.9"]).unwrap();
      match cli.command {
          Some(Commands::VersionRemind { old }) => assert_eq!(old, "1.10.9"),
          other => panic!("expected VersionRemind, got {:?}", other),
      }
  }

  #[test]
  fn version_remind_requires_positional() {
      let result = Cli::try_parse_from(["owl", "version-remind"]);
      assert!(result.is_err());
  }
  ```

  In `src/common/owlery.rs`: add the encapsulating helper (RESEARCH Risk #2):
  ```rust
  /// Phase 34: sentinel path for the version-change detector. Sibling to `owlery/`,
  /// NOT inside it (per VERS-01). Centralized here so the convention is encapsulated
  /// rather than scattered across hook + subcommand call sites.
  pub fn sentinel_path() -> std::path::PathBuf {
      spt_home().join("last-seen-version.json")
  }
  ```
  Insert alongside the existing `pub fn spt_home()` / `pub fn owlery_dir()` definitions (around lines 18-32 / 65). Add doc comment per the existing style in the file.

  Author the co-located `#[cfg(test)] mod tests` block in `src/owl/version_changelog.rs` covering ALL behaviors listed in `<behavior>`. Reuse the `ENV_LOCK` + `EnvSnapshot` pattern from `src/owl/hook_idle.rs:100-264` (lines 108-135 are the canonical shape). For SPT_HOME mutations, use tempfile::tempdir() per test.

  Final step: run `cargo test --lib version_changelog` and confirm all tests green. Then `cargo test --lib` (full library suite) to confirm no regressions elsewhere. Then `cargo test --test version_changelog` will fail (file not yet created — that's Task 3).

  Commit:
  ```
  feat(34): add version_changelog module + version-remind subcommand

  - New src/owl/version_changelog.rs: sentinel I/O, hand-rolled Keep-a-Changelog
    parser, payload XML builder, hook entry point, version-remind handler.
  - src/cli.rs: new VersionRemind { old } variant + clap parse tests.
  - src/owl/mod.rs: dispatch arm + module declaration.
  - src/common/owlery.rs: pub fn sentinel_path() encapsulation helper.

  NO regex crate — all parsing hand-rolled. Block-body <instructions>
  element emits placeholder [PLAN_03_INSTRUCTIONS_PLACEHOLDER]; Plan 03
  will replace with the AUQ + render wording.
  ```
    </action>
    <verify>
      <automated>cargo test --lib version_changelog 2>&amp;1 | tee /tmp/t.log &amp;&amp; grep -qE "test result: ok\. [0-9]+ passed" /tmp/t.log &amp;&amp; cargo test --lib 2>&amp;1 | grep -qE "test result: ok"</automated>
    </verify>
    <done>Module file exists; all co-located unit tests pass; clap parse tests pass; full lib test suite green; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 2: Wire version-change check into hook_idle.rs::run() (after perch resolution, before set_idle_ready)</name>
    <files>src/owl/hook_idle.rs, src/common/hook_output.rs</files>
    <behavior>
  - `hook_idle.rs::run()` continues to early-return on missing perch (existing behavior, lines 25-32 unchanged).
  - After perch resolution succeeds, `OWL_ECHO_COMMUNE` is checked at the top of the version-change code path; if set, skip the version-change check entirely.
  - If `stop_hook_active` is true in stdin JSON, skip the version-change check entirely (RESEARCH Pitfall 4 defensive guard).
  - Otherwise call `version_changelog::maybe_emit_version_change_block()` which handles its own internal mismatch logic + sentinel write + block emission.
  - After the version-change call, `inbox::set_idle_ready(&owl_id)` continues to fire (unchanged behavior — sentinel + version-change is additive, not a short-circuit).
  - After `set_idle_ready`, `spawn_echo_commune_if_live(&owl_id)` continues to fire (unchanged).

  Behavior tests live in `tests/version_changelog.rs` (Task 3). Co-located unit tests in hook_idle.rs are extended only if existing tests depend on `run()` not printing to stdout — verify and adjust as needed.
    </behavior>
    <action>
  Step A: confirm `parse_hook_stdin` surface in `src/common/hook_output.rs` includes a `stop_hook_active: bool` field. Read the file; if absent, add it:
  ```rust
  #[derive(Deserialize, Default)]
  pub struct ParsedHookStdin {
      ...existing fields...,
      #[serde(default)]
      pub stop_hook_active: bool,
  }
  ```
  If the struct shape already covers it via a different name, use that name. Keep changes minimal; this is the smallest possible additive change.

  Step B: edit `src/owl/hook_idle.rs::run()`:

  Current shape (lines 14-43):
  ```rust
  pub fn run() {
      let stdin = hook_output::parse_hook_stdin();
      let session_id = std::env::var("OWL_SESSION_ID")
          .ok()
          .filter(|s| !s.is_empty())
          .or(stdin.session_id);
      let owl_id = match &session_id {
          Some(sid) => hook_output::find_perch_cached(sid),
          None => hook_output::find_perch_by_parent_pid(),
      };
      let owl_id = match owl_id {
          Some(id) => id,
          None => return,
      };
      inbox::set_idle_ready(&owl_id);
      spawn_echo_commune_if_live(&owl_id);
  }
  ```

  Modify to:
  ```rust
  pub fn run() {
      // 1. Parse stdin JSON — Stop has common fields only.
      let stdin = hook_output::parse_hook_stdin();

      let session_id = std::env::var("OWL_SESSION_ID")
          .ok()
          .filter(|s| !s.is_empty())
          .or(stdin.session_id.clone());

      // 2. Find perch — perch resolution is the active-spt-session predicate
      //    for both legacy set_idle_ready / spawn_echo_commune AND the Phase 34
      //    version-change emission (CONTEXT D-02).
      let owl_id = match &session_id {
          Some(sid) => hook_output::find_perch_cached(sid),
          None => hook_output::find_perch_by_parent_pid(),
      };
      let owl_id = match owl_id {
          Some(id) => id,
          None => return,
      };

      // Phase 34 (D-01..D-06): version-change detection + block emission.
      // MUST run BEFORE set_idle_ready / spawn_echo_commune_if_live so the
      // block emission (println! to stdout) is the only thing on stdout this
      // turn — set_idle_ready and spawn_echo_commune are side-effect calls
      // and emit nothing to stdout.
      //
      // OWL_ECHO_COMMUNE recursion guard: haiku echo-commune subprocesses
      // MUST NOT see a version-change block. Mirrors lines 64-69 below.
      // stop_hook_active defensive guard: prevent infinite-loop pathologies
      // (RESEARCH Pitfall 4).
      let skip_version_change =
          std::env::var("OWL_ECHO_COMMUNE")
              .map(|v| !v.is_empty())
              .unwrap_or(false)
          || stdin.stop_hook_active;
      if !skip_version_change {
          crate::owl::version_changelog::maybe_emit_version_change_block();
      }

      // 3. Set .idle-ready so poll can deliver (per 18.1 D-02, D-04).
      inbox::set_idle_ready(&owl_id);

      // 4. Phase 18.3 D-01..D-03: write .more-done sentinel if live+psyche-ready.
      spawn_echo_commune_if_live(&owl_id);
  }
  ```

  Step C: update the file-doc comment at lines 1-5:
  ```rust
  // hook-idle subcommand -- Stop hook handler
  // Sets .idle-ready so poll can deliver messages.
  // Phase 34 (D-04): also emits a `<spt-version-changelog>` block via
  // `decision:"block"` + `reason` on version mismatch (compile-time
  // CARGO_PKG_VERSION vs $SPT_HOME/last-seen-version.json sentinel).
  // Stop hook output schema supports `decision`/`reason` only — no
  // hookSpecificOutput envelope. The block IS the only stdout this turn.
  // On no-mismatch / first-install / echo-commune guard hit: silent (no stdout).
  //
  // Phase 18.3 (D-01..D-04): no longer spawns `_echo-commune` directly.
  // (rest of existing comment unchanged)
  ```

  Step D: existing unit tests in `src/owl/hook_idle.rs::tests` (lines 100-264) MAY rely on `run()` producing no stdout. Read those tests; if any assert no stdout, they will need adjustment because the version-change path now CAN write to stdout when there's a mismatch. Strategy:
    - Add an `OWL_ECHO_COMMUNE=1` env var to those tests (or `stop_hook_active=true` stdin) to bypass the version-change path. This keeps existing tests focused on the `.more-done` sentinel behavior.
    - Alternatively, set `SPT_OVERRIDE_PKG_VERSION` to match the sentinel value (or set sentinel to current version via `write_sentinel(env!("CARGO_PKG_VERSION"))`).
    - Preserve existing test semantics — Phase 18.3's `.more-done` behavior MUST keep working.

  Run `cargo test --lib hook_idle` and confirm green. Run `cargo test --lib` and confirm full suite green.

  Commit:
  ```
  feat(34): wire version-change emission into hook_idle.rs::run()

  Adds version_changelog::maybe_emit_version_change_block call after
  perch resolution, gated by OWL_ECHO_COMMUNE (D-03) and stop_hook_active
  (RESEARCH Pitfall 4). Block emission precedes set_idle_ready so stdout
  carries only the JSON envelope when a mismatch fires.

  Updates hook_idle file-doc comment to reflect the new stdout channel
  (Stop hooks now CAN emit on this hook path; previously side-effect-only).
  ```
    </action>
    <verify>
      <automated>cargo test --lib hook_idle 2>&amp;1 | grep -qE "test result: ok" &amp;&amp; cargo test --lib 2>&amp;1 | grep -qE "test result: ok" &amp;&amp; grep -q "version_changelog::maybe_emit_version_change_block" src/owl/hook_idle.rs &amp;&amp; grep -q "stop_hook_active" src/owl/hook_idle.rs</automated>
    </verify>
    <done>hook_idle.rs::run() invokes maybe_emit_version_change_block before set_idle_ready; OWL_ECHO_COMMUNE + stop_hook_active guards in place; all library tests green; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 3: Create tests/version_changelog.rs integration suite (10 cases) using SPT_HOME sandbox + perch fixture pattern</name>
    <files>tests/version_changelog.rs</files>
    <behavior>
  Integration tests (subprocess-driven via `assert_cmd::Command::cargo_bin("owl")`):

  1. `first_install_silent_writes_sentinel`
     - Setup: empty SPT_HOME, OVERRIDE=1.10.10, no sentinel, perch fixture written, synthetic CHANGELOG.md present.
     - Invoke: `owl hook-idle` with stdin `{}`.
     - Assert: exit 0; stdout does NOT contain `"decision":"block"`; sentinel file now contains `{"version":"1.10.10"}`.

  2. `single_step_mismatch_emits_block`
     - Setup: sentinel `{"version":"1.10.9"}`, OVERRIDE=1.10.10, perch fixture, CHANGELOG with both versions.
     - Invoke: `owl hook-idle` with stdin `{}`.
     - Assert: exit 0; stdout JSON has `decision=block`; reason contains `<old>1.10.9</old>`, `<new>1.10.10</new>`, `<step_count>1</step_count>`; sentinel post-call is `1.10.10`.

  3. `multi_step_mismatch_emits_block_with_step_count`
     - Setup: sentinel `1.10.5`, OVERRIDE=1.10.10, CHANGELOG with H2 sections for 1.10.5, 1.10.6, 1.10.7, 1.10.8, 1.10.9, 1.10.10.
     - Assert: step_count == 5; reason has `<step_count>5</step_count>`.

  4. `sentinel_equal_no_emit`
     - Setup: sentinel `1.10.10`, OVERRIDE=1.10.10.
     - Assert: no `decision:"block"` in stdout; sentinel unchanged.

  5. `owl_echo_commune_guard_skips_emission`
     - Setup: mismatch present; cmd.env("OWL_ECHO_COMMUNE", "1").
     - Assert: no block emitted; sentinel UNCHANGED (guard runs before mismatch check + sentinel write).

  6. `no_perch_skips_emission`
     - Setup: SPT_HOME tempdir BUT no perch fixture written.
     - Assert: no block; sentinel unchanged.

  7. `stop_hook_active_skips_emission`
     - Setup: mismatch present; stdin `{"stop_hook_active":true}`.
     - Assert: no block; sentinel unchanged.

  8. `version_remind_rollback_round_trip`
     - Setup: sentinel = 1.10.10.
     - Invoke: `owl version-remind 1.10.9`.
     - Assert: exit 0; sentinel reads `{"version":"1.10.9"}`.

  9. `version_remind_missing_arg_exits_nonzero`
     - Invoke: `owl version-remind` (no positional).
     - Assert: exit code != 0; stderr non-empty (clap default).

  10. `version_remind_non_semver_exits_1`
      - Invoke: `owl version-remind not-a-version`.
      - Assert: exit code == 1; stderr matches `semver` substring; sentinel unchanged (or doesn't exist).
    </behavior>
    <action>
  Create `tests/version_changelog.rs` mirroring `tests/auto_pick_integration.rs:1-120` SPT_HOME sandbox pattern verbatim.

  File outline:
  ```rust
  //! Phase 34 integration tests: version-change detection + sentinel + version-remind.
  //! Mirrors tests/auto_pick_integration.rs SPT_HOME sandbox pattern.

  use std::fs;
  use std::path::Path;
  use std::sync::Mutex;

  static ENV_LOCK: Mutex<()> = Mutex::new(());

  const CHANGELOG_FIXTURE: &str = r#"# Changelog

  ## [1.10.10] - 2026-05-16

  - Multi-step test entry for 1.10.10.

  ## [1.10.9] - 2026-05-15

  - Entry for 1.10.9.

  ## [1.10.8] - 2026-05-14

  - Entry for 1.10.8.

  ## [1.10.7] - 2026-05-13

  - Entry for 1.10.7.

  ## [1.10.6] - 2026-05-12

  - Entry for 1.10.6.

  ## [1.10.5] - 2026-05-11

  - Entry for 1.10.5.
  "#;

  struct SptHomeGuard {
      prior_spt: Option<String>,
      prior_plugin_root: Option<String>,
      prior_override: Option<String>,
  }

  impl SptHomeGuard {
      fn set(spt_home: &Path, plugin_root: &Path, override_version: Option<&str>) -> Self {
          let prior_spt = std::env::var("SPT_HOME").ok();
          let prior_plugin_root = std::env::var("CLAUDE_PLUGIN_ROOT").ok();
          let prior_override = std::env::var("SPT_OVERRIDE_PKG_VERSION").ok();
          std::env::set_var("SPT_HOME", spt_home);
          std::env::set_var("CLAUDE_PLUGIN_ROOT", plugin_root);
          match override_version {
              Some(v) => std::env::set_var("SPT_OVERRIDE_PKG_VERSION", v),
              None => std::env::remove_var("SPT_OVERRIDE_PKG_VERSION"),
          }
          Self { prior_spt, prior_plugin_root, prior_override }
      }
  }

  impl Drop for SptHomeGuard {
      fn drop(&mut self) {
          match &self.prior_spt {
              Some(v) => std::env::set_var("SPT_HOME", v),
              None => std::env::remove_var("SPT_HOME"),
          }
          match &self.prior_plugin_root {
              Some(v) => std::env::set_var("CLAUDE_PLUGIN_ROOT", v),
              None => std::env::remove_var("CLAUDE_PLUGIN_ROOT"),
          }
          match &self.prior_override {
              Some(v) => std::env::set_var("SPT_OVERRIDE_PKG_VERSION", v),
              None => std::env::remove_var("SPT_OVERRIDE_PKG_VERSION"),
          }
      }
  }

  fn write_live_perch(spt_home: &Path, id: &str) {
      let perch_dir = spt_home.join("owlery").join(id);
      fs::create_dir_all(&perch_dir).unwrap();
      fs::write(perch_dir.join("ready"), "").unwrap();
      let info = serde_json::json!({
          "owl_id": id,
          "pid": std::process::id() as u64,
          // N1: parent_pid = test-process PID so hook_output::find_perch_by_parent_pid()
          // resolves THIS test's perch when the owl subprocess (spawned by assert_cmd
          // under the test process) walks up the parent chain. Mirrors the
          // auto_pick_integration.rs fixture convention.
          "parent_pid": std::process::id() as u64,
          "mode": "listen",
          "session_id": "test-session",
          "state": "Ready",
          "project_history": [],
      });
      fs::write(perch_dir.join("info.json"), serde_json::to_string(&info).unwrap()).unwrap();
  }

  fn write_changelog_fixture(plugin_root: &Path) {
      fs::write(plugin_root.join("CHANGELOG.md"), CHANGELOG_FIXTURE).unwrap();
  }

  fn write_sentinel_fixture(spt_home: &Path, version: &str) {
      let body = format!("{{\"version\":\"{}\"}}", version);
      fs::write(spt_home.join("last-seen-version.json"), body).unwrap();
  }

  // ... 10 #[test] fns covering the behaviors above ...
  ```

  For each test:
    - `let _lock = ENV_LOCK.lock().unwrap();`
    - `let tmp = tempfile::tempdir().unwrap();`
    - Both SPT_HOME and CLAUDE_PLUGIN_ROOT point at `tmp.path()` for simplicity (CHANGELOG.md sits at `tmp.path()/CHANGELOG.md`; sentinel at `tmp.path()/last-seen-version.json`; perch at `tmp.path()/owlery/{id}/`).
    - `let _guard = SptHomeGuard::set(tmp.path(), tmp.path(), Some("1.10.10"));`
    - Write fixtures: `write_changelog_fixture`, `write_live_perch("test-owl")`, `write_sentinel_fixture(..)` as required by the test case.
    - Invoke the subprocess:
      ```rust
      let mut cmd = assert_cmd::Command::cargo_bin("owl").expect("owl binary must be built");
      let output = cmd
          .arg("hook-idle")
          .env("SPT_HOME", tmp.path())
          .env("CLAUDE_PLUGIN_ROOT", tmp.path())
          .env("SPT_OVERRIDE_PKG_VERSION", "1.10.10")
          .env("HOME", tmp.path())
          .env("USERPROFILE", tmp.path())
          .env_remove("OWL_ECHO_COMMUNE")    // per-test may re-add
          .write_stdin("{}")
          .output()
          .expect("owl subprocess");
      ```
    - For the `version-remind` tests: replace the args with `["version-remind", "<arg>"]` and skip stdin/perch fixtures.
    - Assert via `output.status.success()`, `String::from_utf8_lossy(&output.stdout)`, and reading the sentinel file post-call.

  For test 5 (`owl_echo_commune_guard_skips_emission`):
    ```rust
    .env("OWL_ECHO_COMMUNE", "1")
    ```

  For test 7 (`stop_hook_active_skips_emission`):
    ```rust
    .write_stdin(r#"{"stop_hook_active":true}"#)
    ```

  For tests 9 + 10 (subcommand error cases): use `output.status.code()` and `String::from_utf8_lossy(&output.stderr)`.

  CRITICAL: `assert_cmd` requires the binary to be built. Add a `cargo build` step in the test setup OR rely on `cargo test`'s implicit build. The `cargo_bin` helper points at the test binary's `target/debug/owl` — `cargo test` builds it automatically.

  After authoring, run `cargo test --test version_changelog` and confirm all 10 tests pass.

  Commit:
  ```
  test(34): integration tests for version-change hook + version-remind subcommand

  10 cases covering first-install silent, single + multi-step mismatch,
  sentinel-equal no-emit, OWL_ECHO_COMMUNE + stop_hook_active + no-perch
  guards, version-remind round-trip + missing-arg + non-semver-arg.

  Uses SPT_OVERRIDE_PKG_VERSION test seam (no rebuild required per case).
  Fixture CHANGELOG.md spans 1.10.5..1.10.10 to exercise step_count math.
  ```
    </action>
    <verify>
      <automated>cargo test --test version_changelog 2>&amp;1 | tee /tmp/it.log &amp;&amp; grep -qE "test result: ok\. 10 passed" /tmp/it.log</automated>
    </verify>
    <done>tests/version_changelog.rs exists with 10 #[test] fns; all pass; commit landed.</done>
  </task>

  </tasks>

  <verification>
  After all 3 tasks:

  1. Full test suite green:
     ```
     cargo test
     ```
     Expect `test result: ok` for both lib and integration test runs.

  2. New module wired correctly:
     ```
     grep -c "version_changelog" src/owl/mod.rs src/owl/hook_idle.rs src/cli.rs
     ```
     Expect at least 4 hits (mod declaration + dispatch arm + hook call + Plan 03 placeholder reference if Plan 03 hasn't started).

  3. Single sentinel write site (VERS-09 audit):
     ```
     grep -rn "last-seen-version" src/
     ```
     Expect exactly 2 hits: one in `src/common/owlery.rs::sentinel_path()` and one in tests OR one in `src/owl/version_changelog.rs` (the consumer of `sentinel_path`).
     No `last-seen-version` literal in `hook_idle.rs` (must go through the module API).

  4. No regex crate added (Cargo.toml audit):
     ```
     grep -c "^regex" Cargo.toml
     ```
     Expect 0.

  5. Version-change emission ordering preserved:
     ```
     grep -n "version_changelog\|set_idle_ready\|spawn_echo_commune" src/owl/hook_idle.rs
     ```
     Manual check: `maybe_emit_version_change_block` line number is LESS than `set_idle_ready` line number, which is LESS than `spawn_echo_commune_if_live` line number.

  6. Three commits landed:
     ```
     git log --oneline -3
     ```
     Expect:
     - `test(34): integration tests for version-change hook + version-remind subcommand`
     - `feat(34): wire version-change emission into hook_idle.rs::run()`
     - `feat(34): add version_changelog module + version-remind subcommand`
  </verification>

  <success_criteria>
  - Plan 02 maps to VERS-01 (sentinel path encapsulated in owlery::sentinel_path), VERS-02 (CARGO_PKG_VERSION via current_version() with test seam; NEVER reads plugin.json at runtime), VERS-03 (first-install silent path tested), VERS-05 (atomic_write_string used at sentinel write site), VERS-09 (single write site at hook entry; grep audit confirms 2 callers via sentinel_path).
  - Block-body XML scaffold complete with placeholder for Plan 03's wording.
  - All 30+ unit tests + 10 integration tests pass.
  - No new crate dependencies.
  - hook_idle.rs change preserves Phase 18.3 `.more-done` sentinel behavior (existing tests still pass).
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | Claude Code -> hook subprocess | stdin JSON (untrusted shape but trust-boundary-internal — Claude Code is local) |
  | Claude -> $OWL bash invocation | `version-remind <old>` argv crosses Claude's tool-call boundary to the owl subprocess |
  | disk -> hook | CHANGELOG.md content at $CLAUDE_PLUGIN_ROOT (author-controlled) is read and inlined into the block reason |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-05 | Tampering | `version-remind <old>` argv injection | mitigate | `is_plain_semver` validator rejects any non-`X.Y.Z` arg BEFORE sentinel write (RESEARCH security V5); test 10 covers this |
  | T-34-06 | Tampering | Sentinel path traversal | mitigate | Path constructed via `spt_home().join("last-seen-version.json")` — no user input in the path; encapsulated in `owlery::sentinel_path()` |
  | T-34-07 | DoS | Infinite Stop-hook block loop | mitigate | `stop_hook_active` guard short-circuits regardless of mismatch (RESEARCH Pitfall 4); test 7 covers this; belt-and-suspenders with sentinel pre-write per CONTEXT D-05 |
  | T-34-08 | DoS | OWL_ECHO_COMMUNE recursion (haiku subprocesses) | mitigate | Guard runs BEFORE the version-change call (lines 64-69 pattern mirrored); test 5 covers this |
  | T-34-09 | Information disclosure | CHANGELOG.md content injection | accept | Author-controlled file; content reaches Claude only (no XSS / code-exec surface); plaintext markdown |
  | T-34-10 | Tampering | Sentinel write failure leaves stale value | mitigate | Best-effort pattern per RESEARCH Pitfall 1: on Err, skip emission (no false notification); next Stop re-checks; stop_hook_active is the bounded loop guard |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-02-SUMMARY.md` documenting:
  - Three commit SHAs
  - Final test counts (unit + integration)
  - Confirmation that the `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` literal appears exactly once in `src/owl/version_changelog.rs`
  - Any deviations from the line-citation anchors in `<interfaces>` (hook_idle.rs / cli.rs / owlery.rs may have drifted; record actual line numbers)
  - Confirmation `cargo test` ends green with no warnings introduced by Plan 02 code
  </output>

  #### 34-03-PLAN.md
  ---
  phase: 34-version-change-changelog
  plan: 03
  type: execute
  wave: 3
  depends_on: [34-01, 34-02]
  files_modified:
    - src/owl/version_changelog.rs
    - tests/version_changelog.rs
    - tests/golden/version_changelog_block.txt
  autonomous: false
  requirements:
    - VERS-04
    - VERS-06
  user_setup: []

  must_haves:
    truths:
      - "The `<instructions>` element inside the `<spt-version-changelog>` block carries the verbatim 4-option AUQ wording locked in CONTEXT D-08: `Yes, full changelog` / `Yes, highlights only` / `Remind me later` / `Skip`"
      - "The `Yes, full changelog` instruction tells Claude to read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and print every H2 section where `{old} < version <= {new}`, verbatim markdown (RESEARCH Pitfall 6)"
      - "The `Yes, highlights only` instruction tells Claude to extract 2-5 most impactful feature adds and fixes across the range, preferring user-facing behavior changes, skipping internal refactors and test-only changes; appends pointer to $CLAUDE_PLUGIN_ROOT/CHANGELOG.md (CONTEXT D-08 / specifics)"
      - "The `Remind me later` instruction surfaces the literal invocation string `$OWL version-remind <old_version_substituted_verbatim>` so Claude passes the `old` value with no ambiguity (CONTEXT D-08 + follow-up clarification)"
      - "The question text uses the `step_count` field: single-step `SPT plugin updated v{old} → v{new}. Print changelog?`; multi-step `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`"
      - "Dates surface in Claude's pre-question prose, NOT in the AUQ question line (RESEARCH open question Q3)"
      - "A golden fixture at `tests/golden/version_changelog_block.txt` snapshots the canonical block reason string for a fixed 1.10.9->1.10.10 transition; an integration test diffs the live output against it"
      - "End-to-end UAT documentation captures the manual test protocol for confirming the AUQ surfaces correctly in a real Claude Code session"
    artifacts:
      - path: "src/owl/version_changelog.rs"
        provides: "Replaced `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` with the final wording inside `build_block_reason`"
        contains: "Yes, full changelog"
      - path: "tests/version_changelog.rs"
        provides: "New `block_reason_matches_golden_snapshot` test covering the 1.10.9->1.10.10 canonical case"
        contains: "version_changelog_block.txt"
      - path: "tests/golden/version_changelog_block.txt"
        provides: "Canonical snapshot of the `reason` string for the 1.10.9->1.10.10 transition fixture"
        min_lines: 25
    key_links:
      - from: "build_block_reason output"
        to: "tests/golden/version_changelog_block.txt"
        via: "snapshot equality assertion (with template substitution)"
        pattern: "version_changelog_block.txt"
      - from: "<instructions> element text"
        to: "Claude's AUQ rendering at re-engagement"
        via: "Stop hook reason -> Claude continuation -> AskUserQuestion tool call (RESEARCH Q1/Q2 GREEN)"
        pattern: "AskUserQuestion"
  ---

  <objective>
  Phase 34 user-facing wording + golden snapshot + UAT.

  Purpose: Replace Plan 02's placeholder with the production AUQ instruction text. This is the only file in the repo whose exact prose determines whether Claude reliably invokes AskUserQuestion with the four locked options on hook re-engagement. Wording quality matters more than code volume.

  Output: One module edit (replace placeholder), one new test (golden snapshot), one new fixture file. End-to-end UAT documented for `cargo build --release` + `DEPLOY.ps1` + manual Claude Code session verification.

  This plan has ONE checkpoint at the end (manual UAT against a real Claude Code session — `checkpoint:human-verify`).
  </objective>

  <execution_context>
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
  @$HOME/.claude/get-shit-done/templates/summary.md
  </execution_context>

  <context>
  @.planning/PROJECT.md
  @.planning/ROADMAP.md
  @.planning/STATE.md
  @.planning/REQUIREMENTS.md
  @.planning/phases/34-version-change-changelog/34-CONTEXT.md
  @.planning/phases/34-version-change-changelog/34-RESEARCH.md
  @.planning/phases/34-version-change-changelog/34-PATTERNS.md
  @.planning/phases/34-version-change-changelog/34-01-SUMMARY.md
  @.planning/phases/34-version-change-changelog/34-02-SUMMARY.md
  @src/owl/version_changelog.rs
  @tests/version_changelog.rs

  <interfaces>
  <!-- Locked invariants from CONTEXT D-08 + RESEARCH Q11 -->

  AUQ 4-option labels (LOCKED — CONTEXT D-08, verbatim):
    1. `Yes, full changelog`
    2. `Yes, highlights only`
    3. `Remind me later`
    4. `Skip`

  `Yes, full changelog` behavior (LOCKED — CONTEXT D-08 + RESEARCH Pitfall 6):
    Claude reads `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and prints EVERY H2 section where `{old} < version <= {new}`, VERBATIM markdown. Single-step renders one section; multi-step renders all interim sections in order (newest first or oldest first — author chooses ONE convention and locks it in the instructions text).

  `Yes, highlights only` framing (LOCKED — CONTEXT specifics, suggested wording):
    "Pick 2-5 most impactful feature adds and fixes across the range; prefer items that change user-facing behavior; skip purely-internal refactors and test-only changes. Append `See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail.`"

  `Remind me later` invocation (LOCKED — CONTEXT D-08 + clarification):
    Claude invokes `$OWL version-remind <old>` via Bash, where `<old>` is the LITERAL value from the `<old>` element in the block. Block MUST surface the exact invocation string verbatim with the old value substituted — Claude cannot infer the rollback target from any other source.
    Example for 1.10.9->1.10.10 transition: `$OWL version-remind 1.10.9`

  `Skip` behavior (LOCKED):
    No action. Sentinel was already updated by the hook at run-entry; no re-fire. Claude acknowledges briefly and proceeds.

  Question text template (LOCKED — CONTEXT specifics):
    Single-step (`step_count == 1`): `SPT plugin updated v{old} → v{new}. Print changelog?`
    Multi-step (`step_count > 1`): `SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?`
    (Note: `versions` is always plural in the multi-step variant; pluralization is unconditional because step_count > 1 means at least 2.)

  Pre-question prose (Claude's discretion, but the instructions SHOULD recommend a shape per RESEARCH open question Q3):
    Before invoking AskUserQuestion, Claude states a one-line context line such as:
    `SPT plugin updated v1.10.9 (released 2026-05-15) → v1.10.10 (released 2026-05-16). Print changelog?`
    Dates are taken from `<old_date>` / `<new_date>` elements; omit the parenthetical when the element is empty (CHANGELOG.md unresolvable case).

  XML element ordering (LOCKED — Plan 02's `build_block_reason`):
    `<old>` -> `<new>` -> `<old_date>` -> `<new_date>` -> `<step_count>` -> `<changelog_path>` -> `<invocation_for_remind>` -> `<instructions>`

  NO ESCAPING of `$OWL` / `$CLAUDE_PLUGIN_ROOT` in the XML body — these are LITERAL substrings Claude reads and passes to Bash. The Bash tool expands them at invocation time. If we escape `$` to `\$`, Claude will pass the escaped form to Bash and the invocation will fail.

  `<instructions>` element wording — DRAFT (planner-authored, locked in this plan; refine inside the action below):
  ```
  Render an AskUserQuestion to confirm whether the user wants to see the changelog for this SPT plugin update.

  Before invoking AskUserQuestion, write one short pre-question line that names the version transition (and dates if both <old_date> and <new_date> are non-empty), e.g. "SPT plugin updated v{old} (released {old_date}) → v{new} (released {new_date}). Print changelog?". Omit the dates if either element is empty.

  Then invoke AskUserQuestion with EXACTLY these four options (labels verbatim, do not paraphrase):

    1. "Yes, full changelog" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version <= {new}, verbatim markdown (preserve the original H2 lines, bullet structure, and any H3 subsections). Render newest-first (the order entries appear in the file). Do not summarize, do not paraphrase.

    2. "Yes, highlights only" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and synthesize 2-5 highlight bullets across the H2 sections where {old} < version <= {new}. Prefer items that change user-facing behavior. Skip purely-internal refactors and test-only changes. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."

    3. "Remind me later" — On selection: invoke Bash with EXACTLY this command (substitute the {old} value verbatim from the <old> element above, do NOT paraphrase or compute):
       $OWL version-remind {old}
       This rolls the sentinel back so the next Stop hook re-fires this prompt. Do not run any other commands as part of this option.

    4. "Skip" — On selection: take no action. Acknowledge briefly ("OK, skipping.") and continue with the user's prior intent.

  If the user provides freeform input via AskUserQuestion's "Other" option:
    - "not now" / "later" / "skip for now" → treat as "Remind me later" (invoke the Bash command above).
    - "ignore" / "stop asking" / "no" → treat as "Skip".

  When the question text mentions {step_count} versions (i.e. step_count > 1), the multi-version skip rendering applies: "Yes, full changelog" prints ALL interim H2 sections, NOT just the endpoints.

  After completing the user's choice, return control to whatever the user was working on before this prompt.
  ```

  CRITICAL — `{old}` and `{new}` and `{old_date}` and `{new_date}` and `{step_count}` are TEMPLATE TOKENS in the instructions element that `build_block_reason` substitutes at emit time. The actual emitted text MUST have the real values inline (not `{old}` placeholders). E.g., for a 1.10.9->1.10.10 transition the `<instructions>` block reads `$OWL version-remind 1.10.9` LITERALLY — with `1.10.9` baked in.

  Golden snapshot strategy (RESEARCH open question Q1):
    Snapshot just the parts of the reason string that are stable: the XML element shape + the literal AUQ option labels + the literal invocation string. Body content of the highlights instructions is fragile (any wording tweak invalidates the snapshot), so the test asserts SUBSTRING matches for stable invariants rather than full-string equality:
      - `<old>1.10.9</old>`
      - `<new>1.10.10</new>`
      - `<step_count>1</step_count>`
      - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
      - Literal labels: `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"`
      - `Pick 2-5 most impactful` (highlights rubric anchor)
      - `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md` (instructions for both Yes options)
      - `$OWL version-remind 1.10.9` (instructions text for Remind, with substitution)

    The golden FILE stores the full expected reason string for reference / human review, but the TEST uses substring assertions for resilience.

  UAT protocol (CONTEXT canonical_refs + RESEARCH plan-boundary Q8 Plan 03):
    1. `cargo build --release` — clean build with all Phase 34 code.
    2. `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1` — sync to marketplace + cache.
    3. `claude plugin install spt@cplugs` (DEPLOY.ps1 does this; verify it ran).
    4. Run `/reload-plugins` manually inside Claude Code.
    5. Synthesize a version mismatch: `node -e 'fs.writeFileSync(process.env.LOCALAPPDATA + "/spt/last-seen-version.json", JSON.stringify({version:"0.0.1"}))'` (Windows) or equivalent Unix path. Now the sentinel claims v0.0.1 and the installed plugin claims its real current version.
    6. Inside Claude Code: start an spt session (e.g., run `/spt:listen` or `/spt:live`).
    7. End the agent's turn naturally. Stop hook fires.
    8. EXPECTED: Claude responds with the pre-question line + AskUserQuestion showing the four locked options.
    9. Test each branch:
       - Pick `Yes, full changelog`: Claude prints CHANGELOG.md sections.
       - Pick `Yes, highlights only`: Claude renders 2-5 bullets + pointer line.
       - Pick `Remind me later`: Claude invokes `$OWL version-remind 0.0.1` via Bash; sentinel rolls back; next Stop re-fires.
       - Pick `Skip`: Claude says "OK, skipping" and proceeds; sentinel stays at current version; next Stop does NOT re-fire.
    10. Confirm sentinel state at each step via `cat $SPT_HOME/last-seen-version.json` (or `type %LOCALAPPDATA%\spt\last-seen-version.json` on Windows).
  </interfaces>
  </context>

  <tasks>

  <task type="auto" tdd="true">
    <name>Task 1: Replace [PLAN_03_INSTRUCTIONS_PLACEHOLDER] in build_block_reason with the locked 4-option AUQ wording (with template substitution)</name>
    <files>src/owl/version_changelog.rs</files>
    <behavior>
  - `build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true)` returns a string containing:
    - `<old>1.10.9</old>`
    - `<new>1.10.10</new>`
    - `<old_date>2026-05-15</old_date>`
    - `<new_date>2026-05-16</new_date>`
    - `<step_count>1</step_count>`
    - `<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
    - Inside `<instructions>`: literal substrings `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"`, `Pick 2-5 most impactful`, `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md`, `$OWL version-remind 1.10.9` (with the 1.10.9 substituted verbatim).
  - The literal token `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` is REMOVED from the file (grep -n returns no results).
  - For the multi-step case `build_block_reason("1.10.5", "1.10.10", ..., step_count=5, ...)`:
    - `<step_count>5</step_count>`
    - The pre-question hint inside instructions surfaces the multi-step form: `SPT plugin updated v1.10.5 → v1.10.10 (5 versions). Print changelog?`
    - The invocation_for_remind substitutes `1.10.5`: `$OWL version-remind 1.10.5`
  - For the unresolvable-CHANGELOG case `build_block_reason("1.10.9", "1.10.10", None, None, 1, false)`:
    - `<old_date></old_date>` (empty element)
    - `<new_date></new_date>` (empty element)
    - `<changelog_path>NOT_RESOLVED</changelog_path>`
    - Instructions still surface the four options; the Yes-full / Yes-highlights branches degrade gracefully (Claude sees NOT_RESOLVED and skips the CHANGELOG read, just states a generic "no changelog detail available" message). The instructions text MUST handle this branch explicitly so Claude doesn't try to read a nonexistent file.
    </behavior>
    <action>
  Step A: Read `src/owl/version_changelog.rs` to confirm the current shape of `build_block_reason` (Plan 02 left a placeholder `[PLAN_03_INSTRUCTIONS_PLACEHOLDER]` inside the `<instructions>` element).

  Step B: Replace the placeholder with the verbatim instructions wording in `<interfaces>` above, applying string templating for `{old}`, `{new}`, `{old_date}`, `{new_date}`, `{step_count}`. The `format!` macro is the natural fit:

  ```rust
  let question_text = if step_count == 1 {
      format!("SPT plugin updated v{old} → v{new}. Print changelog?")
  } else {
      format!("SPT plugin updated v{old} → v{new} ({step_count} versions). Print changelog?")
  };

  let date_hint = match (old_date, new_date) {
      (Some(od), Some(nd)) if !od.is_empty() && !nd.is_empty() => {
          format!(" (released {od} → released {nd})")
      }
      _ => String::new(),
  };

  let changelog_path_element = if changelog_path_resolved {
      "$CLAUDE_PLUGIN_ROOT/CHANGELOG.md".to_string()
  } else {
      "NOT_RESOLVED".to_string()
  };

  let resolved_branch_extra = if changelog_path_resolved {
      String::new()  // Yes-full / Yes-highlights work normally
  } else {
      "  NOTE: <changelog_path> is NOT_RESOLVED — $CLAUDE_PLUGIN_ROOT/CHANGELOG.md was not readable at hook time. For 'Yes, full changelog' and 'Yes, highlights only', state briefly that the changelog file was not available and offer to retry after the user verifies the plugin install. Do not attempt to read the file.\n".to_string()
  };

  let instructions = format!(r#"Render an AskUserQuestion to confirm whether the user wants to see the changelog for this SPT plugin update.

  Before invoking AskUserQuestion, write one short pre-question line that names the version transition (and dates if both <old_date> and <new_date> are non-empty), e.g. "{question_text}"{date_hint}.

  Then invoke AskUserQuestion with EXACTLY these four options (labels verbatim, do not paraphrase):

    1. "Yes, full changelog" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and print every H2 section where {old} < version <= {new}, verbatim markdown (preserve the original H2 lines, bullet structure, and any H3 subsections). Render in the order they appear in the file (newest first). Do not summarize, do not paraphrase.

    2. "Yes, highlights only" — On selection: read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md and synthesize 2-5 highlight bullets across the H2 sections where {old} < version <= {new}. Prefer items that change user-facing behavior. Skip purely-internal refactors and test-only changes. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."

    3. "Remind me later" — On selection: invoke Bash with EXACTLY this command (the {old} value is already substituted; pass it verbatim, do not paraphrase or recompute):
       $OWL version-remind {old}
       This rolls the sentinel back so the next Stop hook re-fires this prompt. Do not run any other commands as part of this option.

    4. "Skip" — On selection: take no action. Acknowledge briefly ("OK, skipping.") and continue with the user's prior intent.

  If the user provides freeform input via AskUserQuestion's "Other" option:
    - "not now" / "later" / "skip for now" → treat as "Remind me later" (invoke the Bash command in option 3).
    - "ignore" / "stop asking" / "no" → treat as "Skip".

  When the question text mentions multiple versions (step_count > 1), the multi-version skip rendering applies: "Yes, full changelog" prints ALL interim H2 sections (not just the endpoints).
  {resolved_branch_extra}
  After completing the user's choice, return control to whatever the user was working on before this prompt."#);
  ```

  CRITICAL: do NOT escape `$` in the `$OWL` or `$CLAUDE_PLUGIN_ROOT` substrings. These literally need to reach Claude as `$OWL` and `$CLAUDE_PLUGIN_ROOT` for Bash expansion. Rust's `format!` macro does NOT touch `$` characters — they pass through verbatim. Sanity-check: print the output of `build_block_reason("1.10.9", "1.10.10", None, None, 1, true)` once locally during development and confirm `$OWL version-remind 1.10.9` appears as-is.

  CRITICAL: do NOT include `{old}`/`{new}`/etc. AS LITERAL TEXT in the final emitted string — `format!` substitutes them. If you need the LITERAL characters `{old}` in the instructions (for some reason), escape with `{{old}}`. Audit the output: a successful build_block_reason call should have NO unsubstituted `{name}` patterns in the output (a regex `\{[a-z_]+\}` should match zero times).

  Step C: extend the co-located `#[cfg(test)] mod tests` block with three additional tests covering the substitution invariants in `<behavior>`:
  ```rust
  #[test]
  fn build_block_reason_single_step_with_dates_substitutes_correctly() {
      let out = build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true);
      assert!(out.contains("<old>1.10.9</old>"));
      assert!(out.contains("<new>1.10.10</new>"));
      assert!(out.contains("<old_date>2026-05-15</old_date>"));
      assert!(out.contains("<new_date>2026-05-16</new_date>"));
      assert!(out.contains("<step_count>1</step_count>"));
      assert!(out.contains("<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>"));
      assert!(out.contains("<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>"));
      assert!(out.contains("\"Yes, full changelog\""));
      assert!(out.contains("\"Yes, highlights only\""));
      assert!(out.contains("\"Remind me later\""));
      assert!(out.contains("\"Skip\""));
      assert!(out.contains("Pick 2-5 most impactful") || out.contains("2-5 highlight bullets"));
      assert!(out.contains("$OWL version-remind 1.10.9"));
      assert!(!out.contains("[PLAN_03_INSTRUCTIONS_PLACEHOLDER]"));
      // No unsubstituted template tokens:
      let token_re_hits = out.matches('{').filter(|_| true).count();
      // A handful of literal `{` are acceptable (none in our wording though)
      // — assert no `{old}` / `{new}` / `{step_count}` literal remains:
      assert!(!out.contains("{old}"));
      assert!(!out.contains("{new}"));
      assert!(!out.contains("{step_count}"));
      assert!(!out.contains("{old_date}"));
      assert!(!out.contains("{new_date}"));
  }

  #[test]
  fn build_block_reason_multi_step_substitutes_step_count() {
      let out = build_block_reason("1.10.5", "1.10.10", Some("2026-05-11"), Some("2026-05-16"), 5, true);
      assert!(out.contains("<step_count>5</step_count>"));
      assert!(out.contains("(5 versions)"));
      assert!(out.contains("$OWL version-remind 1.10.5"));
  }

  #[test]
  fn build_block_reason_unresolved_changelog_degrades_gracefully() {
      let out = build_block_reason("1.10.9", "1.10.10", None, None, 1, false);
      assert!(out.contains("<old_date></old_date>"));
      assert!(out.contains("<new_date></new_date>"));
      assert!(out.contains("<changelog_path>NOT_RESOLVED</changelog_path>"));
      assert!(out.contains("NOT_RESOLVED") || out.contains("not available"));
      // Four options still present even when CHANGELOG unresolvable:
      assert!(out.contains("\"Yes, full changelog\""));
      assert!(out.contains("\"Skip\""));
  }
  ```

  Step D: run `cargo test --lib version_changelog` and confirm all tests pass (the existing Plan 02 tests + the three new ones).

  Step E: Audit for unsubstituted placeholders across the entire file:
  ```
  grep -c "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/owl/version_changelog.rs
  ```
  MUST return 0. (Plan 02 left exactly one occurrence; Plan 03 must remove it.)

  Step F: Commit:
  ```
  feat(34): author 4-option AUQ instructions for version-change block

  Replaces Plan 02's [PLAN_03_INSTRUCTIONS_PLACEHOLDER] with the locked
  AUQ wording per CONTEXT D-08:
  - "Yes, full changelog" / "Yes, highlights only" / "Remind me later" / "Skip"
  - Substitutes {old}/{new}/{step_count}/{old_date}/{new_date} verbatim
  - Surfaces $OWL version-remind {old} invocation literally (Claude has no
    way to infer the rollback target otherwise — CONTEXT D-08 clarification)
  - Highlights rubric: 2-5 most impactful items, user-facing-behavior bias
  - Multi-step path renders ALL interim H2 sections (RESEARCH Pitfall 6)
  - NOT_RESOLVED branch degrades gracefully when CHANGELOG.md unreadable

  Three new unit tests assert substitution invariants + no unsubstituted
  tokens remain.
  ```
    </action>
    <verify>
      <automated>! grep -q "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/owl/version_changelog.rs &amp;&amp; cargo test --lib version_changelog 2>&amp;1 | grep -qE "test result: ok"</automated>
    </verify>
    <done>Placeholder removed; three new substitution tests pass; all prior Plan 02 tests still pass; commit landed.</done>
  </task>

  <task type="auto" tdd="true">
    <name>Task 2: Add golden snapshot fixture + integration test covering the end-to-end block reason string</name>
    <files>tests/golden/version_changelog_block.txt, tests/version_changelog.rs</files>
    <behavior>
  - `tests/golden/version_changelog_block.txt` exists with the full expected reason string for the canonical 1.10.9 -> 1.10.10 transition (step_count=1, both dates present, CHANGELOG resolved).
  - New integration test `block_reason_matches_golden_snapshot` invokes `owl hook-idle` with a mismatch fixture mirroring the golden's input parameters; captures stdout; parses the JSON envelope; extracts the `reason` field; asserts it equals the golden file content (after stripping leading/trailing whitespace) OR asserts a curated set of substring invariants per the RESEARCH open-question Q1 recommendation (snapshot just the stable parts).
  - Choose the SUBSTRING approach for resilience: any wording tweak in Task 1 invalidates a full-string snapshot but should NOT break the test. Substring invariants:
    - `<old>1.10.9</old>`
    - `<new>1.10.10</new>`
    - `<step_count>1</step_count>`
    - `<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>`
    - `"Yes, full changelog"`, `"Yes, highlights only"`, `"Remind me later"`, `"Skip"` (literal labels)
    - `read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md`
    - `$OWL version-remind 1.10.9` (with substituted old version)
  - The golden FILE serves as human-reviewable reference but the test's PASS/FAIL is on the substring set.
    </behavior>
    <action>
  Step A: Generate the canonical reason string by running locally:
  ```
  cargo test --lib version_changelog::tests::build_block_reason_single_step_with_dates_substitutes_correctly -- --nocapture 2>&1 | grep -A 200 "^---"
  ```
  OR write a tiny helper test that prints the canonical output:
  ```rust
  #[test]
  #[ignore]  // run with `cargo test -- --ignored` to regenerate golden
  fn print_canonical_block_reason_for_golden() {
      let out = build_block_reason("1.10.9", "1.10.10", Some("2026-05-15"), Some("2026-05-16"), 1, true);
      println!("---GOLDEN-BEGIN---");
      println!("{}", out);
      println!("---GOLDEN-END---");
  }
  ```
  Run with `cargo test --lib print_canonical_block_reason_for_golden -- --ignored --nocapture`, capture the section between the markers, write to `tests/golden/version_changelog_block.txt`.

  Step B: Add the integration test to `tests/version_changelog.rs`:
  ```rust
  #[test]
  fn block_reason_matches_golden_snapshot() {
      let _lock = ENV_LOCK.lock().unwrap();
      let tmp = tempfile::tempdir().unwrap();

      // Fixture: sentinel at 1.10.9, OVERRIDE to 1.10.10, CHANGELOG covering both.
      let _guard = SptHomeGuard::set(tmp.path(), tmp.path(), Some("1.10.10"));
      write_changelog_fixture(tmp.path());
      write_live_perch(tmp.path(), "golden-perch");
      write_sentinel_fixture(tmp.path(), "1.10.9");

      let output = assert_cmd::Command::cargo_bin("owl")
          .expect("owl binary must be built")
          .arg("hook-idle")
          .env("SPT_HOME", tmp.path())
          .env("CLAUDE_PLUGIN_ROOT", tmp.path())
          .env("SPT_OVERRIDE_PKG_VERSION", "1.10.10")
          .env("HOME", tmp.path())
          .env("USERPROFILE", tmp.path())
          .env_remove("OWL_ECHO_COMMUNE")
          .write_stdin("{}")
          .output()
          .expect("owl subprocess");

      assert!(output.status.success(), "owl exited non-zero: {:?}", output);
      let stdout = String::from_utf8_lossy(&output.stdout);

      // Parse the JSON envelope and extract `reason`.
      let envelope: serde_json::Value = serde_json::from_str(&stdout.trim())
          .expect(&format!("stdout is not valid JSON: {}", stdout));
      assert_eq!(envelope["decision"], "block");
      let reason = envelope["reason"]
          .as_str()
          .expect("reason field missing or not a string");

      // Substring invariants per RESEARCH Q1 recommendation. Any one failure
      // means a wording regression vs the locked CONTEXT D-08 surface.
      let invariants: &[&str] = &[
          "<spt-version-changelog>",
          "</spt-version-changelog>",
          "<old>1.10.9</old>",
          "<new>1.10.10</new>",
          "<step_count>1</step_count>",
          "<changelog_path>$CLAUDE_PLUGIN_ROOT/CHANGELOG.md</changelog_path>",
          "<invocation_for_remind>$OWL version-remind 1.10.9</invocation_for_remind>",
          "\"Yes, full changelog\"",
          "\"Yes, highlights only\"",
          "\"Remind me later\"",
          "\"Skip\"",
          "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md",
          "$OWL version-remind 1.10.9",
      ];
      for inv in invariants {
          assert!(
              reason.contains(inv),
              "reason missing required substring: {:?}\nfull reason:\n{}",
              inv,
              reason
          );
      }

      // Golden file is a human-readable reference. Diff-check it for completeness,
      // but allow drift (the test passes on substring invariants alone).
      let golden_path = std::path::Path::new("tests/golden/version_changelog_block.txt");
      if let Ok(golden) = std::fs::read_to_string(golden_path) {
          if golden.trim() != reason.trim() {
              eprintln!("NOTE: golden file at {} differs from live output. Regenerate via `cargo test -- --ignored print_canonical_block_reason_for_golden`.", golden_path.display());
          }
      } else {
          eprintln!("NOTE: golden file at {} missing. Regenerate via `cargo test -- --ignored print_canonical_block_reason_for_golden`.", golden_path.display());
      }
  }
  ```

  Step C: ensure `tests/golden/` directory exists; commit both the fixture and the test together.

  Step D: run `cargo test --test version_changelog` and confirm all 11 integration tests pass (10 from Plan 02 + this new one).

  Step E: commit:
  ```
  test(34): golden snapshot for version-change block reason

  Adds tests/golden/version_changelog_block.txt as human-reviewable
  reference for the canonical 1.10.9 -> 1.10.10 transition reason
  string. Integration test asserts a curated set of substring
  invariants (XML elements + literal AUQ labels + $OWL invocation)
  rather than full-string equality, per RESEARCH open question Q1
  recommendation: snapshot the stable parts, allow prose drift.
  ```
    </action>
    <verify>
      <!-- C2 fix: original regex `test result: ok\. (11|12) passed` over-constrained the
       count, coupling the gate to a fragile arithmetic invariant. Relax to `[0-9]+ passed`
       for the count assertion AND add a SEPARATE substring assertion that the new test
       name `block_reason_matches_golden_snapshot` actually appears in the output. This
       decouples "tests pass" from "the specific new behavior was tested" and survives
       future test additions in this file. -->
  <automated>test -f tests/golden/version_changelog_block.txt &amp;&amp; cargo test --test version_changelog 2>&amp;1 | tee /tmp/it2.log &amp;&amp; grep -qE "test result: ok\. [0-9]+ passed" /tmp/it2.log &amp;&amp; grep -q "block_reason_matches_golden_snapshot" /tmp/it2.log</automated>
    </verify>
    <done>Golden fixture committed; new integration test passes; full integration suite (now 11 tests) green; commit landed.</done>
  </task>

  <task type="checkpoint:human-verify" gate="blocking">
    <name>Task 3: UAT — manually verify the AUQ surfaces correctly in a real Claude Code session</name>
    <what-built>
  The full Phase 34 stack:
  - Stop-hook version-change detection with sentinel at `$SPT_HOME/last-seen-version.json`
  - `<spt-version-changelog>` block emission via `decision:"block"` + `reason`
  - 4-option AUQ flow rendered by Claude on re-engagement
  - `$OWL version-remind <old>` rollback subcommand
  - DEPLOY.ps1 syncs CHANGELOG.md alongside the binary
    </what-built>
    <how-to-verify>
  1. **Clean build + deploy:**
     ```
     cargo build --release
     powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1
     ```
     Verify the deploy log shows the two new `Copy CHANGELOG.md ->` substeps. After deploy, run `/reload-plugins` in Claude Code.

  2. **Verify CHANGELOG.md landed in plugin cache:**
     Windows:
     ```
     type %LOCALAPPDATA%\..\..\.claude\plugins\cache\cplugs\spt\<version>\CHANGELOG.md
     ```
     (Path varies; verify against `installed_plugins.json`.) The file should contain the curated Keep-a-Changelog content authored in Plan 01.

  3. **Synthesize a version mismatch by rewriting the sentinel to a fake old version:**
     Windows PowerShell:
     ```
     Set-Content -Path "$env:LOCALAPPDATA\spt\last-seen-version.json" -Value '{"version":"0.0.1"}'
     ```
     Unix:
     ```
     echo '{"version":"0.0.1"}' > ~/.spt/last-seen-version.json
     ```

  4. **Trigger a Stop hook:** open a Claude Code session in any spt-aware repo (this one works). Run a no-op task that ends a turn (e.g., "what files are in this directory?"). When Claude finishes, the Stop hook fires.

  5. **EXPECTED behavior on re-engagement:**
     a. Claude writes a pre-question line naming the version transition (and dates, if visible in the block payload).
     b. Claude invokes AskUserQuestion with EXACTLY four options (labels verbatim):
        - `Yes, full changelog`
        - `Yes, highlights only`
        - `Remind me later`
        - `Skip`
     c. Note: the question text mentions "X versions" if step_count > 1 (when 0.0.1 -> current, step_count will be large because CHANGELOG.md has many entries between).

  6. **Test each branch (in 4 separate Claude Code sessions or by re-synthesizing the mismatch between each):**
     - **Yes, full changelog:** Claude prints H2 sections from CHANGELOG.md verbatim. Confirm formatting preserved (bullets, H3 subsections, etc.). Sentinel after: current version.
     - **Yes, highlights only:** Claude renders 2-5 highlight bullets across the range + literal pointer line "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail." Sentinel after: current version.
     - **Remind me later:** Claude invokes `$OWL version-remind 0.0.1` via Bash. Sentinel after: `{"version":"0.0.1"}` (rolled back). The next Stop hook re-fires the block.
     - **Skip:** Claude says "OK, skipping." or similar; sentinel after: current version; next Stop does NOT re-fire.

  7. **Verify sentinel state after each branch** by running `type %LOCALAPPDATA%\spt\last-seen-version.json` (Windows) or `cat ~/.spt/last-seen-version.json` (Unix).

  8. **Test the "Other" freeform fallback** (one of the branches): when the AUQ surfaces, type something like "not now" into Other. Verify Claude maps it to `Remind me later` and invokes `$OWL version-remind 0.0.1`.

  9. **Regression check** — confirm the existing Phase 18.3 `.more-done` sentinel still works:
     - Start a `/spt:live` session with a psyche.
     - End a turn.
     - Verify `$SPT_HOME/owlery/<psyche_id>/.more-done` exists after Stop fires.
     - Verify the version-change block did NOT cancel out this behavior.

  10. **OWL_ECHO_COMMUNE regression check** — manually invoke `OWL_ECHO_COMMUNE=1 owl hook-idle <<< '{}'` after re-synthesizing a mismatch. Verify stdout is EMPTY (no block emitted; guard fires) and sentinel is UNCHANGED.

  If all 10 checks pass: type `approved` to gate-pass this checkpoint.

  If any check fails: describe the failure precisely (which step, what was expected, what actually happened, any stderr/stdout captured). The orchestrator will route the failure back through `/gsd-plan-phase --gaps` for closure.
    </how-to-verify>
    <resume-signal>Type "approved" if all 10 UAT checks pass, or describe the failure for gap closure routing.</resume-signal>
  </task>

  </tasks>

  <verification>
  After all 3 tasks (Tasks 1+2 automated, Task 3 manual UAT):

  1. Placeholder fully removed:
     ```
     grep -r "PLAN_03_INSTRUCTIONS_PLACEHOLDER" src/ tests/
     ```
     Expect 0 hits.

  2. Full test suite green:
     ```
     cargo test
     ```
     Expect ~30+ unit tests + 11 integration tests, all passing.

  3. Golden fixture present and non-empty:
     ```
     test -s tests/golden/version_changelog_block.txt
     ```
     Plus a manual eyeball: does the golden content read as the AUQ wording you authored?

  4. Two new commits in addition to Plan 01 + Plan 02:
     ```
     git log --oneline -2
     ```
     Expect:
     - `test(34): golden snapshot for version-change block reason`
     - `feat(34): author 4-option AUQ instructions for version-change block`

  5. UAT approved by human via Task 3 checkpoint.

  6. End-to-end stack: `cargo build --release && powershell docs/DEPLOY.ps1 -Bump patch -DryRun` exits 0 and surfaces the new CHANGELOG.md stub-append substep (regression check against Plan 01 work).
  </verification>

  <success_criteria>
  - Plan 03 maps to VERS-04 (4-option AUQ wording authored verbatim; AskUserQuestion surface verified in UAT) and VERS-06 (multi-version path uses the same 4-option AUQ; "Yes, full changelog" renders all interim H2 sections per Pitfall 6).
  - The `<instructions>` element in the block reason is the only file where wording lives — change-control is centralized.
  - Golden snapshot resilience strategy (substring invariants, not full-string) accommodates future wording tweaks without breaking the test.
  - UAT confirms the entire Phase 34 surface works in a real Claude Code session.
  - After approval, Phase 34 is shippable in the next end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6).
  </success_criteria>

  <threat_model>
  ## Trust Boundaries

  | Boundary | Description |
  |----------|-------------|
  | hook reason -> Claude continuation | the `<instructions>` element text drives Claude's next turn; wording reliability determines whether the AUQ fires correctly |
  | Claude -> Bash | `$OWL version-remind <old>` invocation crosses Claude's Bash tool boundary; `<old>` is substituted by the hook, not by Claude |

  ## STRIDE Threat Register

  | Threat ID | Category | Component | Disposition | Mitigation Plan |
  |-----------|----------|-----------|-------------|-----------------|
  | T-34-11 | Tampering | Wording drift in `<instructions>` causes Claude to invoke wrong tool / wrong args | mitigate | Substring-invariant integration test (Task 2) asserts the 4 option labels + literal invocation string survive any prose tweak; UAT (Task 3) catches semantic regressions |
  | T-34-12 | Spoofing | Claude paraphrases `$OWL version-remind <old>` to a different value | mitigate | Instructions text explicitly says "substitute the {old} value verbatim, do NOT paraphrase or recompute"; the `<old>` value is pre-substituted by the hook so Claude only needs to copy-paste; the `is_plain_semver` validator in version-remind rejects garbage arg |
  | T-34-13 | Information disclosure | Pre-question prose leaks unintended detail | accept | Author-controlled wording; only surfaces version numbers + dates from CHANGELOG.md (already public artifact) |
  | T-34-14 | DoS | Highlights branch reads CHANGELOG.md for every turn | accept | Read only happens on user-explicit "Yes, highlights only" pick, bounded by user action; no automated re-read |
  </threat_model>

  <output>
  After completion, create `.planning/phases/34-version-change-changelog/34-03-SUMMARY.md` documenting:
  - Two commit SHAs (Task 1 wording, Task 2 golden)
  - UAT result (approved / failed-with-detail)
  - Final substring invariant count in the integration test
  - Snapshot file SHA-256 (for future drift detection)
  - Confirmation `grep -r PLAN_03_INSTRUCTIONS_PLACEHOLDER src/ tests/` returns 0 hits
  - Any wording tweaks made between Task 1 draft and the final committed text (preserve the audit trail)
  - Confirmation Phase 34 is ready for the next end-of-milestone DEPLOY.ps1 -Bump patch (ROADMAP SC6)
  </output>

  ## Review Instructions

  Analyze each plan and provide:
  1. Summary — one-paragraph assessment
  2. Strengths (bullets)
  3. Concerns (bullets, severity HIGH/MEDIUM/LOW)
  4. Suggestions (specific bullets)
  5. Risk Assessment (LOW/MEDIUM/HIGH + justification)

  Focus on: missing edge cases, dependency ordering, scope creep, security, performance, whether plans achieve phase goals.
  Output in markdown.


