> Idea: A CLI that reads my git history and roasts my commit messages with savage but funny one-liners, with a leaderboard of my worst commits.

Build a command-line tool called `roast` that reads a local Git repository's commit history, scores each commit message on how lazy, vague, or embarrassing it is, prints a savage-but-funny one-liner roast for the worst offenders, and renders a "Hall of Shame" leaderboard of the user's most roastable commits. The tool must be genuinely useful as a self-deprecating code-quality nudge while being fun enough to screenshot and share. Build it end to end as a single distributable binary-style CLI. Make every decision yourself; do not stop to ask questions.

## Goal

Ship a polished, fast, offline-first CLI that turns a developer's own commit history into comedy. Running `roast` inside any Git repo should: parse commit messages, compute a per-commit "Shame Score" from deterministic heuristics, attach a witty roast line to each scored commit, and surface a ranked leaderboard of the worst commits with totals per author. The default experience is a single screenshot-worthy terminal view. Secondary experiences include filtering by author/date/range, a `--json` machine output for piping into other tools, a `stats` summary, and an optional LLM-powered roast mode that falls back gracefully to a built-in roast engine when no API key is present. The humor must be savage but never slur-based, never punching at protected characteristics, never leaking secrets — it roasts the *message*, not the person's identity. The tool must run with zero network access by default and must never write to the repo.

## Tech stack & language choice

Use **Node.js (>= 18) with TypeScript**, compiled to a single CLI entry point and exposed via a `bin` field. Justification: the job is text parsing and string scoring over Git output, not CPU-bound number crunching, so raw Go/Rust performance buys nothing; Node + TypeScript gives the fastest path to a clean argument surface, rich colored output, and trivial JSON emission, and it is the stack an LLM coding agent can produce most reliably without subtle bugs. Distribution via `npx roast` / global `npm i -g` is friction-free for the target audience (developers who already have Node).

Libraries (keep the dependency list lean and boring):
- **commander** — argument/subcommand/flag parsing and auto-generated `--help`.
- **picocolors** — fast, tiny ANSI coloring (preferred over chalk for size); respect `NO_COLOR` and non-TTY.
- **cli-table3** — leaderboard table rendering with box-drawing borders.
- **execa** — spawning `git` as a child process (safer/clearer than raw `child_process`).
- No git library binding — shell out to the system `git` so it works with the user's real config, hooks, and worktrees. Do not vendor libgit2.
- For the optional LLM mode: use the native `fetch` (Node 18+) against the Anthropic Messages API. No SDK dependency required. Read the model id and key from env; do not hardcode a model name beyond a sensible default constant.

Language target: TypeScript strict mode on (`"strict": true`, `noUncheckedIndexedAccess": true`). All source in `src/`, compiled to `dist/`. Entry shebang `#!/usr/bin/env node` on the built bin.

## Project structure

```
roast/
  package.json            # name "git-roast", bin { "roast": "dist/cli.js" }, type "module"
  tsconfig.json           # strict, ES2022, moduleResolution "bundler", outDir dist
  README.md               # usage, screenshots-as-text, examples, --json schema
  LICENSE                 # MIT
  src/
    cli.ts                # commander setup, subcommand wiring, global flags, exit handling
    commands/
      roast.ts            # default command: score + roast + leaderboard render
      stats.ts            # aggregate stats summary (no per-commit roasts)
      leaderboard.ts      # leaderboard-only view, alias of roast --top with no body
    git/
      repo.ts             # detect repo root, verify git installed, guard non-repo
      log.ts              # run `git log` with a stable pretty format, parse to Commit[]
    scoring/
      score.ts            # computeShameScore(commit) -> { score, reasons[] }
      heuristics.ts       # individual rule functions, each returns weighted points
      categories.ts       # maps reasons -> shame category labels
    roasting/
      engine.ts           # selectRoast(commit, score, category) -> string
      lines.ts            # large curated bank of roast templates by category/severity
      llm.ts              # optional Anthropic-backed roast with timeout + fallback
      sanitize.ts         # secret/PII redaction before any roast or LLM call
    render/
      human.ts            # colored terminal output, header art, leaderboard table
      json.ts             # stable machine schema + serializer
      badges.ts           # emoji/severity badge mapping
    util/
      args.ts             # parse/validate shared options (since, until, author, top, min-score)
      time.ts             # relative time formatting ("3 years ago")
      env.ts              # NO_COLOR, CI, TTY, ANTHROPIC_API_KEY detection
    types.ts              # Commit, ScoredCommit, RoastResult, CliOptions interfaces
  test/
    scoring.test.ts       # deterministic scoring assertions
    parse.test.ts         # git log parsing incl. weird messages
    sanitize.test.ts      # redaction of tokens/keys/emails
    fixtures/
      log-sample.txt      # canned `git log` output for offline tests
```

Use **vitest** as the dev-only test runner. Tests must run without a real Git repo by feeding fixtures into the parser.

## Commands & flags

Default invocation `roast` runs the `roast` command on the current repo. Provide these subcommands and global flags.

**Global flags (apply to all subcommands):**
- `-C, --cwd <path>` — run as if started in `<path>` (resolve repo root from there). Default: `process.cwd()`.
- `--json` — emit machine-readable JSON to stdout instead of human output; suppresses all decorative output and colors.
- `--no-color` — force-disable ANSI color (also auto-disabled when `NO_COLOR` set or stdout is not a TTY).
- `--since <date>` — only commits after this date; passed through to `git log --since`. Accepts git-native date strings ("2 weeks ago", "2024-01-01").
- `--until <date>` — only commits before this date.
- `--author <pattern>` — filter to commits whose author name/email matches (passed to `git log --author`, regex-friendly).
- `--branch <ref>` — analyze a specific ref/branch instead of HEAD. Default: current HEAD.
- `--max <n>` — cap how many commits to read from history (default 1000, hard ceiling 50000 to bound memory).
- `-v, --version` — print version, exit 0.
- `-h, --help` — print rich help, exit 0.

**`roast` (default command) flags:**
- `-n, --top <n>` — number of worst commits to show in the leaderboard. Default 10.
- `--min-score <n>` — only roast commits at/above this Shame Score (0–100). Default 0 (roast everything eligible).
- `--mode <heuristic|llm|auto>` — roast source. `heuristic` uses the built-in line bank; `llm` calls the Anthropic API; `auto` uses llm only if `ANTHROPIC_API_KEY` is set, else heuristic. Default `auto`.
- `--severity <mild|medium|savage>` — tone dial for the roast bank. Default `savage`.
- `--self` — only roast commits whose author email matches the repo's configured `user.email` (roast yourself, spare coworkers). Default off, but documented prominently.
- `--no-leaderboard` — print per-commit roasts only, skip the leaderboard table.
- `--seed <n>` — fix the RNG seed so roast selection is reproducible (useful for tests/screenshots).

**`stats` subcommand:** prints aggregate shame metrics (no per-commit roast lines): total commits analyzed, average Shame Score, distribution by category, worst author by average score, count of one-word commits, count of commits matching profanity/WIP/"fix" patterns. Honors all global filters and `--json`.

**`leaderboard` subcommand:** the leaderboard table only (equivalent to `roast --no per-commit body --top N`); accepts `--top`, `--self`, and global filters.

`--help` must be genuinely useful: show a one-line description, usage synopsis, grouped flags with defaults, and 4–5 real examples (e.g. `roast --self --top 5`, `roast --since "1 year ago" --json`, `roast stats --author "you@example.com"`).

## Behavior & subcommands

**1. Repo discovery & guards (`git/repo.ts`).**
On start: verify `git` is on PATH (run `git --version`); if absent, exit with a clear message and code 3. Resolve the repo root by running `git rev-parse --show-toplevel` from `--cwd`; if not inside a repo, print "Not a git repository (or no commits yet) — nowhere to roast." and exit code 2. If the repo has zero commits, print a friendly "Spotless. Not a single commit to roast. Suspicious." and exit 0.

**2. History extraction (`git/log.ts`).**
Run a single `git log` with a machine-stable pretty format using a unit-separator delimiter and a record terminator so multi-line bodies parse unambiguously, e.g. format fields: full hash, short hash, author name, author email, author date (ISO strict), subject, and body — joined by `\x1f` and terminated by `\x1e`. Apply `--since/--until/--author/--branch/--max` here. Parse into `Commit { hash, shortHash, author, email, dateISO, subject, body }`. Handle: empty bodies, merge commits (flag `isMerge` when subject starts with "Merge "), revert commits, and messages containing the delimiter bytes (extremely rare — guard by splitting on the terminator first). Never execute anything other than `git log`/`git rev-parse`/`git config user.email`; pass all user input as discrete argv items to execa (never string-interpolate into a shell) to prevent command injection.

**3. Scoring (`scoring/`).** `computeShameScore` runs a set of independent heuristic rules, each contributing weighted points (clamped to 0–100), and returns `{ score, reasons[], category }`. Rules (tune weights so a genuinely lazy commit lands 70–100 and a thoughtful one lands < 20):
- **One-word / too-short subject** (e.g. "fix", "wip", "stuff", "done", "update"): +35. Subject ≤ 3 chars: +45.
- **Vague verb-only / filler** ("fix", "fixes", "changes", "minor", "misc", "cleanup", "tweak", "stuff", "things", "asdf", "test", "."): +25 each match, capped.
- **WIP / temporary markers** ("wip", "tmp", "temp", "do not merge", "revert me"): +30.
- **Profanity / frustration** ("ugh", "finally", "why", "hate", "kill me", plus a small profanity set): +20 (counts toward "emotional damage" category, never reproduced verbatim in a slur-amplifying way).
- **No body on a non-trivial subject** (subject suggests substance but body empty): +10.
- **ALL CAPS RAGE** (subject mostly uppercase, length > 4): +20.
- **Trailing/leading whitespace, emoji-only, or "." subject**: +30.
- **"fix typo" / "oops" / "nvm" / "actually"**: +15 (the "human error" category).
- **Auto-generated default messages** ("Initial commit", "Update README.md", merge commits): low/no shame — explicitly *reduce* score so the tool doesn't pile on unavoidable commits; merges get a max score cap of 15.
- **Long, well-formed conventional commit** (matches `type(scope): summary` with a body): *negative* points (−20) — reward good behavior so the leaderboard means something.
Each rule returns a human-readable reason string used both for the JSON `reasons` array and to pick a roast category. Final `category` is the highest-weighted matched reason (e.g. `LAZY`, `VAGUE`, `WIP`, `RAGE`, `EMPTY`, `TYPO`, `CLEAN`). Scoring must be fully deterministic given a fixed commit set.

**4. Roasting (`roasting/`).** `selectRoast` picks a one-liner for a scored commit. In `heuristic` mode it draws from `lines.ts`, a curated bank organized by `category` × `severity`, with at least **12 distinct lines per category per severity tier** (so a repo never feels repetitive). Use the seeded RNG plus the commit hash to choose, so the same commit always gets the same roast within a run but different commits vary. Templates may interpolate `{subject}`, `{age}` (relative time), and `{shortHash}`, e.g. for `LAZY`/savage: "`{shortHash}`: 'fix'. Fix WHAT, your sleep schedule?" or "A one-word commit aged {age}. Hemingway is rolling." Keep lines punchy (< 120 chars), genuinely funny, PG-13, and never targeting the author's identity, gender, race, etc. Before any roast is generated or sent to an LLM, run `sanitize.ts` to redact anything resembling a secret (API keys, tokens, AWS keys, JWTs, bearer strings, emails inside the subject) so the tool never prints or transmits a leaked credential it found in a commit message.

**5. Optional LLM mode (`roasting/llm.ts`).** When mode resolves to `llm`: send the sanitized subjects of the top-N worst commits in a single batched request to the Anthropic Messages API (default model held in a `DEFAULT_MODEL` constant), with a system prompt that instructs: savage-but-kind roasting of commit *messages only*, one line each, no slurs, no targeting protected attributes, return strict JSON array mapping hash→roast. Enforce a hard 8-second timeout and a single retry; on any error, missing key, non-200, malformed JSON, or timeout, **silently fall back** to the heuristic engine (print a dim note "(llm unavailable — using built-in roasts)" to stderr, never stdout). Never block the leaderboard on the network. Never send commit bodies or file contents — subjects only, post-sanitization.

**6. Rendering & leaderboard (`render/`).** Human output: a small ASCII banner header ("🔥 git roast"), a list of the worst commits (each: severity badge emoji, short hash dimmed, score colored by tier — green < 30, yellow 30–69, red ≥ 70 — the original subject in quotes, and the roast line), then a "🏆 Hall of Shame" leaderboard table (rank, short hash, score, author, truncated subject) sorted by score desc and tie-broken by oldest date. Footer line: total commits analyzed, average score, and the single worst commit called out as "Worst offender." Colors auto-disable for non-TTY/`NO_COLOR`/`--json`. Subjects truncated with ellipsis to keep the table within ~100 cols.

## Output format (human + machine)

**Human (default):** colored, emoji-badged, table-bordered, screenshot-friendly. Order: banner → per-commit roasts (worst first, limited by `--top` unless `--no-leaderboard` shows roasts-only) → leaderboard table → summary footer. All decorative output goes to stdout; diagnostic/fallback notes go to stderr so piping stays clean.

**Machine (`--json`):** emit a single stable JSON object to stdout and nothing else. Schema:
```json
{
  "repo": "/abs/path",
  "branch": "main",
  "analyzedCount": 742,
  "averageScore": 41.3,
  "generatedAt": "2026-06-11T00:00:00.000Z",
  "mode": "heuristic",
  "worstOffender": { "hash": "…", "score": 96 },
  "leaderboard": [
    {
      "rank": 1,
      "hash": "f3a9c01…",
      "shortHash": "f3a9c01",
      "author": "Zach",
      "email": "z@example.com",
      "dateISO": "2023-02-14T09:11:00Z",
      "subject": "fix",
      "score": 96,
      "category": "LAZY",
      "reasons": ["one-word subject", "vague filler verb", "no body"],
      "roast": "'fix'. Fix WHAT, your life choices?"
    }
  ],
  "stats": { "byCategory": { "LAZY": 31, "VAGUE": 22, "WIP": 9 }, "oneWordCount": 18, "wipCount": 9 }
}
```
JSON output must validate against this shape, use stable key ordering, omit ANSI codes entirely, and be parseable by `jq`. `stats --json` emits the `stats` block plus `analyzedCount`/`averageScore` without the `leaderboard` array.

## Error handling & exit codes

Define meaningful, documented exit codes and route all user-facing errors through a single handler that prints a one-line, non-stacktrace message (full stack only when `DEBUG=roast` env is set):
- `0` — success (including the "spotless, no commits" and "clean repo" friendly cases).
- `1` — generic/unexpected internal error (caught at top level; print short message + hint to set `DEBUG=roast`).
- `2` — not a git repository / invalid `--cwd` / unknown `--branch`.
- `3` — `git` binary not found on PATH.
- `4` — invalid arguments (bad `--top`, `--min-score` out of 0–100, mutually exclusive flags, unparseable date), with the offending flag named.
Edge cases to handle explicitly: repos with a single commit; detached HEAD; non-UTF8 / emoji-only commit subjects (don't crash; treat replacement chars gracefully); enormous repos (respect `--max` and stream-parse rather than buffering all of `git log` if it exceeds the cap); a `--since`/`--until` range that matches zero commits (print "No commits in that range — you're either innocent or hiding evidence." exit 0); `--author` matching no one (same friendly empty state); LLM mode with no key (auto-fallback, never error); `NO_COLOR`/piped output (plain text, no ANSI); and SIGINT during an LLM call (abort the fetch, fall back, exit cleanly). Never write to, commit to, or modify the repository under any flag.

## Definition of done

- [ ] `roast` run inside any git repo with commits prints banner, per-commit roasts, a Hall of Shame leaderboard, and a summary footer — all colored on a TTY, plain when piped.
- [ ] Scoring is deterministic: the same repo + same `--seed` yields identical scores and roast lines on repeated runs; tests in `scoring.test.ts` assert exact scores for crafted fixtures (lazy commit ≥ 70, conventional commit < 20, merge ≤ 15).
- [ ] Git log parser handles multi-line bodies, merges, empty bodies, and weird/emoji subjects from `fixtures/log-sample.txt` without a real repo; covered by `parse.test.ts`.
- [ ] `--json` and `stats --json` emit only valid, `jq`-parseable JSON matching the documented schema with no ANSI codes; verified by a test that `JSON.parse`s captured stdout.
- [ ] Roast bank has ≥ 12 lines per category per severity tier; no line targets protected characteristics; `--severity mild|medium|savage` measurably changes tone.
- [ ] `sanitize.ts` redacts API keys, tokens, JWTs, AWS keys, bearer strings, and emails before any roast/LLM use; covered by `sanitize.test.ts`.
- [ ] `--mode llm` calls Anthropic with sanitized subjects only, enforces an 8s timeout + single retry, and falls back to heuristics on any failure with a stderr-only note; missing `ANTHROPIC_API_KEY` under `auto` silently uses heuristics.
- [ ] All filters work and compose: `--since`, `--until`, `--author`, `--branch`, `--self`, `--top`, `--min-score`, `--max`, `--cwd`.
- [ ] Exit codes 0–4 are wired exactly as specified; non-repo, missing-git, and bad-arg paths print one-line messages and never dump a stack unless `DEBUG=roast`.
- [ ] `--help`, `-h`, `-v`, `--version` work; help lists grouped flags with defaults and 4–5 real examples.
- [ ] No network access occurs in default/`auto`-without-key/`heuristic` modes; the repo is never modified.
- [ ] `package.json` exposes the `roast` bin with a node shebang; `tsc` builds `dist/` clean under strict mode; `vitest` passes; README documents usage, the `--json` schema, and the roast-safety policy.
