aimpg 0.6.0__tar.gz → 0.6.2__tar.gz

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.
Files changed (98) hide show
  1. {aimpg-0.6.0 → aimpg-0.6.2}/PKG-INFO +27 -18
  2. {aimpg-0.6.0 → aimpg-0.6.2}/README.md +26 -17
  3. {aimpg-0.6.0 → aimpg-0.6.2}/action.yml +3 -1
  4. aimpg-0.6.2/aimpg/card.py +151 -0
  5. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/cli.py +22 -1
  6. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/coach.py +22 -6
  7. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/cost.py +3 -0
  8. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/live.py +14 -4
  9. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/prices.json +17 -2
  10. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/receipt.py +4 -2
  11. aimpg-0.6.2/aimpg/replay/chain.py +67 -0
  12. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/cli.py +25 -4
  13. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/fake_agent.py +10 -1
  14. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/run.py +44 -13
  15. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/sandbox.py +25 -6
  16. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/select.py +24 -6
  17. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/setups.py +45 -2
  18. aimpg-0.6.2/aimpg/replay/study.py +215 -0
  19. aimpg-0.6.2/aimpg/replay/study_cli.py +176 -0
  20. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/verify.py +35 -11
  21. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/workspace.py +71 -10
  22. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/scoreboard.py +6 -1
  23. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/share.py +1 -1
  24. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/team.py +3 -2
  25. aimpg-0.6.2/docs/FAQ.md +56 -0
  26. aimpg-0.6.2/docs/designs/launch-brief.md +82 -0
  27. aimpg-0.6.2/docs/receipt-card.png +0 -0
  28. aimpg-0.6.2/docs/study/report-1/PREREGISTRATION.md +73 -0
  29. aimpg-0.6.2/docs/study/report-1/plan.json +28 -0
  30. {aimpg-0.6.0 → aimpg-0.6.2}/pyproject.toml +2 -2
  31. {aimpg-0.6.0 → aimpg-0.6.2}/tests/conftest.py +3 -0
  32. aimpg-0.6.2/tests/test_card.py +31 -0
  33. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_coach.py +2 -1
  34. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_cost_and_equivalence.py +10 -0
  35. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_live.py +1 -1
  36. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_e2e.py +26 -0
  37. aimpg-0.6.2/tests/test_security.py +96 -0
  38. aimpg-0.6.2/tests/test_study.py +101 -0
  39. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_verify.py +14 -3
  40. {aimpg-0.6.0 → aimpg-0.6.2}/uv.lock +1 -1
  41. {aimpg-0.6.0 → aimpg-0.6.2}/.github/workflows/publish.yml +0 -0
  42. {aimpg-0.6.0 → aimpg-0.6.2}/.github/workflows/test.yml +0 -0
  43. {aimpg-0.6.0 → aimpg-0.6.2}/.gitignore +0 -0
  44. {aimpg-0.6.0 → aimpg-0.6.2}/LICENSE +0 -0
  45. {aimpg-0.6.0 → aimpg-0.6.2}/TODOS.md +0 -0
  46. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/__init__.py +0 -0
  47. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/attribution.py +0 -0
  48. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/codex_logs.py +0 -0
  49. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/cursor_usage.py +0 -0
  50. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/defaults.py +0 -0
  51. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/durable.py +0 -0
  52. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/energy.py +0 -0
  53. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/equivalence.py +0 -0
  54. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/equivalences.json +0 -0
  55. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/factors.json +0 -0
  56. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/gitkept.py +0 -0
  57. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/ledger.py +0 -0
  58. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/logs.py +0 -0
  59. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/model.py +0 -0
  60. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/__init__.py +0 -0
  61. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/hints.py +0 -0
  62. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/picker.py +0 -0
  63. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/proxy.py +0 -0
  64. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/replay/stats.py +0 -0
  65. {aimpg-0.6.0 → aimpg-0.6.2}/aimpg/sci.py +0 -0
  66. {aimpg-0.6.0 → aimpg-0.6.2}/docs/STRATEGY.md +0 -0
  67. {aimpg-0.6.0 → aimpg-0.6.2}/docs/designs/aimpg-design.md +0 -0
  68. {aimpg-0.6.0 → aimpg-0.6.2}/docs/designs/scoreboard-brief.md +0 -0
  69. {aimpg-0.6.0 → aimpg-0.6.2}/evals/attribution_eval.py +0 -0
  70. {aimpg-0.6.0 → aimpg-0.6.2}/evals/label.py +0 -0
  71. {aimpg-0.6.0 → aimpg-0.6.2}/spikes/agent/RESULTS.md +0 -0
  72. {aimpg-0.6.0 → aimpg-0.6.2}/spikes/agent/allowlist_proxy.py +0 -0
  73. {aimpg-0.6.0 → aimpg-0.6.2}/spikes/agent/make_profile.py +0 -0
  74. {aimpg-0.6.0 → aimpg-0.6.2}/spikes/egress/RESULTS.md +0 -0
  75. {aimpg-0.6.0 → aimpg-0.6.2}/spikes/egress/allowlist_proxy.py +0 -0
  76. {aimpg-0.6.0 → aimpg-0.6.2}/spikes/egress/replay.sb +0 -0
  77. {aimpg-0.6.0 → aimpg-0.6.2}/tests/gitrepo.py +0 -0
  78. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_attribution.py +0 -0
  79. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_codex_logs.py +0 -0
  80. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_cursor_usage.py +0 -0
  81. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_defaults.py +0 -0
  82. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_durable.py +0 -0
  83. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_energy.py +0 -0
  84. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_eval_scoring.py +0 -0
  85. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_gitkept.py +0 -0
  86. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_logs.py +0 -0
  87. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_perf.py +0 -0
  88. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_cli.py +0 -0
  89. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_hints.py +0 -0
  90. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_picker.py +0 -0
  91. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_proxy.py +0 -0
  92. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_sandbox.py +0 -0
  93. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_replay_stats.py +0 -0
  94. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_sandbox_required.py +0 -0
  95. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_sci.py +0 -0
  96. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_scoreboard.py +0 -0
  97. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_share.py +0 -0
  98. {aimpg-0.6.0 → aimpg-0.6.2}/tests/test_team.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aimpg
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Real-world energy per solved task for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/kumarganduri/aimpg
6
6
  Project-URL: Issues, https://github.com/kumarganduri/aimpg/issues
@@ -26,25 +26,27 @@ Description-Content-Type: text/markdown
26
26
  uvx aimpg report
27
27
  ```
28
28
 
29
+ **New here? [aimpg in 5 minutes](https://kumarganduri.github.io/aimpg-scoreboard/start/)** walks you from install to your first numbers, the live status line, and your team's GitHub Action, with copy buttons for every command.
30
+
29
31
  ```
30
32
  YOUR AI CODING
31
- Energy 14.46 kWh – 130.59 kWh
32
- ≈ boiling a litre of water in a kettle 140–1,300 times · driving an electric car 85–770 km · 6.6–59.8 kg CO₂
33
- Money $1,284 API-equivalent (Anthropic's published prices)
34
- Output 353 commits made with AI help
33
+ Energy 17.31 kWh – 156.95 kWh
34
+ Money $1,567 at API list prices (Anthropic's and OpenAI's; a subscription costs less)
35
+ Output 454 commits made with AI help
35
36
 
36
- A typical kept commit: 36.6 Wh – 313.0 Wh ≈ 2–18 full phone charges · $2.05
37
+ A typical kept commit: 20.0 Wh – 179.3 Wh · $1.63
37
38
 
38
39
  WHAT WOULD HAVE SAVED THE MOST (measured on your logs; upper bounds that overlap)
39
- 1. Start a fresh session after each commit: up to 45% less energy, $397
40
- 45% of your AI energy went to re-reading conversation from before your last commit.
41
- → After committing, start a new session (or /clear) for the next task.
42
- 2. Use a mid-size model for routine work: up to 16% less energy, $333
43
- 94% of your AI energy ran on the largest models (Opus/Fable class).
44
- → Check a cheaper model is good enough on your own commits: `aimpg replay models`.
40
+ 1. Start a fresh session after each commit: up to 41%–46% less energy, up to $457
41
+ 57% of the tokens your agents processed were conversation carried over from before your last commit.
42
+ A fresh session has to rebuild some context, which isn't counted here, so the real saving is smaller.
43
+ → After committing, start a new session (or /clear) for the next task, and give it a one-line summary if needed.
44
+ 2. Use a mid-size model for routine work: up to 17% less energy, $408
45
+ 95% of your AI energy ran on the largest models (Opus/Fable class).
46
+ → Try Sonnet for routine fixes; check it's good enough on your own commits with `aimpg replay` (model picker).
45
47
 
46
48
  WORKING CHANGES (commits that shipped and lasted 30 days)
47
- Available on 2026-10-22: aimpg is keeping your history so it can tell.
49
+ Available on 2026-10-13: aimpg is keeping your history so it can tell.
48
50
  ```
49
51
 
50
52
  *(Real output from the author's last 30 days.)*
@@ -68,7 +70,11 @@ aimpg report --cursor-usage ~/Downloads/usage-events.csv --cursor-repo ~/code/my
68
70
  Team export? Add `--cursor-user "Your Name"` to count only your rows.
69
71
 
70
72
  ### 1. See your AI footprint in terms you can picture
71
- `aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the API-equivalent cost at Anthropic's published prices (for subscribers, what the same work would cost on the API).
73
+ `aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the cost at Anthropic's and OpenAI's API list prices; on a subscription you pay less, so read it as the value of the work, not your bill.
74
+
75
+ `aimpg report --share` also writes a card to post, with totals only (no repo names, commit messages or code), and prints a one-line post to go with it:
76
+
77
+ ![aimpg share card](docs/receipt-card.png)
72
78
 
73
79
  ### 2. Get tips measured on your own habits, not generic advice
74
80
  The receipt replays your own logs under "what if" rules and tells you what would have saved the most, in Wh and dollars. For example: how much energy went into re-reading old conversation in long sessions, or into running the biggest model for routine fixes.
@@ -113,7 +119,7 @@ VERDICT: NOT PROVEN
113
119
 
114
120
  Energy, same tasks: +10% (interval -11%…+35%, 17 shared solved commits)
115
121
  ```
116
- That's the real result on the author's Legwork repo: no saving shown, far from the advertised 60–90%. Your repo may differ, and now you can check.
122
+ That's the real result on the author's Legwork repo: no reliable energy saving shown on one repo (RTK advertises token savings; a larger, pre-registered study across several public repos is under way). Your repo may differ, and now you can check.
117
123
 
118
124
  Every run writes a **record** (`~/.aimpg/verify/*.record.json`) holding versions, both setups, each run's tokens, $ and outcome, and the verdict, but never code, diffs or commit messages. Repo and commits are hashed unless you add `--public`. Anyone can recheck the math and the record's consistency for free with `aimpg verify --check record.json` (it can't prove the runs happened), and rerun a public record on their own machine with `aimpg verify --rerun record.json --repo <clone>`, which first shows every command, hook and prompt the record would run and asks before running them. Private records keep only fingerprints (SHA-256) of your prompts, CLAUDE.md, settings and commands, never the text. Records also list every redone attempt and excluded commit.
119
125
 
@@ -147,7 +153,7 @@ jobs:
147
153
  steps:
148
154
  - uses: actions/checkout@v4
149
155
  with: {fetch-depth: 0}
150
- - uses: kumarganduri/aimpg@v0.6.0
156
+ - uses: kumarganduri/aimpg@v0.6.2 # or pin a commit SHA: it runs with your repo token
151
157
  with:
152
158
  mode: ${{ github.event_name == 'pull_request' && 'pr' || 'rollup' }}
153
159
  comment: "true"
@@ -177,14 +183,17 @@ For a standard figure, `aimpg sci --json sci.json` reports your carbon intensity
177
183
 
178
184
  **Working changes:** aimpg keeps a small history of what each commit cost (never prompts or code), because Claude Code deletes its own logs after 30 days. Once a commit is 30 days old, it is judged: still on the main branch and not reverted or largely rewritten counts as a *working change*. The receipt then shows **cost per working change** and a **waste ratio** (AI spend on work that didn't last).
179
185
 
180
- **Energy:** a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
186
+ **Energy:** an estimate with stated assumption ranges, not a measurement: a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
181
187
 
182
188
  **Replays:** each run starts from the code just before your commit, in a fresh, history-free copy, inside a sandbox (Seatbelt on macOS, bubblewrap on Linux). The only network allowed is the model API, through an allowlisting proxy. Your own tests judge the result, and edited tests are always restored before judging. Every token count is cross-checked against Claude Code's own totals. Details are in [docs/designs/aimpg-design.md](docs/designs/aimpg-design.md).
183
189
 
190
+ Questions people ask (is it sponsored, are the energy numbers guesses, is it safe to run): see the [FAQ](docs/FAQ.md).
191
+
184
192
  ## Limits (honest list)
185
193
 
186
194
  - Claude Code and Codex CLI logs are read automatically. Cursor keeps token usage on its servers, so it needs its usage export (below), and that export doesn't say which folder the work was in: give `--cursor-repo` and requests are matched to your next own commit there by time alone (shown as "by time only"). Replays and `verify` need macOS or Linux and an API key (about $0.10–0.30 per run). On Linux install bubblewrap (`sudo apt install bubblewrap`); Ubuntu 24.04+ also needs a one-time AppArmor rule for bwrap, which aimpg prints if it's missing.
187
- - Energy is an estimate with a wide range; dollars are close (within about 7% of Claude Code's own session totals on the author's logs).
195
+ - Windows: `report`, `live` and the team commands are untested there; replays and `verify` need macOS or Linux.
196
+ - Energy is an estimate with a wide range (model sizes are guesses within stated bounds); dollars are close (within about 7% of Claude Code's own session totals on the author's logs).
188
197
  - Tips are upper bounds and overlap; they can't be added together.
189
198
  - Replays from commit messages alone are hard (12% solved on the author's repo); `--task-mode tests` shows the agent the tests, which makes tasks easier than real work but keeps comparisons fair.
190
199
 
@@ -6,25 +6,27 @@
6
6
  uvx aimpg report
7
7
  ```
8
8
 
9
+ **New here? [aimpg in 5 minutes](https://kumarganduri.github.io/aimpg-scoreboard/start/)** walks you from install to your first numbers, the live status line, and your team's GitHub Action, with copy buttons for every command.
10
+
9
11
  ```
10
12
  YOUR AI CODING
11
- Energy 14.46 kWh – 130.59 kWh
12
- ≈ boiling a litre of water in a kettle 140–1,300 times · driving an electric car 85–770 km · 6.6–59.8 kg CO₂
13
- Money $1,284 API-equivalent (Anthropic's published prices)
14
- Output 353 commits made with AI help
13
+ Energy 17.31 kWh – 156.95 kWh
14
+ Money $1,567 at API list prices (Anthropic's and OpenAI's; a subscription costs less)
15
+ Output 454 commits made with AI help
15
16
 
16
- A typical kept commit: 36.6 Wh – 313.0 Wh ≈ 2–18 full phone charges · $2.05
17
+ A typical kept commit: 20.0 Wh – 179.3 Wh · $1.63
17
18
 
18
19
  WHAT WOULD HAVE SAVED THE MOST (measured on your logs; upper bounds that overlap)
19
- 1. Start a fresh session after each commit: up to 45% less energy, $397
20
- 45% of your AI energy went to re-reading conversation from before your last commit.
21
- → After committing, start a new session (or /clear) for the next task.
22
- 2. Use a mid-size model for routine work: up to 16% less energy, $333
23
- 94% of your AI energy ran on the largest models (Opus/Fable class).
24
- → Check a cheaper model is good enough on your own commits: `aimpg replay models`.
20
+ 1. Start a fresh session after each commit: up to 41%–46% less energy, up to $457
21
+ 57% of the tokens your agents processed were conversation carried over from before your last commit.
22
+ A fresh session has to rebuild some context, which isn't counted here, so the real saving is smaller.
23
+ → After committing, start a new session (or /clear) for the next task, and give it a one-line summary if needed.
24
+ 2. Use a mid-size model for routine work: up to 17% less energy, $408
25
+ 95% of your AI energy ran on the largest models (Opus/Fable class).
26
+ → Try Sonnet for routine fixes; check it's good enough on your own commits with `aimpg replay` (model picker).
25
27
 
26
28
  WORKING CHANGES (commits that shipped and lasted 30 days)
27
- Available on 2026-10-22: aimpg is keeping your history so it can tell.
29
+ Available on 2026-10-13: aimpg is keeping your history so it can tell.
28
30
  ```
29
31
 
30
32
  *(Real output from the author's last 30 days.)*
@@ -48,7 +50,11 @@ aimpg report --cursor-usage ~/Downloads/usage-events.csv --cursor-repo ~/code/my
48
50
  Team export? Add `--cursor-user "Your Name"` to count only your rows.
49
51
 
50
52
  ### 1. See your AI footprint in terms you can picture
51
- `aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the API-equivalent cost at Anthropic's published prices (for subscribers, what the same work would cost on the API).
53
+ `aimpg report` reads the Claude Code (and Codex CLI) logs already on your machine and ties every AI request to the git commit it produced. Energy is shown as an honest range and translated into kettles, phone charges, EV kilometres and CO₂. Money is the cost at Anthropic's and OpenAI's API list prices; on a subscription you pay less, so read it as the value of the work, not your bill.
54
+
55
+ `aimpg report --share` also writes a card to post, with totals only (no repo names, commit messages or code), and prints a one-line post to go with it:
56
+
57
+ ![aimpg share card](docs/receipt-card.png)
52
58
 
53
59
  ### 2. Get tips measured on your own habits, not generic advice
54
60
  The receipt replays your own logs under "what if" rules and tells you what would have saved the most, in Wh and dollars. For example: how much energy went into re-reading old conversation in long sessions, or into running the biggest model for routine fixes.
@@ -93,7 +99,7 @@ VERDICT: NOT PROVEN
93
99
 
94
100
  Energy, same tasks: +10% (interval -11%…+35%, 17 shared solved commits)
95
101
  ```
96
- That's the real result on the author's Legwork repo: no saving shown, far from the advertised 60–90%. Your repo may differ, and now you can check.
102
+ That's the real result on the author's Legwork repo: no reliable energy saving shown on one repo (RTK advertises token savings; a larger, pre-registered study across several public repos is under way). Your repo may differ, and now you can check.
97
103
 
98
104
  Every run writes a **record** (`~/.aimpg/verify/*.record.json`) holding versions, both setups, each run's tokens, $ and outcome, and the verdict, but never code, diffs or commit messages. Repo and commits are hashed unless you add `--public`. Anyone can recheck the math and the record's consistency for free with `aimpg verify --check record.json` (it can't prove the runs happened), and rerun a public record on their own machine with `aimpg verify --rerun record.json --repo <clone>`, which first shows every command, hook and prompt the record would run and asks before running them. Private records keep only fingerprints (SHA-256) of your prompts, CLAUDE.md, settings and commands, never the text. Records also list every redone attempt and excluded commit.
99
105
 
@@ -127,7 +133,7 @@ jobs:
127
133
  steps:
128
134
  - uses: actions/checkout@v4
129
135
  with: {fetch-depth: 0}
130
- - uses: kumarganduri/aimpg@v0.6.0
136
+ - uses: kumarganduri/aimpg@v0.6.2 # or pin a commit SHA: it runs with your repo token
131
137
  with:
132
138
  mode: ${{ github.event_name == 'pull_request' && 'pr' || 'rollup' }}
133
139
  comment: "true"
@@ -157,14 +163,17 @@ For a standard figure, `aimpg sci --json sci.json` reports your carbon intensity
157
163
 
158
164
  **Working changes:** aimpg keeps a small history of what each commit cost (never prompts or code), because Claude Code deletes its own logs after 30 days. Once a commit is 30 days old, it is judged: still on the main branch and not reverted or largely rewritten counts as a *working change*. The receipt then shows **cost per working change** and a **waste ratio** (AI spend on work that didn't last).
159
165
 
160
- **Energy:** a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
166
+ **Energy:** an estimate with stated assumption ranges, not a measurement: a physical formula, not a price proxy. Prefill compute for new input, one KV-cache re-read per output token (so long contexts cost more), plus datacenter overhead. Model sizes aren't public, so every number is a range. Overheads follow Google's full-stack measurement of a median Gemini prompt (chips, host CPU and memory, idle capacity, cooling ≈ 1.7× the chips alone), and a chat-sized prompt on a mid-size model comes out at 0.017–0.34 Wh, bracketing Google's disclosed 0.24 Wh ([arXiv 2508.15734](https://arxiv.org/pdf/2508.15734)). Sources for every factor are in [`aimpg/factors.json`](aimpg/factors.json), [`aimpg/prices.json`](aimpg/prices.json) and [`aimpg/equivalences.json`](aimpg/equivalences.json).
161
167
 
162
168
  **Replays:** each run starts from the code just before your commit, in a fresh, history-free copy, inside a sandbox (Seatbelt on macOS, bubblewrap on Linux). The only network allowed is the model API, through an allowlisting proxy. Your own tests judge the result, and edited tests are always restored before judging. Every token count is cross-checked against Claude Code's own totals. Details are in [docs/designs/aimpg-design.md](docs/designs/aimpg-design.md).
163
169
 
170
+ Questions people ask (is it sponsored, are the energy numbers guesses, is it safe to run): see the [FAQ](docs/FAQ.md).
171
+
164
172
  ## Limits (honest list)
165
173
 
166
174
  - Claude Code and Codex CLI logs are read automatically. Cursor keeps token usage on its servers, so it needs its usage export (below), and that export doesn't say which folder the work was in: give `--cursor-repo` and requests are matched to your next own commit there by time alone (shown as "by time only"). Replays and `verify` need macOS or Linux and an API key (about $0.10–0.30 per run). On Linux install bubblewrap (`sudo apt install bubblewrap`); Ubuntu 24.04+ also needs a one-time AppArmor rule for bwrap, which aimpg prints if it's missing.
167
- - Energy is an estimate with a wide range; dollars are close (within about 7% of Claude Code's own session totals on the author's logs).
175
+ - Windows: `report`, `live` and the team commands are untested there; replays and `verify` need macOS or Linux.
176
+ - Energy is an estimate with a wide range (model sizes are guesses within stated bounds); dollars are close (within about 7% of Claude Code's own session totals on the author's logs).
168
177
  - Tips are upper bounds and overlap; they can't be added together.
169
178
  - Replays from commit messages alone are hard (12% solved on the author's repo); `--task-mode tests` shows the agent the tests, which makes tasks easier than real work but keeps comparisons fair.
170
179
 
@@ -27,7 +27,9 @@ outputs:
27
27
  runs:
28
28
  using: composite
29
29
  steps:
30
- - uses: astral-sh/setup-uv@v6
30
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6, pinned: users run this with their repo's token
31
+ with:
32
+ version: "0.11.29"
31
33
  - name: fetch aimpg notes
32
34
  shell: bash
33
35
  working-directory: ${{ inputs.path }}
@@ -0,0 +1,151 @@
1
+ """`aimpg report --share`: a receipt card for social posts (1200×630, the size link
2
+ previews use). Totals only: never repo names, commit messages, paths or code.
3
+
4
+ Writes an SVG (always) and a PNG next to it when the machine can render one
5
+ (macOS Quick Look, rsvg-convert, or cairosvg), and returns a one-line post.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import html
11
+ import re
12
+ import shutil
13
+ import statistics
14
+ import subprocess
15
+ import tempfile
16
+ import time
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ from aimpg.coach import tips as coach_tips
21
+ from aimpg.cost import total_cost
22
+ from aimpg.energy import total_wh
23
+ from aimpg.receipt import KEPT, task_energy
24
+
25
+ W, H = 1200, 630
26
+
27
+
28
+ @dataclass
29
+ class CardNumbers:
30
+ days: int
31
+ usd: float
32
+ commits: int
33
+ kwh: tuple[float, float]
34
+ typical_usd: float | None
35
+ typical_label: str
36
+ tip: str | None
37
+ cost_per_working: float | None
38
+
39
+
40
+ def numbers(parsed, attribution, since: float, now: float, judged=None) -> CardNumbers:
41
+ in_window = [r for r in parsed.requests if r.ts >= since]
42
+ energies = task_energy(attribution.tasks)
43
+ usd, _ = total_cost(in_window)
44
+ wh = total_wh(in_window)
45
+ with_work = [e for e in energies if e.task.requests]
46
+ kept = [e for e in with_work if e.task.status in KEPT]
47
+ pool = kept if len(kept) >= 5 else with_work
48
+ typical = statistics.median(e.usd for e in pool) if pool else None
49
+ found = coach_tips(in_window, attribution)
50
+ tip = None
51
+ fresh = next((t for t in found if t.title.startswith("Start a fresh session")), None)
52
+ pct = re.match(r"\d+%", fresh.measured) if fresh else None
53
+ if pct:
54
+ tip = f"{pct.group(0)} of my agents' tokens re-read conversation from before my last commit"
55
+ cpw = judged.cost_per_durable() if judged is not None and judged.judged else None
56
+ return CardNumbers(round((now - since) / 86400), usd, len(energies), (wh.low / 1000, wh.high / 1000), typical,
57
+ "per kept commit" if pool is kept else "per commit", tip, cpw)
58
+
59
+
60
+ def _money(x: float) -> str:
61
+ return f"${x:,.0f}" if x >= 100 else f"${x:,.2f}"
62
+
63
+
64
+ def svg(n: CardNumbers) -> str:
65
+ e = html.escape
66
+ big = _money(n.cost_per_working) if n.cost_per_working is not None else (_money(n.typical_usd) if n.typical_usd else "—")
67
+ big_label = "per working change (still standing after 30 days)" if n.cost_per_working is not None else n.typical_label
68
+ kwh = f"{n.kwh[0]:,.1f}–{n.kwh[1]:,.0f} kWh" if n.kwh[1] >= 10 else f"{n.kwh[0]:,.2f}–{n.kwh[1]:,.1f} kWh"
69
+ rows = [("AI spend at API list prices", _money(n.usd)), ("Commits made with AI help", f"{n.commits:,}"),
70
+ ("Energy (estimate, range)", kwh)]
71
+ row_svg = "".join(
72
+ f'<text x="72" y="{392 + i * 52}" class="k">{e(k)}</text><text x="1128" y="{392 + i * 52}" class="v" text-anchor="end">{e(v)}</text>'
73
+ f'<line x1="72" y1="{408 + i * 52}" x2="1128" y2="{408 + i * 52}" class="rule"/>'
74
+ for i, (k, v) in enumerate(rows))
75
+ tip = f'<text x="72" y="566" class="tip">↺ {e(n.tip)}</text>' if n.tip else ""
76
+ return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">
77
+ <style>
78
+ .bg{{fill:#10292a}} .k{{font:500 26px "IBM Plex Sans",Helvetica,Arial,sans-serif;fill:#9fb9b2}}
79
+ .v{{font:600 28px "IBM Plex Mono",Menlo,monospace;fill:#e2ece8}} .rule{{stroke:#24433f;stroke-width:1.5}}
80
+ .eyebrow{{font:600 22px "IBM Plex Mono",Menlo,monospace;fill:#f0ad3a;letter-spacing:3px}}
81
+ .big{{font:800 132px "Archivo","Helvetica Neue",Arial,sans-serif;fill:#ffffff}}
82
+ .biglabel{{font:500 30px "IBM Plex Sans",Helvetica,Arial,sans-serif;fill:#e2ece8}}
83
+ .tip{{font:500 24px "IBM Plex Sans",Helvetica,Arial,sans-serif;fill:#f0ad3a}}
84
+ .foot{{font:500 22px "IBM Plex Mono",Menlo,monospace;fill:#9fb9b2}}
85
+ </style>
86
+ <rect class="bg" width="{W}" height="{H}"/>
87
+ <rect x="0" y="0" width="{W}" height="8" fill="#f0ad3a"/>
88
+ <text x="72" y="92" class="eyebrow">MY AI CODING · LAST {n.days} DAYS</text>
89
+ <text x="66" y="236" class="big">{e(big)}</text>
90
+ <text x="72" y="290" class="biglabel">{e(big_label)}</text>
91
+ {row_svg}
92
+ {tip}
93
+ <text x="1128" y="606" class="foot" text-anchor="end">see yours: uvx aimpg report</text>
94
+ </svg>
95
+ '''
96
+
97
+
98
+ def post_text(n: CardNumbers) -> str:
99
+ head = (f"My AI coding costs {_money(n.cost_per_working)} per working change"
100
+ if n.cost_per_working is not None else
101
+ f"My AI coding: {_money(n.typical_usd)} {n.typical_label}" if n.typical_usd else "My AI coding")
102
+ return (f"{head} ({_money(n.usd)} over {n.commits} commits in {n.days} days, at API list prices). "
103
+ "Measured from my own Claude Code/Codex logs with aimpg. See yours: uvx aimpg report")
104
+
105
+
106
+ CHROMES = ("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
107
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
108
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
109
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
110
+ "google-chrome", "chromium", "chromium-browser", "microsoft-edge")
111
+
112
+
113
+ def to_png(svg_path: Path) -> Path | None:
114
+ """Best effort, no new dependencies: rsvg-convert, a headless Chrome-family browser, or cairosvg."""
115
+ png = svg_path.with_suffix(".png")
116
+ if shutil.which("rsvg-convert"):
117
+ if subprocess.run(["rsvg-convert", "-w", str(W), "-o", str(png), str(svg_path)], capture_output=True).returncode == 0:
118
+ return png
119
+ for browser in CHROMES:
120
+ exe = browser if Path(browser).exists() else shutil.which(browser)
121
+ if not exe:
122
+ continue
123
+ png.unlink(missing_ok=True)
124
+ with tempfile.TemporaryDirectory() as profile:
125
+ proc = subprocess.Popen([exe, "--headless", "--disable-gpu", "--hide-scrollbars", "--no-first-run",
126
+ "--no-default-browser-check", "--disable-extensions", f"--user-data-dir={profile}",
127
+ f"--window-size={W},{H}", f"--screenshot={png}", svg_path.resolve().as_uri()],
128
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
129
+ # Chrome sometimes writes the screenshot and then doesn't exit: wait for the file, then stop it
130
+ deadline = time.time() + 60
131
+ while time.time() < deadline and proc.poll() is None and not (png.exists() and png.stat().st_size > 0):
132
+ time.sleep(0.25)
133
+ time.sleep(0.5)
134
+ if proc.poll() is None:
135
+ proc.kill()
136
+ proc.wait(timeout=10)
137
+ if png.exists() and png.stat().st_size > 0:
138
+ return png
139
+ try:
140
+ import cairosvg # type: ignore
141
+
142
+ cairosvg.svg2png(url=str(svg_path), write_to=str(png), output_width=W)
143
+ return png
144
+ except Exception:
145
+ return None
146
+
147
+
148
+ def write(n: CardNumbers, path: Path) -> tuple[Path, Path | None]:
149
+ path = path.with_suffix(".svg")
150
+ path.write_text(svg(n))
151
+ return path, to_png(path)
@@ -53,7 +53,10 @@ def _analyze(args):
53
53
  def main(argv: list[str] | None = None) -> int:
54
54
  parser = argparse.ArgumentParser(prog="aimpg", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
55
55
  sub = parser.add_subparsers(dest="command", required=True)
56
- _common(sub.add_parser("report", help="print the energy receipt"), 30)
56
+ report_p = sub.add_parser("report", help="print the energy receipt")
57
+ _common(report_p, 30)
58
+ report_p.add_argument("--share", type=Path, nargs="?", const=Path("aimpg-receipt.svg"), default=None, metavar="FILE",
59
+ help="also write a share card (totals only; SVG, plus PNG where possible)")
57
60
 
58
61
  pr = sub.add_parser("pr", help="AI energy and cost of this branch's commits, as a PR comment")
59
62
  _common(pr, 90)
@@ -98,6 +101,9 @@ def main(argv: list[str] | None = None) -> int:
98
101
  verify_mod.add_parser(sub)
99
102
  scoreboard_mod.add_parsers(sub)
100
103
  defaults_mod.add_parser(sub)
104
+ from aimpg.replay import study_cli
105
+
106
+ study_cli.add_parser(sub)
101
107
  args = parser.parse_args(argv)
102
108
  if args.command == "replay":
103
109
  return replay_cli.main(args)
@@ -107,6 +113,10 @@ def main(argv: list[str] | None = None) -> int:
107
113
  return scoreboard_mod.main(args)
108
114
  if args.command == "defaults":
109
115
  return defaults_mod.main(args)
116
+ if args.command == "study":
117
+ from aimpg.replay import study_cli
118
+
119
+ return study_cli.main(args)
110
120
  if args.command in ("statusline", "hook", "live"):
111
121
  from aimpg import live as live_mod
112
122
 
@@ -219,6 +229,17 @@ def main(argv: list[str] | None = None) -> int:
219
229
  ledger.record(task_energy(attribution.tasks))
220
230
  judged = durable.judge(ledger.load(), now)
221
231
  sys.stdout.write(render(parsed, attribution, since, now, judged))
232
+ if args.share:
233
+ from aimpg import card
234
+
235
+ n = card.numbers(parsed, attribution, since, now, judged)
236
+ try:
237
+ svg_path, png_path = card.write(n, args.share)
238
+ except Exception as exc: # the receipt above is already printed; a renderer problem must not hide it
239
+ svg_path, png_path = args.share.with_suffix(".svg"), None
240
+ print(f"\n(Couldn't render the PNG: {exc}; the SVG is still written.)")
241
+ print(f"\nShare card: {png_path or svg_path} (totals only: no repo names, commit messages or code)")
242
+ print(f"Post text: {card.post_text(n)}")
222
243
  return 0
223
244
 
224
245
 
@@ -12,9 +12,12 @@ the receipt, so it's never generic advice:
12
12
  request after c2 re-reads: base + (everything up to c2) + new work
13
13
  fresh session after c2: base + new work
14
14
 
15
- The carried-over part is removed from that request's cache reads (and from
16
- the context each output token attends over). It's an upper bound on the
17
- saving: some of that history is genuinely useful to the next task.
15
+ The carried-over part is removed from that request's cache reads. What's
16
+ measured is the share of tokens carried over; the saving is an upper bound,
17
+ because a fresh session has to rebuild some context (re-reading files,
18
+ CLAUDE.md, a summary), which this doesn't charge. In energy it depends on
19
+ what re-reading cached context costs, so it's given as a range across the
20
+ energy assumptions, from nearly nothing (cache still in fast memory) to a lot.
18
21
 
19
22
  2. Model size. Requests on large-class models (Opus, Fable) re-priced and
20
23
  re-estimated as if they had run on a mid-class model (Sonnet). Also an upper
@@ -42,7 +45,9 @@ class Tip:
42
45
  action: str # what to do
43
46
  saving_wh: WhRange
44
47
  saving_usd: float
45
- share: float # of the window's energy (mid estimate)
48
+ share: float # of the window's energy (mid estimate), for ordering
49
+ saving_text: str = "" # how the saving is stated, when a single % would overclaim
50
+ caveat: str = ""
46
51
 
47
52
 
48
53
  def _sum(requests: list[Request]) -> tuple[WhRange, float]:
@@ -64,6 +69,7 @@ def fresh_session_tip(requests: list[Request], attribution: Attribution) -> Tip
64
69
  by_session[r.session_id].append(r)
65
70
 
66
71
  actual, simulated = [], []
72
+ carried_tokens = 0
67
73
  for session, reqs in by_session.items():
68
74
  reqs.sort(key=lambda r: r.ts)
69
75
  main = [r for r in reqs if not r.is_sidechain]
@@ -83,22 +89,32 @@ def fresh_session_tip(requests: list[Request], attribution: Attribution) -> Tip
83
89
  simulated.append(r)
84
90
  continue
85
91
  cut = min(carried_at[i], r.usage.cache_read)
92
+ carried_tokens += cut
86
93
  simulated.append(replace(r, usage=replace(r.usage, cache_read=r.usage.cache_read - cut)))
87
94
  (a_wh, a_usd), (s_wh, s_usd) = _sum(actual), _sum(simulated)
88
95
  saved = WhRange(a_wh.low - s_wh.low, a_wh.high - s_wh.high)
89
96
  if a_wh.mid == 0 or saved.mid / a_wh.mid < 0.02:
90
97
  return None
91
98
  share = saved.mid / a_wh.mid
99
+ tokens = sum(r.usage.ctx_len + r.usage.output for r in actual)
100
+ lo, hi = sorted((saved.low / a_wh.low if a_wh.low else 0.0, saved.high / a_wh.high if a_wh.high else 0.0))
101
+ usd = a_usd - s_usd
92
102
  return Tip(
93
103
  title="Start a fresh session after each commit",
94
- measured=f"{share:.0%} of your AI energy went to re-reading conversation from before your last commit",
104
+ measured=f"{carried_tokens / tokens:.0%} of the tokens your agents processed were conversation carried over from before your last commit",
95
105
  action="After committing, start a new session (or /clear) for the next task, and give it a one-line summary if needed",
96
106
  saving_wh=saved,
97
- saving_usd=a_usd - s_usd,
107
+ saving_usd=usd,
98
108
  share=share,
109
+ saving_text=f"up to {lo:.0%}–{hi:.0%} less energy, up to {_usd_short(usd)}",
110
+ caveat="A fresh session has to rebuild some context, which isn't counted here, so the real saving is smaller",
99
111
  )
100
112
 
101
113
 
114
+ def _usd_short(x: float) -> str:
115
+ return f"${x:,.0f}" if x >= 100 else f"${x:,.2f}"
116
+
117
+
102
118
  def model_size_tip(requests: list[Request]) -> Tip | None:
103
119
  large = [r for r in requests if model_class(r.model)[0] == "large"]
104
120
  if not large:
@@ -31,6 +31,9 @@ def usage_cost(usage: Usage, model: str) -> float | None:
31
31
  p = price_for(model)
32
32
  if p is None:
33
33
  return None
34
+ tier = p.get("long_prompt")
35
+ if tier and usage.ctx_len > tier["over_tokens"]: # e.g. Haiku 5.5: the whole request at the higher prices
36
+ p = tier
34
37
  one_hour = min(usage.cache_write_1h, usage.cache_write)
35
38
  return (
36
39
  usage.fresh_in * p["input"]
@@ -20,6 +20,7 @@ Nothing here talks to the network or changes how the agent works.
20
20
  from __future__ import annotations
21
21
 
22
22
  import json
23
+ import shlex
23
24
  import os
24
25
  import statistics
25
26
  import subprocess
@@ -259,7 +260,7 @@ def status_line(data: dict, state: SessionState, usual: float | None) -> str:
259
260
  else:
260
261
  parts.append(f"ctx {_k(ctx)}")
261
262
  if share >= CLEAR_HINT_SHARE and len(state.task) <= 3:
262
- parts.append(f"/clear ≈ -{share:.0%} per request")
263
+ parts.append("next task? /clear")
263
264
  five = ((data.get("rate_limits") or {}).get("five_hour") or {}).get("used_percentage")
264
265
  if isinstance(five, (int, float)):
265
266
  parts.append(f"5h {five:.0f}%")
@@ -270,7 +271,8 @@ def commit_note(done: dict) -> str:
270
271
  msg = f"aimpg: that commit cost {_usd(done['cost'])} over {done['requests']} AI requests."
271
272
  share = done.get("stale_share", 0.0)
272
273
  if share >= CLEAR_HINT_SHARE:
273
- msg += f" {share:.0%} of the context is from earlier work: /clear before the next task saves ~{share:.0%} per request."
274
+ msg += (f" {share:.0%} of the context is from earlier work. If the next task doesn't need it, /clear"
275
+ " (with a one-line summary) stops re-reading it on every request.")
274
276
  return msg
275
277
 
276
278
 
@@ -338,9 +340,17 @@ PREVIOUS = LIVE_DIR / "previous_statusline.json"
338
340
  OURS = ("aimpg statusline", "aimpg hook post-commit")
339
341
 
340
342
 
343
+ def _write_json(path: Path, data: dict) -> None:
344
+ """Write via a temp file and rename, so Claude Code never reads a half-written settings file."""
345
+ tmp = path.with_name(f".{path.name}.aimpg-tmp")
346
+ tmp.write_text(json.dumps(data, indent=2) + "\n")
347
+ os.replace(tmp, path)
348
+
349
+
341
350
  def planned_settings(current: dict, exe: str) -> dict:
342
351
  """The settings after install: our status line (wrapping any existing one) + a commit hook."""
343
352
  new = json.loads(json.dumps(current))
353
+ exe = shlex.quote(exe) # a path with spaces or quotes must stay one argument
344
354
  new["statusLine"] = {"type": "command", "command": f"{exe} statusline"}
345
355
  hooks = new.setdefault("hooks", {}).setdefault("PostToolUse", [])
346
356
  if not any(h.get("command", "").endswith("hook post-commit") for e in hooks for h in e.get("hooks", [])):
@@ -366,7 +376,7 @@ def install(settings: Path = SETTINGS, exe: str = "aimpg", *, confirm=input) ->
366
376
  backup = settings.with_suffix(f".json.aimpg-backup-{int(time.time())}")
367
377
  backup.write_text(settings.read_text())
368
378
  settings.parent.mkdir(parents=True, exist_ok=True)
369
- settings.write_text(json.dumps(new, indent=2) + "\n")
379
+ _write_json(settings, new)
370
380
  return "Installed. It shows up after the next AI message. Undo any time with `aimpg live uninstall`."
371
381
 
372
382
 
@@ -388,7 +398,7 @@ def uninstall(settings: Path = SETTINGS) -> str:
388
398
  current["hooks"].pop("PostToolUse")
389
399
  if not current["hooks"]:
390
400
  current.pop("hooks")
391
- settings.write_text(json.dumps(current, indent=2) + "\n")
401
+ _write_json(settings, current)
392
402
  PREVIOUS.unlink(missing_ok=True)
393
403
  return "Removed. Your previous status line (if any) is back."
394
404
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "2026-10-03b",
3
- "source": "https://platform.claude.com/docs/en/about-claude/pricing (Model pricing table, fetched 2026-10-03)",
2
+ "version": "2026-10-11",
3
+ "source": "https://platform.claude.com/docs/en/about-claude/pricing (Model pricing table, fetched 2026-10-11). Claude Haiku 5.5 is priced by prompt length: a request whose prompt (all input incl. cache reads and writes) is over 100,000 tokens pays the long_prompt prices.",
4
4
  "unit": "USD per million tokens",
5
5
  "note": "Standard (global) API prices. For subscription users this is the API-equivalent value of the work, not a bill.",
6
6
  "models": {
@@ -101,6 +101,21 @@
101
101
  "cache_write_1h": 1.75,
102
102
  "cache_read": 0.175,
103
103
  "output": 14.0
104
+ },
105
+ "claude-haiku-5-5": {
106
+ "input": 0.1,
107
+ "cache_write_5m": 0.125,
108
+ "cache_write_1h": 0.2,
109
+ "cache_read": 0.01,
110
+ "output": 0.5,
111
+ "long_prompt": {
112
+ "over_tokens": 100000,
113
+ "input": 0.5,
114
+ "cache_write_5m": 0.625,
115
+ "cache_write_1h": 1.0,
116
+ "cache_read": 0.05,
117
+ "output": 2.5
118
+ }
104
119
  }
105
120
  },
106
121
  "openai_source": "https://developers.openai.com/api/docs/pricing (Standard tier, fetched 2026-10-03). gpt-5.3-codex lists no cache-write price; writes are priced as input."
@@ -59,7 +59,7 @@ def summary(in_window, energies: list[TaskEnergy]) -> list[str]:
59
59
  "YOUR AI CODING",
60
60
  f" Energy {_fmt_wh(everything)}",
61
61
  f" ≈ {equivalence.everyday(everything)}",
62
- f" Money {_usd(usd)} API-equivalent (Anthropic's and OpenAI's published prices"
62
+ f" Money {_usd(usd)} at API list prices (Anthropic's and OpenAI's; a subscription costs less"
63
63
  + (f"; {unknown} requests from unpriced models left out)" if unknown else ")"),
64
64
  f" Output {len(energies)} commits made with AI help",
65
65
  ]
@@ -134,8 +134,10 @@ def render(parsed: ParseResult, attribution: Attribution, since: float, now: flo
134
134
  add("")
135
135
  add("WHAT WOULD HAVE SAVED THE MOST (measured on your logs; upper bounds that overlap)")
136
136
  for i, t in enumerate(found, 1):
137
- add(f" {i}. {t.title}: up to {t.share:.0%} less energy, {_usd(t.saving_usd)}")
137
+ add(f" {i}. {t.title}: {t.saving_text or f'up to {t.share:.0%} less energy, {_usd(t.saving_usd)}'}")
138
138
  add(f" {t.measured}.")
139
+ if t.caveat:
140
+ add(f" {t.caveat}.")
139
141
  add(f" ≈ {equivalence.everyday(t.saving_wh)}")
140
142
  add(f" → {t.action}.")
141
143
  if judged is not None: