commit
Requires: the sibling
protocolsskill (shared protocol masters); usesskills.config.jsonwhen present. Missing protocols → tell the user to install the full supermodo package.
One line. Conventional Commits (conventionalcommits.org/en/v1.0.0). The diff is
the source of truth; the message is its index entry — written for someone
scanning git log --oneline a year from now.
The message is always the deliverable. Running the commit is an opt-in extra the skill offers after the message exists — never the default, never silent.
The project's process (read FIRST)
Before reading the diff, read .supermodo/rules/commit.md if it exists. It IS
the commit process for this project — sequence, message format, trailers — and
it replaces the defaults below wherever the two describe the same thing. Then
read .supermodo/rules/INDEX.md if present and load only the cross-cutting
files whose applies-to names commit (typically vcs.md). Open nothing else
in that folder. Contract: ../protocols/references/rules.md.
Absent file → the first-use gate, folded into the consent gate that already
exists in "Offer to commit" — never a separate interruption. Absence alone is
not first run: read .skills/supermodo/config-manifest.json and gate only
when the file is absent AND rulesDeclined does not name "commit", or a user
who declined once is asked again on every commit forever.
What NEVER comes from that file, in any project: never commit secrets; never
run git without the explicit yes or the configured auto policy; git init asks
even in auto mode; a case-(c) scope mismatch always halts for an explicit
answer; explicit paths only; the staged-diff preview before the commit line;
single-quoted -m. Those are invariants — they are not in the file, so they
cannot be removed from it.
Every invocation delivers all four, in order — none is optional:
- the message (clipboard + printed);
- the changelog fragment WRITTEN to disk (default ON — see the fragment section for the only skip conditions);
- the exact command plan printed in one fenced block;
- ONE consent question under that plan.
Stopping after the message alone is an incomplete run; running git without the yes (or a configured auto policy) is a violation.
Read the changes
FIRST, before any branching below: files in <changelog.dir>/ (default
changes/) whose scope frontmatter matches paths in the current
uncommitted change are pending-fragment METADATA of that change, not part
of it — exclude them from the substantive scope in EVERY case (staged,
unstaged, no-HEAD, no-repository); they never drive the message or a
mixed-concern split, and they route to the fragment section's
reuse/replace check.
git rev-parse --is-inside-work-tree— not a repository? Don't halt. The whole directory is the change: derive the message from the files themselves (read them; survey the tree first if many), skip steps 1–4 below (they require a repository), and follow the no-repository path when offering to commit (below).git status --porcelain— what's staged, unstaged, untracked.- Anything staged → describe ONLY the staged changes (
git diff --cached). Staged is what the commit will contain; unstaged noise is not your business. - Nothing staged → describe all uncommitted work:
git diff HEADplus untracked files (read them if small,--statview if large). Repo with no commits yet (HEADunresolvable) →git diff HEADfails and everything is untracked: read the files directly, same as step 0. git log --oneline -15— absorb the repo's vocabulary and format habits (skip when there is no history). Also settle what happened to any plan you proposed earlier. The user may have run it themselves, committed by hand, or changed the scope — git is the only witness. NEVER state from memory that a commit "is still pending" or "was never made": a proposal that went unanswered in chat is not evidence of an unchanged repository. CompareHEADand the working tree with what that plan would have produced; if the work is in, say so and move on to what remains. Getting this wrong tells the user their work vanished.- Session context tells you intent; the diff tells you what actually changed. On conflict, the diff wins.
Large diff: read --stat first, then open only files whose purpose the stat
doesn't reveal.
Format
<type>(<scope>)!: <imperative description>
The type vocabulary is the project's — vcs.commit.types in
skills.config.json, defaulting to feat, fix, refactor, perf, docs, test,
chore, build, ci, style, revert. release derives the semver bump from that
same key, so the two skills can never disagree about what a type means. Never
invent a type that is not in the configured list.
Choosing among them is the judgement this skill contributes:
- feat = a new capability, fix = wrong behavior corrected, refactor = same behavior, new shape. Choose by what the change does, not where it lives.
- Scope: diff confined to one package/app/area → its short name
(
packages/data→data,apps/dashboard→dashboard). Multiple areas → omit scope. Never invent junk scopes like (core) or (misc) — an uninformative scope is worse than none. - Breaking marker (
vcs.commit.breakingMarker, default!) goes after type/scope when the change breaks consumers: removed or renamed public API, changed behavior callers rely on, schema/format change. - Description: imperative mood ("add", "fix", "remove" — not "added",
"adds"), lowercase start (acronyms and proper nouns keep their caps), within
vcs.commit.subjectSoftCapwhere possible (default 50), hard capvcs.commit.subjectHardCap(default 72), no trailing period. - Language: English. Reuse the project's own vocabulary from the diff and the log — package names, task IDs (MC-1, RA-14), domain terms (materialize, watermark). The message should read like the team wrote it.
Message shape is the project's
Whether a message is one line or carries a body, and what may never appear in
it, come from .supermodo/rules/commit.md — from rules-templates/ when the
project has not written one. Both shipped variants are one-line: a breaking
change is carried by the marker, not by prose. Do not add a body unless the
project's rules file says to.
Quoting — single quotes, always
Every git commit -m this skill prints or runs wraps the message in SINGLE
quotes:
git commit -m 'feat(api)!: rename /v1/orders to /v1/checkout'
Inside DOUBLE quotes an interactive shell (bash, zsh, and the terminals built
on them) reads ! as a history expansion and refuses the line — so the
messages that carry a breaking change are exactly the ones that fail when the
user pastes them. Single quotes are literal, and nothing in a Conventional
Commits subject ever needs interpolation.
Corollary: no apostrophe in the description. The imperative lowercase
style never needs one, and the escape that would survive the quoting
(''') costs more than the word is worth.
Mixed-concern diffs
When the diff contains genuinely unrelated changes, produce two outputs:
Default message — one line covering everything honestly. Same type: concatenate with
+(docs: lease spec + entity bridge design). Different types: lead with the dominant type (feat > fix > refactor > perf > docs > test > chore) and+the rest.Split suggestion — after the message, propose the cleaner history. Each suggested commit is a pair of ready-to-run commands —
git addwith its exact files, thengit commit -m— in its own fenced code block so the user can click-copy and run the whole pair. If the session is working in a worktree or a non-main branch checkout (comparegit rev-parse --show-toplevel/pwdwith where the user's shell likely sits), open with acdblock to that path so the commands land in the right tree:Better as 2 commits: ```bash cd /path/to/worktree ``` 1. ```bash git add packages/data/src/watermark.ts git commit -m 'fix(data): guard null watermark' ``` 2. ```bash git add docs/architecture/lease.md git commit -m 'docs: pipeline lease spec' ```Every changed file appears in exactly one suggested commit. Skip this section entirely for single-concern diffs — don't manufacture splits.
Deliver
Both steps, in order — neither substitutes for the other:
Copy to clipboard via the Bash tool (skip silently if
pbcopyunavailable):pbcopy <<'EOF' <message> EOFPrint the message in your FINAL text response, alone in its own fenced code block, so the user sees it and can click-copy it. The pbcopy tool call is invisible to the user — a message that only appears inside the heredoc was never delivered. Then say it's on the clipboard.
A standalone commit writes no report and renders no page — see "What
earns a report" in ../protocols/references/reports.md. The commit IS the
artifact: permanent, diffable, and already the record. A second copy under
.skills/supermodo/ says nothing git show does not, and a browser tab per
commit is an interruption charged against the most frequent action in the
package. A declined commit leaves the message in chat and the fragment in the
working tree, which is exactly where the user needs both.
Unless the commit did not happen and the index moved. That rule rests on
the commit existing; when it does not, its reasoning inverts. The plan runs
git restore --staged and git add lines BEFORE git commit, so a commit
rejected by a hook, a signing failure or any other error leaves a mutated
index, no commit, and — under an unconditional no-report rule — no durable
trace of the message, the exact commands, or which of them ran. That state is
precisely "reasoning and state that exist nowhere else", and losing the
session loses it entirely.
So: write a report whenever any git or index mutation ran without producing
the intended commit — the message, every planned command marked ran or
not-run, the error verbatim, and the resulting index state (git status --porcelain). status: failed. Render it and name the page, because the user
now has a half-staged tree to sort out. Suppress the report only for a clean
success or a message-only run that touched nothing.
Inside a flow run this does NOT apply: stage 8 writes its stage report like
every other stage (below) — there the report is the handoff medium, not a
courtesy copy.
Changelog fragment (default ON)
The model writing the commit is the one holding the full context of what
changed and why — capture the user-facing changelog line NOW, so release
never has to reconstruct it from one-line subjects. Governed by changelog
in skills.config.json (see ../protocols/references/config.md): default
ON, in configured and unconfigured repos alike. Skip ONLY when
changelog.fragments is false or the invocation carries --no-changelog
(also skip for diffs that are pure release bookkeeping — version bump +
changelog edits — a release commit never gets a fragment of itself).
Write ONE fragment file once the message is FINAL — at delivery time for a
message-only run; if the offer flow below later reconciles the scope and
changes the description (step 2), rewrite the fragment from the final
description (delete the stale file — never two fragments for one commit).
Before writing, check <changelog.dir>/ for a pending fragment from a
previous invocation whose scope frontmatter exactly matches the current
substantive paths. A path match only NOMINATES the candidate — paths
don't uniquely identify a change (the user may have abandoned one edit
and started another in the same files). Identity is confirmed only when
the fragment's prose still accurately describes the current diff; then
reuse or replace that one file (stating its path) — never accumulate a
second fragment for an unchanged uncommitted scope. No scope, a
non-matching one, or prose that no longer fits the diff → the fragment is
UNTOUCHABLE: leave it and ask the user. Target, in <changelog.dir>/
(default changes/, created if missing):
changes/<YYYYMMDD-HHMMSS>-<slug>.md
<slug> = kebab-case from the description. Content:
---
bump: patch | minor | major # from the commit type: feat → minor,
# fix/others → patch, `!`/BREAKING → major
# (0.x alpha: breaking → minor)
section: Added | Changed | Fixed | Deprecated | Removed | Security
scope: ["path/a.ts", "path/b.md"] # the substantive files of the change —
# the CANDIDATE key for reuse/replace;
# identity also needs the prose to
# still describe the current diff
---
<1–3 sentences for USERS of the project: what changed for them, not how.
Written in Keep-a-Changelog voice — this text lands in the changelog
almost verbatim at release time.>
The fragment is part of the change: when the agent commits (below), stage it with the commit's files; when the user declines (message only), leave the fragment in the working tree and NAME it in the final response so the user includes it in their manual commit. One commit, one fragment — never retro-write fragments for past commits.
Offer to commit (after the message)
Consent policy comes from confirmations in skills.config.json (see
../protocols/references/config.md). Rule: consent to MUTATE is only ever
asked under a fully shown command plan; a scope-selection question mutates
nothing and merely decides what that plan will contain. In order:
Classify — read-only.
git status --porcelain; nothing runs here:- (a) Message describes the staged diff (the staged-changes path of
"Read the changes"): the index IS the commit — the plan will be the
git commit -m '…'line, plus onegit add -- <fragment-path>when a changelog fragment exists; the user's already-staged paths are NEVER re-added. A mixed-hunk file (staged + unstaged hunks) whose staged hunks the message describes stays in this case: plaingit committakes only the staged hunks, and the plan never re-adds that file. - (b) Index empty, message describes the working tree: the plan
will be
git add <paths>(fragment included) +git commit -m '…'. - (c) Mismatch: the index holds changes the message does NOT
describe — the user's own in-flight work. SELECT SCOPE first: name
the exact paths and ask a CLOSED MENU per the questions protocol
(default and recommended: stop) — keep the index as-is and commit it
(message will be regenerated to describe it) / also include a WHOLE
path (the plan gains its
git add;git addis path-level — first show every unstaged hunk it would capture, and if the user wants only a subset, STOP and let them stage hunks themselves, e.g.git add -p; never present path-levelgit addas selecting hunks) / plan an unstage of the undescribed paths (git restore --staged) / stop. The answer only shapes the plan — no git command runs during selection. Never resolve (c) silently, not even in auto mode.
- (a) Message describes the staged diff (the staged-changes path of
"Read the changes"): the index IS the commit — the plan will be the
Reconcile message + fragment with the final scope. If the chosen scope differs from what the message was derived from — typically after case (c) — regenerate or explicitly reconfirm the message against the final scope, and rewrite the changelog fragment from the final description (one fragment, final slug, stale file deleted). The message must describe the commit that will actually happen.
Show the exact command plan for that scope — EVERY git/index mutation as a literal line in one fenced block: any
git restore --staged <path>, anygit add <path>(explicit paths only, the fragment's exact path among them — nevergit add -A/.), then thegit commit -m '…'(single quotes, per "Quoting" above — the plan is a block the user may paste). Case (a): the fragment add (if any) plus the commit line. (Fragment file operations are not git mutations and live outside the plan: the skill may create, rewrite, or delete ONLY the fragment file this invocation authored, or the ONE prior pending fragment positively identified — per the fragment section's check — as describing this same still-uncommitted change, named by its exact path. Every other workspace file is untouchable.)Ask, under the shown plan. Default
ask: a SIMPLE CONFIRMATION per the question protocol (../protocols/references/questions.md) — one plain line, the default named, no ordered-choice list. Example:Run exactly these commands? No push. (default: no — message only)
First run (no
.supermodo/rules/commit.mdAND no recorded decline — see "The project's process" above): ask the process question HERE, above the command question, in the same message — an ordered choice per../protocols/references/questions.mdlisting the shipped starting points (conventional,issue-prefixed), plus customize and show-full. Materialize throughconfig --rules commit, never by writing the file directly — that procedure owns real-path containment, exclusive-temp writing, validate-before-rename, index regeneration and the manifest record; duplicating five safety steps here would get one of them wrong. Then continue under the new file.confirmations.mode: "auto"does NOT skip this: choosing a process is a class-(c) preference. A decline ("just do it, don't save a file") is recorded asrulesDeclined: ["commit"]and never asked again; the run proceeds on the bundled default.Decline (the default) → done. Message only. Never touch git state. With
confirmations.mode: "auto"(orperSkill.commit: "auto"), skip this question and proceed — the plan is still shown, and a case-(c) scope selection still HALTS for its explicit answer (auto mode never sweeps user changes in silently).Execute the plan verbatim. The explicit yes (which overrides any standing no-commit policy for exactly the shown plan and THIS commit only) or the configured auto policy authorizes the shown lines and nothing else. Run them in order, with one mandatory checkpoint: after the staging/unstaging lines (case (a): after the fragment add) — preview the staged diff (
git diff --cached --stat, then the fullgit diff --cached); anything unexpected → stop and ask before running the commit line. Never amend, neverpush, never force. One commit.
No repository yet (step 0 of "Read the changes"): same order, and
git init is its OWN consent gate — NEVER covered by
confirmations.mode: "auto". Step 3's fenced plan opens with git init,
step 4's question names the init explicitly, and execution stays verbatim:
git init → the staging lines → the staged-diff preview → the commit
line. The yes covers exactly the shown plan; nothing runs without it.
Still never: push, merge, rebase, amend, force-anything, or any git
mutation not covered by the authorization above — the explicit yes or the
configured auto policy (git init and case-(c) scope resolution: explicit
answers only, always).
Flow integration
When invoked by the flow orchestrator, commit is stage 8 (final):
- Diff scope is the flow baseline. Flow records the git status/diff
baseline before stage 1; the message is generated ONLY from flow-owned
changes (including librarian's stage-1/7 doc edits), never from pre-existing
user changes in the tree. Read the baseline from the run state
(
.skills/supermodo/runs/<run-id>/state.json). - Ambiguous overlap → ask, don't guess. If flow-owned changes overlap or interleave with pre-existing user changes (mixed hunks, shared files), stop and ask the user rather than deciding what belongs to the flow.
- Write the stage report per
../protocols/references/reports.mdto.skills/supermodo/runs/<run-id>/08-commit.md(skill: commit,status,summary= the message + whether a commit was made or just proposed). - The ask-to-commit prompt and its safety steps above are unchanged in flow; decline default still means message only.
Examples
Staged diff: rounding bug fixed in invoice totals
✅ fix(billing): round invoice totals half-up
Staged diff: new CSV export module in packages/data with tests
✅ feat(data): add csv export for fact tables
Unstaged diff: crash guard in config loader + unrelated README quickstart
✅ fix: guard empty config on boot + docs quickstart
…followed by a 2-commit split suggestion.