---
name: commit-categorizer
description: Fetch commits since last changelog sync and categorize them for changelog update
allowed-tools:
  - Bash
  - Read
  - Glob
  - Grep
---

# Commit Categorizer Agent

Fetch commits since last CHANGELOG.md sync, categorize them, and return a compact proposal for the main conversation to present to the user.

## Input

You receive no special input. All information is derived from the repository state.

## Process

### Step 1: Fetch Commits

Run the changelog-commits command to get all commits since the last "As of" marker:

```bash
erk-dev changelog-commits --json-output
```

This command:

- Parses CHANGELOG.md to find the "As of <commit>" marker
- Gets commits using `--first-parent` (excludes feature branch commits)
- Excludes paths: `.claude/`, `docs/learned/`, `.impl/`
- Returns JSON with commit details including PR numbers

**JSON output structure:**

```json
{
  "success": true,
  "since_commit": "b6a2bb40b",
  "head_commit": "af8fa25c9",
  "commits": [
    {
      "hash": "af8fa25c9",
      "subject": "Fix artifact sync... (#3619)",
      "body": "...",
      "files_changed": ["src/erk/artifacts/sync.py", ...],
      "pr_number": 3619
    }
  ]
}
```

**Error handling:**

If `success` is false and error indicates missing "As of" marker:

1. Read the CHANGELOG.md to find the most recent release version (e.g., `## [0.3.3]`)
2. Use `git log --oneline -1 --grep="X.Y.Z"` to find the release commit hash
3. Get commits since that release using `erk-dev changelog-commits --since <release-hash> --json-output`
4. Continue to Step 2 to categorize those commits

**Important:** A missing marker means the Unreleased section needs all commits since the last release, not just an empty marker at HEAD. The `--since` option allows you to specify a commit directly when no marker exists.

If `success` is false for other reasons, output:

```
STATUS: ERROR
ERROR_MESSAGE: <the error message>
```

And stop.

If `commits` array is empty, output:

```
STATUS: NO_COMMITS
HEAD_COMMIT: <head_commit from JSON>
SINCE_COMMIT: <since_commit from JSON>
TOTAL_COMMITS: 0
```

And stop.

### Step 2: Read Existing CHANGELOG Entries

Read the CHANGELOG.md unreleased section to understand what entries already exist. This prevents suggesting duplicate entries.

### Step 3: Categorize Commits

Use the commit details from the JSON output (subject, body, files_changed) to categorize each commit.

#### Categories

**Major Changes** (significant features or breaking changes):

- New user-facing systems or major capabilities
- Breaking changes that users need to know about
- CLI command reorganization or removal
- Features that warrant special attention in release notes

**IMPORTANT: Major Changes must be USER-VISIBLE.** The test: "Does an end user running `erk` commands see significantly different behavior?" Internal architecture improvements, gateway refactoring, retry mechanisms, schema-driven config, and infrastructure changes are NEVER Major Changes—they are implementation details that should be filtered entirely.

**Major Change Entry Requirements:**

When writing a Major Change entry, always include:

1. **What it does** - Brief description of the feature/change
2. **Motivation/purpose** - Why we built it, the problem it solves
3. **Value to user** - What benefit users get from this

**Merge related commits:** Roll up all related commits (fixes, aliases, extensions) into a single Major Change entry with explanatory prose. Don't list implementation details or internal commits separately.

**Never expose implementation details** like function names, internal checks, or architecture patterns. Focus on user-visible behavior and benefits.

Example format:

```markdown
- **Feature name**: Brief description of what it does. Motivation explaining why we built it. Value statement about user benefit. (primary_commit_hash)
```

**Added** (new features):

- Commits with "add", "new", "implement", "create" in message
- Feature PRs

**Changed** (improvements):

- Commits with "improve", "update", "enhance", "move", "migrate" in message
- Non-breaking changes to existing functionality

**Fixed** (bug fixes):

- Commits with "fix", "bug", "resolve", "correct" in message
- Issue fixes

**Removed** (removals):

- Commits with "remove", "delete", "drop" in message

#### Filter Out (do not include)

**Always filter:**

- **Local-only commands** - ANY commit adding or modifying `.claude/commands/local/*` files. These are developer-only commands not shipped to users. Filter even if the commit message sounds like a new feature (e.g., "Add /local:foo command")
- **Release housekeeping** - version bumps ("Bump version to X"), CHANGELOG finalization, lock file updates for releases
- CI/CD-only changes (.github/workflows/)
- Documentation-only changes (docs/, .md files in .erk/)
- Test-only changes (tests/)
- Internal code conventions (frozen dataclasses, parameter defaults)
- Gateway method additions (abc.py + real.py + fake.py pattern)
- Build tooling (Makefile, pyproject.toml deps)
- Merge commits with no substantive changes
- Internal-only refactors that don't affect user-facing behavior
- Infrastructure/architecture changes invisible to users
- Vague commit messages like "update", "WIP", "wip"
- Internal abstraction consolidation (merging/refactoring internal types like Terminal+UserFeedback->Console)
- Changes to erk-dev commands and internal development tooling
- **`erk exec` commands** - All changes to `src/erk/cli/commands/exec/scripts/` are internal tooling and should always be filtered

**Likely internal (verify before including):**

- "Refactor", "Relocate", "Consolidate" - check if user-visible
- Skill/agent documentation updates - usually internal
- "Harden", "Strengthen" - usually internal enforcement

**Exception - capability workflows ARE external-facing:**

- Changes to `.github/workflows/` that affect capabilities (e.g., `dignified-python-review.yml`) ARE user-facing and should be included, since capabilities are user-installable features

#### Internal-Only Patterns (always filter)

**By path:**

- Changes only in `tests/` -> internal
- Changes only in `scripts/` (unless CLI-facing) -> internal
- Changes only to `**/fake*.py` -> internal
- Changes only to `Makefile` -> internal
- Changes only to `.github/workflows/` -> internal

**By content:**

- Gateway ABC method additions (`abc.py`, `real.py`, `fake.py`, `dry_run.py`, `printing.py`) -> internal
- Code convention migrations (frozen dataclasses, default params) -> internal
- Import reorganization -> internal

**By commit message:**

- "Refactor X to Y" with no user-visible change -> internal
- "Consolidate", "Relocate", "Migrate" internal modules -> internal
- "Eliminate default parameter values" -> internal
- "Migrate dataclasses to frozen=True" -> internal

#### Abstraction Consolidation Pattern

When internal abstractions are merged, consolidated, or refactored, filter them out even if many files change. The test is **user-visible behavior**, not code organization.

Examples of internal consolidation (always filter):

- "Consolidate X and Y into Z" where X, Y, Z are internal types
- "Unify X gateway" where the gateway interface is internal
- "Merge X module into Y" for internal modules

The key question: Does an end user calling `erk` commands see different behavior? If no, filter it.

#### Internal Tooling Pattern

Changes to development tooling used only by erk developers should be filtered:

- `erk-dev` commands and their implementations
- Changelog tooling (`changelog-commits`, marker parsing)
- Internal scripts in `scripts/`
- Test utilities and fixtures

#### Roll-Up Detection

When multiple commits are part of a larger initiative, group them under a single Major Change entry:

**Detection patterns:**

- Multiple commits mentioning same keyword (e.g., "kit", "artifact", "hook")
- Commits with sequential PR numbers on same topic
- Commits that reference same GitHub issue/objective

**Roll-up examples:**

- 5+ commits about "kit" removal -> "Eliminate kit infrastructure entirely"
- 3+ commits about "artifact sync" -> "Add unified artifact distribution system"
- Multiple "objective skill" commits -> single entry or filter entirely

**Presentation:**

When roll-up detected, include in proposal:

```
**Detected Roll-Up:** {n} commits appear related to "{topic}"
Suggest consolidating into single Major Change: "{proposed description}"
Commits: {list of hashes}
```

#### Confidence Flags

Mark entries as **low-confidence** when:

- Commit message is ambiguous (e.g., "update X" could be Changed or internal)
- Scope is unclear (could be user-facing or internal-only)
- Category is borderline (e.g., "Add X" but it's really a refactor)
- Large architectural changes that might or might not affect users
- Commits that touch both user-facing and internal code

### Step 4: Format Output

Output the result in this exact format:

```
STATUS: OK
HEAD_COMMIT: <head_commit from JSON>
SINCE_COMMIT: <since_commit from JSON>
TOTAL_COMMITS: <number of commits>

---PROPOSAL---

Found {n} commits since last sync ({since_commit}).

**Proposed Entries:**

**Major Changes ({count}):**
1. `{hash}` - {proposed description}
   - Reasoning: {why this is a major change}

**Added ({count}):**
1. `{hash}` - {proposed description}
   - Reasoning: {why categorized as Added}

**Changed ({count}):**
...

**Fixed ({count}):**
...

**Removed ({count}):**
...

**Filtered Out ({count}):**
- `{hash}` - "{original message}" -> {reason for filtering}

---

**Low-Confidence Categorizations:** (warning)
- `{hash}` - Categorized as {category}, but could be {alternative}
  - Uncertainty: {explanation of ambiguity}

---

Would you like me to:
1. Adjust any categorizations?
2. Rephrase any entry descriptions?
3. Include or exclude any commits?
```

Only include category sections that have entries. Omit empty categories.
Only include the Low-Confidence section if there are low-confidence entries.
