Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -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
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# commitment

> AI-powered commit message generator
> AI coding assistant-powered commit message generator

<img width="500" height="304" alt="commitment - AI-powered commit messages" src="https://github.com/user-attachments/assets/827862c0-8f1d-4eb0-a989-4ed7f37ea721" />

Expand All @@ -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
Expand All @@ -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!
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -102,6 +106,8 @@ Use a specific AI agent:

```bash
npx commitment --agent codex
# or
npx commitment --agent gemini
```

## How It Works
Expand Down Expand Up @@ -138,7 +144,7 @@ test: update test naming conventions and mock patterns

| Option | Description | Default |
|--------|-------------|---------|
| `--agent <name>` | AI agent to use (`claude` or `codex`) | `claude` |
| `--agent <name>` | 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 <path>` | Working directory | current directory |
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down
132 changes: 122 additions & 10 deletions docs/constitutions/v3/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/`)

Expand All @@ -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.
Expand Down Expand Up @@ -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__/<agent-name>.test.ts`

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading