Skip to content

Commit 7f2c7fd

Browse files
authored
Merge branch 'main' into claude/ci-margin-correction
2 parents 2508766 + 41a8c49 commit 7f2c7fd

5 files changed

Lines changed: 1095 additions & 0 deletions

File tree

docs/WORKTREES.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,58 @@ pwsh -NoProfile -File scripts\hooks\collision_gate.ps1 -PathOverride docs\BACKLO
364364
Empty output means no live session holds it. Documented in-script as a test affordance; surfaced here
365365
because a session that needed the answer found it by reading the source.
366366

367+
## Account usage — knowing before a session is cut off
368+
369+
**What it fixes.** Sessions were hitting the plan limit mid-task and losing work. The account's real
370+
quota state exists — Settings > Usage shows it — but it is not visible from inside a session, so nobody
371+
knew how much headroom was left until it ran out.
372+
373+
**The one place the numbers arrive.** Claude Code hands `rate_limits` to a **statusLine command's stdin
374+
and nowhere else**. Not `SessionStart`, not `UserPromptSubmit`, not `Stop` — the payloads were enumerated
375+
in the shipped binary and it appears in exactly one of them. So quota state cannot be subscribed to; it
376+
has to be *collected* by a statusLine and published somewhere shared. That single fact determines the
377+
whole shape:
378+
379+
| | |
380+
|---|---|
381+
| [`usage-collect.ps1`](../scripts/coord/usage-collect.ps1) | the statusLine. Publishes to `~/.claude/mefor-usage/latest.json` |
382+
| [`usage.ps1`](../scripts/coord/usage.ps1) | reads it, adds burn rate, answers *will this run out before it resets* |
383+
| [`install-usage-statusline.ps1`](../scripts/coord/install-usage-statusline.ps1) | wires it (owner, plain terminal) |
384+
385+
**One publisher, N readers.** The quota is **account-wide** — every session in every repo draws down the
386+
same 5-hour and 7-day pools — so any one session's reading is the truth for all of them. Do not run a
387+
collector per session expecting to sum them; that double-counts a shared pool. The publish path is
388+
user-level for the same reason: the data is a property of the account, not of a checkout.
389+
390+
**It only runs in an interactive session.** The statusLine is part of the TUI's render tree and never
391+
executes under `claude -p` or the SDK. A headless coordinator can *read* what this publishes and can
392+
never publish it itself. `refreshInterval` is set because statusLine updates are event-driven and go
393+
silent when a session is idle — Anthropic's docs name *"a coordinator waits on background subagents"* as
394+
exactly the case where that leaves you blind.
395+
396+
**Two of the four Settings > Usage numbers are not available at all.** The payload carries `five_hour`
397+
and `seven_day` only. The **per-model weekly buckets** (the Fable/Opus/Sonnet bars) and the **plan tier**
398+
are absent, and the request to expose them was closed as not-planned. `usage.ps1` prints that on every
399+
run rather than burying it: if Opus is being burned hard across many sessions, the bucket most likely to
400+
stop you is the one nothing here can see. Two green bars and an invisible third is worse than no tool.
401+
402+
```powershell
403+
pwsh -NoProfile -File scripts\coord\usage.ps1 # human
404+
pwsh -NoProfile -File scripts\coord\usage.ps1 -Json # coordinator
405+
```
406+
407+
Exit codes so a coordinator can branch without parsing prose: **0** ok, **10** warn, **11** critical,
408+
**20** unknown. `UNKNOWN` is a real answer here and is returned whenever the reading is stale, undateable
409+
or future-dated — a percentage is never extrapolated from a dead publisher, and every number is printed
410+
with its own age. **Do not read a missing bucket as an empty one.**
411+
412+
> **`ccusage` does not do this**, despite being the tool everyone recommends and despite several
413+
> summaries claiming it "fetches real rate limit data". It parses transcripts for tokens and dollars; its
414+
> "5-hour block" is a client-side reconstruction and its statusline percentage is context-window.
415+
> `claude-usage-tracker` is the same mistake in cruder form — real token parsing compared against a
416+
> hardcoded limit table. Anything reading plan state from either is confidently wrong at exactly the
417+
> moment it matters. Tokens and plan-limit consumption are different quantities.
418+
367419
## Announcing yourself (UserPromptSubmit hook)
368420

369421
**What it fixes.** Everything above is **pull**-based: a new session discovers its peers and the peers
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<#
2+
.SYNOPSIS
3+
Wire usage-collect.ps1 as the Claude Code statusLine, so the account's plan limits get published.
4+
5+
.DESCRIPTION
6+
Run this ONCE, from a plain terminal. It writes `statusLine` into the USER-level
7+
~/.claude/settings.json, so every session on this machine publishes -- and reads -- the same
8+
account-wide quota state. See usage-collect.ps1 for why the statusLine is the only source.
9+
10+
IT TAKES EFFECT IN NEWLY STARTED SESSIONS. Existing sessions keep the config they booted with, the
11+
same as the coordination hooks. And it only ever runs in an INTERACTIVE session: the statusLine is
12+
part of the TUI's render tree and never executes under `claude -p` or the SDK, so a headless
13+
coordinator can read what this publishes but can never publish it itself.
14+
15+
WHY IT POINTS AT AN ABSOLUTE PATH rather than resolving the repo per invocation: the statusLine runs
16+
on every assistant message behind a 300ms debounce, and a `git rev-parse` per fire is latency on the
17+
render path for a value that never changes. The trade is that moving or deleting the checkout breaks
18+
it -- so the wired command TESTS FOR THE SCRIPT and degrades to a quiet marker instead of erroring
19+
into the status bar on every message.
20+
21+
refreshInterval is set because statusLine updates are EVENT-DRIVEN -- a new assistant message,
22+
/compact, a permission-mode change -- and go silent when a session is idle. Anthropic's own docs
23+
name "a coordinator waits on background subagents" as the case where that leaves you blind, which is
24+
exactly this repo's situation.
25+
26+
.EXAMPLE
27+
pwsh -NoProfile -File scripts\coord\install-usage-statusline.ps1
28+
pwsh -NoProfile -File scripts\coord\install-usage-statusline.ps1 -Status
29+
pwsh -NoProfile -File scripts\coord\install-usage-statusline.ps1 -Uninstall
30+
#>
31+
[CmdletBinding(SupportsShouldProcess)]
32+
param(
33+
[switch]$Uninstall,
34+
[switch]$Status,
35+
# Milliseconds. Minimum honoured by Claude Code is 1000.
36+
[int]$RefreshInterval = 10000,
37+
[string]$SettingsPath = (Join-Path $env:USERPROFILE ".claude\settings.json"),
38+
# Which collector to wire. Defaults to the PRIMARY checkout's copy, deliberately: a worktree is
39+
# disposable and a user-level statusLine pointing into one dies with it. Overridable so tests can
40+
# drive the real installer against a fixture instead of asserting a copy of its rules.
41+
[string]$CollectorPath
42+
)
43+
44+
$ErrorActionPreference = "Stop"
45+
$MARKER = "mefor-usage"
46+
47+
# The primary checkout, not this worktree: a worktree is disposable and the statusLine outlives it.
48+
$common = (& git rev-parse --path-format=absolute --git-common-dir 2>$null)
49+
if ($LASTEXITCODE -ne 0 -or -not $common) { throw "Not inside a git repository -- run this from the MessageFoundry checkout." }
50+
$primary = Split-Path ($common.Trim()) -Parent
51+
$script = if ($CollectorPath) { $CollectorPath } else { Join-Path $primary "scripts/coord/usage-collect.ps1" }
52+
53+
function Get-Settings {
54+
if (-not (Test-Path -LiteralPath $SettingsPath)) { return [ordered]@{} }
55+
$raw = Get-Content -LiteralPath $SettingsPath -Raw
56+
if (-not $raw.Trim()) { return [ordered]@{} }
57+
return ($raw | ConvertFrom-Json -AsHashtable)
58+
}
59+
60+
if ($Status) {
61+
$s = Get-Settings
62+
$sl = $s['statusLine']
63+
Write-Host ""
64+
if (-not $sl) { Write-Host "statusLine: NOT CONFIGURED" -ForegroundColor Yellow }
65+
else {
66+
$isOurs = ([string]$sl['command']) -like "*$MARKER*"
67+
Write-Host ("statusLine: CONFIGURED" + $(if ($isOurs) { " (ours)" } else { " (SOMEONE ELSE'S -- install would replace it)" })) -ForegroundColor $(if ($isOurs) { "Green" } else { "Yellow" })
68+
Write-Host " command : $($sl['command'])"
69+
Write-Host " refreshInterval: $($sl['refreshInterval'])"
70+
}
71+
Write-Host " script exists : $(Test-Path -LiteralPath $script) ($script)"
72+
$latest = Join-Path $env:USERPROFILE ".claude\mefor-usage\latest.json"
73+
# A RECEIPT, NOT A CONFIG READ. Whether the settings file names the script says nothing about
74+
# whether it has ever run -- that distinction is the one this repo keeps paying for.
75+
Write-Host " has published : $(Test-Path -LiteralPath $latest) ($latest)" -ForegroundColor $(if (Test-Path -LiteralPath $latest) { "Green" } else { "Yellow" })
76+
Write-Host ""
77+
exit 0
78+
}
79+
80+
$settings = Get-Settings
81+
82+
if ($Uninstall) {
83+
if ($settings['statusLine'] -and ([string]$settings['statusLine']['command']) -like "*$MARKER*") {
84+
$settings.Remove('statusLine')
85+
if ($PSCmdlet.ShouldProcess($SettingsPath, "remove the mefor-usage statusLine")) {
86+
Copy-Item -LiteralPath $SettingsPath -Destination "$SettingsPath.bak-usage" -Force -ErrorAction SilentlyContinue
87+
($settings | ConvertTo-Json -Depth 20) | Set-Content -LiteralPath $SettingsPath -Encoding UTF8
88+
Write-Host "statusLine REMOVED from $SettingsPath" -ForegroundColor Yellow
89+
}
90+
}
91+
else { Write-Host "Nothing to remove: the statusLine is absent or is not ours." -ForegroundColor Yellow }
92+
exit 0
93+
}
94+
95+
if (-not (Test-Path -LiteralPath $script)) {
96+
throw "Collector not found at $script. The primary checkout ($primary) does not carry it yet -- merge the branch that adds it, or advance the primary, before installing."
97+
}
98+
99+
if ($settings['statusLine'] -and ([string]$settings['statusLine']['command']) -notlike "*$MARKER*") {
100+
Write-Host ""
101+
Write-Host "REFUSING: a statusLine is already configured and it is not ours." -ForegroundColor Red
102+
Write-Host " command: $($settings['statusLine']['command'])"
103+
Write-Host ""
104+
Write-Host "Silently replacing someone's status bar is not this script's call. Remove it yourself, or"
105+
Write-Host "merge the two commands by hand, then re-run."
106+
exit 1
107+
}
108+
109+
# The guard is inline so a missing script degrades to a marker rather than erroring into the status bar
110+
# on every single message -- a statusLine that shouts an exception is worse than one that says nothing.
111+
$cmd = "# $MARKER`n" +
112+
"`$s = '$($script -replace "'", "''")'; if (Test-Path -LiteralPath `$s) { & pwsh -NoProfile -File `$s } else { Write-Output '${MARKER}: collector missing' }"
113+
114+
$settings['statusLine'] = [ordered]@{
115+
type = "command"
116+
command = $cmd
117+
refreshInterval = $RefreshInterval
118+
}
119+
120+
if ($PSCmdlet.ShouldProcess($SettingsPath, "install the mefor-usage statusLine")) {
121+
if (Test-Path -LiteralPath $SettingsPath) { Copy-Item -LiteralPath $SettingsPath -Destination "$SettingsPath.bak-usage" -Force }
122+
$json = $settings | ConvertTo-Json -Depth 20
123+
# Never leave the file unparseable: a broken settings.json degrades every session on this machine.
124+
try { $null = $json | ConvertFrom-Json } catch { throw "Refusing to write: generated settings JSON is invalid. $_" }
125+
$json | Set-Content -LiteralPath $SettingsPath -Encoding UTF8
126+
Write-Host ""
127+
Write-Host "statusLine INSTALLED (user level -- every session on this machine)" -ForegroundColor Green
128+
Write-Host " collector : $script"
129+
Write-Host " refreshInterval: $RefreshInterval ms"
130+
Write-Host " publishes to : $(Join-Path $env:USERPROFILE '.claude\mefor-usage\latest.json')"
131+
Write-Host " backup : $SettingsPath.bak-usage"
132+
Write-Host ""
133+
Write-Host " Takes effect in NEWLY STARTED sessions. Interactive only -- never under 'claude -p'."
134+
Write-Host " Then read it with: pwsh -NoProfile -File scripts\coord\usage.ps1"
135+
Write-Host ""
136+
}

0 commit comments

Comments
 (0)