feat(cli): require description for -B flag

Amolith created

The -B flag now accepts a string argument containing the breaking change
description instead of being a boolean flag. This description is
formatted and inserted as a BREAKING CHANGE: footer between the body and
git trailers, following the Conventional Commits specification.

The flag still adds ! to the subject line. When used with heredoc
syntax, multi-line breaking change descriptions are supported.

Also re-added the -a/--amend flag that was present in main but missing
from this feature branch.

Implements: bug-e75a648
Assisted-by: Claude Sonnet 4.5 via Crush

Change summary

AGENTS.md | 33 ++++++++++++++++++++++++++++-----
main.go   | 32 ++++++++++++++++++++++----------
2 files changed, 50 insertions(+), 15 deletions(-)

Detailed changes

AGENTS.md 🔗

@@ -104,14 +104,37 @@ Assisted-by: This is a very long value, with spaces and
 - `-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`: Boolean flag for breaking changes (adds `!` to subject)
+- `-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` / `--add`: Boolean flag to stage all modified files before committing (optional)
-- `--amend`: Boolean flag to amend the previous commit instead of creating a new one (optional)
+- `-a` / `--amend`: Boolean flag to amend the previous commit instead of creating a new one
 
-Trailer format detail: Each `-T` flag takes a complete trailer string like `-T "Assisted-by: GLM 4.6 via Crush"`, NOT separate key and value arguments.
+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 must be piped to `git commit -F -` to read from stdin. When the `--amend` flag is used, it pipes to `git commit --amend -F -` instead.
+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.

main.go 🔗

@@ -37,15 +37,16 @@ body wrapping, and trailer formatting.`,
 # With Assisted-by
 formatted-commit -t feat -m "do a thing" -T "Assisted-by: GLM 4.6 via Crush"
 
-# Breaking change with longer body
-formatted-commit -t feat -m "do a thing that borks a thing" -B -b "$(cat <<'EOF'
-Multi-line
-- Body
-- Here
-
-This is what borked because of new shiny, this is how migrate
+# Breaking change with description
+formatted-commit -t feat -m "remove deprecated API" \
+  -B "The old /v1/users endpoint is removed. Use /v2/users instead."
+
+# Breaking change with multi-line description using heredoc
+formatted-commit -t feat -m "restructure config format" -B "$(cat <<'EOF'
+Configuration format changed from JSON to TOML.
+Migrate by running: ./migrate-config.sh
 EOF
-)"
+)" -b "Improves readability and supports comments"
 
 # Including scope for more precise changes
 formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
@@ -79,6 +80,16 @@ formatted-commit upgrade -a
 			commitMsg.WriteString(formattedBody)
 		}
 
+		if breakingChange != "" {
+			formattedBreaking, err := formatBody(breakingChange)
+			if err != nil {
+				return fmt.Errorf("failed to format breaking change: %w", err)
+			}
+			commitMsg.WriteString("\n\n")
+			commitMsg.WriteString("BREAKING CHANGE: ")
+			commitMsg.WriteString(formattedBreaking)
+		}
+
 		if len(trailers) > 0 {
 			trailersBlock, err := buildTrailersBlock(trailers)
 			if err != nil {
@@ -96,6 +107,7 @@ formatted-commit upgrade -a
 			gitArgs = append(gitArgs, "--amend")
 		}
 		gitArgs = append(gitArgs, "-F", "-")
+
 		gitCmd := exec.Command("git", gitArgs...)
 		gitCmd.Stdout = os.Stdout
 		gitCmd.Stderr = os.Stderr
@@ -143,7 +155,7 @@ func init() {
 	}
 }
 
-func buildAndValidateSubject(commitType, scope, message string, breaking bool) (string, error) {
+func buildAndValidateSubject(commitType, scope, message string, breaking string) (string, error) {
 	var subject strings.Builder
 
 	subject.WriteString(commitType)
@@ -154,7 +166,7 @@ func buildAndValidateSubject(commitType, scope, message string, breaking bool) (
 		subject.WriteString(")")
 	}
 
-	if breaking {
+	if breaking != "" {
 		subject.WriteString("!")
 	}