Skip to content

leeovery/mint

Repository files navigation

🌿 Mint

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.

License: MIT Go

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.

Why Mint?

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 --atomic is 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 .git lock and clears provably stale ones, so a background agent or editor holding the index lock can't blow up a release.
  • Unattended means unattended. -y runs end to end or fails loud. Mint never hangs on a hidden prompt and never commits an empty message because nobody was there.

Install

Homebrew

brew install leeovery/tools/mint

From source

go build -o mint ./cmd/mint

Quick Start

mint 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 unattended

Setup

mint 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.)

Commands

init

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

release

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 nothing

release regenerate

Regenerate 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 + changelog

commit

Mint 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 yourself

version

Print mint's own version. mint --version is equivalent.

help

mint help lists the commands; every verb also takes -h/--help.

Configuration

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.

Shared engine keys

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)

[release]

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

[release.hooks]

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

[commit]

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.

The AI Transport

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 contract

The 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.

Safety Model

  • Mutate nothing until accept. mint commit computes 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 release does 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_release hook warns, with the tool's own output passed through verbatim, and keeps the work.
  • Lock-resilient mutations. Every git mutation retries contended .git locks and clears provably stale ones.
  • Fail loud, never hang. Non-TTY without -y is a hard error. Unattended runs with no message source abort with one clear line.

Output

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.

License

MIT

About

Go CLI that cuts releases and writes commits with AI-generated notes, wrapped in git-safe automation that reviews everything before it mutates anything.

Resources

License

Stars

Watchers

Forks

Contributors