Skip to content

feat(synopsis): add AI-generated commit narrative synopses#566

Open
jwiegley wants to merge 3 commits intomainfrom
johnw/synopsis
Open

feat(synopsis): add AI-generated commit narrative synopses#566
jwiegley wants to merge 3 commits intomainfrom
johnw/synopsis

Conversation

@jwiegley
Copy link
Collaborator

@jwiegley jwiegley commented Feb 20, 2026

Summary

  • Adds a new git ai synopsis command that generates blog-article-style narrative descriptions of commits, capturing the full story behind a change — not just the diff, but the thinking and exploration that led to it
  • Collects three input sources: the Claude Code AI conversation (auto-detected from ~/.claude/projects/), the commit diff, and the commit message, then sends them to the Anthropic Claude API
  • Stores synopses as git notes under refs/notes/ai-synopsis, which can be pushed/pulled alongside the repository

Motivation

Traditional commits record what changed. When working with AI assistants, there's rich context — dead ends explored, tradeoffs weighed, approaches debated — that gets lost at commit time. This feature captures that context as a readable narrative for future readers of the code.

New Commands

# Generate a synopsis for the most recent commit
git ai synopsis generate

# Generate for a specific commit, with explicit API key
git ai synopsis generate --commit abc1234 --api-key $KEY

# Generate without conversation context; target shorter output
git ai synopsis generate --no-conversation --length brief

# Preview the prompt that would be sent (no API call)
git ai synopsis generate --dry-run

# Read back a stored synopsis
git ai synopsis show HEAD
git ai synopsis show abc1234

# List all commits with synopses
git ai synopsis list

Implementation Details

Module layout (src/synopsis/):

File Responsibility
types.rs Core structs: Synopsis, SynopsisMetadata, ConversationLog, DiffBundle, SynopsisInput
config.rs SynopsisConfig with defaults from env vars
conversation.rs Claude Code JSONL parser; project-hash derivation; time-window filter
collector.rs Assembles all three input sources; conversation loading is non-fatal
generator.rs Builds the structured prompt; calls Anthropic Messages API via minreq
storage.rs Reads/writes git notes under refs/notes/ai-synopsis
commands.rs generate, show, list CLI subcommands

Conversation auto-detection: Claude Code stores sessions as JSONL files under ~/.claude/projects/<project-hash>/. The hash is derived by replacing / path separators with - and stripping the leading -. The most recently modified .jsonl file in that directory is used and filtered to exchanges within the configurable time window (default: 60 min before the last exchange).

API call: Uses minreq (already a project dependency) to POST to https://api.anthropic.com/v1/messages. Requires ANTHROPIC_API_KEY (or --api-key). Model defaults to claude-opus-4-6, overridable via GIT_AI_SYNOPSIS_MODEL or --model.

Storage: Synopsis JSON (metadata + markdown content) is stored as a git note via git notes --ref=ai-synopsis add -f -F -, using the same stdin-piped pattern as the existing authorship tracking system.

Test Plan

  • 14 unit tests pass (cargo test --lib synopsis)
  • Project builds cleanly with no errors (cargo build)
  • Manual: git ai synopsis generate on a real repo with Claude Code session
  • Manual: git ai synopsis show HEAD reads back the stored note
  • Manual: git ai synopsis generate --dry-run shows prompt without API call
  • Manual: git ai synopsis generate --no-conversation works without a JSONL file present

🤖 Generated with Claude Code


Open with Devin

@git-ai-cloud-dev
Copy link

git-ai-cloud-dev bot commented Feb 20, 2026

Stats powered by Git AI

🧠 you    █░░░░░░░░░░░░░░░░░░░  6%
🤖 ai     ░███████████████████  94%
More stats
  • 0.0 lines generated for every 1 accepted
  • 0 seconds waiting for AI
  • Top model: claude:: (182 accepted lines, 0 generated lines)

AI code tracked with git-ai

@git-ai-cloud
Copy link

git-ai-cloud bot commented Feb 20, 2026

Stats powered by Git AI

🧠 you    █░░░░░░░░░░░░░░░░░░░  6%
🤖 ai     ░███████████████████  94%
More stats
  • 0.0 lines generated for every 1 accepted
  • 0 seconds waiting for AI
  • Top model: claude:: (182 accepted lines, 0 generated lines)

AI code tracked with git-ai

Git commits capture *what* changed (the diff) and a brief *why* (the
commit message), but they lose the rich context of *how the developer
got there*. When AI-assisted tools like Claude Code are used, there is
often a conversational trail — hypotheses explored, approaches debated,
dead ends encountered, design tradeoffs weighed — that evaporates the
moment the commit is made.

This feature adds `git ai synopsis`, an opt-in command that generates a
narrative, blog-article-style document for any commit. Future readers of
the code get the full story: not just the diff, but the thinking behind
it.

Three input sources are collected and sent to the Anthropic Claude API:

1. **AI conversation context** — the Claude Code JSONL session file
   for the repository is located automatically under
   `~/.claude/projects/<project-hash>/`, parsed, and filtered to
   exchanges within a configurable time window (default: 60 min).
   Conversation loading is non-fatal; if it fails the synopsis is
   generated from the diff and commit message alone.

2. **The diff** — `git show --stat` and `git show -U<N>` are run
   against the target commit to produce a stat summary and a unified
   diff with expanded context (default: 10 lines). Large diffs are
   truncated to stay within the model's context window.

3. **The commit message** — retrieved via `git log -1 --format=%B`.

A structured prompt instructs Claude to write a technical blog post with
six sections: TL;DR, Background and Motivation, The Journey, The
Solution, Key Files Changed, and Reflections. Target length is
configurable (brief / standard / detailed).

The generated synopsis is stored as a git note under
`refs/notes/ai-synopsis`, using the same stdin-piped `git notes add`
pattern already used by the authorship tracking system. Notes can be
pushed and pulled alongside the repository.

```
git ai synopsis generate [--commit <sha>] [--model <m>]
                         [--api-key <key>] [--length brief|standard|detailed]
                         [--conversation <path>] [--no-conversation]
                         [--notes-ref <ref>] [--dry-run]

git ai synopsis show [<commit>]   # default: HEAD
git ai synopsis list
```

- `ANTHROPIC_API_KEY` or `GIT_AI_SYNOPSIS_API_KEY` — API key
- `GIT_AI_SYNOPSIS_MODEL` — model override (default: claude-opus-4-6)
- `GIT_AI_SYNOPSIS=1` — enable auto-generation on every commit

```
src/synopsis/
  types.rs        — Synopsis, SynopsisMetadata, ConversationLog, DiffBundle, ...
  config.rs       — SynopsisConfig with env-var defaults
  conversation.rs — Claude Code JSONL parser and time-window filter
  collector.rs    — diff, commit message, and conversation collection
  generator.rs    — Anthropic Messages API call and prompt construction
  storage.rs      — git notes read/write under refs/notes/ai-synopsis
  commands.rs     — generate, show, list subcommand handlers
```

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude Code stores conversation files at:
  ~/.claude/projects/-Users-foo-myrepo/<uuid>.jsonl

The project hash is derived by replacing `/` with `-`, which produces a
leading `-` for absolute Unix paths. The original implementation stripped
this leading dash, so `find_claude_code_conversation` would look for
`Users-foo-myrepo` instead of `-Users-foo-myrepo` and always come up
empty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jwiegley jwiegley marked this pull request as ready for review February 20, 2026 00:30
@jwiegley jwiegley requested a review from svarlamov February 20, 2026 00:30
@jwiegley jwiegley self-assigned this Feb 20, 2026
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

// Truncate to fit within budget
let remaining = max_chars.saturating_sub(out.len());
if remaining > 64 {
out.push_str(&line[..remaining]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Byte-index slicing of String panics on multi-byte UTF-8 characters

In render_conversation, the expression &line[..remaining] at line 269 performs byte-index slicing on a String. If remaining falls in the middle of a multi-byte UTF-8 character (e.g., emoji, CJK characters, accented letters in conversation text), Rust will panic at runtime with byte index N is not a char boundary.

Root Cause and Impact

The line variable is built from user conversation text via format!("{}{}", prefix, exchange.text.trim()). Conversation logs from Claude Code sessions can easily contain non-ASCII characters (code comments in other languages, emoji, special symbols, etc.).

The remaining value is computed from max_chars.saturating_sub(out.len()), where both max_chars and out.len() are byte counts. However, when used as &line[..remaining], this byte offset may land inside a byte sequence of a multi-byte character in line, causing a panic.

For example, if line contains "**User**: café\n\n" and remaining is 14, the slice &line[..14] would cut into the middle of the é character (which is 2 bytes in UTF-8), causing a runtime panic.

Impact: Any synopsis generation that involves conversation text with multi-byte UTF-8 characters and hits the truncation path will crash the process.

Suggested change
out.push_str(&line[..remaining]);
let safe_remaining = line.floor_char_boundary(remaining);
out.push_str(&line[..safe_remaining]);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…kend

Three related improvements:

**Automation via GIT_AI_SYNOPSIS=1**

The post-commit hook now checks GIT_AI_SYNOPSIS. When set, it spawns
`git-ai synopsis generate` as a detached background process immediately
after the commit lands, so the terminal is not blocked. On Unix the
child is moved into its own process group to avoid receiving signals
meant for the parent session.

**ANTHROPIC_BASE_URL support**

The SynopsisConfig now reads the standard ANTHROPIC_BASE_URL env var
(the same variable used by the Anthropic SDK and most proxies) as the
API base URL override. Previously the only way to change the base URL
was to edit source code.

**`claude` CLI backend (--via-claude)**

A new GenerationBackend::ClaudeCli variant pipes the prompt directly to
`claude --print` instead of calling the Anthropic API. This uses
Claude Code's existing authentication — no separate API key is needed.
Select it with --via-claude on the command line, or set:

  GIT_AI_SYNOPSIS_BACKEND=claude

Usage examples:

  # Use the claude CLI (no API key required)
  git ai synopsis generate --via-claude

  # Use a corporate API gateway
  ANTHROPIC_BASE_URL=https://my-proxy/anthropic git ai synopsis generate

  # Auto-generate for every commit (background)
  GIT_AI_SYNOPSIS=1 GIT_AI_SYNOPSIS_BACKEND=claude git commit -m "..."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments