claude-attribution 1.2.9 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,70 @@
1
1
  # claude-attribution
2
2
 
3
- > **Hey team** we've built a tool that automatically tracks and documents AI contribution in your PRs so you don't have to do it manually.
4
- >
5
- > **One-time setup:**
6
- > ```bash
7
- > npm install -g claude-attribution
8
- > claude-attribution install ~/Code/your-repo
9
- > claude-attribution init --ai # repo built with Claude Codeor --human if human/mixed
10
- > git add .claude/settings.json .gitignore && git commit -m "chore: install claude-attribution hooks"
11
- > ```
12
- > From then on, just work normally. After each `git commit` you'll see a one-line attribution summary in your terminal. When you're ready to open a PR, run `/pr` in Claude Code (or `claude-attribution pr "feat: your title"`) — it fills in the metrics automatically, no copy-paste needed.
13
- >
14
- > **Using Copilot?** The tool still works for tracking Claude usage alongside Copilot. Copilot line-level attribution isn't supported yet — for Copilot-specific stats, use the GitHub Copilot usage dashboard. Both tools' org-level data flows into the VP Datadog dashboard automatically on every PR merge.
15
- >
16
- > **Requirements:** [Bun](https://bun.sh) (preferred) or Node 18+, and `gh` (GitHub CLI) authenticated for the `/pr` command.
3
+ AI code attribution for Claude Code. After every `git commit`, a one-line summary appears in your terminal:
4
+
5
+ ```
6
+ [claude-attribution] a3f1b2c — 142 AI / 38 human / 4 mixed lines (77% AI)
7
+ ```
8
+
9
+ When a PR is opened, full metrics model usage, token counts, tool calls, and attribution percentages are injected automatically into the PR body. No copy-paste, no manual tracking.
10
+
11
+ **Quick start:**
12
+
13
+ ```bash
14
+ npm install -g claude-attribution
15
+ claude-attribution install ~/Code/your-repo
16
+ claude-attribution init --ai # if repo was built with Claude Code; use --human otherwise
17
+ git add .claude/settings.json .github/workflows/claude-attribution-pr.yml .gitignore
18
+ git commit -m "chore: install claude-attribution hooks"
19
+ git push
20
+ ```
21
+
22
+ **Using Copilot or `@claude` (claude-code-action)?** Bot commits are auto-detected and attributed as 100% AI — no extra steps needed. See [AI Actor Attribution](#ai-actor-attribution-copilot-bot-claude-gha).
23
+
24
+ **Requirements:** [Bun](https://bun.sh) (preferred) or Node 18+, and `gh` (GitHub CLI) authenticated for the `/pr` command.
25
+
26
+ ---
27
+
28
+ Measures which lines in a commit were written by Claude vs. a human — using checkpoint snapshots and line-level SHA-256 comparison, not gross write-operation counts.
17
29
 
18
30
  ---
19
31
 
20
- AI code attribution tracking for Claude Code. Measures which lines in a commit were written by Claude vs. a human — using checkpoint snapshots and line-level SHA-256 comparison, not gross write-operation counts.
32
+ ## GitHub Actions Requirements
33
+
34
+ Up to three workflows are installed into repos that use this tool — one always, two optionally. Each requires specific GitHub Actions to be allowed in your org. If your org enforces an action allowlist, you must approve the third-party action before the workflows will run.
35
+
36
+ ### Workflows and the actions they use
37
+
38
+ | Workflow file | Trigger | Install | GitHub-owned actions | Third-party actions |
39
+ |---|---|---|---|---|
40
+ | `claude-attribution-pr.yml` | PR opened / pushed | Always | `actions/checkout@v4`, `actions/github-script@v7` | `oven-sh/setup-bun@v2` |
41
+ | `claude-attribution-export.yml` | PR merged | Optional (prompted) | `actions/checkout@v4` | `oven-sh/setup-bun@v2` |
42
+ | `claude-attribution-gha.yml` | Every push | Optional (claude-code-action detected) | `actions/checkout@v4` | `oven-sh/setup-bun@v2` |
43
+
44
+ **GitHub-owned actions** (`actions/*`) are pre-approved in all orgs by default.
45
+
46
+ **`oven-sh/setup-bun@v2`** is a third-party action from the [Bun](https://bun.sh) team. It installs the Bun runtime so the `claude-attribution` CLI can execute without falling back to `npx tsx`. If your org requires explicit action approvals, add `oven-sh/setup-bun` to the allowlist at:
47
+
48
+ > Settings → Actions → General → Allow actions and reusable workflows → add `oven-sh/setup-bun@*`
49
+
50
+ ### Workflow permissions
51
+
52
+ | Workflow | `contents` | `pull-requests` | Why |
53
+ |---|---|---|---|
54
+ | `claude-attribution-pr.yml` | read | write | Reads git history; writes metrics into the PR body |
55
+ | `claude-attribution-export.yml` | read | — | Reads git notes and exports metrics via OTLP/webhook |
56
+ | `claude-attribution-gha.yml` | write | — | Pushes attribution git notes back to origin |
57
+
58
+ ### Required secrets
59
+
60
+ | Secret / Variable | Workflow | Required | Notes |
61
+ |---|---|---|---|
62
+ | `GITHUB_TOKEN` | All | Automatic | Provided by GitHub Actions; no setup needed |
63
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | `claude-attribution-export.yml` | One of these | OTLP/HTTP base URL (e.g. `https://otlp.datadoghq.com`, `https://otlp-gateway-<zone>.grafana.net/otlp`, `http://localhost:4318`). Unset = dry-run mode. |
64
+ | `OTEL_EXPORTER_OTLP_HEADERS` | `claude-attribution-export.yml` | Depends on backend | Comma-separated `key=value` auth headers (e.g. `DD-Api-Key=xxx` for Datadog, `x-honeycomb-team=xxx` for Honeycomb) |
65
+ | `DATADOG_API_KEY` | `claude-attribution-export.yml` | Datadog shortcut | Org-level secret; auto-configures the Datadog OTLP endpoint without needing `OTEL_EXPORTER_OTLP_ENDPOINT`. |
66
+ | `DATADOG_SITE` | `claude-attribution-export.yml` | No | Org-level variable; used with `DATADOG_API_KEY`. Defaults to `datadoghq.com`. |
67
+ | `METRICS_WEBHOOK_URL` | `claude-attribution-export.yml` | No | Posts a flat JSON payload to any HTTP endpoint on each PR merge. Can be used alongside OTLP. |
21
68
 
22
69
  ---
23
70
 
@@ -31,30 +78,16 @@ AI code attribution tracking for Claude Code. Measures which lines in a commit w
31
78
 
32
79
  ### One-time setup (per developer machine)
33
80
 
34
- **Option A — npm (recommended):**
35
-
36
81
  ```bash
37
82
  npm install -g claude-attribution
38
83
  ```
39
84
 
40
- **Option B — clone (if npm isn't available):**
41
-
42
- ```bash
43
- git clone git@github.com:DTS-Productivity-Engineering/claude-attribution.git ~/Code/claude-attribution
44
- cd ~/Code/claude-attribution
45
- bun install
46
- ```
47
-
48
85
  ### Install into a repo (per repo, per developer)
49
86
 
50
87
  **Step 1 — Run the installer:**
51
88
 
52
89
  ```bash
53
- # npm install:
54
90
  claude-attribution install ~/Code/your-repo
55
-
56
- # clone install:
57
- bun ~/Code/claude-attribution/src/setup/install.ts ~/Code/your-repo
58
91
  ```
59
92
 
60
93
  **Step 2 — Declare your attribution baseline (`init`):**
@@ -70,6 +103,9 @@ claude-attribution init --human
70
103
 
71
104
  # Not sure? Run with no flag — same as --human, prints a confirmation:
72
105
  claude-attribution init
106
+
107
+ # Repo has mixed history — mark commits after a specific date as AI-written:
108
+ claude-attribution init --ai-since 2025-01-01
73
109
  ```
74
110
 
75
111
  > **Why this matters:** Without `init`, the codebase-wide AI% starts at 0% and grows only from new commits. If your repo is all Claude Code, run `init --ai` now or the metrics will be misleading until the entire codebase has been re-committed line by line.
@@ -103,9 +139,14 @@ The installer makes the following changes to the target repo:
103
139
 
104
140
  **`.github/workflows/claude-attribution-pr.yml`** — GitHub Actions workflow that fires on every PR open and push. Injects metrics into the PR body automatically for PRs created outside Claude (Copilot, manual `gh pr create`, GitHub UI). Skips injection if the local `post-bash` hook already injected metrics on `opened`; always updates on `synchronize` (new commits).
105
141
 
106
- **`.claude/commands/`**installs two slash commands:
142
+ **`.github/workflows/claude-attribution-export.yml`**fires on every PR merge. Exports AI attribution metrics via OTLP/HTTP or a generic webhook. Supports Datadog, Grafana Cloud, Splunk Observability, New Relic, Honeycomb, and any OpenTelemetry Collector. Runs in dry-run mode (prints payload to stdout, exits 0) when no export destination is configured. See [Metrics Export](#metrics-export).
143
+
144
+ **`.github/workflows/claude-attribution-gha.yml`** *(optional — installed when claude-code-action is detected)* — fires on every push. Writes a 100% AI git note for commits made by known AI actors (e.g. `@claude` via claude-code-action, Copilot coding agent). Silent no-op for human commits. See [AI Actor Attribution](#ai-actor-attribution-copilot-bot-claude-gha).
145
+
146
+ **`.claude/commands/`** — installs three slash commands:
107
147
  - `/metrics` — generate a PR metrics report
108
- - `/start` — mark the start of a new Jira ticket session
148
+ - `/start` — mark the start of a new ticket session
149
+ - `/pr` — create a PR with metrics embedded
109
150
 
110
151
  **`.gitignore`** — adds `.claude/logs/` so tool usage logs don't end up in version control.
111
152
 
@@ -139,16 +180,12 @@ claude-attribution init --ai # only if repo was built 100% with Claude Code
139
180
  git push origin refs/notes/claude-attribution-map
140
181
  ```
141
182
 
142
- ### Re-installing after moving this directory
183
+ ### Re-installing
143
184
 
144
- If you move `~/Code/claude-attribution` to a different path, re-run the installer — it updates the absolute paths in `settings.json` and the git hook:
185
+ If you reinstall `claude-attribution` globally (e.g. after upgrading), re-run the installer — it updates the absolute paths in `settings.json` and the git hook:
145
186
 
146
187
  ```bash
147
- # npm install:
148
188
  claude-attribution install ~/Code/your-repo
149
-
150
- # clone install:
151
- bun src/setup/install.ts ~/Code/your-repo
152
189
  ```
153
190
 
154
191
  ### Uninstalling
@@ -156,14 +193,10 @@ bun src/setup/install.ts ~/Code/your-repo
156
193
  To remove claude-attribution from a repo:
157
194
 
158
195
  ```bash
159
- # npm install:
160
196
  claude-attribution uninstall ~/Code/your-repo
161
-
162
- # clone install:
163
- bun ~/Code/claude-attribution/src/setup/uninstall.ts ~/Code/your-repo
164
197
  ```
165
198
 
166
- This removes hooks from `.claude/settings.json`, removes `.git/hooks/post-commit`, removes the slash commands, removes `.github/workflows/claude-attribution-pr.yml`, and removes any legacy `pre-push` hooks (for example `.husky/pre-push` or `.git/hooks/pre-push`) if present. Attribution state (`.claude/attribution-state/`) and logs (`.claude/logs/`) are left in place. The `remote.origin.push` refspec is also removed from git config.
199
+ This removes hooks from `.claude/settings.json`, removes `.git/hooks/post-commit`, removes the slash commands, removes `.github/workflows/claude-attribution-pr.yml`, `.github/workflows/claude-attribution-export.yml`, and `.github/workflows/claude-attribution-gha.yml` (if present), and removes any legacy `pre-push` hooks (for example `.husky/pre-push` or `.git/hooks/pre-push`) if present. Attribution state (`.claude/attribution-state/`) and logs (`.claude/logs/`) are left in place. The `remote.origin.push` refspec is also removed from git config.
167
200
 
168
201
  ---
169
202
 
@@ -199,33 +232,36 @@ Metrics are injected automatically — no command needed.
199
232
 
200
233
  **On every new push to an open PR**: the workflow fires on `synchronize` and updates the attribution percentages to reflect new commits.
201
234
 
202
- The metrics block looks like (when the cumulative minimap exists):
203
-
204
- ```markdown
205
- ## Claude Code Metrics
235
+ The metrics block injected into the PR body looks like (when the cumulative minimap exists):
206
236
 
207
- **Codebase: ~77% AI** (3200 / 4150 lines)
208
- **This PR:** 184 lines changed (4% of codebase) · 77% Claude edits · 142 AI lines · Active: 8m
237
+ > ## Claude Code Metrics
238
+ >
239
+ > **Codebase: ~77% AI** (3200 / 4150 lines)
240
+ > **This PR:** 184 lines changed (4% of codebase) · 77% Claude edits · 142 AI lines · Active: 8m
241
+ >
242
+ > | Model | Calls | Input | Output | Cache |
243
+ > |-------|-------|-------|--------|-------|
244
+ > | Sonnet | 45 | 120K | 35K | 10K |
245
+ > | **Total** | 45 | 120K | 35K | 10K |
246
+ >
247
+ > **Human prompts (steering effort):** 12
248
+ >
249
+ > <details><summary>Tools · Agents · Files</summary>
250
+ >
251
+ > **Tools:** Edit ×47, Read ×31, Bash ×12
252
+ >
253
+ > </details>
209
254
 
210
- | Model | Calls | Input | Output | Cache |
211
- |-------|-------|-------|--------|-------|
212
- | Sonnet | 45 | 120K | 35K | 10K |
213
- | **Total** | 45 | 120K | 35K | 10K |
255
+ Before running `init --ai` (or on a fresh install with no minimap), the headline falls back to the session-only view:
214
256
 
215
- **Human prompts (steering effort):** 12
257
+ > **AI contribution: ~77%** (142 of 184 committed lines) · Active: 8m
216
258
 
217
- <details>
218
- <summary>Tools · Agents · Files</summary>
259
+ The block is wrapped in HTML comments for idempotent updates — re-running replaces the existing block rather than appending:
219
260
 
220
- **Tools:** Edit ×47, Read ×31, Bash ×12
221
- ...
222
- </details>
223
261
  ```
224
-
225
- Before running `init --ai` (or on a fresh install with no minimap), the headline falls back to the session-only view:
226
-
227
- ```markdown
228
- **AI contribution: ~77%** (142 of 184 committed lines) · Active: 8m
262
+ <!-- claude-attribution metrics -->
263
+ ...metrics content...
264
+ <!-- /claude-attribution metrics -->
229
265
  ```
230
266
 
231
267
  #### Manual option
@@ -233,7 +269,7 @@ Before running `init --ai` (or on a fresh install with no minimap), the headline
233
269
  If you need to create a PR with metrics outside of Claude, use the `/pr` slash command or CLI directly:
234
270
 
235
271
  ```bash
236
- claude-attribution pr "feat: COMM-1234 add user authentication"
272
+ claude-attribution pr "feat: PROJ-1234 add user authentication"
237
273
  claude-attribution pr "feat: my feature" --draft
238
274
  claude-attribution pr "feat: my feature" --base develop
239
275
  ```
@@ -246,7 +282,6 @@ To see the metrics output without creating a PR, use `/metrics` or run directly:
246
282
 
247
283
  ```bash
248
284
  claude-attribution metrics
249
- # or: bun ~/Code/claude-attribution/src/metrics/calculate.ts
250
285
  ```
251
286
 
252
287
  The output is markdown you paste into your PR description:
@@ -313,7 +348,7 @@ Example output:
313
348
  {
314
349
  "commit": "a3f1b2c",
315
350
  "session": "abc-123-...",
316
- "branch": "feature/COMM-1234",
351
+ "branch": "feature/PROJ-1234",
317
352
  "timestamp": "2026-03-26T15:32:00.000Z",
318
353
  "files": [
319
354
  { "path": "src/components/Foo.tsx", "ai": 82, "human": 10, "mixed": 2, "total": 94, "pctAi": 87 }
@@ -428,36 +463,139 @@ To force re-detection of the TypeScript runtime (e.g., after installing Bun): `r
428
463
 
429
464
  ---
430
465
 
431
- ## VP Dashboard (Datadog)
466
+ ## Metrics Export
432
467
 
433
- Attribution data is pushed to Datadog automatically on every PR merge via GitHub Actions (`.github/workflows/claude-attribution.yml`). No developer holds the API key it lives in an org-level GHA secret.
468
+ Attribution metrics are exported automatically on every PR merge via GitHub Actions (`.github/workflows/claude-attribution-export.yml`). The workflow uses the OpenTelemetry OTLP/HTTP JSON format and supports any OTel-compliant backend.
434
469
 
435
- **Metrics pushed on each merged PR:**
470
+ **Metrics exported on each merged PR:**
436
471
 
437
- | Metric | Description |
438
- |--------|-------------|
439
- | `claude_attribution.ai_lines` | Lines written by Claude and committed unchanged |
440
- | `claude_attribution.human_lines` | Lines written or left unchanged by the developer |
441
- | `claude_attribution.total_lines` | Total committed lines in the PR |
442
- | `claude_attribution.pct_ai` | Percentage of lines attributed to Claude (this PR) |
443
- | `claude_attribution.codebase_pct_ai` | Cumulative codebase-wide AI% at PR merge time (requires minimap) |
444
- | `claude_attribution.codebase_total_lines` | Total codebase lines tracked in the minimap |
445
- | `github_copilot.acceptance_rate` | Org-level Copilot suggestion acceptance rate |
446
- | `github_copilot.lines_accepted` | Copilot lines accepted org-wide |
447
- | `github_copilot.lines_suggested` | Copilot lines suggested org-wide |
472
+ | Metric | Unit | Description |
473
+ |--------|------|-------------|
474
+ | `claude_attribution.ai_lines` | lines | Lines written by Claude and committed unchanged |
475
+ | `claude_attribution.human_lines` | lines | Lines written or left unchanged by the developer |
476
+ | `claude_attribution.total_lines` | lines | Total committed lines in the PR |
477
+ | `claude_attribution.pct_ai` | % | Percentage of lines attributed to Claude (this PR) |
478
+ | `claude_attribution.codebase_pct_ai` | % | Cumulative codebase-wide AI% at PR merge time (requires minimap) |
479
+ | `claude_attribution.codebase_total_lines` | lines | Total codebase lines tracked in the minimap |
480
+ | `claude_attribution.cost_usd` | $ | Estimated Claude API cost for this PR (requires v1.5.0+ notes) |
481
+ | `claude_attribution.input_tokens` | tokens | Total input tokens consumed by Claude in this PR |
482
+ | `claude_attribution.output_tokens` | tokens | Total output tokens generated by Claude in this PR |
483
+ | `claude_attribution.cache_read_tokens` | tokens | Cache read tokens in this PR |
484
+ | `claude_attribution.cache_creation_tokens` | tokens | Cache creation tokens in this PR |
448
485
 
449
- All metrics are tagged `repo:`, `pr:`, `branch:`, `author:`, `tool:` — enabling side-by-side Claude vs. Copilot comparison on a single dashboard.
486
+ Token and cost metrics are only emitted when the git notes contain token data (written by v1.5.0+ hooks). All metrics carry attributes `pr`, `branch`, `author`, `tool` — enabling per-PR trend analysis.
450
487
 
451
- > **Important limitation — Copilot metrics are org-level aggregates, not per-PR or per-developer:**
452
- > GitHub's Copilot usage API returns org-wide daily totals (suggestions shown, lines accepted, acceptance rate). These are the same numbers regardless of which PR triggered the push. The Copilot rows in Datadog reflect organization-wide Copilot activity at the time of the push — they cannot be scoped to a specific PR, branch, or developer. In contrast, `claude_attribution.*` metrics are per-PR and per-file, derived from git notes written at commit time.
453
- >
454
- > If you need per-seat or per-repo Copilot breakdowns, use the GitHub Copilot usage dashboard directly. The org-level Copilot numbers here are useful as a trend signal alongside Claude attribution data.
488
+ **Supported backends:**
489
+
490
+ | Backend | Configuration |
491
+ |---------|---------------|
492
+ | **Datadog** (shortcut) | Set `DATADOG_API_KEY` secret (and optionally `DATADOG_SITE` org variable, defaults to `datadoghq.com`). Endpoint is auto-configured. |
493
+ | **Datadog** (explicit) | `OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.datadoghq.com`, `OTEL_EXPORTER_OTLP_HEADERS=DD-Api-Key=<key>` |
494
+ | **Grafana Cloud** | `OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-<zone>.grafana.net/otlp`, `OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic <base64(user:token)>` |
495
+ | **Splunk Observability** | `OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.<realm>.signalfx.com/v2/datapoint/otlp`, `OTEL_EXPORTER_OTLP_HEADERS=X-SF-Token=<token>` |
496
+ | **New Relic** | `OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net`, `OTEL_EXPORTER_OTLP_HEADERS=api-key=<key>` |
497
+ | **Honeycomb** | `OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io`, `OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=<key>` |
498
+ | **OTel Collector** | `OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4318` |
499
+ | **Generic webhook** | `METRICS_WEBHOOK_URL=https://...` — POSTs a flat JSON payload with `pr`, `repo`, `author`, `branch`, `ai_lines`, `human_lines`, `total_lines`, `pct_ai`, and optionally `codebase_pct_ai` / `codebase_total_lines` / `cost_usd` / `input_tokens` / `output_tokens` |
500
+
501
+ When no destination is configured, the workflow runs in **dry-run mode** — it prints the OTLP payload to stdout and exits 0. This makes it safe to install and test before secrets are set.
502
+
503
+ Set secrets at the org level so they apply to all repos that use this tool. Multiple export destinations can be active simultaneously (e.g. both `OTEL_EXPORTER_OTLP_ENDPOINT` and `METRICS_WEBHOOK_URL`).
504
+
505
+ **Model pricing (org variables — update when Anthropic changes pricing):**
506
+
507
+ | Variable | Default | Description |
508
+ |----------|---------|-------------|
509
+ | `CLAUDE_PRICE_OPUS_INPUT` | `15.00` | $ per 1M input tokens (Claude Opus) |
510
+ | `CLAUDE_PRICE_OPUS_OUTPUT` | `75.00` | $ per 1M output tokens (Claude Opus) |
511
+ | `CLAUDE_PRICE_SONNET_INPUT` | `3.00` | $ per 1M input tokens (Claude Sonnet) |
512
+ | `CLAUDE_PRICE_SONNET_OUTPUT` | `15.00` | $ per 1M output tokens (Claude Sonnet) |
513
+ | `CLAUDE_PRICE_HAIKU_INPUT` | `0.80` | $ per 1M input tokens (Claude Haiku) |
514
+ | `CLAUDE_PRICE_HAIKU_OUTPUT` | `4.00` | $ per 1M output tokens (Claude Haiku) |
515
+ | `CLAUDE_PRICE_CACHE_READ_MULT` | `0.1` | Fraction of input price for cache reads |
516
+ | `CLAUDE_PRICE_CACHE_WRITE_MULT` | `1.25` | Fraction of input price for cache writes |
517
+
518
+ Unrecognized model names (new Claude releases) fall back to Opus pricing. Set these as org-level **variables** (not secrets) — they're not sensitive.
519
+
520
+
521
+ ---
522
+
523
+ ## AI Actor Attribution (Copilot Bot, @claude GHA)
524
+
525
+ The local post-commit hook only runs when code is committed on a developer's machine. Commits made server-side by AI bots — such as `@claude` via [claude-code-action](https://github.com/anthropics/claude-code-action) or the Copilot coding agent — bypass the local hook entirely and would otherwise appear as 100% human in metrics.
526
+
527
+ claude-attribution handles these two ways:
455
528
 
456
- **Required secrets (set once at org level):**
457
- - `DATADOG_API_KEY` — Datadog API key
458
- - `DATADOG_SITE` variable e.g. `datadoghq.com`
529
+ ### 1. Auto-detection in metrics (no setup required)
530
+
531
+ When generating PR metrics, branch commits that have no git note are checked for known AI actor signals:
532
+
533
+ | Signal | Example |
534
+ |---|---|
535
+ | Bot author email/name | `github-actions[bot]`, `copilot[bot]` |
536
+ | `Co-authored-by:` trailer | `Co-authored-by: Claude <...>` |
537
+ | `Co-authored-by:` trailer | `Co-authored-by: GitHub Copilot <...>` |
538
+
539
+ If detected, all non-blank committed lines are counted as AI in the metrics output. No git note is written — this is a metrics-time synthesis only.
540
+
541
+ **Not detectable:** Copilot "Commit suggestion" (the button on PR review comments). Those commits are authored as the human who clicked the button — there is no metadata distinguishing them from a human edit. They will count as HUMAN.
542
+
543
+ ### 2. `note-ai-commit` command (writes a permanent git note)
544
+
545
+ For `@claude` via claude-code-action, you can write a permanent git note at CI time so attribution is durable (survives metrics regeneration, appears in Datadog dashboard):
546
+
547
+ ```bash
548
+ # Write a 100% AI note for HEAD (then push the note):
549
+ claude-attribution note-ai-commit --push
550
+
551
+ # Only write the note if the commit looks like an AI actor commit (safe on every push):
552
+ claude-attribution note-ai-commit --if-ai-actor --push
553
+
554
+ # Write a note for a specific SHA:
555
+ claude-attribution note-ai-commit abc1234 --push
556
+ ```
557
+
558
+ #### Setting up for claude-code-action
559
+
560
+ If your repo uses [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action), run `claude-attribution install` — it detects the action and offers to install `claude-attribution-gha.yml`, which records attribution automatically on every AI actor push.
561
+
562
+ To install the workflow manually, add `.github/workflows/claude-attribution-gha.yml`:
563
+
564
+ ```yaml
565
+ name: Claude Attribution — AI Actor Commits
566
+
567
+ on:
568
+ push:
569
+ branches: ["**"]
570
+
571
+ permissions:
572
+ contents: write
573
+
574
+ jobs:
575
+ note-ai-commit:
576
+ name: Record AI actor commit attribution
577
+ runs-on: ubuntu-latest # replace with your self-hosted runner label if needed
578
+ steps:
579
+ - uses: actions/checkout@v4
580
+ with:
581
+ fetch-depth: 0
582
+ token: ${{ secrets.GITHUB_TOKEN }}
583
+
584
+ - name: Fetch attribution notes
585
+ run: git fetch origin refs/notes/claude-attribution:refs/notes/claude-attribution || true
586
+
587
+ - uses: oven-sh/setup-bun@v2
588
+
589
+ - name: Install claude-attribution
590
+ run: |
591
+ npm install -g --prefix "${HOME}/.npm-global" claude-attribution
592
+ echo "${HOME}/.npm-global/bin" >> "$GITHUB_PATH"
593
+
594
+ - name: Record attribution if AI actor commit
595
+ run: claude-attribution note-ai-commit --if-ai-actor --push
596
+ ```
459
597
 
460
- **Future:** When Faros is purchased, ADMPLAT-9609 will provide a Faros pipeline. Switching destinations is a one-line change to `src/export/pr-summary.ts` the data collection, notes format, and GHA trigger all stay the same.
598
+ The `--if-ai-actor` flag makes this a silent no-op for human commits it only fires when the commit author or message matches a known AI actor pattern.
461
599
 
462
600
  ---
463
601
 
@@ -504,7 +642,7 @@ Each hook invocation is a short-lived process that exits immediately. The trace
504
642
 
505
643
  The post-commit hook may not have run. Check:
506
644
  1. Is `bun` (or `tsx`) on your PATH in a git hook context? Run `which bun` from your shell, then check if that path is in `.git/hooks/post-commit`.
507
- 2. Did you run `bun src/setup/install.ts <repo>` for this specific repo?
645
+ 2. Did you run `claude-attribution install <repo>` for this specific repo?
508
646
  3. Check `.claude/logs/attribution.jsonl` — if it's empty, the hook isn't firing.
509
647
 
510
648
  **Attribution is 0% AI even though Claude wrote everything**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-attribution",
3
- "version": "1.2.9",
3
+ "version": "1.5.0",
4
4
  "description": "AI code attribution tracking for Claude Code sessions — checkpoint-based line diff approach",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@ import {
34
34
  type LineAttribution,
35
35
  hashLine,
36
36
  } from "./differ.ts";
37
+ import { parseTranscript } from "../metrics/transcript.ts";
37
38
  import {
38
39
  writeNote,
39
40
  headSha,
@@ -146,6 +147,21 @@ async function main() {
146
147
  totals: aggregateTotals(fileResults),
147
148
  };
148
149
 
150
+ // Attach token usage from the Claude session transcript (non-fatal if unavailable)
151
+ if (sessionId) {
152
+ const tx = await parseTranscript(sessionId, repoRoot).catch(() => null);
153
+ if (tx && tx.byModel.length > 0) {
154
+ result.modelUsage = tx.byModel.map((m) => ({
155
+ modelFull: m.modelFull,
156
+ modelShort: m.modelShort,
157
+ inputTokens: m.inputTokens,
158
+ outputTokens: m.outputTokens,
159
+ cacheCreationTokens: m.cacheCreationTokens,
160
+ cacheReadTokens: m.cacheReadTokens,
161
+ }));
162
+ }
163
+ }
164
+
149
165
  // Write git note
150
166
  await writeNote(result, repoRoot);
151
167
 
@@ -21,6 +21,16 @@ export interface FileAttribution {
21
21
  pctAi: number;
22
22
  }
23
23
 
24
+ /** Token usage captured from the Claude session transcript at commit time. */
25
+ export interface CommitModelUsage {
26
+ modelFull: string;
27
+ modelShort: "Opus" | "Sonnet" | "Haiku" | "Unknown";
28
+ inputTokens: number;
29
+ outputTokens: number;
30
+ cacheCreationTokens: number;
31
+ cacheReadTokens: number;
32
+ }
33
+
24
34
  export interface AttributionResult {
25
35
  commit: string;
26
36
  /** Session ID from .claude/attribution-state/current-session, or null if committed outside a Claude session. */
@@ -30,6 +40,8 @@ export interface AttributionResult {
30
40
  timestamp: string;
31
41
  files: FileAttribution[];
32
42
  totals: Omit<FileAttribution, "path">;
43
+ /** Token usage from the Claude session that produced this commit. Absent for commits outside a Claude session or pre-v1.5.0 notes. */
44
+ modelUsage?: CommitModelUsage[];
33
45
  }
34
46
 
35
47
  /**
@@ -13,7 +13,11 @@
13
13
  */
14
14
  import { execFile } from "child_process";
15
15
  import { promisify } from "util";
16
- import type { AttributionResult } from "./differ.ts";
16
+ import {
17
+ aggregateTotals,
18
+ type AttributionResult,
19
+ type FileAttribution,
20
+ } from "./differ.ts";
17
21
 
18
22
  const execFileAsync = promisify(execFile);
19
23
  const NOTES_REF = "refs/notes/claude-attribution";
@@ -183,3 +187,129 @@ export async function committedContent(
183
187
  return null;
184
188
  }
185
189
  }
190
+
191
+ /**
192
+ * Read the committed content of a file at any commit SHA.
193
+ * Returns null for deleted files or binary files should not be processed.
194
+ */
195
+ export async function committedContentAt(
196
+ repoRoot: string,
197
+ sha: string,
198
+ relPath: string,
199
+ ): Promise<string | null> {
200
+ try {
201
+ return await runRaw("git", ["show", `${sha}:${relPath}`], repoRoot);
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * List relative paths of files changed in any commit (not just HEAD).
209
+ */
210
+ export async function filesInCommitAt(
211
+ repoRoot: string,
212
+ sha: string,
213
+ ): Promise<string[]> {
214
+ const output = await run(
215
+ "git",
216
+ ["diff-tree", "--no-commit-id", "-r", "--name-only", sha],
217
+ repoRoot,
218
+ );
219
+ return output ? output.split("\n").filter(Boolean) : [];
220
+ }
221
+
222
+ export interface CommitMeta {
223
+ authorName: string;
224
+ authorEmail: string;
225
+ /** Full commit message body. */
226
+ message: string;
227
+ /** Committer date in ISO 8601 format. */
228
+ timestamp: string;
229
+ }
230
+
231
+ /**
232
+ * Get author, email, message, and timestamp for any commit SHA.
233
+ */
234
+ export async function getCommitMeta(
235
+ repoRoot: string,
236
+ sha: string,
237
+ ): Promise<CommitMeta> {
238
+ const nameEmail = await run(
239
+ "git",
240
+ ["log", "-1", "--format=%an\n%ae", sha],
241
+ repoRoot,
242
+ );
243
+ const [authorName = "", authorEmail = ""] = nameEmail.split("\n");
244
+ const message = await run("git", ["log", "-1", "--format=%B", sha], repoRoot);
245
+ const timestamp = await run(
246
+ "git",
247
+ ["log", "-1", "--format=%cI", sha],
248
+ repoRoot,
249
+ );
250
+ return { authorName, authorEmail, message, timestamp };
251
+ }
252
+
253
+ /**
254
+ * Return true if the commit was made by a known AI actor:
255
+ * - Bot authors: github-actions[bot], copilot[bot], etc.
256
+ * - Co-authored-by trailers referencing Claude or Copilot.
257
+ *
258
+ * Used to synthesize 100% AI attribution for commits that bypass the local
259
+ * post-commit hook (e.g. @claude via claude-code-action, Copilot coding agent).
260
+ */
261
+ export function isKnownAiActorCommit(meta: CommitMeta): boolean {
262
+ const { authorName, authorEmail, message } = meta;
263
+ if (authorEmail.includes("[bot]") || authorName.includes("[bot]"))
264
+ return true;
265
+ if (/co-authored-by:.*claude/i.test(message)) return true;
266
+ if (/co-authored-by:.*copilot/i.test(message)) return true;
267
+ return false;
268
+ }
269
+
270
+ /**
271
+ * Build a 100% AI AttributionResult for a commit without running the
272
+ * checkpoint-based differ. All non-blank committed lines are marked AI.
273
+ *
274
+ * Used by `note-ai-commit` (to write git notes in GHA) and by `collect.ts`
275
+ * (to synthesize attribution at metrics time for unattributed AI actor commits).
276
+ */
277
+ export async function buildAllAiResult(
278
+ repoRoot: string,
279
+ sha: string,
280
+ ): Promise<AttributionResult> {
281
+ const [changedFiles, meta] = await Promise.all([
282
+ filesInCommitAt(repoRoot, sha),
283
+ getCommitMeta(repoRoot, sha),
284
+ ]);
285
+
286
+ const fileResults: FileAttribution[] = (
287
+ await Promise.all(
288
+ changedFiles.map(async (relPath): Promise<FileAttribution | null> => {
289
+ const content = await committedContentAt(repoRoot, sha, relPath);
290
+ if (content === null || content.includes("\0")) return null;
291
+ const lines = content.split("\n");
292
+ const ai = lines.filter((l) => l.trim().length > 0).length;
293
+ const human = lines.length - ai;
294
+ const total = lines.length;
295
+ return {
296
+ path: relPath,
297
+ ai,
298
+ human,
299
+ mixed: 0,
300
+ total,
301
+ pctAi: total > 0 ? Math.round((ai / total) * 100) : 0,
302
+ };
303
+ }),
304
+ )
305
+ ).filter((f): f is FileAttribution => f !== null);
306
+
307
+ return {
308
+ commit: sha,
309
+ session: null,
310
+ branch: null,
311
+ timestamp: meta.timestamp,
312
+ files: fileResults,
313
+ totals: aggregateTotals(fileResults),
314
+ };
315
+ }
package/src/cli.ts CHANGED
@@ -30,6 +30,12 @@ switch (cmd) {
30
30
  case "pr":
31
31
  await import("./commands/pr.ts");
32
32
  break;
33
+ case "note-ai-commit":
34
+ await import("./commands/note-ai-commit.ts");
35
+ break;
36
+ case "pr-summary":
37
+ await import("./export/pr-summary.ts");
38
+ break;
33
39
  case "init":
34
40
  await import("./commands/init.ts");
35
41
  break;
@@ -93,6 +99,8 @@ Commands:
93
99
  metrics [id] Generate PR metrics report
94
100
  pr [title] Create PR with metrics embedded (--draft, --base <branch>)
95
101
  init [--ai | --ai-since <YYYY-MM-DD>] Set attribution baseline in the cumulative minimap
102
+ note-ai-commit [sha] [--push] [--if-ai-actor] Write 100% AI git note for a commit (GHA use)
103
+ pr-summary Export PR attribution metrics via OTLP/webhook (GHA use)
96
104
  start Mark session start for per-ticket scoping
97
105
  hook <name> Run an internal hook (used by installed git hooks)
98
106
  version Print version