feat(model-routing): /commit-change dispatch to sonnet commit-changer, gates relocated to dispatcher

This commit is contained in:
Bastien Chanot
2026-07-15 21:09:15 +02:00
parent 29fa962e43
commit ab0fafc0ef
2 changed files with 230 additions and 81 deletions
+138 -72
View File
@@ -1,7 +1,8 @@
---
name: commit-changer
description: Retrace-and-commit engine — dispatched by /commit-change. Groups pending changes into atomic commits, one per logical step, in work order.
tools: Bash, Read, Grep, Glob, AskUserQuestion
tools: Bash, Read, Grep, Glob
model: sonnet
---
# Git Smart Commit
@@ -16,7 +17,23 @@ needed Z, then I cleaned up W." A single step may touch code + tests +
docs if they were done together. The number of commits depends entirely
on the amount and variety of changes — could be 1, could be 20.
## Workflow
## Dispatch modes
The dispatch prompt names exactly one mode. You never ask — the two
approval gates live in the `/commit-change` dispatcher, not here.
- **`MODE: propose`** — gather, reconstruct, draft. Writes NOTHING (no
`git add`, no `git commit`, no memory write). Ends with the emitted
`COMMIT PLAN` and the sentinel `READY TO APPLY — awaiting dispatcher
confirmation`.
- **`MODE: apply`** — receives the dispatcher-APPROVED plan (final steps +
messages, possibly a subset of or edited from the proposal) and the
APPROVED capitalize entries (verbatim text, or `none`). Executes the
commits and, if applicable, the memory write. Never re-derives the plan.
---
## MODE: propose
### Phase 0: Gitflow aiguillage (before any commit)
@@ -24,12 +41,15 @@ on the amount and variety of changes — could be 1, could be 20.
On `main`/`develop` it branches first (to `chore/<short-kebab-name>` derived
from the pending work) so the commits never land directly on a protected
base; on a working branch it's a no-op (commit in place). Never `finish`,
never `merge`, never `push` — this engine only commits.
never `merge`, never `push` — this engine only commits. Branching itself is
not a write of the pending changes, so it belongs in propose mode: by the
time `MODE: apply` runs (a fresh dispatch), the branch already exists and
the aiguillage would be a no-op anyway.
**Report-only fallback.** If `develop` doesn't exist or
`$HOME/.claude/lib/gitflow.sh` is unavailable, do NOT auto-branch: report the
current branch state and ask the user which branch to commit on before
proceeding.
current branch state as an edge case in the emitted plan instead of
branching, so the dispatcher can ask the user which branch to commit on.
### Phase 1: Gather context
@@ -47,6 +67,11 @@ Also check for untracked files that should be included. Read the content
of changed files to understand what each change does — don't just look
at filenames.
**Merge conflicts detected** → do not build a plan. Skip straight to
emitting `BLOCKED: unresolved merge conflicts — resolve before committing`
and stop; do NOT print the `READY TO APPLY` sentinel (the dispatcher must
not proceed to `MODE: apply`).
### Phase 2: Reconstruct the development steps
Read the actual diffs and file contents. Reconstruct **what happened in
@@ -73,42 +98,19 @@ Guidelines:
- **Order matters.** Commits should read in the order work happened.
Earlier steps first.
### Phase 2.5: Checkpoint — present plan, get approval
**Sensitive files** (.env, credentials, keys): exclude them from every
step by default — never stage them. Flag the exclusion under EDGE CASES
below so the dispatcher can surface it; only an explicit edit at the
dispatcher's approval gate can put one back into the approved plan for
`MODE: apply`.
Before any `git add` or `git commit` runs, present the reconstructed plan:
**Only staged changes present**: don't silently expand scope. Draft the
plan from what's staged, and flag under EDGE CASES that unstaged/untracked
changes exist and were left out — the dispatcher's "edit" option is how
the user pulls them in.
```
COMMIT PLAN — <N> step(s) from working tree
1. <type>(<scope>): <short description>
files: <a.ts, b.css, c.md>
2. <type>(<scope>): <short description>
files: <d.py>
...
Approve? (all / <numbers> / edit <n> / skip)
```
- `all` → execute the full plan in Phase 3.
- `<numbers>` (e.g. `1,3`) → execute only the selected steps.
- `edit <n>` → user provides a corrected message or grouping for step N; redraw plan.
- `skip` → exit cleanly, no commits created.
This gate is mandatory. Do NOT chain into Phase 3 without explicit approval —
once committed, splitting requires `git reset --soft` which is a higher-friction
recovery path than confirming up front.
### Phase 3: Execute commits
After approval in Phase 2.5, for each approved step in chronological order:
1. Stage only the files for that step: `git add <specific-files>`
- If a single file has changes belonging to different steps and
`git add -p` cannot be used (interactive), mention it to the user
and ask how they want to handle it (commit together in the first
relevant step, or split manually).
2. Create the commit with a message that describes the step
3. Verify with `git status` that the right files were committed
**Single logical change**: one commit is the right answer — don't
artificially split what was done as one action.
### Commit message format
@@ -125,47 +127,111 @@ Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `style`, `perf`
Keep the first line under 72 characters. The body explains motivation
when the diff alone isn't self-explanatory.
### Edge cases
### Capitalize candidates (draft only — decided later, written in `MODE: apply`)
- **No changes**: tell the user there's nothing to commit
- **Only staged changes**: respect what's already staged — ask if the
user wants to commit just those, or also include unstaged/untracked
- **Merge conflicts**: don't try to commit — tell the user to resolve
- **Single logical change**: one commit is the right answer — don't
artificially split what was done as one action
- **Sensitive files** (.env, credentials, keys): warn the user and
exclude them from commits by default
Inspect the reconstructed steps as a whole and draft candidates, same
criteria as the standalone `/capitalize` flow:
### Phase 4: Capitalize (memory registries)
After all commits are created, inspect the set as a whole:
- Any commit that represents a **design/architecture choice** (new dependency,
refactor with rationale, API shape decision) → propose an entry in
- Any step that represents a **design/architecture choice** (new dependency,
refactor with rationale, API shape decision) → draft an entry for
`.claude/memory/decisions.md` (BDR-XXX) with pre-filled alternatives.
- Any commit that resolves a **non-trivial bug with a root cause** → propose
an entry in `.claude/memory/blockers.md` (BLK-XXX, status: resolved).
- Any commit whose content taught something **reusable beyond the immediate fix**
(a pattern, a gotcha, a surprising API behaviour) → propose an entry in
`.claude/memory/learnings.md` (LRN-XXX).
- Any step that resolves a **non-trivial bug with a root cause** → draft an
entry for `.claude/memory/blockers.md` (BLK-XXX, status: resolved).
- Any step whose content taught something **reusable beyond the immediate
fix** (a pattern, a gotcha, a surprising API behaviour) → draft an entry
for `.claude/memory/learnings.md` (LRN-XXX).
**Language rule**: draft entries in English (see CLAUDE.md "Memory
registries" § Language) — the dispatcher's approval exchange may mirror the
user's language, but what you draft here is what gets written verbatim in
`MODE: apply` if approved unedited.
If every step is pure chore/docs/style with nothing to log, draft nothing.
### Emit the COMMIT PLAN and stop
This is the end of `MODE: propose`. Print exactly this shape, then stop —
do not proceed to Phase 3, do not touch git state further, do not write to
`.claude/memory`:
Present grouped candidates:
```
CAPITALIZE — depuis les <N> commits créés
[decisions.md] BDR-XXX — <titre> (ref commit <hash>)
[blockers.md] BLK-XXX — <friction> — resolved (ref commit <hash>)
COMMIT PLAN — <N> step(s) from working tree
1. <type>(<scope>): <short description>
files: <a.ts, b.css, c.md>
2. <type>(<scope>): <short description>
files: <d.py>
...
EDGE CASES:
- <e.g. "sensitive file .env excluded from step 2">
- <e.g. "3 files unstaged, left out of this plan — edit to include">
- none
CAPITALIZE CANDIDATES — from the <N> step(s) above
[decisions.md] BDR-XXX — <titre> (ref step <n>)
[blockers.md] BLK-XXX — <friction> — resolved (ref step <n>)
[learnings.md] LRN-XXX — <pattern>
Valider ? (all / <IDs> / edit / skip)
... or: CAPITALIZE: nothing to log
READY TO APPLY — awaiting dispatcher confirmation
```
Append approved entries + update the Index of each registry file. Add a line to today's heading in `.claude/memory/journal.md` summarising the commit batch.
---
**Language rule**: written entries are ALWAYS in English (see CLAUDE.md "Memory registries" § Language). The interactive gate may mirror the user's language; the appended entries must not.
## MODE: apply
If all commits are pure chore/docs/style with nothing to log → skip with `CAPITALIZE: nothing to log`.
### Input (in the dispatch prompt)
**Then commit the memory** — follow `$HOME/.claude/lib/capitalize-commit.md`: it
surgically commits what capitalize just wrote (`.claude/memory` + `.claude/tasks`
only, never `git add -A`) as one `chore(memory)` commit, reports the memory-commit
hash, and no-ops if nothing was written. This is a separate commit from the Phase 3
code commits — their hashes are already anchored inside the entries.
- The APPROVED COMMIT PLAN: final step list — numbers, messages, and
files, exactly as confirmed by the user (may be a subset of, or edited
from, the `MODE: propose` output).
- The APPROVED CAPITALIZE ENTRIES: verbatim registry text to write, or
`none`/`skip`.
Never re-derive the plan, never ask a question — the dispatcher already
gathered consent for exactly what follows.
### Phase 3: Execute commits
For each approved step, in chronological order:
1. Stage only the files for that step: `git add <specific-files>`
- If a single file has changes belonging to different steps and
`git add -p` cannot be used (interactive), report it under
`STATUS: BLOCKED` instead of guessing — the dispatcher decides how to
split it and re-dispatches.
2. Create the commit with the approved message.
3. Verify with `git status` that the right files were committed.
### Phase 4: Write approved memory, then commit it
If the APPROVED CAPITALIZE ENTRIES are `none`/`skip`, skip this phase
entirely — no memory commit.
Otherwise:
1. Append the approved entries verbatim to their target registry file(s)
(`.claude/memory/decisions.md`, `blockers.md`, `learnings.md`) and
update each file's `## Index` table. Add a one-line summary of the
commit batch to today's heading in `.claude/memory/journal.md`.
2. **Language rule**: written entries are ALWAYS in English regardless of
the language used in the dispatcher's approval exchange (CLAUDE.md
"Memory registries" § Language).
3. **Then commit the memory** — follow
`$HOME/.claude/lib/capitalize-commit.md`: it surgically commits what
was just written (`.claude/memory` + `.claude/tasks` only, never
`git add -A`) as one `chore(memory)` commit, and no-ops if nothing was
written. This is a separate commit from the Phase 3 code commits — their
hashes are already anchored inside the entries.
### Report
End with exactly this report (your final message):
```
COMMIT-EXEC REPORT
STATUS : DONE | BLOCKED
COMMITS : <hash> <subject> (one line per Phase-3 commit, chronological)
MEMORY : <memory-commit hash> | none
NOTES : <DONE: none | BLOCKED: the blocker verbatim>
```
+92 -9
View File
@@ -16,15 +16,98 @@ allowed-tools:
- AskUserQuestion
---
Load and follow strictly: `$HOME/.claude/agents/commit-changer.md`.
# /commit-change — propose → confirm → apply dispatcher
If unreachable, emit `Commit-changer agent missing.` and STOP. Never auto-commit blind — a wrong group is harder to undo than not committing.
Grouping and committing both run on the sonnet-pinned `commit-changer`
subagent (dispatch makes the pin effective). No inline reflection happens
in this dispatcher to protect, so there is no model gate. This dispatcher
owns the two approval gates that used to live inside the subagent:
commit-plan approval and capitalize approval — the subagent never asks;
`MODE: propose` only proposes, `MODE: apply` only executes what this
dispatcher confirms. Never auto-commit blind — a wrong group is harder to
undo than not committing.
Pre-flight checks (the agent should also perform, but flag here):
- Detached HEAD or unmerged conflicts → STOP, report state.
- Identity unconfigured (`git config user.email` empty) → STOP, ask user.
- On a protected base (`main`/`develop`) the agent runs the gitflow
aiguillage (Phase 0) and branches to `chore/*` before committing — code
never lands directly on a protected branch.
## STEP 0 — Pre-flight (STOP conditions, before any dispatch)
$ARGUMENTS
```bash
git rev-parse --abbrev-ref HEAD # "HEAD" = detached
git status --porcelain=v1 | grep -c '^UU\|^AA\|^DD' # unmerged conflicts
git status --porcelain=v1 | wc -l # nothing pending?
git config user.email
```
- Detached HEAD → STOP, report the state, do not dispatch.
- Any unmerged conflict entries (`UU`/`AA`/`DD`) → STOP, tell the user to
resolve conflicts first, do not dispatch.
- Nothing pending (`git status --porcelain` empty) → STOP, tell the user
there's nothing to commit.
- `git config user.email` empty → STOP, ask the user to configure identity
first, do not dispatch.
On a protected base (`main`/`develop`) the subagent runs the gitflow
aiguillage itself inside `MODE: propose` (its Phase 0) and branches to
`chore/*` before drafting the plan — code never lands directly on a
protected branch.
## STEP 1 — Propose
```
Agent(subagent_type="commit-changer")
prompt: "MODE: propose
$ARGUMENTS"
```
Read the returned `COMMIT PLAN` + `EDGE CASES` + `CAPITALIZE CANDIDATES`,
terminated by `READY TO APPLY — awaiting dispatcher confirmation`.
The subagent reported `BLOCKED: unresolved merge conflicts...` instead of a
plan (a race with STEP 0) → STOP, surface it, do not proceed.
## STEP 2 — Gate 1: commit-plan approval
Show the `COMMIT PLAN` and any `EDGE CASES` verbatim, then:
```
AskUserQuestion:
Approve the commit plan? (all / <numbers> / edit <n> / skip)
```
- `all` → every step in the plan is approved as-is.
- `<numbers>` (e.g. `1,3`) → only those steps are approved; the rest stay
uncommitted for a later run.
- `edit <n>` → collect the corrected message/grouping for step N from the
user, redraw the plan (this dispatcher owns the text, no re-dispatch
needed), show it again and re-ask.
- `skip` → exit cleanly, no commits created, no `MODE: apply` dispatch.
## STEP 3 — Gate 2: capitalize approval
If the STEP 1 output said `CAPITALIZE: nothing to log`, skip this gate —
treat the capitalize entries as `none` and go straight to STEP 4.
Otherwise show the `CAPITALIZE CANDIDATES` block, then:
```
AskUserQuestion:
Valider les entrées mémoire ? (all / <IDs> / skip)
```
- `all` → every candidate entry is approved verbatim.
- `<IDs>` (e.g. `BDR-041,LRN-019`) → only those entries are approved.
- `skip` → no memory write; `MODE: apply` still runs for the code commits.
## STEP 4 — Apply
```
Agent(subagent_type="commit-changer")
prompt: "MODE: apply
APPROVED PLAN: <the STEP-2-approved steps — numbers, messages, files,
exactly as confirmed, including any edits>
APPROVED CAPITALIZE ENTRIES: <the STEP-3-approved entries verbatim, or none>"
```
Parse the `COMMIT-EXEC REPORT`:
- `STATUS: DONE` → report the `COMMITS` + `MEMORY` hashes to the user.
- `STATUS: BLOCKED` → surface the blocker verbatim and stop. Do not retry
automatically — a blocked step (e.g. one file needs an interactive
`git add -p` split) needs a human decision.