commit-report
One skill, two modes, two deliveries.
- Modes —
quick(default): git log of the current repo → prose + bullets, zero config.standup(--standup): multi-source — GitHub PRs/reviews/comments, git across configured repos, deploys, optional tracker → impact narrative. - Deliveries — every run ends with a copy-ready channel block (the paste-anywhere "send"). Add
--docto also write the full report to a file for bigger-scope handoffs. - Audience —
dev(tech-heavy) /pm(mid) /client(no jargon). Applies to both modes and to the channel block.
Guiding principle (both modes): name work by what it was, not by its ID. "Fixed the image cache eviction" beats "Merged PR #481." IDs live in refs/footnotes, never in the headline.
Quick start
/commit-report # quick mode, dev, --since-mine, prints channel block
/commit-report pm # PM-friendly summary of latest batch
/commit-report client --last 4h # client-safe wording, last 4h
/commit-report dev --count 5 # last 5 commits by user
/commit-report pm --since a3f2c1d # commits since this hash up to HEAD
/commit-report pm --range main..HEAD # explicit range
/commit-report dev --path apps/web # restrict to subdir
/commit-report dev --include-merges # include merge commits (dev only)
/commit-report --standup # multi-source standup, last working day → now
/commit-report pm --standup # standup, PM-framed
/commit-report --standup --repos a,b,c # override local repo roots for this run
/commit-report pm --standup --doc # write full report to a doc AND print channel block
/commit-report pm --standup --doc ~/standups/today.md # explicit doc path
/commit-report --metrics # append flow metrics for the window
/commit-report dev --metrics --last week
Flag precedence: explicit scope flag > mode default window. --doc, --metrics, and audience compose with any mode.
Modes
quick (default)
Git log of the current repo only. Zero config. Produces ## Report (prose) + ## Bullets, then the channel block. This is the original commit-report behavior — unchanged.
standup (--standup / --full)
Multi-source. Requires one-time config. Produces the full standup format (Yesterday / Recap / Today / Channel / Refs), then the channel block. Default window = last working day → now (see Time window).
If --standup is requested but config is missing, run First-run config first.
Delivery
Both modes always end by printing the copy-ready channel block — a tight, paste-anywhere summary shaped for a chat channel:
## For the channel
<one line naming the day's / batch's theme>
- <outcome, not implementation>
- <outcome>
Rules for the block: outcome-focused, never a changelog. No SHAs, no file paths, no function names (even at dev — the channel block is the compact, human-facing cut). 2–5 bullets. This is the default "send."
--doc [path] — additionally write the full report (mode-appropriate format) to a Markdown file, for bigger-scope handoffs.
- Default path:
standups/YYYY-MM-DD.md(relative to cwd). Create thestandups/dir if it doesn't exist. Override per-run with--doc <path>, or setdocDirin config. YYYY-MM-DD= the window's end date (today), from a real timestamp (git log -1 --date=shortor system date) — never invented.- After writing, print the file path AND the channel block (so the user can both paste and hand off).
First-run config (standup only)
Config lives at ~/.claude/skills/commit-report/config.json. Quick mode ignores it entirely — never require config for a git-only report.
On first --standup run with no config, ask for and persist:
{
"githubLogin": "octocat",
"displayName": "Matheus",
"voice": "first",
"repoScope": "org:my-org",
"localRepoRoots": ["C:/Users/malag/projects/work", "C:/Users/malag/projects/side"],
"tracker": null,
"docDir": "standups"
}
githubLogin— forghsearches (author:,reviewed-by:,commenter:).displayName+voice(first|third) — narrative voice.repoScope—org:name,user:login, or an explicit list ofowner/repo.localRepoRoots— dirs to scan for local commits (walk one level for git repos).tracker— optional MCP for ticket titles (e.g.linear,jira), elsenull.docDir— default--docdirectory.null/absent → use the delivery default above.
Keep setup to the minimum needed for the flags actually used; don't prompt for a tracker if none is configured and no ticket refs appear.
Workflow
- Resolve mode:
--standup/--full→ standup, else quick. - Resolve identity:
git config user.email. In quick mode, bail with a clear error if unset. For author matching, scan both the--authorfield ANDCo-Authored-By:trailers — pair-programming commits where the user is co-author must be included, not silently dropped. - Resolve audience (positional first arg, default
dev):dev|pm|client. - Resolve scope / window:
- quick (precedence: explicit flag > default
--since-mine):--since-mine(default): contiguous commits at HEAD authored by user. Walk back viagit log --pretty='%H%x09%ae'; stop at first commit whose author email ≠ user. If HEAD's author ≠ user → fall back to--last todayand note the fallback in output.--since <hash>:git log <hash>..HEAD --author="$email". Hash = exclusive lower bound. Short or full SHA.--last <duration>:1h,4h,today,week→git log --author="$email" --since=<duration>.--count <n>:git log --author="$email" -n <n>.--range <ref..ref>:git log --author="$email" <range>.--path <dir>: append-- <dir>to anygit log. Repo cwd is default.
- standup: default window = last working day → now. Any explicit quick-scope flag overrides the window.
- quick (precedence: explicit flag > default
- Collect data:
- quick — per commit: subject, body, short SHA, files changed (
git show --stat --pretty=format:'%h%n%H%n%s%n%b' <sha>). Skip merge commits unlessdev+--include-merges. - standup — run these in parallel (see Multi-source collection).
- quick — per commit: subject, body, short SHA, files changed (
- Parse Conventional Commits prefixes (
feat:,fix:,chore:,refactor:,docs:,test:,perf:,build:,ci:) as the primary bucketing signal: features ship, fixes resolve, refactor/chore/test = "groundwork" for client audience. No prefix → content heuristics. Scope in parens (feat(auth):) is apm/clientframing hint. - Synthesize per audience + mode (Audience map, Standup format). Group related work; do not echo subjects 1:1. Name by outcome, not ID.
- Deliver: render the mode's format, then always the channel block. If
--metrics, insert the flow-metrics section before the channel block. If--doc, write the full report to file and print its path.
Multi-source collection (standup)
Query in parallel, then merge and dedupe:
- Authored PRs —
gh search prs --author={githubLogin} --merged(and open/draft for Today) within the window, scoped byrepoScope. - Reviews given —
gh search prs --reviewed-by={githubLogin}in the window. - Comments left —
gh search issues --commenter={githubLogin}, deduped against 1–2. - Local commits —
git log --author="$email" --since=<window> --date=format:'%a %Y-%m-%d %H:%M'across eachlocalRepoRootsentry (and its child repos). Use the real weekday from the timestamp; never infer it. - Deploys — release/deploy workflow runs after merge, matched by SHA or time proximity (
gh run list). Note success/failure. - Linked tickets — extract Linear/Jira/etc. refs from PR titles/bodies; resolve titles via the
trackerMCP if configured. If no tracker, keep the raw ref. - Today's queue — carry-overs (open/draft PRs, unpushed branches), pending review requests (
review-requested:{githubLogin}), assigned tickets by priority. Cap at 5.
Any source that errors or is unconfigured is skipped silently — a partial report beats a failed one. Note in the report which sources were unavailable only if it materially changes the picture.
Flow metrics (--metrics)
Opt-in. Appends a ## Flow metrics section measuring how the work moved, not what shipped. dev audience only — these are engineering-process numbers; a PM or client report gets the outcomes, not the pipeline telemetry. If --metrics is combined with pm/client, compute it into the --doc body but keep it out of the channel block.
Every figure below is derived from git and gh alone — no new tooling, no instrumentation to install. That also means several are proxies, and a proxy reported as a fact is worse than no metric. Label them as marked, and when the window is too small for a number to mean anything (fewer than ~5 PRs), print the raw counts and skip the percentage rather than reporting "100% first-pass" off a sample of one.
Leading — cheap signals that move before quality does:
| Metric | How to derive | Note |
|---|---|---|
| Review coverage | Report it three ways: PRs with a human review, PRs with a bot review, PRs with neither. Filter on .user.type=="User" vs "Bot" — see automated-reviewers |
Compute this first — it calibrates everything below |
| Rework signal | inline review comments per PR (repos/{owner}/{repo}/pulls/{n}/comments), median and worst. Where review coverage is high, also count commits pushed after the first review's submittedAt |
The real "had to redo work" signal |
| First-pass merge rate | merged PRs with no CHANGES_REQUESTED ÷ all merged PRs — but only report it when review coverage is meaningful and the state is actually in use |
See the trap below |
The CHANGES_REQUESTED trap. Plenty of teams review thoroughly and never touch that button; they approve and leave inline comments instead. On such a repo this metric reports a flawless 100% that means nothing, and it reports the same 100% for a repo that merges entirely unreviewed. Two opposite realities, one flattering number.
So: if zero PRs in the window carry a CHANGES_REQUESTED, do not print a first-pass rate. Print not computable — team does not use the CHANGES_REQUESTED state and lead with review coverage instead. Before blaming the data, confirm gh is returning reviews at all by spot-checking one PR you know was approved (gh pr view <n> --json reviews) — an empty array everywhere can mean a broken query rather than an unreviewed repo, and those need opposite responses.
Never filter bots by login suffix. Use .user.type. GitHub Copilot files its review under copilot-pull-request-reviewer[bot] but authors its inline comments as plain Copilot, which sails through a suffix check and gets counted as a person. That single mistake turned a real 9/25 human-review figure into a reassuring 21/25 on a repo whose review gap was the finding. user.type is "Bot" for both identities.
gh api "repos/$OWNER/$REPO/pulls/$N/comments" -q '[.[] | select(.user.type=="User")] | length'
Bot review is worth counting — separately, never folded into the human number. A repo where bots review everything and humans review little is a different situation from one with no review at all, and both differ from thorough human review; one figure cannot say which you're looking at. Deploy bots (vercel[bot], cloudflare-workers-and-pages[bot]) are not reviewers at all and belong in neither column.
If a configured reviewer is paused, absent, or stale, give it a line. An automated reviewer reporting "reviews are paused for this user" on the repo with the highest fix share is a finding, not a footnote.
| Stage-artifact lag | git log --diff-filter=A --format=%aI over docs/intent/, docs/prds/, docs/plans/, then the first code commit citing that slug. Report the gaps | Measures the dev-flow chain itself; only meaningful on repos that use it |
Lagging — the ones that actually tell you whether the process is working:
| Metric | How to derive | Note |
|---|---|---|
| Spec churn after build | commits touching docs/prds/NNNN-<slug>.md dated after the first code commit for that slug |
Proxy. High churn means to-prd ran on thin context |
| Caught vs escaped | fix: commits on a branch before its PR merged (caught) vs fix: commits landing after a merge that touch files the merge introduced (escaped) |
Proxy, and the softest one — conventional-commit discipline is the only signal available. Say so |
| Repeat classes | run sentinel's scanner — py -3 ~/.claude/skills/sentinel/scripts/scan_git_signals.py --repo . --since <window> — and report its recurring bucket |
Proxy for "same incident twice". Don't hand-roll this: a raw fix-count ranking is dominated by locale catalogs and same-day iteration, which is why that scanner buckets against the repo's own distribution. A file recurring three windows running is the finding, not the count |
Output shape:
## Flow metrics
- Review coverage: human 9/25, bot 25/25, unreviewed 0/25
- Rework: median 3 inline comments/PR, worst #482 at 11
- First-pass merge: not computable — no PR used CHANGES_REQUESTED this window
- Artifact lag: intent → PRD 2h, PRD → plan 1d, plan → first commit 3h
- Spec churn after build: 1 PRD amended post-code (#0012 payments-retry)
- Caught vs escaped [proxy]: fix share 0.09, 0 reverts
- Repeat classes [proxy]: `src/api/orders.ts` 8x over 19d (sentinel, recurring)
Rules: never invent a figure to fill a row — drop the row and say which data was unavailable. Never present a proxy without its [proxy] tag. Trend beats level; a single window's number is close to meaningless, so when prior --doc reports exist in docDir, read the last one and show the delta.
Time window
Used by standup mode (quick mode uses its scope flags). "Yesterday" = last working day, window ends now (captures work done earlier today):
- Monday → previous Friday 00:00
- Tue–Fri → previous calendar day 00:00
- Sat/Sun → most recent Friday 00:00
Every temporal claim must trace to a real timestamp from git / GitHub / MCP. Never invent a weekday or time.
Audience map
| Concept | dev | pm | client |
|---|---|---|---|
useEffect cleanup leak |
"fixed memory leak in useEffect cleanup on Dashboard" |
"fixed memory leak in dashboard" | "dashboard runs smoother" |
| DB index added | "added btree index on orders.user_id" |
"sped up order lookups" | "orders load faster" |
| Pure refactor, no UX delta | "extracted AuthGuard HOC, dedup in 4 routes" |
"cleaned up auth code for maintainability" | "ongoing improvements to keep things stable" |
| Auth bug | "fixed JWT expiry off-by-one in validateToken" |
"fixed login bug where sessions expired early" | "login is more reliable" |
| New endpoint | "added POST /api/exports with stream response" |
"shipped exports endpoint for the export flow" | "you can now export your data" |
Rules per level:
- dev: backticks for files/functions/symbols. Short SHAs in parens, e.g.
(a3f2c1d). May mention internals. (Not the channel block — see Delivery.) - pm: no SHAs, no file paths, no function names. Frame as features / fixes / improvements / cleanup. Light tech terms OK ("API", "auth flow", "caching").
- client: outcomes only. No tech terms, no internal names. Include every commit — frame refactor/chore/test work as softened reliability/stability statements ("ongoing improvements", "behind-the-scenes work to keep things stable", "groundwork for upcoming features"). Never drop commits silently.
Output formats
quick format
## Report
<prose, level-appropriate. dev ≈ 2 short paragraphs; pm ≈ 1 paragraph (~120 words); client ≈ 1 paragraph (~80 words)>
## Bullets
- <shipped change, verb-first, level-vocab> (5–8 items; 1–2 for a single commit)
## For the channel
<theme line>
- <outcome>
standup format
# Standup: {DATE}
> Covering: {WINDOW} | {COUNTS}
## Yesterday
<2–3 sentence impact paragraph — what changed about the system, who's unblocked, what risk surfaced>
## Recap
<2–5 paragraphs, grouped by work thread (not by source). Lead with system impact, name root causes, reframe reviews as judgment calls, say what didn't ship and why. Vary sentence openings.>
## Today
<carry-overs → pending reviews → queue; capped at 5>
## For the channel
<theme line>
- <outcome>
## Refs
<footnote: ticket IDs and PR links>
The channel block is the only part printed when neither a doc nor the full body is wanted — but by default print the full mode format followed by the channel block. --doc writes the full body to file; the channel block is still printed to chat.
Edge cases
- No commits / no activity in window → say so plainly ("quiet day, nothing landed in code" beats fabricated activity); suggest widening (
--last today,--last week, or--standupfor cross-repo work). - quick
--since-mine+ HEAD not user's → fall back to--last today, prepend: "HEAD not authored by you — showing today's commits instead." - Mixed merge commits → skip by default; include only with
dev --include-merges. - Single commit → still produce Report + Bullets + channel block (Bullets may be 1–2 items).
- Repo not initialized / outside repo → quick mode bails with a clear error; standup mode can still run from
localRepoRoots+ GitHub if configured. - standup with missing config → run first-run setup, don't guess.
- A standup source errors → skip it silently; a partial report beats a failure.
- Commit body / PR body contains secrets/tokens → surface only subject + summarized intent, never raw body.
--doctarget dir doesn't exist → create it, then write.
Output rules
- Every run ends with the
## For the channelblock. In quick and standup mode the full format precedes it. - Channel block is outcome-only: no SHAs, no file paths, no function names, at any audience.
- No emoji.
- Code/file refs in backticks at
devonly, and never in the channel block. - No commit hashes in
pm/client. - Never invent timestamps, weekdays, or activity. Every claim traces to real data.
- Caveman mode does NOT apply to report content — write normal prose. Commands/code stay verbatim.
Humanize the written prose (if available)
Before writing generated prose to a file (--doc), if the humanizer skill is installed, run it on the drafted body so the document reads naturally and free of AI tells; skip silently if unavailable. Apply to the human-facing document body only — never to code, frontmatter, file paths, IDs, or literal templates.