AI-minted releases and commits
A Go CLI that cuts releases and writes commits with AI-generated notes,
wrapped in git-safe automation that reviews everything before it mutates anything.
Install · Quick Start · Commands · Configuration · The AI Transport · Safety Model
Mint replaces the per-project release script. mint release runs the whole pipeline (version bump, AI release notes, changelog, tag, atomic push, provider release) with a review gate before anything is written. mint commit mints a Conventional Commits message from your diff, shows it to you, and commits on accept.
Everything interactive is reviewable, everything unattended fails loud, and nothing touches your repo until you say yes.
Release scripts accrete: bump the version file, regenerate the changelog, remember the tag prefix, push the tag and the branch, create the GitHub release, don't forget the build hook. Every project grows its own slightly-broken copy.
Mint folds that into one binary with one config file:
- AI does the writing. Release notes are generated from the actual diff and commit history; commit messages from the staged changes. You review, edit, or regenerate at a gate; the AI never gets the last word.
- One atomic point of no return. Everything before the
git push --atomicis unwindable: if anything fails pre-push, mint surgically unwinds its own commits, tag, and stash, and only its own, leaving the repo exactly as it found it. - Mutations are lock-safe. Every git mutation retries past a contended
.gitlock and clears provably stale ones, so a background agent or editor holding the index lock can't blow up a release. - Unattended means unattended.
-yruns end to end or fails loud. Mint never hangs on a hidden prompt and never commits an empty message because nobody was there.
Homebrew
brew install leeovery/tools/mintFrom source
go build -o mint ./cmd/mintmint init # drop .mint.toml + the release shim into your repo
mint release # cut a patch release: notes → review → tag → push → publish
mint release -m # minor bump
mint commit -a # stage tracked changes, mint a commit message, review, commit
mint commit -Apy # stage everything, auto-accept, push: fully unattendedmint is an AI tool for commits & releases. To configure it for your project, pass the prompt below to your AI of choice — Claude, Codex, or whatever you run (we find Opus-level models do the best work here):
Run `mint setup` and follow what it prints.mint setup emits a version-matched setup guide that inspects your project and proposes a config — it is the source of truth, so this README does not reproduce it here. (This assumes mint is already installed; see Install.)
Scaffold mint into a repo: writes a minimal .mint.toml (empty body plus a short header comment pointing to the GitHub docs and mint setup) and a release shim at the git-resolved repo root. Idempotent: existing files are skipped unless --force.
mint init [--force] [--plain]| Flag | Description |
|---|---|
--force |
regenerate (overwrite) existing files |
--plain |
force plain (un-styled) output |
Cut a release. The pipeline: preflight gates (clean tree, on the release branch, tag free, remote in sync, provider auth) → resolve the new version → generate AI release notes from the diff → notes review gate → changelog update + bookkeeping commit → pre_tag hook → annotated tag → atomic push (branch + tag in one git push --atomic) → provider release → post_release hook.
mint release [-p | -m | -M | --set-version X.Y.Z] [options]| Flag | Description |
|---|---|
-p, --patch |
patch bump (default) |
-m, --minor |
minor bump |
-M, --major |
major bump |
--set-version V |
explicit version X.Y.Z (mutually exclusive with bump flags) |
-d, --dry-run |
read-only run: print the plan, make no changes |
-y, --yes |
skip the confirmation/notes-review gate |
--no-ai |
skip the AI notes path; use the commit-subject fallback body |
--autostash |
stash/restore unrelated WIP around the run |
--any-branch |
bypass the release-branch gate |
--plain |
force plain (un-styled) output |
At the notes review gate, a single keypress (no Enter needed): y accept (Enter also accepts), n abort, e edit in $EDITOR, r regenerate, Ctrl-C aborts cleanly.
A --dry-run generates and caches the notes (~1 hour); a real run within the window reuses the previewed bytes instead of calling the AI again.
mint release # patch release, interactive
mint release -m -y # minor release, unattended
mint release --set-version 2.0.0 # explicit version
mint release -d # preview the full plan, change nothingRegenerate the notes for an existing release and rewrite the chosen surface(s): the provider release body, CHANGELOG.md, or both. The source (where the notes come from) and the target (what gets written) are independent — any source can write any surface.
mint release regenerate <version> [options]
mint release regenerate --all [options]| Flag | Description |
|---|---|
--source SOURCE |
where notes come from: fresh (re-diff + AI, default), tag (annotation body), or release (existing provider release body) |
--target SURFACE |
surface(s) to write: release, changelog, or both |
--all |
regenerate every version, oldest → newest |
-y, --yes |
skip the confirmation / per-version review gate |
--plain |
force plain (un-styled) output |
The two axes are symmetric and independent: --source picks where the notes come from, --target picks what gets written. The tag and release sources run no AI — they read existing notes verbatim, which makes them a fast way to backfill a CHANGELOG.md from tags or releases you already have. --source release needs a resolvable provider (e.g. a GitHub origin). Under -y a --target is required (mint never guesses which live surface to rewrite unattended). In an --all run, a version with no usable source (a tag with no annotation, a release with no body, or a diff that fails AI generation) is skipped and reported — the other versions still land.
Targets differ in what they touch: a changelog (or both) target commits CHANGELOG.md and pushes it; a release target updates the provider release in place with no git changes. Regenerated changelog entries keep each version's original release date, not today's. both is non-atomic — the changelog is committed and pushed first, then the provider release. There are only two surfaces, so both is how you write more than one; there is no partial multi-select beyond release, changelog, or both.
mint release regenerate 1.4.0 # interactive: asks source then target
mint release regenerate v1.4.0 --source tag # tag body → GitHub release, no AI
mint release regenerate --all --source tag --target changelog # build the whole changelog from tag annotations
mint release regenerate --all --source release --target changelog # …or from the published GitHub releases
mint release regenerate --all --source fresh --target both # re-diff every version, rewrite releases + changelogMint an AI-generated Conventional Commits message from the would-be-committed diff, review it at the gate, and create the commit. Nothing is staged or committed until you accept; a decline leaves the index byte-for-byte untouched.
mint commit [-a | -A] [-p] [-y] [--no-ai] [--plain]| Flag | Description |
|---|---|
-a, --all |
stage tracked changes at accept (git commit -a semantics) |
-A, --add-all |
stage everything incl. untracked at accept (git add -A) |
-p, --push |
push after a successful commit (mint never pushes without this flag) |
-y, --yes |
auto-accept the review gate |
--no-ai |
skip AI generation; write the message in $EDITOR |
--plain |
force plain (un-styled) output |
Short flags bundle: -Ap, -Apy, -ay all work.
At the gate, a single keypress (no Enter needed): y accept (Enter also accepts), n abort, e edit in $EDITOR (loops back to the gate), r regenerate with a one-time context line, Ctrl-C aborts cleanly.
When the AI can't produce a message (--no-ai, a transport failure, or a diff over max_diff_lines), mint opens $EDITOR (resolved via git's own chain: GIT_EDITOR, core.editor, $VISUAL, $EDITOR) and the save becomes the accept. Unattended runs with no message source fail loud instead.
A failed -p push never unwinds the commit: mint warns once with git's own stderr passed through verbatim, keeps the commit, and exits non-zero.
mint commit # commit the index as staged
mint commit -a # stage tracked changes too
mint commit -Ap # stage everything, commit, push
mint commit --no-ai # skip the AI, write it yourselfPrint mint's own version. mint --version is equivalent.
mint help lists the commands; every verb also takes -h/--help.
mint init writes a minimal .mint.toml at the repo root: an empty body plus a short header comment that points to the GitHub docs (this human config reference) and to mint setup (AI-assisted setup). The file is fully optional — every key has a compiled default, so an empty file is valid and changes nothing; add a key only to set a value that differs from its default.
# .mint.toml — mint configuration. This file is fully OPTIONAL: every default is
# compiled into mint, so an empty file is valid and changes nothing. Add a key only
# to set a value that differs from the default.
#
# Config reference (every key, level, and default): https://github.com/leeovery/mint
# AI-assisted setup (inspects your project and proposes config): run `mint setup`The per-key reference tables below are the authoritative human config reference — every key, its level, and its default.
ai_command and timeout live at both the shared (top) level and per-verb ([release] / [commit]); the rest are shared-only. Each of the two resolves per-key independently through [verb].<key> → top-level shared <key> → shipped default — so a verb override repoints only that key for that verb, and an unset override falls through to the shared value, then to the compiled default.
| Key | Default | Description |
|---|---|---|
ai_command |
claude -p --model sonnet |
the AI invocation: prompt on stdin, message on stdout; resolves [verb] → shared → default (see The AI Transport) |
timeout |
60 |
per-attempt AI deadline in seconds; 0 = no limit; resolves [verb] → shared → default. Raise it if your ai_command runs slowly (see The AI Transport) |
max_diff_lines |
50000 |
diffs over this (post-exclusion) skip the AI |
diff_exclude |
[] |
pathspec globs kept out of every AI diff (lockfiles, generated code) |
| Key | Default | Description |
|---|---|---|
tag_prefix |
v |
tag name prefix (v1.4.0) |
commit_prefix |
🌿 |
brand prefix on mint's bookkeeping commit |
release_branch |
auto | branch releases must run on (default: derived from origin/HEAD) |
publish |
true |
create the provider (GitHub) release |
changelog |
true |
maintain CHANGELOG.md |
provider |
auto | publishing driver (default: detected from the remote host) |
context |
project guidance injected into the notes prompt | |
prompt |
path to a full notes-prompt override file | |
on_notes_failure |
abort |
abort fails loud; fallback uses the commit-subject list (or fallback string) |
fallback |
fixed fallback body, used verbatim by on_notes_failure = 'fallback' and --no-ai |
|
version_file |
write the new version into this file (omit = tag-only release) | |
version_pattern |
line to replace inside version_file (omit = the whole file is the version) |
|
ai_command |
shared | optional per-verb override of the AI command for release only; resolves [release] → shared → default |
timeout |
shared | optional per-verb override of the per-attempt AI deadline (seconds) for release only; resolves [release] → shared → default |
| Hook | Runs |
|---|---|
preflight |
before any release work; failure aborts |
pre_tag |
after notes, before the tag (string or array of commands) |
post_release |
after the release is published |
| Key | Default | Description |
|---|---|---|
context |
project guidance injected into the commit-message prompt | |
prompt |
path to a full commit-prompt override file | |
ai_command |
shared | optional per-verb override of the AI command for commit only; resolves [commit] → shared → default |
timeout |
shared | optional per-verb override of the per-attempt AI deadline (seconds) for commit only; resolves [commit] → shared → default |
Both verbs share the two-knob model: context injects into the default prompt; prompt replaces it. Unknown or mistyped keys fail loud at load; mint never silently ignores config.
Mint owns the prompt; the command is just transport. ai_command is any executable that reads a finished prompt on stdin and writes the message body to stdout:
ai_command = 'claude -p --model sonnet' # the shipped default
ai_command = 'claude -p --model opus' # pin a different model
ai_command = 'llm -m gpt-4o' # any CLI with the same stdin/stdout contractThe shipped default pins --model sonnet so zero-config behaviour is fixed regardless of the model your Claude CLI happens to default to. Both ai_command and timeout can be set shared (top-level) or per-verb under [release] / [commit], each resolving [verb] → shared → default.
The transport applies a per-attempt deadline — timeout seconds, default 60 — retries bad output (empty/non-zero exit) exactly once, and routes failures by cause: release follows on_notes_failure; commit drops to the $EDITOR fallback. A timeout is fatal: it is reported immediately and never retried (the single retry covers bad content only). A Ctrl-C is a clean abort, never a retry.
Setting timeout = 0 disables the per-attempt deadline entirely: the AI call then runs unbounded. This is a deliberate, operator-chosen exception to mint's "fail loud, never hang" posture — you are opting a slow command into an open-ended run and you own that trade-off.
ai_command and timeout resolve independently per verb, so overriding one does not touch the other. If you pin a slower model for a verb, raise that verb's timeout in the same [release] / [commit] table — mint does not auto-bump the timeout, warn, or require the pair, so a slow command left under the default 60 will hit the fatal per-attempt deadline. Overriding both keys together for a slow verb is the supported pattern; it is your responsibility, not enforced.
- Mutate nothing until accept.
mint commitcomputes the would-be-committed diff read-only; staging (git add) happens only after you accept. Declining is a true no-op. - Surgical unwind before the point of no return. Everything
mint releasedoes before the atomic push is tracked; on any pre-push failure (including Ctrl-C) mint removes its own commits, tag, and autostash (never your work) and reports exactly what it undid. - One atomic push. Branch and tag go up in a single
git push --atomic; there is no window where the tag exists without its commit. - Never unwind after success. A failed post-commit push or
post_releasehook warns, with the tool's own output passed through verbatim, and keeps the work. - Lock-resilient mutations. Every
gitmutation retries contended.gitlocks and clears provably stale ones. - Fail loud, never hang. Non-TTY without
-yis a hard error. Unattended runs with no message source abort with one clear line.
Mint renders styled output on a TTY and plain byte-pure lines when piped (or under --plain). Failures are mirrored to stderr for scripting. Exit codes: 0 success, 1 runtime failure or user abort, 2 usage error.
MIT