claude-code-ultimate-guide/guide/workflows/changelog-fragments.md
Florian BRUNIAUX b6ce1ef72f docs: add RPI workflow, changelog fragments, smart-suggest hook + LLM variance
- guide/workflows/rpi.md (new): Research → Plan → Implement, 3-phase pattern
  with explicit GO gates, slash command templates, worked example
- guide/workflows/changelog-fragments.md (new): per-PR YAML fragment enforcement,
  3-layer system (CLAUDE.md rule + UserPromptSubmit hook + CI gate)
- examples/hooks/bash/smart-suggest.sh (new): UserPromptSubmit behavioral coach,
  3-tier priority (enforcement/discovery/contextual), ROI logging
- guide/core/known-issues.md: LLM Day-to-Day Performance Variance section,
  4 root causes (probabilistic inference, MoE routing, infra, context sensitivity)
- guide/workflows/README.md: added RPI entry + quick selection row
- machine-readable/reference.yaml: added entries for changelog_fragments, smart_suggest
- CHANGELOG.md: [Unreleased] entries for all 4 new items
- IDEAS.md: prompt-caching MCP plugin research note (testing in progress)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 16:22:57 +01:00

8.7 KiB

Changelog Fragments: Enforced Per-PR Documentation

A 3-layer enforcement pattern that ensures every PR is documented at write time, never at release time.


The Problem

Single CHANGELOG.md files break on active teams. Three open feature branches all touching the same file means merge conflicts on every merge. Someone resolves the conflict, drops a line, and release notes are wrong before they're published.

The deeper problem is timing. "Document at release time" sounds reasonable until you're staring at PR #840 three weeks after it merged, trying to reconstruct what changed for users. The commit says fix session handling. The developer is in a different timezone. Context is gone.

Enforcement without CI gates means changelogs become a nag job. Someone has to chase people before every release, under time pressure, filling in blanks from git log.

The solution: one YAML fragment per PR, written while implementing, validated by CI, assembled automatically at release.


The 3-Layer Architecture

The system works because enforcement happens at three independent levels. Each layer catches a different failure mode.

Layer 1: CLAUDE.md Workflow Rule

The first layer is a rule loaded into Claude Code's context at every session. It encodes the entire fragment workflow so Claude can complete it autonomously when asked to create a PR.

# git-workflow.md (loaded via CLAUDE.md)

## Changelog Fragment — Required Before Every PR

Before creating a PR, always generate a changelog fragment.

### Steps

1. **Infer from git diff** — analyze `git diff main...HEAD` to determine:
   - `type`: feat | fix | perf | refactor | security | docs | chore
   - `scope`: the functional area affected (auth, sessions, api, etc.)
   - `title`: one-line user-facing summary (< 80 chars)

2. **Create the fragment**  run `pnpm changelog:add` or write directly:

changelog/fragments/{PR_NUMBER}-{slug}.yml


3. **Validate** — run `pnpm changelog:validate changelog/fragments/{file}.yml`

4. **Commit alongside the PR** — include the fragment in the same branch

### Fragment schema

```yaml
pr: 886                    # must match filename prefix
type: fix                  # feat|fix|perf|refactor|security|docs|chore
scope: "visiochat"
title: "Fix empty chat after SSE race condition"   # < 80 chars
description: |             # optional — explain user impact, not implementation
SSE workplan fires before AI stream completes, causing ChatWrapper
to mount with 0 messages.
breaking: false
migration: false           # set true if PR adds a DB migration

Bypass

Add label skip-changelog for PRs with no user impact (CI config, deps updates, release commits).


This rule makes Claude Code a participant in enforcement, not just a coding tool. When a developer says "make the PR," Claude infers the fragment content from the diff and creates it before opening the PR.

### Layer 2: UserPromptSubmit Hook (Behavioral Detection)

The second layer intercepts intent before it becomes action. When the developer types something that signals PR creation intent, the hook checks whether a changelog fragment was mentioned.

```bash
# .claude/hooks/smart-suggest.sh (excerpt — Tier 0 enforcement)

# PR creation intent detected
if echo "$PROMPT_LC" | grep -qE '(create.*pr|open.*pr|make.*pr|pull.?request|push.*pr)'; then
    # Fragment not mentioned → redirect to creation step first
    if ! echo "$PROMPT_LC" | grep -qE '(changelog|fragment|skip-changelog)'; then
        suggest "pnpm changelog:add" \
            "REQUIRED before merge — creates changelog/fragments/{PR}-{slug}.yml"
    else
        # Already mentioned → suggest the PR command normally
        suggest "/pr" "PR creation with structured description"
    fi
fi

The hook is UserPromptSubmit: non-blocking, max one suggestion per prompt, silent on no match. It runs before Claude Code processes the prompt, so the developer sees the reminder inline before Claude starts doing anything.

The conditional logic (if X without Y) is the key pattern here. It's not a blanket blocker — it adapts to context. If the developer already mentioned the fragment, they get the normal suggestion. If they didn't, they get the enforcement reminder.

Full hook with 3-tier architecture: examples/hooks/bash/smart-suggest.sh

Layer 3: CI Enforcement (GitHub Actions)

The third layer is the hard gate. Two independent jobs run on every PR targeting the main branch.

check-fragment job: checks bypass labels first (closed list), then requires changelog/fragments/{PR_NUMBER}-*.yml to exist and pass structural validation.

- name: Check fragment exists and is valid
  env:
    PR_NUMBER: ${{ github.event.pull_request.number }}
    PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
  run: |
    SKIP_LABELS=("skip-changelog" "dependencies" "release" "chore: deps")
    for LABEL in "${SKIP_LABELS[@]}"; do
      if echo "$PR_LABELS" | grep -q "\"$LABEL\""; then
        echo "Bypass label detected — fragment not required"
        exit 0
      fi
    done

    FRAGMENT=$(ls "changelog/fragments/${PR_NUMBER}-"*.yml 2>/dev/null | head -1)
    if [ -z "$FRAGMENT" ]; then
      echo "Fragment missing. Run: pnpm changelog:add"
      exit 1
    fi

    pnpm tsx changelog/scripts/validate.ts "$FRAGMENT"

check-migration-flag job (runs independently, no bypass): detects new SQL migration files with git diff --name-only --diff-filter=A. If migrations are present and migration: false in the fragment, it fails. This job cannot be bypassed by labels — a skip-changelog PR that adds a migration still triggers the check.

The two jobs are independent by design. A PR can bypass fragment creation (via label) but still fail the migration check.


Fragment Assembly at Release

Fragments accumulate in changelog/fragments/ as PRs merge. At release time, one command assembles them into a versioned CHANGELOG section.

pnpm changelog:assemble --version 1.8.0 [--dry-run]

What it does:

  1. Reads all changelog/fragments/*.yml
  2. Groups by type in fixed order (feat, fix, perf, refactor, security, docs, chore)
  3. Pulls breaking: true entries into a dedicated 🔨 Breaking Changes section
  4. Annotates migration: true entries inline with ⚠️ Migration DB.
  5. Replaces the ## [Next Release] placeholder in CHANGELOG.md
  6. Archives fragments to changelog/fragments/released/{version}/

Output:

## [1.8.0] - 2026-03-15

### 🔨 Breaking Changes
- **Remove legacy token format (#871)** — Tokens issued before v1.6.0 are invalid.

### ✨ New Features
- **Add real-time presence indicators (#892)**

### 🔧 Bug Fixes
- **Fix empty chat after SSE race condition (#886)** — SSE workplan fires before
  AI stream completes, causing ChatWrapper to mount with 0 messages.

Why 3 Layers, Not 1

Each layer catches a different failure mode:

Layer Failure caught When
CLAUDE.md rule Claude forgets the workflow Every session
UserPromptSubmit hook Developer types "make the PR" without thinking Pre-prompt
CI gate Fragment was skipped or corrupt Pre-merge

A single CI gate catches the issue too late — the developer has to context-switch back after their PR is already open. The hook catches it at intent time. The CLAUDE.md rule means Claude handles it autonomously when given the task.

The layers don't conflict. They reinforce each other. A developer who sees the hook suggestion will run pnpm changelog:add. Claude will follow the CLAUDE.md rule and validate the output. CI confirms everything before merge.


Adopting This Pattern

The TypeScript scripts (add, validate, assemble, audit) are specific to the Méthode Aristote stack. The 3-layer enforcement pattern is not — it works with any fragment format, any CI system, any assembler.

Minimum viable setup:

  1. Define your fragment schema (YAML, JSON, whatever fits your stack)
  2. Add a CLAUDE.md rule encoding the creation workflow so Claude can handle it autonomously
  3. Add a UserPromptSubmit hook with the if PR-intent without fragment-mention → suggest pattern
  4. Add a CI job that checks for fragment existence before merge

The hook pattern generalizes to any mandatory workflow step. Substitute "changelog fragment" with "ADR", "migration flag", "test coverage check" — the conditional detection logic is the same.