# Quick Task 260520-uc2 — Limit Changelog to UX Language

**Researched:** 2026-05-20
**Domain:** CHANGELOG curation discipline + AUQ instruction text (src/owl/version_changelog.rs) + DEPLOY.md authoring guidance
**Confidence:** HIGH (entirely codebase-local; no external libraries involved)

## Summary

Today the version-change Stop-hook builds an `<spt-version-changelog>` payload whose `<instructions>` element tells Claude to read `$CLAUDE_PLUGIN_ROOT/CHANGELOG.md` and "print every H2 section ... verbatim markdown". CHANGELOG.md as-authored mixes UX bullets (e.g. "version-change owl message routed via TCP-first delivery") with deep BTS detail (file paths, struct/fn names, commit SHAs, internal-only reverts, doc-comment cleanups). The "Yes, full changelog" path surfaces all of it; "Yes, highlights only" already filters loosely ("Skip purely-internal refactors and test-only changes") but only because the synthesizer downstream applies that hint — the source bullets are still mixed.

**Primary recommendation:** introduce a fourth Keep-a-Changelog-compatible section per H2 — `### BTS` — and add a hook-time filter directive that excludes it from BOTH surfacing branches. Keep the K-a-C 1.1.0 categories (Added / Changed / Fixed / Removed / Deprecated / Security / Reverted) untouched; BTS sits alongside them. DEPLOY.md grows a "Bucket discipline for stub fill-in" subsection with bright-line rules for what goes where. AUQ wording (`build_block_reason` resolved branch) gains an explicit "skip `### BTS` sections" clause in both options. Two tests pin the now-stale wording and need updating.

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Bucket scheme + authoring rules | Documentation (DEPLOY.md) | — | Pure authoring contract; consumed by humans/LLMs filling the stub |
| AUQ instruction text (filter directive) | `src/owl/version_changelog.rs::build_block_reason` | — | The directive must travel inside the `<spt-version-changelog>` payload so the downstream Claude session honors it without out-of-band context |
| Section enforcement at write time | None (out of scope) | — | DEPLOY.ps1 stub-append could add a section template but is not load-bearing — the discipline lives in the author's head and in DEPLOY.md |

## Standard Stack

N/A — pure prose + format-string changes. No new deps.

## Authoring Bucket Scheme (Q1)

**Recommendation: Option (a) — keep all K-a-C categories, add a sibling `### BTS` section.**

Rationale:
- The K-a-C spec is widely understood and DEPLOY.md already names it as the authoring contract. Collapsing to four custom buckets (Added/Changed/Fixed/BTS) would break that link, force re-explanation, and lose the semantic distinction between `Fixed` (bug fix) and `Reverted` (rollback) that v1.10.20's entry currently leverages.
- The hook's `<instructions>` text only needs a single filter rule ("skip `### BTS`"). Whether other K-a-C categories also need filtering is orthogonal (see Q3).
- Existing CHANGELOG.md entries from v1.7.x → v1.10.20 remain compliant; no migration needed (see Q5).
- The fourth bucket the user asked for slots in cleanly as a new section name without disturbing the surrounding shape.

Concretely each H2 may contain any non-empty subset of:
- `### Added` — new user-facing capability (UX language)
- `### Changed` — modified user-facing behavior (UX language)
- `### Fixed` — bug fix the user observed (UX language)
- `### Removed` / `### Deprecated` / `### Security` / `### Reverted` — used when applicable, UX language
- `### BTS` — anything off the UX path; never surfaced

The user's "added / changed / fixed / bts" is preserved as the spiritually canonical four; the other three K-a-C categories are escape hatches for the unusual case (a real removal, deprecation, security-advisory rev, or rollback that the user must know about).

## Authoring Guidance for DEPLOY.md (Q2)

Drop the following subsection into `docs/DEPLOY.md` immediately AFTER the "Two-phase changelog authoring (Phase 34 D-08 round 2)" block (line ~67) and BEFORE the "Parameters" heading (line ~68). Exact text:

```markdown
### Bucket discipline for stub fill-in (quick-260520-uc2)

The version-change Stop hook surfaces curated CHANGELOG bullets to the user
on the next session boundary after a plugin upgrade. The user sees only the
release-facing buckets — implementation noise is filtered out at hook time.
The deploying agent (human or LLM) filling the `- TODO: changelog entry`
stub MUST follow these rules:

**Section vocabulary per H2 (`## [vX.Y.Z] - YYYY-MM-DD`):**

| Section | Use for | Surfaced to user? |
|---------|---------|---------------------|
| `### Added` | New user-facing capability (a flag, command, message, prompt, hook trigger they will notice) | YES |
| `### Changed` | Modified user-facing behavior (different message, different default, different timing) | YES |
| `### Fixed` | Bug fix the user actually observed or could observe | YES |
| `### Removed` | A user-facing capability that no longer exists | YES |
| `### Deprecated` | A capability the user should stop relying on | YES |
| `### Security` | Anything with user-relevant security impact | YES |
| `### Reverted` | A user-visible behavior was rolled back to a prior shape | YES |
| `### BTS` | Anything else: internal refactors, repo plumbing, deploy script changes, doc-string drift, test-only changes, code reverts that don't change user-visible behavior, commit SHAs, file-path callouts | **NO** — filtered out at hook time |

**UX-language rule for Added / Changed / Fixed (and the other surfaced
buckets):**
- Phrase as what the user sees, does, or runs. "`/spt:live` now offers an
  Other option" — not "added `AuqOption::Other` to `live::auto_pick`".
- Do NOT mention: code paths (`src/...`), function or struct names, prompt
  XML element names (`<spt-foo>`), commit SHAs, planning doc references
  (`Phase 34 D-08`, `quick-260517-n4b`), Rust types, file paths inside
  `plugin/spt/`, internal env-var names that are not part of the
  documented user surface.
- Keep bullets terse. One sentence each is the target; two short sentences
  is the ceiling. If you need more, you are probably writing BTS content.
- The Stop-hook "Yes, highlights only" path picks 2-5 bullets across the
  whole transition; bullets that are themselves short and UX-focused give
  it the right material to work with.

**BTS rule (anything off the UX path):**
- Repo-management changes (Cargo manifest tweaks unless they change runtime
  behavior, CI config, .gitignore, planning docs).
- Deploy-sequence changes (DEPLOY.ps1, DEPLOY.md, marketplace cache logic)
  unless the user's deploy workflow changes.
- Commit reverts where the reverted code never reached the user's machine.
- Doc-string / comment drift fixes.
- Test-only additions or framework changes.
- Internal refactors with no observable behavior change.
- Naming, struct-field, or fn-signature changes inside `src/`.

**When in doubt, ask: "Would a user typing `/spt:live` notice the
difference?"** If no, it's BTS.

**Examples of correct bucketing** (from real prior bullets, reframed):

- v1.10.20 "version-change owl message routed via TCP-first deliver_body" →
  `### Fixed`: "Version-change announcements arrive in-line with other
  high-priority messages instead of waiting for the next poll tick."
  (The file-path / fn-name detail moves to `### BTS` as: "Routed
  `version_changelog` payload through `deliver_body` like every other owl
  message.")
- v1.10.20 "WORKING_PERCH_NOTICE TCP-first flip reverted" — no user-visible
  symptom changed, so it belongs entirely in `### BTS`, not `### Reverted`.
- v1.10.18 the entire "260520-r2f commune-reference write-path fix" — the
  user-visible bullet is `### Fixed`: "Fresh `/spt:live` start now delivers
  first-commune context to the live agent reliably." Every file/line
  citation in the existing entry moves to `### BTS`.
- v1.10.17 chunking work — `### Fixed`: "Long poll-listener events no
  longer get truncated by the Claude Code Monitor at ~500 characters."
  Internal const names and chunk-boundary detail → `### BTS`.

**Backward compatibility:** Pre-v1.10.21 entries that pre-date this
discipline are left as-is. The hook's "skip `### BTS`" filter is a no-op
on them (they have no `### BTS` section) and the full-changelog path will
surface them verbatim per the existing behavior. The new discipline
applies prospectively starting with the next bump.
```

That's roughly one screen — short enough that the deploying agent reads it on stub-fill, long enough to be unambiguous.

## AUQ Instruction Text Changes (Q3)

The wording lives in `src/owl/version_changelog.rs::build_block_reason`, lines 342-387, RESOLVED branch only (DEGRADED branch is unaffected per Q6).

**Recommended replacement strings.** Diff the resolved-branch `instructions` format-string body. Old → new:

### Option 1: `Yes, full changelog`

OLD (line 352):
```
  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.
```

NEW:
```
  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 the user-facing H3 subsections (Added / Changed / Fixed / Removed / Deprecated / Security / Reverted). OMIT any "### BTS" subsection entirely — those are behind-the-scenes notes the user does not want to see. If a version has only a "### BTS" subsection with no user-facing content, render the H2 header and date alone with a single line "(no user-facing changes this release)". Render in the order they appear in the file (newest first). Do not summarize, do not paraphrase the bullets you do show.
```

### Option 2: `Yes, highlights only`

OLD (line 354):
```
  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}. 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 exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."
```

NEW:
```
  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}. Draw ONLY from the "### Added", "### Changed", "### Fixed", "### Removed", "### Deprecated", "### Security", and "### Reverted" subsections. Do NOT draw from "### BTS" subsections — those are behind-the-scenes notes and must never appear in highlights. Pick the most impactful user-facing items across the range. Append exactly this pointer at the end: "See $CLAUDE_PLUGIN_ROOT/CHANGELOG.md for full detail."
```

### How forceful does the directive need to be?

The downstream consumer is a Claude session reading the payload as part of the next UserPromptSubmit. It honors directives delivered as imperative second-person verbs ("OMIT", "Do NOT draw from") more reliably than passive hints ("Prefer ..."). The new wording uses imperative + ALL-CAPS verb anchors for the two negative directives. That matches the prior pattern (`do not paraphrase`, `Do NOT attempt to read`) the existing DEGRADED branch uses to good effect.

### What if a version has no `### BTS` section at all?

The wording uses "OMIT any `### BTS` subsection" — an instruction to remove a thing that may or may not be present. This naturally no-ops on entries without the section. Confirmed by the example sentence in the proposed wording ("If a version has only a `### BTS` subsection with no user-facing content, render the H2 header and date alone..."). No special-casing needed; the directive is well-defined for both the present-and-absent cases.

## Test Impact (Q4)

Two assertion sites pin the now-stale wording. Both are in this repo; no external test infrastructure is affected.

### `src/owl/version_changelog.rs` (inline tests)

- `build_block_reason_single_step_resolved_branch_substitutes_correctly` (line ~990):
  - `assert!(out.contains("Pick 2-5 most impactful"));` — the literal "Pick 2-5 most impactful" string is removed in the new highlights wording. **Update or remove this assert.** Replace with a strictly-stronger invariant: `assert!(out.contains("Do NOT draw from"))` plus `assert!(out.contains("### BTS"))` to lock the new filter directive.

### `tests/version_changelog.rs` (integration)

- `block_reason_matches_reference_invariants` (line ~800-811):
  - The `invariants` array currently contains `"read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md"` — this stays (both new strings still issue the read directive).
  - The array does NOT currently pin "Pick 2-5 most impactful" — confirmed; no fix needed here.
  - **Add two new invariants** to lock the BTS-filter directive: `"### BTS"` and `"behind-the-scenes"` (a discriminator phrase that doesn't appear in the unrelated K-a-C category names).

### Reference fixture

- `tests/fixtures/version_changelog_block_reference.txt` is informational only per the comment block in the test (REVIEW-FIX #8 — substring invariants, not full-string golden). The test logs a NOTE on drift but does not fail. Regenerate via the existing `print_canonical_block_reason_for_reference` `--ignored` test if a fresh reference is desired.

**No other test files reference the AUQ wording.** A repo-wide search for "Pick 2-5 most impactful" / "highlight bullets" / "Yes, highlights only" returns hits only in those two files plus the reference fixture.

## Migration of Existing CHANGELOG.md (Q5)

**Recommendation: Option (a) — leave history as-is; apply BTS discipline prospectively from the next bump (v1.10.21+).**

Reasons:
- The hook's "skip `### BTS`" directive is a pure subtractive filter. On entries without that section the filter is a no-op and the user sees the existing prose verbatim — same as today.
- Retrofitting 40 H2 entries by hand is high-effort, error-prone (judging UX-vs-BTS on six-months-old bullets is fuzzier than judging on fresh ones), and yields little value: the user upgrades from whatever version they last had to whatever version they install now, so most historical entries will never re-render in a fresh hook fire after the cutover.
- The first ship with the new discipline (v1.10.21) will produce one entry under the new rules; subsequent entries accrue normally. Within a few weeks the new-style entries dominate the typical upgrade window.

If a future user spikes upgrades multi-version across the discipline boundary (e.g. v1.10.15 → v1.10.30), they will see a mix of "old-style" verbose entries and "new-style" curated entries. That degradation is graceful — verbose entries are just more verbose; nothing breaks.

## Common Pitfalls

### Pitfall 1: Wording-lock tests
**What goes wrong:** The proposed wording change breaks one literal-string assert (`"Pick 2-5 most impactful"`) and the test suite fails.
**How to avoid:** Update the assert in `build_block_reason_single_step_resolved_branch_substitutes_correctly` to the new locking invariants (`Do NOT draw from`, `### BTS`). Replace, don't just delete — the new invariants are stronger.
**Warning signs:** `cargo test build_block_reason_single_step_resolved_branch` red on the implementation commit.

### Pitfall 2: DEGRADED branch confusion (REVIEW-FIX #9)
**What goes wrong:** Someone helpfully adds the BTS filter language to the DEGRADED branch instructions too, which contradicts the REVIEW-FIX #9 invariant (`build_block_reason_unresolved_branch_does_not_instruct_read_for_yes_options`) — the directive phrase "read $CLAUDE_PLUGIN_ROOT/CHANGELOG.md" must NOT appear in the unresolved branch.
**How to avoid:** Apply changes ONLY to the resolved-branch `format!` block (lines 342-387 in `build_block_reason`). The degraded branch has no source content to filter — it tells Claude the file isn't readable at all. No change needed there.
**Warning signs:** `build_block_reason_unresolved_branch_does_not_instruct_read_for_yes_options` red — directive phrase leaked across branches.

### Pitfall 3: Section-name typo robustness
**What goes wrong:** Author writes `### bts` (lowercase) or `### Behind the Scenes` or `### Internal`. The hook directive expects exactly `### BTS` and the typo'd section surfaces to the user.
**How to avoid:** Make `### BTS` (all-caps, exact) the single canonical spelling in DEPLOY.md. Make the AUQ directive specify "the literal string `### BTS`" so a downstream Claude session does case-sensitive matching. Optionally (out of scope here, candidate follow-up): DEPLOY.ps1's curation gate could lint for known-bad spellings, but that's a larger change.
**Warning signs:** A BTS bullet shows up in a "Yes, full changelog" rendering. The fix is to canonicalize the section header in the offending entry.

### Pitfall 4: Empty user-facing section
**What goes wrong:** A version's only changes are BTS. After filtering, the H2 renders bare. The downstream Claude session is unsure what to do.
**How to avoid:** Explicit instruction in the resolved-branch wording ("If a version has only a `### BTS` subsection with no user-facing content, render the H2 header and date alone with a single line `(no user-facing changes this release)`."). Already covered in the proposed Option 1 string above.
**Warning signs:** "Yes, full changelog" renders a bare H2 with no body. Low risk because BTS-only versions are uncommon and the directive covers the case.

### Pitfall 5: Downstream Claude ignoring the filter
**What goes wrong:** The downstream session reads CHANGELOG.md and helpfully prints "everything" including BTS, treating the filter directive as a soft suggestion.
**How to avoid:** Imperative wording ("OMIT", "Do NOT draw from"), ALL-CAPS verb anchors, and the existing pattern-match-with-DEGRADED-branch precedent — Claude honors "Do NOT attempt to read" reliably there; "OMIT any `### BTS` subsection" should perform comparably here.
**Warning signs:** UAT after first BTS-bearing release shows a BTS bullet leaking into the user's view. Mitigation: tighten the directive ("MUST OMIT" + repeat at end of option text); the wording is the only knob.

## Code Examples

The exact `format!` body diff is captured in Q3 above. No new types, no new modules, no API changes. Implementation is a one-function prose edit plus two test updates.

## Sources

### Primary (HIGH confidence)
- `src/owl/version_changelog.rs` (lines 342-387 resolved branch, 364-385 degraded branch, plus the inline test module) — read in full
- `tests/version_changelog.rs` (Test 13 `block_reason_matches_reference_invariants` + corpus) — read in full
- `docs/DEPLOY.md` (Two-phase changelog authoring section at line 42-67) — read in full
- `CHANGELOG.md` v1.10.17 → v1.10.20 entries — read to confirm current bullet-style mix of UX and BTS content
- `docs/DEPLOY.ps1` lines 239-311 (curation/abort gate + stub-append logic; confirms the stub text is literally `- TODO: changelog entry` and does NOT inject section headers)

## Metadata

**Confidence breakdown:**
- Bucket scheme (Q1): HIGH — pure design choice, all alternatives surveyed, recommendation is least-disruptive
- DEPLOY.md authoring text (Q2): HIGH — drop-in text supplied verbatim
- AUQ instruction text (Q3): HIGH — drop-in replacements supplied verbatim, directive force-level reasoned against precedent in the same file (REVIEW-FIX #9 degraded-branch wording)
- Test impact (Q4): HIGH — exhaustive grep confirms two assertion sites
- Migration (Q5): HIGH — pure no-op-by-construction for legacy entries
- Pitfalls (Q6): HIGH — drawn from the file's own REVIEW-FIX history (especially #9) plus the natural failure modes of the directive

**Research date:** 2026-05-20
**Valid until:** stable indefinitely (no external dependencies, no version drift risk)
