commit
Split the working-tree changes into atomic commits, preflight and push them; also rebase, squash and trace history. Add --no-push to the request when the user only asked to commit. Use when committing
SKILL.mdopencode/ac/skills/commit/SKILL.md
GitHub<!-- ported-from: plugins/ac/skills/commit/SKILL.md sha256:ff95ff15947b87d3b074a15f4977d3afdc283ffcb1181ffbba16be58276ccfb8 -->
Phase 2: Preflight (skip if
commit
Commit the working tree as atomic commits in the repo's own style, then push. Rebase, squash and history tracing live in two references that load only when the request asks for them.
The request text is the input; this skill takes no other arguments.
Phase 0: Parse the request and pick the mode
--skip-preflightin the request text setsSKIP_PREFLIGHT.--no-pushin the request text setsNO_PUSH. Also setNO_PUSHwhen.ac/state/active-auto.jsonexists withharness: "opencode"and asession_idequal to theroot_session_idtheac_sessiontool returns: that is anac/autoaway run, which publishes only from its own close, after its verification gate, and only what the user named. A push from here would land before the gate has judged the work.NO_PUSHholds in every mode.- Mode, from the request:
- COMMIT (the default): Phases 1 to 5 below.
- REBASE (rebase, squash, autosquash, reorder, split a commit, cherry-pick, update the branch): read
references/rebase.mdand follow it instead of Phases 1 to 5. - HISTORY (when was X added, who changed this, blame, bisect, history of a file): read
references/history.mdand follow it instead of Phases 1 to 5.
- Other tokens are a free-form note. When they name files or one unit of work, plan only those.
Phase 1: Context
Run these with the shell tool, in parallel; together they are the whole git picture, so do not gather it again.
- Branch:
git branch --show-current - Status:
git status --short - Staged stat:
git diff --staged --stat - Unstaged stat:
git diff --stat - Last 30 commits:
git log -30 --pretty=format:"%h %s" - Upstream:
git rev-parse --abbrev-ref @{upstream} 2>/dev/null || echo "NO_UPSTREAM" - Unpushed commits:
git log --oneline @{upstream}..HEAD 2>/dev/null || git log --oneline $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null)..HEAD 2>/dev/null || echo "NO_BASE"
A clean status means stop with Nothing to commit. Working tree clean. and skip every remaining phase.
Phase 2: Preflight (skip if SKIP_PREFLIGHT)
Catch breakage before writing history. The execute skill's Phase 4 sets the flag because its Phase 2d checked each wave's touched files and ran the project-wide typecheck, and its Phase 3a ran the build, the full suite and the linter; another pass here would be the same commands on the same tree.
- Find the typecheck, test and lint commands in
CLAUDE.mdandCLAUDE.local.md(sections namedcommands,scriptsor "Build / Test / Lint"), else thepackage.jsonscriptsblock. Use the project's own commands, not generic guesses. - Run typecheck, then tests, then lint, each through the
shelltool. - On a failure: stop, print which step failed and the first ~30 lines of its output, stage nothing, commit nothing. The user fixes it and asks again.
- No commands found: print
Preflight: no commands found in CLAUDE.md or package.json; skipping.and continue.
Phase 3: Style and plan (blocking output)
Style. Classify the 30 subjects from Phase 1:
| Style | Pattern | Example |
|---|---|---|
SEMANTIC |
^(feat|fix|chore|refactor|docs|test|ci|style|perf|build)(\(.+\))?: |
feat: add login |
PLAIN |
description with no prefix, more than 3 words | Add login page to the web app |
SENTENCE |
full past-tense sentence | Implemented the new login flow |
SHORT |
1 to 3 words | format |
Count each subject once, then take the first that holds, in this order: SEMANTIC 15 or more of 30, SENTENCE 15 or more, PLAIN 15 or more, SHORT 10 or more, else PLAIN. Print one line before planning: Style: SEMANTIC (22 of 30), e.g. "feat(auth): add token refresh".
Atomic units. One commit per unit that can be reviewed and reverted alone: a function with its test, a migration with the model change that needs it. File count is a hint, not the rule: a rename across callers is one unit in many files, and two files can be two units. Different directories, component types (model, service, view) or concerns (UI, logic, config, test) usually mean separate units; keep files together only when splitting would leave a commit that does not build or cannot be reverted alone. A commit holding two units is a planning bug; split it. Apply this to every change set, not only the first.
Tests go in the same commit as their implementation (test_*.py and *_test.py with *.py, *.test.ts and *.spec.ts with *.ts, __tests__/*.ts with *.ts, tests/*.py with src/*.py).
Plan. Print it before any git commit:
COMMIT PLAN: N files, K commits
1. <message in the detected style>
- path/a.py
- path/a_test.py together because: implementation and its test
2. <message>
- path/b.py
Order by dependency, lowest first: utilities and types, models, services, endpoints, configuration. Check before executing: each commit is one unit, every multi-file commit has its "together because" line, every message matches the style, tests are paired. Replan on any miss.
Phase 4: Commit
Stay on the current branch, with the git identity exactly as git config has it: no -c user.name, -c user.email or --author, and no Co-Authored-By or Signed-off-by trailer naming the operator.
Per commit, in plan order:
git add <files>
git diff --staged --stat
git commit -m "<message>"
A change that completes a commit not yet pushed (its hash is in the "Unpushed commits" list, and the branch is not main or master) may be git commit --fixup=<hash> instead; after all fixups run one GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash <base>, where <base> is the upstream, or the merge base with main/master when there is none. A pushed commit is never a fixup target: rewriting published history is REBASE mode and needs its own request.
Then verify with git status --short and git log --oneline <base>..HEAD.
Phase 5: Push and report
Skip the push when NO_PUSH is set or Phase 1 printed NO_UPSTREAM.
- On
mainormaster, ask first:question({ questions: [{ header: "Push main?", question: "Current branch is `<branch>`. Push directly?", options: [ {label: "Push", description: "Push to <upstream>."}, {label: "Skip push", description: "Leave commits local; user pushes manually."} ] }] }) git push. Plain--forceis never used here.- A rejected push (non-fast-forward) stops the run: print
git log <upstream>..HEADandgit log HEAD..<upstream>and let the user decide.
Then one report block:
## Commit Complete
Branch: <name> [<upstream> | local-only]
Style: <SEMANTIC | PLAIN | SENTENCE | SHORT>
Commits created: <N>
<hash> <message>
Push: <pushed to <remote>/<branch> | skipped (no upstream) | skipped (--no-push) | skipped (user declined)>
Preflight: <PASS | SKIPPED | NOT CONFIGURED>
When the execute skill's Phase 4 started this skill, the report block is the handoff signal that the execution pipeline is done. A conflict during an autosquash means the tree changed under it: print git status, run git rebase --abort and surface it.