Skip to content

Add autter wrapped: a year of AI-assisted coding as a share card - #82

Merged
sagnik11 merged 6 commits into
mainfrom
autter/hungry-keller-3cd4c5
Nov 3, 2026
Merged

sagnik11 merged 6 commits into
mainfrom
autter/hungry-keller-3cd4c5

Conversation

@sagnik11

@sagnik11 sagnik11 commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds autter wrapped [--year <yyyy>] [--author <email> | --all-authors] [--json] [--include-prompt-text] [--no-color] [--ignore <pat>]: a Spotify-Wrapped-style summary of the current user's AI-assisted coding in this repository, drawn as a screenshot-friendly 60-column terminal card, with --json for the share-image pipeline.

  • Aggregation (src/authorship/wrapped.rs): walks the year's non-merge commits via git log (capped at 5,000, reported in capped), keeps the chosen author (default git user.email), reads each note with read_authorship, computes CommitStats with stats_for_commit_stats_with_authorship under effective_ignore_patterns, and folds them into a WrappedSummary.
  • Command (src/commands/wrapped.rs): flag parsing through pre_parse, pretty JSON, and the card: block-digit headline percentage, a 12-bar monthly chart in eighth blocks, top agent/model, most-prompted file, longest prompt, streak, busiest day, totals and an autter.dev/wrapped footer. Uses ui::Header above the card and ui::rail for the empty state (fewer than 3 noted commits). Honors NO_COLOR/--no-color keyed on stdout; ASCII fallback for terminals without Unicode.
  • Privacy: the longest prompt is measured from the local CAS cache only, through a new cas_bridge::resolve_cas_messages_local (cache + sync queue, never the network; resolve_cas_messages now calls it first). Transcripts not on this machine are skipped. snippet is null unless --include-prompt-text, and then holds only the first 120 characters.
  • transcript_paths_by_prompt_key / first_user_prompts_by_path in post_commit.rs are pub(crate) as requested. Wrapped does not end up needing them: working logs are deleted after commit, so for committed prompts the only local source of text is the CAS cache keyed by each record's messages_url.

Terminal card (fixture repo, --no-color):

  ▗▟▀▀▙▟▌     Autter Wrapped 2031
  ▐▌  ▐█▌     test@example.com
  ▝▜▄▄▛▜▌ ██  4 commits · 4 with authorship notes

┌──────────────────────────────────────────────────────────┐
│                                                          │
│    ███  █  █  ██  █                                      │
│   █     █  █     █                                       │
│   ████  █████   █                                        │
│   █   █    █   █                                         │
│    ███     █  █  ██                                      │
│                                                          │
│   of the lines added in 2031 were written with AI        │
│                                                          │
│   AI share by month                      peak Jul 100%   │
│                           ██                             │
│           ▅▅              ██                             │
│           ██              ██                             │
│   ▁▁  ·   ██  ·   ·   ·   ██  ·   ·   ·   ·   ·          │
│   Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec        │
│                                                          │
│   Top agent      mock_ai · 6 lines                       │
│   Top model      mock_ai/unknown-model · 6 lines         │
│   Most prompted  src/app.rs · 3 prompts                  │
│   Longest prompt 155 chars · 29 words · claude           │
│   Streak         2 days in a row with AI commits         │
│   Busiest day    Monday · most AI lines                  │
│                                                          │
│   4 commits · 9 AI lines                                 │
│   5 human · 0 untracked lines                            │
│                                                          │
│                                     autter.dev/wrapped   │
│                                                          │
└──────────────────────────────────────────────────────────┘

JSON schema (autter wrapped --json), the share-image contract

{
  "schema_version": "wrapped/1.0.0",
  "autter_version": "2.4.0",
  "year": 2026,
  "author": "you@company.com",          // null with --all-authors
  "all_authors": false,
  "commits": 94,                         // non-merge commits in the year by the author
  "commits_with_authorship": 90,         // of which carry an authorship note
  "capped": false,                       // true when the walk stopped at commit_cap
  "commit_cap": 5000,
  "added_lines": 5430,                   // sum of git_diff_added_lines
  "ai_lines": 3940,                      // sum of ai_accepted
  "human_lines": 1200,                   // known-human attested
  "untracked_lines": 290,                // no attestation (incl. commits without notes)
  "ai_percent": 72.56,                   // ai_lines / added_lines * 100, 2 decimals
  "by_month": [                          // always 12 entries, month 1..12
    { "month": 1, "ai_accepted": 120, "added_lines": 400, "ai_percent": 30.0, "commits": 9 }
  ],
  "top_agent": { "name": "claude", "lines": 3100 },            // null when no AI lines
  "top_model": { "name": "claude-opus-5-5", "lines": 2400 },   // null when no AI lines
  "agents": [ { "name": "claude", "lines": 3100 } ],           // ranked desc by lines, then name
  "models": [ { "name": "claude-opus-5-5", "lines": 2400 } ],
  "most_prompted_file": { "path": "src/x.rs", "prompts": 42 }, // distinct prompt/session ids on the file; null if none
  "files": [ { "path": "src/x.rs", "prompts": 42 } ],          // top 5
  "prompts_total": 310,                  // distinct prompt/session ids across the year
  "prompts_with_transcript": 280,        // of which have a transcript in the local cache
  "longest_prompt": {                    // null when no transcript is cached locally
    "chars": 1834, "words": 312,
    "agent": "claude", "model": "claude-opus-5-5",
    "commit": "<full sha>",
    "snippet": null                      // first 120 chars only with --include-prompt-text
  },
  "busiest_day": "Tuesday",              // weekday with the most AI lines; null when none
  "streak_days": 14                      // longest run of consecutive days with an AI commit
}

Notes on the definitions:

  • ai_percent uses the same definition as ai_percent in upload_commit_authorship_summary: ai_accepted / git_diff_added_lines. Commits in the window with no note count toward the denominator as untracked.
  • Month and weekday buckets use the commit's own author date and timezone (%ad with --date=format:), so the output does not depend on the machine's timezone.
  • Tool/model names come from CommitStats.tool_model_breakdown keys (tool::model, split on the first ::), the same split autter stats -i uses.

Tests

  • tests/integration/wrapped.rs: a fixture year (2031) built with explicit mock_known_human / mock_ai checkpoints, one Claude-preset commit with a local transcript, and commits dated into January, March and July with GIT_AUTHOR_DATE/GIT_COMMITTER_DATE, plus a commit by another author. Asserts the full JSON contract (percent, all 12 month buckets, top agent/model, ranked lists, most-prompted file, longest_prompt with snippet: null by default and the 120-char snippet with --include-prompt-text, busiest day, streak), author scoping, other years, the empty state, bad arguments, and an insta snapshot of the --no-color card.
  • Unit tests and three insta snapshots (no color, truecolor, ASCII) of the card renderer; unit tests for the streak, weekday, ranking and percent helpers.
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test wrapped (daemon mode) pass locally.

Sharing (second commit)

The backend and share page live in the monorepo (POST /api/cli/wrapped-share, DELETE /api/cli/wrapped-share/:token, page at app.autter.dev/wrapped/<token>, sample at /wrapped/demo). This PR adds the CLI side in src/api/wrapped_share.rs:

  • --share: prints the card (or the JSON) as usual, then POSTs { "wrapped": <the exact --json object>, "repo_url"?: <normalized default remote>, "include_prompt_text": bool, "hide_repo": bool } with the CLI access token as Bearer. The token is resolved like the commit-summary upload: resolve_org_for_repo_cached + access_token_for_org, else ApiContext::new(None). snippet is stripped from the body unless --include-prompt-text was given. On { token, url, image_url } it prints Shared at <url> (stderr with --json, so stdout stays the contract) and opens the page in the browser at a terminal.
  • --hide-repo: sends hide_repo: true; usage error without --share.
  • --unshare <url-or-token>: DELETEs by token; accepts a full /wrapped/<token> URL.
  • Nothing is sent in local mode (platform_sync_enabled_now), when not signed in, or when the year has too few noted commits; each case is one line on stderr and exit 1. Server statuses map to one line each: 401 → run autter login, 403 → not a member of the org, 413 → too large, 422 → nothing to share yet, 400 → the server's error, 404 on unshare → not yours or already removed.
  • ApiContext gains a delete request to match get/post_json.

Tests: mockito unit tests for the request body builder (snippet stripping, repo_url omission), token parsing, status mapping, and the POST/DELETE calls; integration tests that run the binary against a mockito server with AUTTER_API_BASE_URL and a seeded file credential store, covering flag validation, local mode sending nothing, no sign-in sending nothing, the POST with Bearer header and partial-body match (card and --json forms), a 403, and unshare by link and by token including the 404.

🤖 Generated with Claude Code

sagnik11 and others added 5 commits November 4, 2026 00:09
`autter badge` prints the share of added lines written by AI over the
last 90 days (or --since / --all-time) and a paste-ready Markdown
snippet. The number is sum(ai_accepted) / sum(git_diff_added_lines)
over commits with an authorship note, the same ratio each uploaded
commit summary carries as ai_percent; commits without a note are
counted so the output can report coverage.

--json prints a shields.io endpoint payload (schemaVersion, label,
message, color, plus the figures) and --write stores it, by default in
.autter/badge.json, so a committed file can back the badge without an
account. When the repository is linked to Autter and the user is signed
in, the snippet uses the live badge behind POST /cli/repo-badge-link;
otherwise it falls back to a static shields.io badge within 5s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`autter wrapped [--year] [--author|--all-authors] [--json]
[--include-prompt-text] [--no-color]` walks the year's commits, reads each
authorship note and folds the per-commit stats into one summary: AI share
overall and by month, top agent and model, the most-prompted file, the
longest prompt, the longest streak and the busiest weekday. The terminal
output is a deterministic 60-column card for screenshots; `--json` prints
the `wrapped/1.0.0` contract the share-image endpoint will consume.

Prompt text never leaves the machine: the longest prompt is measured from
the local transcript cache only (new `resolve_cas_messages_local`), and its
first 120 characters are included only with `--include-prompt-text`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The backend mounts CLI routes under /api/cli, like every other CLI
endpoint, so the badge link request goes there too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`--share` posts the exact `--json` summary to `/api/cli/wrapped-share`
with the CLI access token (org-scoped when the repository's org has one),
prints the page link after the card and opens it at a terminal.
`--hide-repo` keeps the repository name off the page; `--unshare` takes a
page down by link or token. Nothing is sent in local mode or without a
sign-in, and the prompt snippet is stripped from the body unless
`--include-prompt-text` was given. Server refusals map to one line each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 3.0.0 release PR carries the Wrapped entry in its own section;
changelog/wrapped.md and the README paragraph stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…wrapped

Both branches register a command in the same places; the dispatch table and
help registry now carry badge and wrapped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sagnik11
sagnik11 merged commit 677331b into main Nov 3, 2026
2 checks passed
sagnik11 added a commit that referenced this pull request Nov 3, 2026
Bumps the version to 3.0.0 in Cargo.toml, Cargo.lock and
npm/package.json, and adds the 3.0.0 changelog section for
`autter badge` (#83) and `autter wrapped` (#82), with the badge's
feature doc. Merging tags v3.0.0 and the Release workflow publishes.

Co-authored-by: Claude Opus 5.5 <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