Commit Split
Commit split helps turn a broad staged change set into smaller, related commits.
Quick Start
From the workstation (recommended)
1# Stage your changes
2git add .
3
4# Open the workstation, jump to compose, press S
5coco ui
6# In compose: press S to open the split overlay
7# Review the plan; press y to apply or r to regenerateThe split overlay is a first-class workstation surface — animated spinner during plan generation, scrollable plan review (j/k / arrows / pgup/pgdn / gg/G), per-branch yank / external-editor / create-PR follow-ups after apply. Just-landed commits get a ▎ accent marker in history for a few seconds after apply so you can see exactly what the operation created.
From the CLI
1# Generate a split plan (non-mutating)
2coco commit split --plan -i
3
4# Or use the shorthand
5coco commit --split -i
6
7# Review the plan, then apply it
8coco commit split --apply -iThe -i flag (or "mode": "interactive" in your config) is required so the plan can be
displayed for review. Without it, the plan is written to stdout which works for scripting
but not for the typical interactive workflow.
Commands
coco commit split --plan
coco commit split --apply
coco commit --split # shorthand for --plan--plan is non-mutating. It reads staged changes, condenses the diffs using the existing
diff parser, builds a staged hunk inventory for modified files, and asks the configured
model to group files or hunks into proposed commits.
--apply starts from a generated plan and creates one commit per group. Whole-file groups
stage the listed files. Hunk groups apply selected staged hunks directly to the index with
git apply --cached, which allows separate commits from different parts of the same file.
Whole-file groups intentionally abort if a planned file also has unstaged or untracked changes, because staging the whole file would otherwise mix unrelated work into a generated commit. Hunk groups do not stage the whole file.
Requirements
Commit split requires a configured AI provider since it uses the model to analyze your
staged changes and propose groupings. Make sure your .coco.config.json has a working
service block:
1{
2 "mode": "interactive",
3 "service": {
4 "provider": "openai",
5 "model": "gpt-4o",
6 "authentication": {
7 "type": "APIKey",
8 "credentials": {
9 "apiKey": "sk-..."
10 }
11 }
12 }
13}Any provider works (OpenAI, Anthropic, Ollama). If you are using
dynamic model routing,
the split plan uses the dedicated commitSplit task model — distinct from the
regular commit task and floored at a stronger model even in cost preference
(gpt-4.1-mini for OpenAI, claude-3-5-sonnet-latest for Anthropic,
qwen2.5-coder:14b for Ollama). The split planner's structured-output
constraints are stricter than free-form commit message generation, so using a
nano-class model leads to repeated validator rejections.
The diff-summary pre-pass still uses the regular commit / summarize model
so the cost increase is bounded to the (single) plan-generation call.
Commit Message Style
Split commits inherit the same prompt-context as a regular coco commit:
- Conventional Commits: when
config.conventionalCommitsistrue(or--conventionalis set), each group's title follows the Conventional Commits 1.0.0 spec —<type>(<scope>)<!>: <subject>, imperative mood, <72 chars. - Commitlint integration: if your project has a commitlint config (
.commitlintrc.*,commitlint.config.*, etc.), its rules are fed into the prompt so each group's title respects them. Auto-detected — no flag required. - Branch name context: when
config.includeBranchNameisn't disabled, the current branch is included as context. - Temperature / model selection: split uses the same LLM service config as
coco commit, but the plan generator routes through the dedicatedcommitSplitdynamic-model task (see Requirements above). The diff-summary pre-pass continues to use the regularcommit/summarizemodel.
A typical conventional-commits plan looks like:
1. feat(widgets): add button and modal source files
2. test(widgets): cover button happy path and edge cases
3. docs(widgets): document widget v2 API and migration path
Safety Rules
- Every staged file must appear exactly once in the plan.
- A file can be assigned either as a whole file or by all of its hunk IDs, but not both.
- If a file is split by hunks, every generated hunk for that file must be assigned exactly once.
- Plan files must match real staged files.
- Plan hunks must match real staged hunk IDs.
- Apply mode unstages the current index, stages one group at a time, applies selected hunks
with
git apply --cached, and uses the existingcreateCommitutility. - Unstaged overlap blocks whole-file apply mode.
- Each commit is HEAD-verified post-creation. If
git commitreturns success without advancing HEAD (a rare but real failure mode — some hook configurations), the operation surfaces a clear error per-group rather than silently swallowing it. - Empty groups are filtered out before apply. Rescue passes can occasionally produce a group with zero claims; those are dropped pre-apply.
Resilient Plan Validation
The split prompt asks the model for a JSON plan matching a strict schema. Real LLM output occasionally drifts — common failure modes include inventing hunk IDs that don't exist (when the staged set is all new files), claiming the same file via both files[] and hunks[], or forgetting a staged file.
Rather than failing the whole operation, the validator runs six in-process rescue passes before giving up:
rescueDuplicateFiles— files listed in more than one group'sfiles[]keep their first occurrence (in plan order) and get dropped from subsequent groups.rescueDuplicateHunks— hunk IDs listed in more than one group'shunks[]keep their first occurrence and get dropped from subsequent groups.rescuePhantomHunks— empty-inventorypath::hunk-1entries get promoted to file-level claims.rescueMixedFiles— files claimed via both modes get their hunks dropped (files-level claim wins).rescueMissingFiles— unclaimed staged files get appended as a synthetic "misc" group.dropEmptyGroups— groups left with no claims after the rescues are filtered out.
Only when the rescued plan still fails validation does the retry loop fire (up to 3 attempts, with the validator's specific complaints fed back as previous_attempt_feedback).
Hunk-Level Limits
Hunk-level apply is intentionally fail-closed. If a generated hunk patch cannot be applied cleanly after earlier split commits have changed the file, the command stops and leaves the remaining working tree changes available for manual review.
Troubleshooting
"No result handler provided for interactive mode"
This was a bug in versions before 0.35 where the split path did not properly wire up
interactive output. Update to the latest version. If you still see it, make sure you are
passing -i or have "mode": "interactive" in your config.
"No staged changes found"
Run git add . (or stage specific files) before running the split command.
Plan quality is poor
Try adding context with --additional:
coco commit split --plan -i --additional "This branch adds auth and updates tests"Or override the commitSplit task model in your config to a stronger model than the
floor — see dynamic model routing:
1{
2 "service": {
3 "model": "dynamic",
4 "dynamicModels": { "commitSplit": "gpt-4.1" }
5 }
6}"Failed to produce a valid commit-split plan after 3 attempts"
You'll only see this error message if you set --strict-split (or strictSplit: true in config). The default behaviour since 0.54.0 is the single-commit fallback described below.
Single-commit fallback (default since 0.54.0)
When the plan generator exhausts its retry budget with an invalid plan (duplicate files, mixed file/hunk modes, unknown files that the six rescue passes can't recover from), Coco no longer throws. It returns a single-group fallback that combines every staged file into one chore: combined commit. This is the default since 0.54.0 (#1005); it stops weaker models from stranding users with a staged worktree and no commit.
The fallback is surfaced unmistakably so you don't mistake it for a real LLM split:
- CLI plan output (
coco commit --split --plan) includes the validator reason in the rationale:Fallback plan — LLM could not produce a valid multi-group split. Reason: unknown files: ghost.ts. - Workstation status line announces it at both plan-ready ("Split planner exhausted retries, showing single-commit fallback…") and apply-success ("Split planner fallback applied (combined commit) …"). The status kind shifts from
successtoinfoso the visual treatment matches the degraded outcome. - Re-roll path stays available: press
rin the workstation overlay to re-run the planner. A different model — or just a different roll on the same model — often recovers.
Opting out of the fallback
For CI gates or scripts that need to fail loudly on broken model output, pass --strict-split (or set strictSplit: true in your config):
coco commit --split --strict-split --plan{
"strictSplit": true
}With strict mode on, the planner throws as it did pre-0.54.0 instead of degrading. This is the right setting for automation that can't safely fall back to a combined commit.
Recovering when the fallback isn't what you wanted
If the fallback fired and you don't want the single combined commit:
- Press
rin the workstation to re-roll the planner (free retry, no restage needed). - Override
commitSplitto a stronger model (snippet above) — even thecostfloor isgpt-4.1-mini, but stepping up togpt-4.1resolves most edge cases. - Reduce the staged set size — splits across 30+ files stress weaker models more than 5–10-file splits.
- Add
--additionalcontext to guide the grouping.