<!--
SPDX-FileCopyrightText: Amolith <amolith@secluded.site>

SPDX-License-Identifier: CC0-1.0
-->

# AGENTS.md

This file provides guidance to AI coding agents when working with code in this repository.

## Development Commands

- **Default workflow**: `just` (runs fmt, lint, staticcheck, test, vuln, reuse)
- **Build**: `just build` (outputs binary as `formatted-commit`)
- **Run during development**: `just run [flags]`
- **Format code**: `just fmt` (uses gofumpt)
- **Lint**: `just lint` (uses golangci-lint)
- **Static analysis**: `just staticcheck`
- **Vulnerability check**: `just vuln` (uses govulncheck)
- **License compliance**: `just reuse` (REUSE specification)
- **Test**: `just test` or `go test ./...`
- **Single test**: `go test -v -run TestName ./...` (no tests exist yet)
- **Update dependencies**: `go mod tidy`

Example usage:

```bash
just run -t feat -m "add validation" -T "Co-authored-by: Name <email>"
```

## Project Purpose

This is a CLI tool that formats git commit messages according to Conventional Commits specification and pipes them directly to `git commit -F -`. It enforces:

1. Subject length: max 50 characters in format `type(scope): message`
2. Body wrapping: strictly 72 columns with hanging indents for bullets/numbered lists
3. Trailer validation: follows git's trailer specification

## Architecture

Multi-file CLI application split by concern:

- **main.go**: Cobra CLI setup, flag definitions, subject validation, orchestration, git command execution
- **trailers.go**: Trailer validation and block building following git's RFC 822 folding specification
- **wrapBody.go**: Body text sanitization and custom word-wrapping with hanging indent support

Dependencies:

- **cobra**: CLI framework for flags and commands
- **fang**: Charmbracelet's execution wrapper (version handling, etc.)
- **bluemonday**: HTML/Markdown sanitization using UGCPolicy
- **Custom word wrapping**: Pure-Go implementation for 72-column wrapping with hanging indents

## Critical Implementation Details

### Subject Validation (50 char limit)

The subject is constructed as `type(scope): message` or `type: message` (when scope is empty). Breaking changes add a `!` after the type/scope like `type(scope)!: message`.

When validation fails, clearly mark where the subject exceeds 50 characters in error output.

### Body Formatting

The body (`-b` flag) processing pipeline:

1. **Sanitization**: `bluemonday.UGCPolicy()` strips dangerous HTML/scripts while preserving basic formatting
2. **Line-by-line processing**: Each line is processed based on its type:
   - **Bullets** (`- ` or `* `): Wrapped with 2-space hanging indent for continuation lines
   - **Numbered lists** (`^\d+\.\s`): Wrapped with hanging indent matching the marker length (e.g., `1. `, `10. `)
   - **Plain text**: Standard word-wrap at 72 columns
   - **Blank lines**: Preserved as-is
3. **Word wrapping algorithm**: Greedy wrapping splits on word boundaries, never mid-word
4. **Hanging indent logic**: For bullets/numbered lists, first line gets the marker, continuation lines get spaces equal to marker width
5. **Spacing**: Body separated from subject by one blank line, from breaking change footer (if present) by one blank line, from trailers by one blank line

Example wrapped bullet:

```
- This is a long bullet point that exceeds 72 characters and will
  be wrapped with proper hanging indent alignment on continuation
  lines.
```

### Trailer Formatting (Critical)

Trailers follow git's specification precisely:

- Format: `Key: value` (newline-delimited pairs)
- No whitespace before or inside the key
- Any number of spaces/tabs allowed between key and separator `:`
- Values can be multiline with continuation lines starting with whitespace (RFC 822 folding)
- The trailer group must be:
  - At the end of input, OR
  - The last non-whitespace lines before a line starting with `---`
  - Preceded by one or more empty/whitespace-only lines

Example valid trailer:

```
Co-authored-by: This is a very long value, with spaces and
  newlines in it.
```

### Flag Nuance

- `-t` / `--type`: Commit type (required) - e.g., `feat`, `fix`, `refactor`
- `-m` / `--message`: Commit message (required) - the description after the colon
- `-s` / `--scope`: Commit scope (optional) - goes in parentheses after type
- `-B` / `--breaking`: String flag for breaking change description (optional) - adds `!` to subject AND creates `BREAKING CHANGE:` footer
- `-b` / `--body`: String flag for commit body text (can use heredoc for multiline)
- `-T` / `--trailer`: Repeatable flag accepting full trailer strings in `Key: value` format (not separate key/value args)
- `-a` / `--amend`: Boolean flag to amend the previous commit instead of creating a new one

Breaking change detail: The `-B` flag value becomes the description in a `BREAKING CHANGE:` footer (with space, per Conventional Commits spec). This footer is distinct from git trailers and allows spaces in the key. It's inserted between the body and trailers.

Trailer format detail: Each `-T` flag takes a complete trailer string like `-T "Assisted-by: Claude Sonnet 4.5 via Crush"`, NOT separate key and value arguments.

### Final Output

The formatted commit message structure:

1. Subject: `type(scope)!: message` (or `type!: message` if no scope)
2. Blank line
3. Body (if `-b` provided)
4. Blank line (if body or breaking change present)
5. `BREAKING CHANGE: description` footer (if `-B` provided)
6. Blank line (if trailers present)
7. Trailers (if `-T` provided)

Example with all components:
```
feat!: restructure config

Improves readability and supports comments

BREAKING CHANGE: Configuration format changed from JSON to TOML.
Migrate by running: ./migrate-config.sh

Assisted-by: Claude Sonnet 4.5 via Crush
```

The formatted commit message must be piped to `git commit -F -` to read from stdin. When the `-a`/`--amend` flag is used, it pipes to `git commit --amend -F -` instead.
