Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

## Unreleased

## v0.31.0 — 2026-08-20 — the operator is not a reading

**The ledger row was printing two different X's and calling them both
`x`.** `×` (U+00D7) has always been a cell — the window the pool will
not cover — and `x` was the ASCII letter standing in for multiplication.
The folded tail put them one column apart in the same red: `...×5hx28`,
where the first mark is a fact about your week and the second is
punctuation. It read like a font failure. The multiplication sign is
now `✕` (U+2715) everywhere the product multiplies: the folded future
`...▯5h✕28`, the pace `0.7✕`, the budget notice `- 19✕5h left`, and
`report`'s `(~4.1 ✕ 5h windows unused)`. Cells are the ink; the
operator is punctuation, and it is deliberately the lighter of the two
marks.

**Why that codepoint and not a heavier one.** Row 2 right-anchors the
week row to line 1's edge by counting characters, so a glyph that
counts one and draws two overhangs the edge once per pace suffix — the
same constraint that keeps `≡` and `☠` on their single column. `╳`
(U+2573) is ambiguous-width and doubles under a CJK locale; `✖`
(U+2716) is Emoji=Yes and can fall through to a colour font. `✕` is
neutral-width and not an emoji. Both alternatives look stronger and are
one `MULT_GLYPH=╳` away if your terminal disagrees.

## v0.30.0 — 2026-08-19 — the future folds

**The 7d strip stopped drawing the future one cell at a time.** Thirty
Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Every component earns its place:
| User tier | Neutral white-weight (MAX bold, PRO normal, dim otherwise) — identity, never a status color. Truncated display name. |
| Quota | Integer percentages. The 5h badge always carries its reset time while a window is live — `5h[42%@14:30]` reads "42% used, resets at 14:30" — because on a 5h horizon the reset is the number you plan the current sitting around. Wall-clock, not a countdown, on purpose: Claude Code only re-renders the statusline on activity, so a relative "@1h38m" silently decays into a lie during idle gaps, while "@14:30" stays true in a frozen frame. (The 7d badge is hybrid: day-relative `@5d` while the reset is >= 24h out — decays one day per day, mild and narrow — switching to the same wall-clock `@04:00` inside the last day, where an `@6h`/`@<1h` countdown decayed by the hour exactly when pressure keeps the suffix visible.) When a window's utilization climbs between renders, a reverse-video `+N` token appears right after the badge for ~60s: `5h[44%@14:32]+2` means "you just burned 2%". A drop (window reset) stays quiet — the fresh low number is its own signal. **7d is forecast, not leveled**: a learned per-weekday burn profile (EWMA over your own usage history) plus your recent 24h burn project whether the quota outlasts the window — your heavy Tuesday counts more than a generic average. The verdict is color alone; under pressure the badge shows when relief arrives: `7d[44%@5d]` red means "at your pace, dry days before the reset 5 days from now"; inside the last day it reads `7d[92%@04:00]` — resets at 04:00. Cold start (<14 days history) falls back to window-average pacing. Recovery color when reset is imminent. **Model-scoped weekly quota**: when the usage API carries a per-model weekly limit (`limits[]`, `kind=weekly_scoped`) for the model your session is running, it renders right after the model+context block — `fabl5[1m][12%] fb[67%]` on a Fable 5 session, `op[33%]` on Opus — because the quota is a property of the model you're running, not of the account-wide 5h/7d cluster. It's a weekly number (same reset as the 7d badge), scoped to one model. Other models' scoped quotas stay hidden: only the limit constraining *this* session is signal. Supersedes the legacy `seven_day_opus`/`seven_day_sonnet` fields, which the API now sends as null. |
| Extra usage | Monthly spend, limit, prepaid balance. `--extra auto` shows when quota runs out. |
| Week row | **A row of its own, under the badges**: `5h ▅█▃▮▯ 0.9x @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯5hx19 0.7x @Wed 09:00` — this sitting by the hour, the week as its 5h windows (day-gapped, the far future folded to a counted `...▯5hx19`), height = what each cell burned, `▮` now, `×` where the pool runs dry, each strip ending with its pace and reset. Reconstructed from your own usage log; `auto` shows it once there is history to show. See [Week row](#week-row). |
| Week row | **A row of its own, under the badges**: `5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯5h✕19 0.7✕ @Wed 09:00` — this sitting by the hour, the week as its 5h windows (day-gapped, the far future folded to a counted `...▯5h✕19`), height = what each cell burned, `▮` now, `×` where the pool runs dry, each strip ending with its pace and reset. Reconstructed from your own usage log; `auto` shows it once there is history to show. See [Week row](#week-row). |
| Deadman | **Invisible until a switch is armed.** Surfaces [deadman](https://github.com/thevibeworks/deadman) — a dead man's switch that hands the session off when you stop responding. `[☠ armed 42m]` (dim) counts down to the auto-handoff; `[☠ warned 3m]` (yellow) means the phone warning went out; `[☠ due]` means the handoff fires imminently. Sits on the left lane next to the path — it describes this session's lifecycle, not a quota. One `command -v` when the tool is absent, one fast file read when present; nothing armed renders nothing. `--deadman off` disables it. |
| Cache health | **Quiet until it bites.** Claude Code never re-renders an idle session, and while you work the prompt cache is always freshly ~1 TTL from expiry — so a proactive "expiring soon" isn't honestly observable, and `auto` spends no width on it. It speaks only when a rewrite actually happens: `≡!419k` the instant you resume onto a dead cache (idle longer than the TTL) or a mid-session prefix collapse — a 419k-token re-cache at ~20x the read rate (and the same burn on your 5h/7d quota on subscriptions). **Bold red past 200k** — the premium-band miss. `≡~` while a large prefix rebuilds. `--cache always` additionally keeps the freeze-safe deadline `≡@15:20` (last request + TTL; a past time in a frozen frame reads "expired at 15:20"). TTL defaults to 1h (claude.ai subscriber sessions) or 5m (API-key auth); an observed usage breakdown overrides it. The `≡` glyph (U+2261) reads as stacked cache layers — one terminal column, quiet and distinct. |

Expand Down Expand Up @@ -255,7 +255,7 @@ right edge shared with line 1:

```text
proj (main*) +84/-14 8m $6.72 fabl5[1m][██░░42%] fb[66%] [MAX|@work] 5h[38%@23:00] 7d[39%]
- 3x5h left · 20%/win 5h ▅█▃▮▯ 0.9x 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7x @Wed 09:00
- 3✕5h left · 20%/win 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00
```

The 5h strip prints no reset: line 1's `5h[38%@23:00]` already carries
Expand All @@ -272,12 +272,12 @@ edge and the full sentence sits flush-left beneath them:

```text
proj (main*) fabl5[1m][██░░42%] fb[66%] [MAX|@work] 5h[38%@23:00] 7d[39%]
5h ▅█▃▮▯ 0.9x @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7x @Wed 09:00
- budget ~3x5h left · even 20%/win · heading ~52%
5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00
- budget ~3✕5h left · even 20%/win · heading ~52%
```

Each strip ends with its **pace** (used ÷ elapsed: `0.7x` is on track,
`1.6x` caps early — dim below 1x, pressure-tinted from 1x, hidden for
Each strip ends with its **pace** (used ÷ elapsed: `0.7✕` is on track,
`1.6✕` caps early — dim below 1✕, pressure-tinted from 1✕, hidden for
the first 15 min of a window) and the **reset** its right edge stands
for (`@23:00` inside 24h, `@Wed 09:00` beyond) — axis labels for a
timeline, not badges restated. When the row shows, the advisor's calm
Expand All @@ -292,7 +292,7 @@ fire.
left, the last a 3h stub), height = the 7d points that window burned,
with a thin gap at each local midnight so days read as clusters — and a
day that held five windows shows it — without a ruler. History draws in
full; the future folds: two hollow cells after `▮`, then `...▯5hx28` —
full; the future folds: two hollow cells after `▮`, then `...▯5h✕28` —
28 more 5h slots to the reset, all alike (`×` red when the tail projects
dry). The `week` subcommand's wide ledger still draws every slot.

Expand All @@ -304,7 +304,8 @@ fire.
| `▮` | the cell you are in now |
| `▯` | a cell still ahead of you — the hollow of `▮`, an empty slot waiting |
| `×` | a cell the pool will not cover at the current pace (7d: the learned forecast's dry point, linear when untrained; 5h: linear, the same projection as the badge) |
| `...▯5hx28` | the folded 7d future: 28 more 5h slots to the reset, one token instead of 28 hollow cells (`×` red when the tail ends dry) |
| `...▯5h✕28` | the folded 7d future: 28 more 5h slots to the reset, one token instead of 28 hollow cells (`×` red when the tail ends dry) |
| `✕` | not a cell — the multiplication sign, the row's one operator (`...▯5h✕28`, `0.7✕`, `- 19✕5h left`). Deliberately not `×` (U+00D7), which is already a reading: cells are the ink, the operator is punctuation, and `...×5h✕28` has to say both at once. One terminal column and no emoji fallback, so the row still meets line 1's edge; override with `MULT_GLYPH` (`╳` and `✖` look stronger but are ambiguous-width and emoji-presentation respectively) |

Burn cells take their badge's pressure color; everything else is neutral,
so the row never adds an alarm channel of its own. Both strips are
Expand All @@ -331,7 +332,7 @@ long one:

```text
proj (main*) +84/-14 8m $6.72 fabl5[1m][██░░42%] fb[91%] [MAX|@work] 5h[38%@23:00] 7d[55%]
+ fb 91% vs 7d 55% · go op 5h ▅█▃▮▯ 0.9x 7d ▅▁▂ ▃▅ˍ▃▅ … ▆▆ˍ▂▮▯▯ 0.6x @Wed 09:00
+ fb 91% vs 7d 55% · go op 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ … ▆▆ˍ▂▮▯▯ 0.6✕ @Wed 09:00
+ fb weekly 91% against 7d 55% · the model caps first, not the account; op sits at 33%, so run it for the bulk
```

Expand Down Expand Up @@ -364,7 +365,7 @@ clauses about one window can never disagree.
| `+ 5h ~40m left · 70% unused` | Said only when the *week* is stranding capacity — an unspent 5h window is otherwise headroom, not waste, since the 5h window is a rate limit and not a budget. |
| `+ ~62% will expire · go heavier` | On pace to strand a large chunk of the subscription. Speaks only to an engaged, unsqueezed session. |
| `+ work 5h[8%] free` | A sibling account in the same shared home is idle while this one is pinned. |
| `- 19x5h left · 1.1%/win` | The calm budget: runway, what even looks like, where you land (long form). |
| `- 19✕5h left · 1.1%/win` | The calm budget: runway, what even looks like, where you land (long form). |

`--notice off` keeps row 3 quiet; `--advisor off` silences both. `--check`
and `--week` print the long form, since a terminal command has a whole
Expand All @@ -381,7 +382,7 @@ $ ~/.claude/statusline.sh report # or --days 90
usage report - work (last 28d, 79 samples)

7d windows closed: 1
Tue 07-28 00:00 used 51% expired 49% (~4.7 x 5h windows unused)
Tue 07-28 00:00 used 51% expired 49% (~4.7 5h windows unused)

5h windows closed: 3 avg 95% at close 2 hit the cap
exchange rate: one full 5h window = ~10.46% of the week (~9.6 windows/week, learned)
Expand Down Expand Up @@ -508,7 +509,7 @@ run. Setting only `CLAUDE_CACHE_DIR` keeps the legacy single-dir behavior.
npm exec --yes bats -- t/
```

417 tests across `t/statusline.bats` (405 statusline + integration) and
418 tests across `t/statusline.bats` (406 statusline + integration) and
`t/install.bats` (12 installer). CI runs on push and PR to `main`.

## Project Structure
Expand Down
26 changes: 13 additions & 13 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Claude Code Statusline — a meter for your Claude Code plan</title>
<meta name="description" content="Your plan is metered; this is the meter. Live 5h/7d quota with reset times, per-model weekly limits, context pressure, session cost, burn ledgers with a learned dry-out forecast, and a notice engine that says which limit binds first — in your Claude Code prompt. One bash file, 417 tests, no daemon, no telemetry.">
<meta name="description" content="Your plan is metered; this is the meter. Live 5h/7d quota with reset times, per-model weekly limits, context pressure, session cost, burn ledgers with a learned dry-out forecast, and a notice engine that says which limit binds first — in your Claude Code prompt. One bash file, 418 tests, no daemon, no telemetry.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;600&display=swap" rel="stylesheet">
Expand Down Expand Up @@ -204,7 +204,7 @@

<header class="wrap plate">
<h1>claude-code-statusline <span>— a meter for your Claude Code plan</span></h1>
<div class="tariff">417 tests · jq + curl · no daemon · no telemetry</div>
<div class="tariff">418 tests · jq + curl · no daemon · no telemetry</div>
</header>

<main class="wrap">
Expand Down Expand Up @@ -247,12 +247,12 @@ <h2>Read the meter</h2>
<div class="answer"><b>The 7-day window, forecast-colored.</b> A learned per-weekday burn profile projects whether the pool outlasts the window — your heavy Tuesday counts more than a generic average. Red answers: at your pace, dry before the reset.</div>
</div>
<div class="row">
<div class="reading"><span class="d">5h</span> <span class="g">▂▅█</span><span class="w">▮</span><span class="d2">▯</span> <span class="d">0.8x</span><span class="cite">№ 0001</span></div>
<div class="answer"><b>The 5h ledger — this sitting by the hour.</b> Height is what each hour cost; <code>▮</code> now, <code>▯</code> ahead, <code>ˍ</code> ran-but-negligible, <code>░</code> no sample. Ends with its pace: above 1x you cap before the reset.</div>
<div class="reading"><span class="d">5h</span> <span class="g">▂▅█</span><span class="w">▮</span><span class="d2">▯</span> <span class="d">0.8✕</span><span class="cite">№ 0001</span></div>
<div class="answer"><b>The 5h ledger — this sitting by the hour.</b> Height is what each hour cost; <code>▮</code> now, <code>▯</code> ahead, <code>ˍ</code> ran-but-negligible, <code>░</code> no sample. Ends with its pace: above 1✕ you cap before the reset.</div>
</div>
<div class="row">
<div class="reading"><span class="d">7d</span> <span class="y">▃▅ˍ▃▅ ▃▃▁▂▁</span><span class="w">▮</span><span class="d2">▯▯...▯5hx14</span> <span class="d">0.7x</span><span class="cite">№ 0001</span></div>
<div class="answer"><b>The week ledger — history in full, the future folded.</b> Each cell one 5h window, day-gapped; after the now-marker the empty run folds to a counted tail: <code>...▯5hx14</code> answers "14 slots to the reset", and <code>×</code> red answers "the tail runs dry".</div>
<div class="reading"><span class="d">7d</span> <span class="y">▃▅ˍ▃▅ ▃▃▁▂▁</span><span class="w">▮</span><span class="d2">▯▯...▯5h✕14</span> <span class="d">0.7✕</span><span class="cite">№ 0001</span></div>
<div class="answer"><b>The week ledger — history in full, the future folded.</b> Each cell one 5h window, day-gapped; after the now-marker the empty run folds to a counted tail: <code>...▯5h✕14</code> answers "14 slots to the reset", and <code>×</code> red answers "the tail runs dry" — <code>✕</code> is the operator, <code>×</code> is a reading.</div>
</div>
<div class="row">
<div class="reading"><span class="r">! <b>7d dry ~Tue 04:00</b> · hard stop</span><span class="cite">№ 0008</span></div>
Expand All @@ -277,7 +277,7 @@ <h2>Built like a meter should be</h2>
<div class="val"><h3>Numbers that don't lie</h3>
<p>Wall-clock resets instead of decaying countdowns. A visible <code>[░░░░░░0%]</code> after <code>/compact</code> instead of a hidden bar. Stale data is badged, never passed off as fresh. Burn is a monotone envelope, never a re-earned dip.</p></div>
<div class="val"><h3>Nothing running in the background</h3>
<p>No daemon, no telemetry, no npm. It runs when Claude Code renders the prompt, reads stdin plus one shared cached API call, prints its rows, exits. 417 bats tests on the one file.</p></div>
<p>No daemon, no telemetry, no npm. It runs when Claude Code renders the prompt, reads stdin plus one shared cached API call, prints its rows, exits. 418 bats tests on the one file.</p></div>
</div>
</section>

Expand Down Expand Up @@ -327,14 +327,14 @@ <h2>What it is not</h2>
// Rows beneath line 1 hang as one block: its right edge meets line 1's
// edge, every row shares one left edge — exactly how the script pads them.
// 5h ledger: 5 hour cells. 7d ledger: history in full, future folded.
var L5 = s('d','5h ') + s('g','▂▅█') + s('w','▮') + s('d2','▯') + s('d',' 0.8x');
var L5b = s('d','5h ') + s('g','▂▅█▄') + s('w','▮') + s('d',' 0.9x');
var L7 = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯...▯5hx14') + s('d',' 0.7x @Wed 09:00');
var L7x = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯') + s('r','...×5hx14') + s('d',' 0.9x @Wed 09:00');
var L5 = s('d','5h ') + s('g','▂▅█') + s('w','▮') + s('d2','▯') + s('d',' 0.8✕');
var L5b = s('d','5h ') + s('g','▂▅█▄') + s('w','▮') + s('d',' 0.9✕');
var L7 = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯...▯5h✕14') + s('d',' 0.7✕ @Wed 09:00');
var L7x = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯') + s('r','...×5h✕14') + s('d',' 0.9✕ @Wed 09:00');
// the 5h strip prints no reset: line 1's 5h badge already carries it
var LEDGER = L5 + ' ' + L7, LEDGER_B = L5b + ' ' + L7, LEDGER_X = L5b + ' ' + L7x;
var BUDGET = s('d2','- 10x5h left · 4.5%/win');
var BUDGET_B = s('d2','- 10x5h left · 4.3%/win');
var BUDGET = s('d2','- 10✕5h left · 4.5%/win');
var BUDGET_B = s('d2','- 10✕5h left · 4.3%/win');
var PIN = s('c','+ ') + s('c','<b>fb 91%</b>') + s('c',' vs 7d 55% · go op');
var FLASH = s('c','+ fb weekly 91% against 7d 55% · the model caps first, not the account');
var PIN_DRY = s('r','! ') + s('r','<b>7d dry ~Tue 04:00</b>') + s('r',' · hard stop');
Expand Down
Loading
Loading