1<!--
2SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
3
4SPDX-License-Identifier: CC0-1.0
5-->
6
7# AGENTS.md
8
9This file provides guidance to AI coding agents when working with code in this repository.
10
11## Development Commands
12
13- **Default workflow**: `just` (runs fmt, lint, staticcheck, test, vuln, reuse)
14- **Build**: `just build` (outputs binary as `formatted-commit`)
15- **Run during development**: `just run [flags]`
16- **Format code**: `just fmt` (uses gofumpt)
17- **Lint**: `just lint` (uses golangci-lint)
18- **Static analysis**: `just staticcheck`
19- **Vulnerability check**: `just vuln` (uses govulncheck)
20- **License compliance**: `just reuse` (REUSE specification)
21- **Test**: `just test` or `go test ./...`
22- **Single test**: `go test -v -run TestName ./...` (no tests exist yet)
23- **Update dependencies**: `go mod tidy`
24
25Example usage:
26
27```bash
28just run -t feat -m "add validation" -T "Co-authored-by: Name <email>"
29```
30
31## Project Purpose
32
33This is a CLI tool that formats git commit messages according to Conventional Commits specification and pipes them directly to `git commit -F -`. It enforces:
34
351. Subject length: max 50 characters in format `type(scope): message`
362. Body wrapping: strictly 72 columns with hanging indents for bullets/numbered lists
373. Trailer validation: follows git's trailer specification
38
39## Architecture
40
41Multi-file CLI application split by concern:
42
43- **main.go**: Cobra CLI setup, flag definitions, subject validation, orchestration, git command execution
44- **trailers.go**: Trailer validation and block building following git's RFC 822 folding specification
45- **wrapBody.go**: Body text sanitization and custom word-wrapping with hanging indent support
46
47Dependencies:
48
49- **cobra**: CLI framework for flags and commands
50- **fang**: Charmbracelet's execution wrapper (version handling, etc.)
51- **bluemonday**: HTML/Markdown sanitization using UGCPolicy
52- **Custom word wrapping**: Pure-Go implementation for 72-column wrapping with hanging indents
53
54## Critical Implementation Details
55
56### Subject Validation (50 char limit)
57
58The 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`.
59
60When validation fails, clearly mark where the subject exceeds 50 characters in error output.
61
62### Body Formatting
63
64The body (`-b` flag) processing pipeline:
65
661. **Sanitization**: `bluemonday.UGCPolicy()` strips dangerous HTML/scripts while preserving basic formatting
672. **Line-by-line processing**: Each line is processed based on its type:
68 - **Bullets** (`- ` or `* `): Wrapped with 2-space hanging indent for continuation lines
69 - **Numbered lists** (`^\d+\.\s`): Wrapped with hanging indent matching the marker length (e.g., `1. `, `10. `)
70 - **Plain text**: Standard word-wrap at 72 columns
71 - **Blank lines**: Preserved as-is
723. **Word wrapping algorithm**: Greedy wrapping splits on word boundaries, never mid-word
734. **Hanging indent logic**: For bullets/numbered lists, first line gets the marker, continuation lines get spaces equal to marker width
745. **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
75
76Example wrapped bullet:
77
78```
79- This is a long bullet point that exceeds 72 characters and will
80 be wrapped with proper hanging indent alignment on continuation
81 lines.
82```
83
84### Trailer Formatting (Critical)
85
86Trailers follow git's specification precisely:
87
88- Format: `Key: value` (newline-delimited pairs)
89- No whitespace before or inside the key
90- Any number of spaces/tabs allowed between key and separator `:`
91- Values can be multiline with continuation lines starting with whitespace (RFC 822 folding)
92- The trailer group must be:
93 - At the end of input, OR
94 - The last non-whitespace lines before a line starting with `---`
95 - Preceded by one or more empty/whitespace-only lines
96
97Example valid trailer:
98
99```
100Co-authored-by: This is a very long value, with spaces and
101 newlines in it.
102```
103
104### Flag Nuance
105
106- `-t` / `--type`: Commit type (required) - e.g., `feat`, `fix`, `refactor`
107- `-m` / `--message`: Commit message (required) - the description after the colon
108- `-s` / `--scope`: Commit scope (optional) - goes in parentheses after type
109- `-B` / `--breaking`: String flag for breaking change description (optional) - adds `!` to subject AND creates `BREAKING CHANGE:` footer
110- `-b` / `--body`: String flag for commit body text (can use heredoc for multiline)
111- `-T` / `--trailer`: Repeatable flag accepting full trailer strings in `Key: value` format (not separate key/value args)
112- `-a` / `--amend`: Boolean flag to amend the previous commit instead of creating a new one
113
114Breaking 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.
115
116Trailer 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.
117
118### Final Output
119
120The formatted commit message structure:
121
1221. Subject: `type(scope)!: message` (or `type!: message` if no scope)
1232. Blank line
1243. Body (if `-b` provided)
1254. Blank line (if body or breaking change present)
1265. `BREAKING CHANGE: description` footer (if `-B` provided)
1276. Blank line (if trailers present)
1287. Trailers (if `-T` provided)
129
130Example with all components:
131```
132feat!: restructure config
133
134Improves readability and supports comments
135
136BREAKING CHANGE: Configuration format changed from JSON to TOML.
137Migrate by running: ./migrate-config.sh
138
139Assisted-by: Claude Sonnet 4.5 via Crush
140```
141
142The 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.