Turn whatever is uncommitted in the working tree into logical, atomic Conventional Commits — but only the files that belong to this branch. The non-obvious value is the out-of-scope guard: multi-worktree and multi-agent setups can leave stray files in the working tree that belong to another branch, and this skill never sweeps them into a commit.
This skill is the single source of truth for the commit-grouping contract. It is invoked two ways:
- Standalone (
/commit) — "commit my WIP nicely." It creates the commits on the current branch and stops. It never pushes, opens a PR, writes a changelog, or touches Linear. - Inside a ship flow (e.g.
/send-it) — the commit step that runs before the lint gate, so the branch is clean before the changelog/PR work begins. The ship flow owns everything after the commits (lint, changelog, push, PR, Linear); this skill owns only the staging decision and the commits.
Configuration
One knob lives in config.json beside this skill (a neutral
config.example.json ships as a template):
| Key | Meaning | Default |
|---|---|---|
baseBranch |
The trunk the branch diff is taken against (origin/<baseBranch>), used to compute the merge base for scope classification. |
"main" |
Throughout this document <base> is the baseBranch value.
When a ship flow delegates to this skill (e.g. /send-it), it may direct the
classification against a different base for that run — for instance send-it's
--base for a stacked PR. Honour the base the caller resolves; fall back to
config.json baseBranch only for a standalone /commit run.
Process
git status --porcelain. If clean, there is nothing to commit — say so and stop.- Inspect the uncommitted files:
git status --porcelainfor the list,git diffandgit diff --cachedfor the hunks. - Filter for branch relevance. Decide which uncommitted files are in scope:
- Compute the merge base:
git merge-base HEAD origin/<base>. - Files the branch has already touched directly:
git diff --name-only <merge-base>...HEAD. - In scope by default: an uncommitted file whose path is in that branch-touched list.
- Out of scope (uncertain): everything else once the branch has its own commits. This deliberately includes a file that merely sits in a directory the branch has touched but is not itself a path the branch changed — a shared directory is not enough to claim a file. A stray file from another branch or worktree can easily land in a directory this branch happens to have edited, and silently sweeping it in is exactly the out-of-scope leak this guard exists to prevent. Treat directory-only matches as out of scope unless the user explicitly confirms them.
- Lint surfaces (developer decision). Separately from scope, flag any
uncommitted change to a lint / format config file (
eslint.config.*,.eslintrc*,.markdownlint*,.prettierrc*/.prettierignore,.yamllint*,.shellcheckrc,.github/actionlint.yaml,eslint-suppressions.json) and any ignore / disable directive the diff adds (eslint-disable*,markdownlint-disable*,# yamllint disable,prettier-ignore,shellcheck disable=). These are the developer's call, perreferences/lint-surfaces.md. Never author one yourself to get a commit through a hook. When one is already in the working tree, it may be deliberate, so don't revert it, and don't stage it silently either. - Fresh branch (no commits of its own yet): there is no branch-touched list to diff against, so nothing distinguishes your own work from a stray file left by another branch or worktree. Do not auto-promote every uncommitted file to in scope — that is exactly the leak this guard exists to catch. Treat all uncommitted files as uncertain and have the user confirm which belong before staging any of them.
- Compute the merge base:
- Show the user the staging plan: in-scope files grouped by proposed commit, plus an explicit list of out-of-scope / uncertain files flagged as "uncertain — possibly from another branch/worktree," plus any lint-surface changes from step 3, each named with its file (and the directive, for an ignore), as "developer decision — confirm." Ask: "Stage in-scope files and create the commits below? (yes / no / customise)". Uncertain files are never staged automatically. The confirmation covers the listed lint-surface changes too: the plan has named them, so the developer has seen them before they are committed. On a fresh branch the uncertain list is every uncommitted file (step 3): ask the user to confirm which belong, and treat only the confirmed files as in scope.
- Group in-scope files into logical atomic commits:
- One commit per coherent unit (a feature, a bug fix, a refactor, a docs change, a tooling tweak). Don't bundle unrelated edits.
- Use Conventional Commits subjects (
feat:,fix:,chore:,docs:,refactor:,perf:,test:), with a scope when one is obvious (feat(commit): …). - Keep the header (the entire first line:
type, optional(scope), optional!,:, then the subject text) at 72 characters or under. That is Git's usual subject ceiling; it is not part of the Conventional Commits spec.feat(commit): add out-of-scope guardcounts the type, scope, colon and space — not just the text after the colon. If a natural header would overflow, rewrite until it fits: prefer dropping an optional scope before losing meaning. Do not ellipsis-truncate mid-word, never drop the type prefix, and do not wrap the header onto a second line to dodge the cap (that would make a body, not a shorter header). - For a breaking change, mark it honestly: a
!after the type/scope (feat(api)!: …) and/or aBREAKING CHANGE:footer in the body. A ship flow reads the bump signal back out of these commit messages, so the markers must be accurate.
- On confirmation, create the commits with
git add <specific files>(nevergit add -A) andgit commit. Stage only the files named in the plan; out-of-scope files stay in the working tree, untouched. Pass one-mper block to add a body or footer beyond the subject —git commit -m "<subject>" -m "<body>", and for a breaking changegit commit -m "feat(api)!: <subject>" -m "BREAKING CHANGE: <what changed and the migration>"(orgit commit -F <message-file>for a longer body). A baregit commit -m "<subject>"is fine when no body is needed.
If a pre-commit hook reformats files, the commit still succeeds with the formatted content.
Lint surfaces
Changing how a linter is configured, or telling it to look away, is a developer
decision. The rule (the surfaces covered, the preference order, what to report) is
shared with preflight and triage-pr and lives in
references/lint-surfaces.md, vendored from the
repo's lib/lint-surfaces.md. For this skill the staging plan (step 4) is the
natural stopping point: name each lint-surface change there; never stop
mid-commit to ask about one.
Commit-message prose
Author commit subjects and bodies in the consuming repo's documented prose
language. Across this estate that is British English (colour, behaviour,
-ise/-yse). This governs prose only — never identifiers, dependency names, or
upstream API field names.
The Conventional Commits header — the whole first line — must be ≤ 72
characters. Count type, optional scope, optional !, the colon and space,
and the subject. Rewrite a too-long header; do not wrap it into the body to
dodge the cap. Body and footer line length are out of scope here.
Grouping granularity — per-component splitting is parked
/commit groups by intent (one commit per coherent change), not by component
or package boundaries. Per-component atomic-commit splitting (attributing files to
a package and cutting a commit per package) stays parked (R-374) — no estate
repo does per-component path attribution today. Revisit only if a published
multi-package monorepo doing per-component versioning off squash enters the estate.
Arguments
$ARGUMENTS