Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
dcab543
Allow pathspec commits and list staged paths
rafaeelricco Aug 12, 2026
5929756
Add split command for staged multi-commit plans
rafaeelricco Aug 12, 2026
bbb85d0
Add optional split_commits config defaulting to false
rafaeelricco Aug 12, 2026
a9a74f8
Require should_split on parsed split plans
rafaeelricco Aug 12, 2026
99c4e1b
Ask split prompts for JSON with should_split
rafaeelricco Aug 12, 2026
60a4c25
Generate split plans at minimum model effort
rafaeelricco Aug 12, 2026
9d21694
Prompt for auto-split during setup
rafaeelricco Aug 12, 2026
4c8390b
Route commit through auto-split analysis
rafaeelricco Aug 12, 2026
f511204
Document auto-split setup and commit split behavior
rafaeelricco Aug 12, 2026
38d3e0b
Collapse generateContentWithOpenAI mock onto one line
rafaeelricco Aug 12, 2026
258b03e
Commit split groups from the index not the worktree
rafaeelricco Aug 13, 2026
9864887
List both sides of staged renames
rafaeelricco Aug 13, 2026
b3dd3af
Unblock typecheck on the split-plan decoder
rafaeelricco Aug 13, 2026
f2dde89
Collapse no-split plans to one commit
rafaeelricco Aug 13, 2026
09a484b
Strip only the trailing output instructions from split prompts
rafaeelricco Aug 13, 2026
8258b71
Parse split JSON from the should_split object
rafaeelricco Aug 13, 2026
20593b2
Keep regenerate on the single-message flow
rafaeelricco Aug 13, 2026
9e2bc1b
Lower split-plan validation below the complexity cap
rafaeelricco Aug 13, 2026
24b5cc8
Isolate pathspec commits on unborn branches
rafaeelricco Aug 13, 2026
8be61f8
Fix isolated commits for hooks and dirty unborn trees
rafaeelricco Aug 13, 2026
945caf2
Keep later split groups out of hook restages
rafaeelricco Aug 13, 2026
fd5e01a
Reject isolated commits during in-progress merges
rafaeelricco Aug 13, 2026
6db2fcb
Hide unstaged selected edits while hooks run
rafaeelricco Aug 13, 2026
10fc7ef
Snapshot only dirty index paths for hook isolation
rafaeelricco Aug 13, 2026
8f85fe7
Harden hook snapshots and literal pathspecs
rafaeelricco Aug 13, 2026
0f702af
Run isolated pre-commit hooks outside git commit
rafaeelricco Aug 13, 2026
997055c
Keep file-to-directory replacements visible to hooks
rafaeelricco Aug 13, 2026
4a738fc
Reset hook-foreign index paths from a NUL pathspec file
rafaeelricco Aug 13, 2026
fe57e84
Restore hook-foreign deletions after isolated pre-commit
rafaeelricco Aug 13, 2026
58c247e
Keep later descendants staged after ancestor file-to-directory commits
rafaeelricco Aug 13, 2026
67fcd75
Reset exact index entries and keep case-only rename targets
rafaeelricco Aug 13, 2026
0ae8c88
Restore HEAD blobs as exact index entries during isolate
rafaeelricco Aug 13, 2026
e87d5eb
Fix isolated commit empty groups and hook execution
rafaeelricco Aug 13, 2026
55f082b
Fall back when git hook run is unavailable
rafaeelricco Aug 13, 2026
cbea34d
Run the old-Git pre-commit fallback through sh
rafaeelricco Aug 13, 2026
306a2c0
Honor hook shebangs on the old-Git fallback
rafaeelricco Aug 13, 2026
049cc4f
Dispatch Windows fallback hooks from their shebang
rafaeelricco Aug 13, 2026
ec8d576
Extract routeAnalysis for split vs single commit plans
rafaeelricco Aug 13, 2026
dd2970f
Remove standalone `commit split` command
rafaeelricco Aug 13, 2026
109ee97
Remove manual Split action from generate flow
rafaeelricco Aug 13, 2026
55665b0
Annotate split test metadata as LlmRequestMetadata
rafaeelricco Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ You will be prompted to choose:
- Anthropic: Claude setup-token or API key
- xAI: Sign in with Grok or API key
- **Commit convention**: Conventional, Imperative, or Custom
- **Split commits**: whether `commit` should analyze staged files and open a multi-commit plan when they look independent

If you want to use your claude.ai subscription with Anthropic, run `claude setup-token` in another terminal first, then paste the generated setup-token during `commit setup`.

Expand Down Expand Up @@ -160,6 +161,8 @@ Or explicitly:
commit generate
```

With split enabled in setup, `commit` analyzes staged files and opens a multi-commit plan when they look independent.

### System Checks

Verify your installation, environment, and configuration:
Expand Down
68 changes: 58 additions & 10 deletions src/cli/commit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { Commit };
export { Commit, routeAnalysis, type AnalysisRoute };

import * as p from "@clack/prompts";
import * as pr from "@/infra/github/pr";
Expand All @@ -7,10 +7,21 @@ import * as repo from "@/infra/git/repo";
import { Future } from "@/libs/future";
import { loadConfig } from "@/infra/storage/config";
import { Setup } from "@/cli/setup";
import { Split } from "@/cli/split";
import { type CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config";
import { resolveProvider } from "@/domain/llm/auth-resolver";
import { generateCommitMessage, refineCommitMessage, type GeneratedContent, type LlmRequestMetadata } from "@/domain/llm/router";
import { Nothing, type Maybe, Just } from "@/libs/maybe";
import {
generateCommitMessage,
generateSplitPlan,
refineCommitMessage,
type GeneratedContent,
type LlmRequestMetadata,
type SplitPlanContent
} from "@/domain/llm/router";
import { type SplitPlan } from "@/domain/split/plan";
import { Nothing, type Maybe, Just, fromOptional } from "@/libs/maybe";
import { Failure, Success, type Result } from "@/libs/result";
import { absurd } from "@/libs/types";
import { loading } from "@/infra/ui/spinner";
import { renderCommitNote, renderPushNote } from "@/infra/ui/push-note";

Expand All @@ -19,6 +30,16 @@ import color from "picocolors";
const USER_ACTIONS = ["commit_push", "commit", "regenerate", "adjust", "cancel"] as const;
type UserAction = (typeof USER_ACTIONS)[number];

type AnalysisRoute = { tag: "split"; plan: SplitPlan } | { tag: "single"; message: string };

const routeAnalysis = (plan: SplitPlan): Result<Error, AnalysisRoute> =>
plan.shouldSplit && plan.commits.length >= 2 ?
Success({ tag: "split", plan })
: fromOptional(plan.commits[0]).unwrap<Result<Error, AnalysisRoute>>(
() => Failure(new Error("Split plan: expected at least 1 commit")),
(first) => Success({ tag: "single", message: first.message })
);

class Commit {
private constructor(
private readonly config: Config,
Expand All @@ -39,14 +60,41 @@ class Commit {
run(): Future<Error, void> {
return repo
.checkIsGitRepo()
.chain(() => this.diff())
.chain((diff) => this.generate(diff, this.config.commit_convention, this.config.custom_template).chain((message) => this.interact(diff, message)))
.chain(() => Future.concurrently({ diff: repo.getStagedDiff(), files: repo.listStagedPaths() }))
.chain(({ diff, files }) => this.route(diff, files))
.mapRej((e) => {
p.log.error(color.red(e.message));
return e;
});
}

private route(diff: string, files: readonly string[]): Future<Error, void> {
return this.config.split_commits && files.length >= 2 ?
loading(
"Analyzing staged changes...",
"Ready!",
generateSplitPlan(this.providerConfig, diff, files, this.config.commit_convention, this.config.custom_template)
).chain((content) => this.followAnalysis(diff, files, content))
: this.generate(diff, this.config.commit_convention, this.config.custom_template).chain((message) => this.interact(diff, message));
}

private followAnalysis(diff: string, files: readonly string[], content: SplitPlanContent): Future<Error, void> {
const { plan, metadata } = content;
return routeAnalysis(plan).either(
(e) => Future.reject(e),
(route) => {
switch (route.tag) {
case "split":
return Split.fromResolved(this.config, this.providerConfig).runPlan(diff, files, route.plan, metadata);
case "single":
return this.interact(diff, { text: route.message, metadata });
default:
return absurd(route, "AnalysisRoute");
}
}
);
}

diff(): Future<Error, string> {
return repo.getStagedDiff();
}
Expand Down Expand Up @@ -123,11 +171,11 @@ class Commit {
const action = await p.select({
message: "What would you like to do?",
options: [
{ value: "commit_push", label: "Commit & Push" },
{ value: "commit", label: "Commit" },
{ value: "regenerate", label: "Regenerate" },
{ value: "adjust", label: "Adjust" },
{ value: "cancel", label: "Cancel" }
{ value: "commit_push" as const, label: "Commit & Push" },
{ value: "commit" as const, label: "Commit" },
{ value: "regenerate" as const, label: "Regenerate" },
{ value: "adjust" as const, label: "Adjust" },
{ value: "cancel" as const, label: "Cancel" }
]
});

Expand Down
17 changes: 15 additions & 2 deletions src/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type SetupPreferences = {
readonly customTemplate: string | undefined;
readonly provider: ProviderConfig["provider"];
readonly authMethod: "google_oauth" | "openai_oauth" | "xai_oauth" | "api_key" | "anthropic_setup_token";
readonly splitCommits: boolean;
};

class Setup {
Expand Down Expand Up @@ -67,6 +68,16 @@ class Setup {
customTemplate = template;
}

const splitCommits = await p.select({
message: "Analyze staged files and split them when they look independent?",
options: [
{ value: true, label: "Auto — split when needed (recommended)" },
{ value: false, label: "No — always one commit" }
],
initialValue: true as const
});
if (p.isCancel(splitCommits)) throw new Error("Setup cancelled");

const authMethod = await p.select({
message: "Select authentication method:",
options: getAuthMethodOptions(provider),
Expand All @@ -79,7 +90,8 @@ class Setup {
convention: convention,
customTemplate,
provider: provider,
authMethod: authMethod
authMethod: authMethod,
splitCommits
});
});
}
Expand All @@ -103,7 +115,8 @@ class Setup {
return {
ai,
commit_convention: this.preferences.convention,
custom_template: this.preferences.customTemplate ? Just(this.preferences.customTemplate) : Nothing()
custom_template: this.preferences.customTemplate ? Just(this.preferences.customTemplate) : Nothing(),
split_commits: this.preferences.splitCommits
};
}

Expand Down
Loading