diff --git a/.husky/pre-commit b/.husky/pre-commit
index 3f4f8d6..a0abb46 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,18 @@
#!/bin/sh
# Run linting and build to ensure code quality and dist is up to date
+
+# Get list of staged files before linting
+STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
+
+# Run linting (which may modify files)
bun run lint
bun run build
+
+# Re-stage any files that were modified by linting
+if [ -n "$STAGED_FILES" ]; then
+ echo "$STAGED_FILES" | while IFS= read -r file; do
+ if [ -f "$file" ]; then
+ git add "$file"
+ fi
+ done
+fi
diff --git a/README.md b/README.md
index 9a5f644..aa6e1df 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# commitment
-> AI-powered commit message generator
+> AI coding assistant-powered commit message generator
@@ -10,18 +10,18 @@
We all know we should write better commit messages. But we don't.
-**commitment** analyzes your git diffs using AI and generates professional, conventional commit messages automatically.
+**commitment** analyzes your git diffs using your favorite AI coding assistant and generates professional, conventional commit messages automatically.
## Why commitment?
-- **No API Keys**: Uses your local AI CLI (e.g. Claude or Codex) to generate commit messages
+- **No API Keys**: Uses your local AI CLI (Claude, Codex, or Gemini) to generate commit messages
- **Consistency**: Every commit follows [Conventional Commits](https://www.conventionalcommits.org/) format
- **Context-aware**: AI understands your changes and adds helpful context
- **Frictionless**: Just add the hook and stop committing `wip2` and `formatting`
## Features
-- 🤖 **AI-powered generation** using your local AI CLI (e.g. Claude or Codex) for accurate, context-aware messages - no extra API keys required!
+- 🤖 **AI-powered generation** using your local AI CLI (Claude, Codex, or Gemini) for accurate, context-aware messages - no extra API keys required!
- 📊 **Code analysis** detects functions, tests, types, and patterns in your changes
- ✨ **Conventional Commits** for a standard format (feat:, fix:, docs:, etc.)
- 🚀 **One-command setup** with `commitment init` for automatic hook installation
@@ -38,6 +38,9 @@ npm install -D @arittr/commitment
# 2. Set up git hooks (automatic)
npx commitment init
+# Or configure with a specific AI agent
+npx commitment init --agent gemini
+
# 3. Make changes and commit
git add .
git commit # Message generated automatically!
@@ -68,6 +71,7 @@ bun add -D @arittr/commitment
- **AI CLI** (one of):
- [Claude CLI](https://docs.claude.com/en/docs/claude-code/overview) (recommended) - Install with `npm install -g @anthropic-ai/claude-code`
- [Codex CLI](https://developers.openai.com/codex/cli) - Install with `npm install -g @openai/codex`
+ - [Gemini CLI](https://geminicli.com/docs/) - Install with `npm install -g @google/gemini-cli`
>[!IMPORTANT]
>commitment requires an AI CLI to function.
@@ -102,6 +106,8 @@ Use a specific AI agent:
```bash
npx commitment --agent codex
+# or
+npx commitment --agent gemini
```
## How It Works
@@ -138,7 +144,7 @@ test: update test naming conventions and mock patterns
| Option | Description | Default |
|--------|-------------|---------|
-| `--agent ` | AI agent to use (`claude` or `codex`) | `claude` |
+| `--agent ` | AI agent to use (`claude`, `codex`, or `gemini`) | `claude` |
| `--dry-run` | Generate message without creating commit | `false` |
| `--message-only` | Output only the commit message | `false` |
| `--cwd ` | Working directory | current directory |
@@ -156,6 +162,12 @@ commitment supports multiple hook managers:
| **simple-git-hooks** | `npx commitment init --hook-manager simple-git-hooks` | Lightweight alternative to husky |
| **Plain Git Hooks** | `npx commitment init --hook-manager plain` | No dependencies |
+**Configure default agent:**
+```bash
+npx commitment init --agent gemini # Use Gemini by default
+npx commitment init --agent codex # Use Codex by default
+```
+
See [docs/HOOKS.md](./docs/HOOKS.md) for detailed hook integration guide.
## Troubleshooting
@@ -213,9 +225,9 @@ If hooks override your messages, please [file an issue](https://github.com/aritt
| Platform | CLI Usage | Hooks | AI Agents |
|----------|-----------|-------|-----------|
-| macOS | ✅ | ✅ | ✅ Claude, Codex |
-| Linux | ✅ | ✅ | ✅ Claude, Codex |
-| Windows | ✅ | ⚠️ Git Bash/WSL | ✅ Claude, Codex |
+| macOS | ✅ | ✅ | ✅ Claude, Codex, Gemini |
+| Linux | ✅ | ✅ | ✅ Claude, Codex, Gemini |
+| Windows | ✅ | ⚠️ Git Bash/WSL | ✅ Claude, Codex, Gemini |
> **Note**: Windows users should use Git Bash or WSL for best hook compatibility.
diff --git a/docs/constitutions/v3/architecture.md b/docs/constitutions/v3/architecture.md
index 1e6acdb..e6b6f12 100644
--- a/docs/constitutions/v3/architecture.md
+++ b/docs/constitutions/v3/architecture.md
@@ -91,7 +91,7 @@ Violating these boundaries breaks the architecture and makes the system unmainta
**Responsibility:** Coordinate commit message generation with AI or fallback.
**Allowed:**
-- Instantiate agents directly (no factory)
+- Instantiate agents via factory (simple pattern)
- Decide AI vs fallback
- Construct prompts
- Parse git output
@@ -111,7 +111,7 @@ Violating these boundaries breaks the architecture and makes the system unmainta
**Simplification from v1:**
- No provider chains - just one agent at a time
- No auto-detection - explicit `--agent` flag
-- Direct agent instantiation with simple if/else
+- Simple factory for agent instantiation (replaces v2's if/else, v1's complex factory)
### Agent Module (`src/agents/`)
@@ -124,37 +124,43 @@ Violating these boundaries breaks the architecture and makes the system unmainta
- Use pure utility functions from agent-utils
- Handle agent-specific errors
- Check CLI availability
+- Simple factories (see "Simple Factories" section below)
**Forbidden:**
- Business logic (message generation logic)
- CLI concerns (argument parsing)
- Git operations (delegate to utilities)
- Complex inheritance (>3 extension points)
-- Factories or provider chains
+- Complex factories (chains, auto-detection, state)
**Sub-components:**
- `types.ts` - Agent interface and type definitions
- `base-agent.ts` - Abstract base class with template pattern (~80 LOC)
- `agent-utils.ts` - Pure utility functions (~100 LOC)
+- `factory.ts` - Simple agent factory with ts-pattern (~30 LOC)
- `claude.ts` - Claude agent implementation (~40 LOC)
- `codex.ts` - Codex agent implementation (~60 LOC)
-- `chatgpt.ts` - ChatGPT agent implementation (~50 LOC)
+- `gemini.ts` - Gemini agent implementation (~50 LOC)
- `index.ts` - Agent exports
**Evolution in v3:**
- ✅ Simple base class allowed (BaseAgent with ≤3 extension points)
- ✅ Pure utility functions allowed (stateless helpers in agent-utils)
- ✅ Template method pattern for standard flow
-- ❌ Still banned: factories, provider chains, complex inheritance
+- ✅ Simple factories allowed (single responsibility, pure function, exhaustiveness checking)
+- ❌ Still banned: complex factories, provider chains, complex inheritance
- **Why**: Eliminates 70% duplication while keeping agents readable (~40-60 LOC each)
**Preserved from v2:**
-- No factories (`provider-factory.ts`)
+- No complex factories (v1's `provider-factory.ts` pattern)
- No provider chains
- No auto-detection
-- Direct agent instantiation with simple if/else
- Agent interface unchanged: `{ name, generate() }`
+**Evolution from v2:**
+- v2: if/else for agent selection
+- v3: Simple factory with ts-pattern for type-safe exhaustiveness
+
### Types Module (`src/types/`)
**Responsibility:** Centralize core domain types and schemas.
@@ -287,7 +293,7 @@ const generator = new CommitMessageGenerator({
3. Implement `executeCommand()` extension point
4. Override `cleanResponse()` or `validateResponse()` if needed (optional)
5. Add to `AgentName` type in `src/agents/types.ts`
-6. Update generator's if/else chain in `generateWithAI()`
+6. Update factory in `src/agents/factory.ts` (add new `.with()` case)
7. Export from `src/agents/index.ts`
8. Add tests in `src/agents/__tests__/.test.ts`
@@ -318,14 +324,15 @@ export class MyAgent extends BaseAgent {
}
```
-**Architecture preserved:** Agent interface unchanged, minimal changes to generator.
+**Architecture preserved:** Agent interface unchanged, minimal changes to factory.
**v3 Pattern:**
- ✅ Extend BaseAgent (simple template pattern)
- ✅ Implement only executeCommand() (~20-40 LOC)
- ✅ Inherit availability check, cleaning, validation, error handling
- ✅ Override cleanResponse/validateResponse only if needed
-- ❌ No factories, no chains, no complex inheritance
+- ✅ Update factory with new `.with()` case (type-safe exhaustiveness)
+- ❌ No complex factories, no chains, no complex inheritance
### Adding a New CLI Command
@@ -350,6 +357,61 @@ program
**Architecture preserved:** Commands are isolated, testable modules.
+## Simple Factories
+
+**commitment allows simple factories that meet three criteria.**
+
+### The Three Criteria
+
+A factory is allowed if and only if it meets ALL three:
+
+1. **Single responsibility** - Only instantiation based on discriminator (like AgentName). No chains, no auto-detection, no complex decision trees, no configuration transformation
+2. **Pure function** - No state, no side effects. Takes discriminator and optional config, returns instance
+3. **Exhaustiveness checking** - Uses ts-pattern or TypeScript discriminated unions to ensure compile-time errors if a case is missing
+
+### Allowed Pattern
+
+```typescript
+// src/agents/factory.ts
+import { match } from 'ts-pattern';
+import type { Agent, AgentName } from './types';
+
+export function createAgent(name: AgentName): Agent {
+ return match(name)
+ .with('claude', () => new ClaudeAgent())
+ .with('codex', () => new CodexAgent())
+ .with('gemini', () => new GeminiAgent())
+ .exhaustive(); // ✅ TypeScript error if AgentName updated but case missing
+}
+```
+
+**Why each criterion matters:**
+- Single responsibility → No mixing concerns (instantiation vs configuration vs detection)
+- Pure function → Testable, predictable, no hidden state
+- Exhaustiveness → Compiler catches missing cases when types evolve
+
+**Reasonable config pass-through is allowed:**
+```typescript
+export function createAgent(name: AgentName, options?: AgentOptions): Agent {
+ return match(name)
+ .with('claude', () => new ClaudeAgent(options))
+ .with('codex', () => new CodexAgent(options))
+ .with('gemini', () => new GeminiAgent(options))
+ .exhaustive();
+}
+// ✅ Just forwarding config to constructors
+```
+
+**Complex config logic belongs elsewhere:**
+```typescript
+// ❌ Validation/transformation in factory
+export function createAgent(name: AgentName, rawConfig: unknown): Agent {
+ const validated = validateConfig(rawConfig); // ❌ Belongs in caller
+ const timeout = calculateTimeout(validated); // ❌ Too much logic
+ // ...
+}
+```
+
## Anti-Patterns
### ❌ Circular Dependencies
@@ -477,6 +539,56 @@ export function isCLINotFoundError(error: unknown): boolean {
**The key distinction**: Stateless pure functions (allowed in v3). Stateful utility classes (still banned).
+### ❌ Complex Factories (v1 anti-pattern - still banned in v3)
+
+**Never:**
+```typescript
+// v1 pattern - Factory chains, still banned
+class ProviderFactory {
+ create(config: ProviderConfig): Provider {
+ const primary = this.createPrimary(config);
+ const fallback = this.createFallback(config);
+ return new ProviderChain([primary, fallback]); // ❌ Chaining logic
+ }
+}
+```
+
+```typescript
+// Auto-detection factories - still banned
+export function createAgent(config?: AgentConfig): Agent {
+ // Detect which CLI is available
+ if (await isClaudeAvailable()) return new ClaudeAgent();
+ if (await isCodexAvailable()) return new CodexAgent();
+ throw new Error('No agent available'); // ❌ Detection logic
+}
+```
+
+```typescript
+// Stateful factories - still banned
+class AgentFactory {
+ private lastCreated?: Agent; // ❌ State
+
+ create(name: AgentName): Agent {
+ this.lastCreated = match(name)... // ❌ Side effect
+ return this.lastCreated;
+ }
+}
+```
+
+**v3 allows simple factories:**
+```typescript
+// v3 pattern - Simple, focused factory
+export function createAgent(name: AgentName): Agent {
+ return match(name)
+ .with('claude', () => new ClaudeAgent())
+ .with('codex', () => new CodexAgent())
+ .with('gemini', () => new GeminiAgent())
+ .exhaustive(); // ✅ Type-safe, single responsibility, pure function
+}
+```
+
+**The key distinction**: Simple instantiation based on discriminator (allowed in v3). Complex logic, chains, or state (still banned).
+
## Testing Architecture
**Unit Tests:** Test each layer in isolation with mocked dependencies.
diff --git a/docs/constitutions/v3/meta.md b/docs/constitutions/v3/meta.md
index 7011b50..0c6e22d 100644
--- a/docs/constitutions/v3/meta.md
+++ b/docs/constitutions/v3/meta.md
@@ -6,26 +6,28 @@
## Summary
-Evolution from v2's "no abstraction" to "selective abstraction" philosophy. Allows simple base classes and pure utility functions while maintaining v2's prohibition on factories, chains, and complex inheritance. Recognizes that v2's blanket rejection of abstraction was too extreme.
+Evolution from v2's "no abstraction" to "selective abstraction" philosophy. Allows simple base classes, pure utility functions, and simple factories while maintaining v2's prohibition on complex factories, chains, and complex inheritance. Recognizes that v2's blanket rejection of abstraction was too extreme.
## Rationale
This v3 constitution relaxes v2's strict "no abstraction" rule to allow beneficial patterns while preserving simplicity:
-1. **Selective Abstraction Philosophy**: v2 correctly removed v1's over-engineering (factories, provider chains, auto-detection), but went too far by banning ALL abstraction. Real-world usage revealed ~70% code duplication across agents. v3 permits **simple** abstraction (base classes with <3 extension points, pure utility functions) while maintaining v2's prohibition on **complex** abstraction (factories, chains, deep inheritance hierarchies).
+1. **Selective Abstraction Philosophy**: v2 correctly removed v1's over-engineering (complex factories, provider chains, auto-detection), but went too far by banning ALL abstraction. Real-world usage revealed ~70% code duplication across agents. v3 permits **simple** abstraction (base classes with ≤3 extension points, pure utility functions, simple factories) while maintaining v2's prohibition on **complex** abstraction (factory chains, auto-detection, deep inheritance hierarchies).
2. **BaseAgent Pattern**: Agent implementations duplicated ~70% of code (availability checks, response cleaning, validation, error handling). A simple template method pattern with exactly 3 extension points eliminates duplication without reintroducing v1's complexity. Agents remain readable (~40-60 LOC each) with all core logic visible.
3. **Pure Utility Functions**: Shared logic (response cleaning, commit validation, error detection) extracted to stateless, focused functions. These are genuinely reusable helpers, not the complex utility modules v1 had (cli-executor, cli-response-parser with state and side effects).
-4. **Why Now**: The proactive refactor (spec 57d322) revealed the abstraction sweet spot. v1 had too much (factories, chains, complex inheritance). v2 had too little (70% duplication). v3 finds the balance: simple templates + pure functions, no factories/chains.
+4. **Simple Factories**: The Gemini agent addition revealed that if/else chains for agent selection were becoming repetitive. A simple factory using ts-pattern provides type-safe exhaustiveness checking (compiler error if AgentName updated but factory not updated) while staying focused on pure instantiation. The three criteria (single responsibility, pure function, exhaustiveness checking) prevent v1's complex factory anti-patterns (chains, auto-detection, state).
-5. **Principles Preserved from v2**:
- - ✅ No factories (agents still instantiated with simple if/else)
+5. **Why Now**: The proactive refactor (spec 57d322) and Gemini addition revealed the abstraction sweet spot. v1 had too much (complex factories, chains, complex inheritance). v2 had too little (70% duplication, repetitive if/else). v3 finds the balance: simple templates + pure functions + simple factories, no chains/auto-detection.
+
+6. **Principles Preserved from v2**:
+ - ✅ No complex factories (v1's factory chains and auto-detection still banned)
- ✅ No provider chains (one agent at a time)
- ✅ No auto-detection (explicit `--agent` flag)
- ✅ Agents remain readable end-to-end
- - ✅ Direct instantiation, simple configuration
+ - ✅ Simple instantiation, simple configuration
## What Changed from Previous Version
@@ -34,28 +36,31 @@ This v3 constitution relaxes v2's strict "no abstraction" rule to allow benefici
v2 stated:
> - No base classes (`BaseCLIProvider`, `BaseAPIProvider`)
> - No shared utilities (`cli-executor`, `cli-response-parser`)
+> - No factories (use if/else for agent selection)
> - Each agent is standalone with all logic inline
v3 allows:
> - ✅ Simple base classes (≤3 extension points, template method pattern)
> - ✅ Pure utility functions (stateless, no side effects)
-> - ❌ Still banned: factories, provider chains, complex inheritance (>3 extension points)
+> - ✅ Simple factories (single responsibility, pure function, exhaustiveness checking)
+> - ❌ Still banned: complex factories, provider chains, complex inheritance (>3 extension points)
**New Patterns:**
- Template Method Pattern - BaseAgent with 3 extension points (executeCommand, cleanResponse, validateResponse)
- Pure Utility Functions - agent-utils.ts with stateless helpers
+- Simple Factory Pattern - createAgent() with ts-pattern for type-safe exhaustiveness
- CLI Helper Extraction - display/execution helpers for improved testability
**Architecture Updates:**
- `src/agents/base-agent.ts` - Abstract base class (~80 LOC)
- `src/agents/agent-utils.ts` - Pure utility functions (~100 LOC)
+- `src/agents/factory.ts` - Simple agent factory (~30 LOC)
- `src/cli/helpers.ts` - CLI display/execution helpers (~80 LOC)
- Agent implementations reduced from ~200+ LOC → ~40-60 LOC each
**What Stayed the Same:**
- Agent interface unchanged
-- Generator instantiation unchanged (same if/else pattern)
-- No factories, no chains, no auto-detection
+- No complex factories, no chains, no auto-detection
- Agents remain readable end-to-end
- Layer boundaries preserved
@@ -133,7 +138,7 @@ export class ClaudeAgent extends BaseAgent {
This version documents the evolution:
**v1 - Over-Abstraction** (2,500+ LOC):
-- ❌ Factories for every agent type
+- ❌ Complex factories with chains and auto-detection
- ❌ Provider chains with fallbacks
- ❌ Auto-detection system
- ❌ Complex base classes with many abstract methods
@@ -150,8 +155,8 @@ This version documents the evolution:
**v3 - Selective Abstraction** (1,360 LOC - **current**):
- ✅ Simple template base class (≤3 extension points)
- ✅ Pure utility functions (stateless helpers)
+- ✅ Simple factory with ts-pattern (type-safe exhaustiveness)
- ✅ Extracted CLI helpers for testability
-- ✅ Simple if/else instantiation (no factories)
- ✅ Agents readable at ~40-60 LOC each
- **Solution**: Balance between DRY and simplicity
@@ -162,11 +167,12 @@ Use this to decide when to abstract in future:
**✅ Good Abstraction (v3 allows):**
- Template method pattern with ≤3 extension points
- Pure utility functions (no state, no side effects)
+- Simple factories (single responsibility, pure function, exhaustiveness checking)
- Stateless helper modules for display/formatting
- **Test**: "Does this reduce duplication >50% while keeping code readable?"
**❌ Bad Abstraction (still banned):**
-- Factories (use direct instantiation)
+- Complex factories (chains, auto-detection, state)
- Provider/agent chains (use simple fallback)
- Auto-detection systems (use explicit configuration)
- Complex inheritance (>3 extension points)
diff --git a/src/cli.ts b/src/cli.ts
index f810e8e..a52d3c8 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -126,7 +126,7 @@ async function main(): Promise {
.command('init')
.description('Initialize commitment hooks in your project')
.option('--hook-manager ', 'Hook manager to use: husky, simple-git-hooks, plain')
- .option('--agent ', 'AI agent to use: claude, codex, gemini (default: claude)')
+ .option('--agent ', 'Default AI agent for hooks: claude, codex, gemini')
.option('--cwd ', 'Working directory', process.cwd())
.action(
async (options: {
@@ -134,8 +134,15 @@ async function main(): Promise {
hookManager?: 'husky' | 'simple-git-hooks' | 'plain';
agent?: 'claude' | 'codex' | 'gemini';
}) => {
+ // Parse --agent manually from process.argv due to Commander.js subcommand option conflict
+ const agentFlagIndex = process.argv.indexOf('--agent');
+ const agentValue =
+ agentFlagIndex >= 0 && agentFlagIndex < process.argv.length - 1
+ ? (process.argv[agentFlagIndex + 1] as 'claude' | 'codex' | 'gemini')
+ : undefined;
+
await initCommand({
- agent: options.agent,
+ agent: agentValue,
cwd: options.cwd,
hookManager: options.hookManager,
});
diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts
index 054a771..5dd7a08 100644
--- a/src/cli/commands/init.ts
+++ b/src/cli/commands/init.ts
@@ -277,6 +277,9 @@ export async function initCommand(options: InitOptions): Promise {
// Print next steps
console.log('');
console.log(chalk.green('🎉 Setup complete!'));
+ if (options.agent !== undefined) {
+ console.log(chalk.cyan(` Default agent: ${options.agent}`));
+ }
console.log('');
console.log(chalk.cyan('Next steps:'));
console.log(chalk.white(' 1. Stage your changes: ') + chalk.gray('git add .'));