diff --git a/CHANGELOG.md b/CHANGELOG.md
index e61b734..68bccdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,58 @@
## Unreleased
+## v0.32.0 — 2026-08-23 — say it once, and only when you know it
+
+**Row 3 was restating row 2 at greater length.** The flash reader
+carried the pinned notice's key and never compared against it, so at
+session start the same sentence arrived twice, once short and once
+long: `- 12✕5h left · 5.8%/win` over `- budget ~12✕5h left · even
+5.8%/win · heading ~85%`. The function's own comment already promised
+it "never echoes the pin's own sentence" — now it skips record one and
+starts at record two, and row 3 shows the highest-ranked notice the pin
+is *not* already carrying. A calm start is two rows, the way it was
+supposed to be.
+
+**A 7d projection now needs a day of this window's own evidence.**
+Minutes after a weekly rollover the learned walk was projecting last
+week's Tuesday onto a pool 2% spent — measured: a 2.4h-old window with
+2% used came back `red, dry in 95h`, painted `×` across slot 14
+onward, and put `heading ~100%` on the budget line. Every input for
+that verdict predates the reset, including the trailing-24h blend the
+walk opens with, which describes a day lying on the far side of it.
+`SEVEN_DAY_YOUNG_SECS` (86400) silences the learned walk, the linear
+at-risk pace, and the dry-cell fallback until the window is a day old
+— one gate, so the forecast notices, the `×` cells, the heading, the
+accuracy logger and both subcommands go quiet together. Badges still
+report real percentages, and a young window that is genuinely spent
+still goes red on its own 85%+: the guard mutes pace, never facts. The
+5h window is untouched.
+
+**Pace waits for a fraction of its window, not a fixed 15 minutes.**
+One gate served both strips: 900 seconds, which is 5% of a 5h window
+and 0.1% of a week. An hour into a fresh week, 2% of the pool over
+0.6% of the time rendered `3.0✕` in red. The gate is now length/20 —
+5h still waits exactly 15 minutes, 7d waits ~8.4h, the same fraction
+`seven_day_elapsed` has always called the noise floor.
+
+**The fold token counts windows, not cells: `...▯(✕12)`.** The old
+`...▯5h✕10` counted hollow cells the fold hid, while the budget line
+beside it priced `12✕5h left` — two arithmetics on one row, and the
+reader was left to arbitrate. The 34-cell grid spans 170h against a
+168h period, which is where the two differed. The token is now
+parenthesized, drops the redundant `5h` unit, and counts the 5h
+windows remaining to the true reset: the same number the budget line
+prices, from the same instant. Dry tails still read `...×(✕12)` — `×`
+the cell, `✕` the operator, one column apart and never the same mark.
+
+**The budget voice drops its sigil.** `!` and `+` mark a notice that
+interrupts — you are about to hit something, or there is capacity to
+take. The week's resting reading interrupts nothing, and a leading `-`
+on a dim line reads as a bullet, which made rows 2 and 3 look like a
+two-item list. The pin now reads `12✕5h left · 5.8%/win`. Dim is the
+mark; the sentence carries itself. `--check` and `--week` still label
+their long form with the word `budget`.
+
## 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
diff --git a/DESIGN.md b/DESIGN.md
index 6212ed8..bd3af9f 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -82,17 +82,19 @@ own signal.
## The ledgers (row 2)
```
-5h ▃▄▮▯▯ 0.6x @04:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▮▯▯...▯5hx14 0.7x @Wed 09:00
+5h ▃▄▮▯▯ 0.6x @04:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▮▯▯...▯(x14) 0.7x @Wed 09:00
```
One grammar, two scales. `5h` = this window as 5 hours; `7d` = the
period as 34 five-hour slots, oldest left, a gap at each local midnight
*in history only* — the run from `▮` on is contiguous. History draws in
full; the live row folds the 7d future after two kept cells into
-`...▯5hx14` — the slots left to the reset, all alike, `×` red when the
-tail projects dry (the `week` report still draws every slot). Each strip ends
-with its pace (used ÷ elapsed; dim <1x, pressure ≥1x, hidden under 15 min)
-and the reset its right edge is — axis labels, not restated badges.
+`...▯(x14)` — the 5h windows left before the reset, all alike, `×` red
+when the tail projects dry (the `week` report still draws every slot). The
+count is the budget line's own, priced from the same instant. Each strip ends
+with its pace (used ÷ elapsed; dim <1x, pressure ≥1x, hidden until 5% of the
+window has run) and the reset its right edge is — axis labels, not restated
+badges.
```
▁▂▃▄▅▆▇█ burned; height = points that cell cost (▁ ≤2 … ▅ ≤11 … █ >20)
@@ -100,7 +102,7 @@ and the reset its right edge is — axis labels, not restated badges.
░ unknown — no sample; never drawn as idle
▮ now
▯ ahead — the hollow of ▮
-...▯5hx14 the folded future: 14 more 5h slots to the reset (live 7d row)
+...▯(x14) the folded future: 14 more 5h windows before the reset (live 7d row)
× pace won't cover it (7d: learned forecast, linear when cold; 5h: linear)
```
diff --git a/README.md b/README.md
index 0514085..8b47237 100644
--- a/README.md
+++ b/README.md
@@ -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.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). |
+| Week row | **A row of its own, under the badges**: `5h ▅█▃▮▯ 0.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ …▮▯▯...▯(✕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 `...▯(✕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. |
@@ -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%]
-- 3✕5h left · 20%/win 5h ▅█▃▮▯ 0.9✕ 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @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
@@ -273,12 +273,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.9✕ @23:00 7d ▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ▅ˍ▂▁▁ ˍˍˍ▃▅ ˍˍˍ▅ ▆▆ˍ▂▮▯▯ 0.7✕ @Wed 09:00
- - budget ~3✕5h left · even 20%/win · heading ~52%
+ budget ~3✕5h left · even 20%/win · heading ~52%
```
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
+`1.6✕` caps early — dim below 1✕, pressure-tinted from 1✕, hidden until
+5% of the window has run: 15 min for 5h, ~8h for the week) 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
budget line shows with it (windows left, what even looks like, where
@@ -292,9 +292,11 @@ 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 `...▯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.
+ full; the future folds: two hollow cells after `▮`, then `...▯(✕28)` —
+ 28 more 5h windows before the reset, all alike (`×` red when the tail
+ projects dry). That count is the budget line's own `~28✕5h left`, priced
+ from the same instant: one row, one arithmetic. The `week` subcommand's
+ wide ledger still draws every slot.
| Cell | Meaning |
|------|---------|
@@ -304,8 +306,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) |
-| `...▯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) |
+| `...▯(✕28)` | the folded 7d future: 28 more 5h windows before the reset, one token instead of a run of hollow cells (`×` red when the tail ends dry). The count is windows-to-reset — what the budget line prices — not the number of cells the fold happened to hide |
+| `✕` | not a cell — the multiplication sign, the row's one operator (`...▯(✕28)`, `0.7✕`, `19✕5h left`). Deliberately not `×` (U+00D7), which is already a reading: cells are the ink, the operator is punctuation, and `...×(×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
@@ -365,7 +367,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. |
-| `- 19✕5h 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). It wears no sigil — `!` and `+` interrupt, the week's resting reading does not. |
`--notice off` keeps row 3 quiet; `--advisor off` silences both. `--check`
and `--week` print the long form, since a terminal command has a whole
@@ -509,7 +511,7 @@ run. Setting only `CLAUDE_CACHE_DIR` keeps the legacy single-dir behavior.
npm exec --yes bats -- t/
```
-418 tests across `t/statusline.bats` (406 statusline + integration) and
+423 tests across `t/statusline.bats` (411 statusline + integration) and
`t/install.bats` (12 installer). CI runs on push and PR to `main`.
## Project Structure
diff --git a/docs/index.html b/docs/index.html
index e2ce771..96dd473 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -4,7 +4,7 @@
Claude Code Statusline — a meter for your Claude Code plan
-
+
@@ -204,7 +204,7 @@
claude-code-statusline — a meter for your Claude Code plan
-
418 tests · jq + curl · no daemon · no telemetry
+
423 tests · jq + curl · no daemon · no telemetry
@@ -251,8 +251,8 @@
Read the meter
The 5h ledger — this sitting by the hour. Height is what each hour cost; ▮ now, ▯ ahead, ˍ ran-but-negligible, ░ no sample. Ends with its pace: above 1✕ you cap before the reset.
-
7d▃▅ˍ▃▅ ▃▃▁▂▁▮▯▯...▯5h✕140.7✕№ 0001
-
The week ledger — history in full, the future folded. Each cell one 5h window, day-gapped; after the now-marker the empty run folds to a counted tail: ...▯5h✕14 answers "14 slots to the reset", and × red answers "the tail runs dry" — ✕ is the operator, × is a reading.
+
7d▃▅ˍ▃▅ ▃▃▁▂▁▮▯▯...▯(✕14)0.7✕№ 0001
+
The week ledger — history in full, the future folded. Each cell one 5h window, day-gapped; after the now-marker the empty run folds to a counted tail: ...▯(✕14) answers "14 windows before the reset" — the same 14 the budget line prices — and × red answers "the tail runs dry"; ✕ is the operator, × is a reading.
! 7d dry ~Tue 04:00 · hard stop№ 0008
@@ -277,7 +277,7 @@
Built like a meter should be
Numbers that don't lie
Wall-clock resets instead of decaying countdowns. A visible [░░░░░░0%] after /compact instead of a hidden bar. Stale data is badged, never passed off as fresh. Burn is a monotone envelope, never a re-earned dip.
Nothing running in the background
-
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.
+
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. 423 bats tests on the one file.
@@ -329,12 +329,12 @@
What it is not
// 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.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');
+ var L7 = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯...▯(✕14)') + s('d',' 0.7✕ @Wed 09:00');
+ var L7x = s('d','7d ') + s('y','▅▁▂ ▃▅ˍ▃▅ ▃▃▁▂▁ ') + s('w','▮') + s('d2','▯▯') + s('r','...×(✕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','- 10✕5h left · 4.5%/win');
- var BUDGET_B = s('d2','- 10✕5h 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','fb 91%') + 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','7d dry ~Tue 04:00') + s('r',' · hard stop');
diff --git a/llms.txt b/llms.txt
index d808009..772341d 100644
--- a/llms.txt
+++ b/llms.txt
@@ -4,11 +4,11 @@
> per-model weekly limits), context pressure, session cost, prompt-cache
> health, and git activity in Claude Code's statusLine hook — plus a
> week row (this 5h window by the hour, the 7d period as its 5h
-> windows with the future folded to a counted `...▯5h✕28` tail: where
+> windows with the future folded to a counted `...▯(✕28)` tail: where
> the points went), an advisor row that interprets the
> numbers (cap projections, expiring-surplus and underuse advice, fleet
> relief), and a `report` subcommand that ledgers what each closed window
-> expired unused. 418 bats tests, deps are jq + curl, no daemon, no
+> expired unused. 423 bats tests, deps are jq + curl, no daemon, no
> telemetry.
Key facts for agents:
diff --git a/statusline.sh b/statusline.sh
index 39ee338..efeea21 100755
--- a/statusline.sh
+++ b/statusline.sh
@@ -52,6 +52,12 @@ FIVE_HOUR_RECOVERY_SECS=1800 # recovery color when reset <= 30min
SEVEN_DAY_RECOVERY_SECS=43200 # recovery color when reset <= 12h
QUOTA_BUMP_NOTICE_SECS=60 # how long a quota "+N" bump flash stays up
SEVEN_DAY_WINDOW_SECS=604800 # weekly quota window (fixed 7d) for pace math
+SEVEN_DAY_YOUNG_SECS=86400 # a 7d projection needs a day of THIS window's
+ # own evidence. Learned or linear, every pace
+ # read before that is last week's story told
+ # about a window that has barely started —
+ # and the trailing-24h blend is measuring a
+ # day that lies outside the window entirely
EXTRA_AUTO_UTIL_PCT=50 # show extra when its own utilization >= 50%
CACHE_BREAK_MIN_TOKENS=2000 # ignore cache drops below this (noise)
CACHE_BREAK_DROP_PCT=5 # cache read must drop >5% to count as break
@@ -79,9 +85,9 @@ ADVISOR_UNDERUSE_MIN_5H=25 # underuse advice only in an engaged session (5
CACHE_GLYPH="${CACHE_GLYPH:-≡}"
# Multiplication sign for the ledger rows' two factors: the folded-future
-# count (`...▯5h✕28`) and the pace (`0.6✕`). NOT × (U+00D7) — that glyph is
+# count (`...▯(✕12)`) and the pace (`0.6✕`). NOT × (U+00D7) — that glyph is
# already a strip cell, the one the pool will not cover, so reusing it would
-# print `...×5h×28` and make an operator look like a reading. U+2715 is the
+# print `...×(×12)` and make an operator look like a reading. U+2715 is the
# lighter mark of the pair by design: cells are the ink, the operator is
# punctuation. eaw=N and not an emoji, so it stays one column and the padding
# math holds — ╳ (U+2573) is ambiguous-width (double under a CJK locale) and
@@ -1945,13 +1951,20 @@ weekend_secs_ahead() {
# level: green|yellow|red runway_days: int, -1 when unknown hint: 0|1
# A 10% buffer keeps marginal overshoots quiet (warn only when you'll fall
# >~10% short). Falls back to level thresholds when pace can't be judged
-# (no deadline, or the noisy first ~8h). $2 drives elapsed; $3 the deadline.
+# (no deadline, the noisy first ~8h, or a window younger than
+# SEVEN_DAY_YOUNG_SECS — two hours into a fresh week, 2% used extrapolates to
+# a 98h runway and calls the week at risk on nothing). A genuinely burned
+# young window still goes red on its own percentage; only the pace math waits.
+# $2 drives elapsed; $3 the deadline.
# $1 used% $2 seconds_left $3 effective deadline secs (optional)
seven_day_pace() {
local used; used=$(printf '%.0f' "$1" 2>/dev/null || echo 0)
local secs_left="${2:-}" deadline="${3:-}"
[ -z "$deadline" ] && deadline="$secs_left"
local elapsed; elapsed=$(seven_day_elapsed "$secs_left")
+ if [ -n "$elapsed" ] && [ "$elapsed" -lt "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null; then
+ elapsed=""
+ fi
if [ -z "$elapsed" ] || [ "$used" -le 0 ] 2>/dev/null; then
if [ "$used" -ge 85 ] 2>/dev/null; then echo "red -1 0"
elif [ "$used" -ge 70 ] 2>/dev/null; then echo "yellow -1 0"
@@ -2255,6 +2268,14 @@ _profile_walk() {
local fc="$CLAUDE_ACCOUNT_DIR/forecast.cache"
[ -f "$fc" ] || return 0
[ -n "$secs_left" ] && [ "$secs_left" -gt 0 ] 2>/dev/null || return 0
+ # A young window has no evidence of its own. The profile is learned from
+ # the windows BEFORE this one, so minutes after a rollover the walk
+ # projects last week's Tuesday onto a pool that is 2% spent and calls it
+ # dry — and the L1 recent-24h blend it opens with describes a trailing day
+ # that sits on the far side of the reset. Silence until the window can
+ # speak for itself; the same silence a cold start earns.
+ local since_reset=$(( SEVEN_DAY_WINDOW_SECS - secs_left ))
+ [ "$since_reset" -ge "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null || return 0
local used_int
used_int=$(printf '%.0f' "$used" 2>/dev/null || echo 0)
[ "$used_int" -gt 0 ] 2>/dev/null || return 0
@@ -2633,10 +2654,11 @@ run_session_summary() {
# ▮ the window you are in now
# ▯ a window still ahead of you
# × a window the pool will not cover at the current pace
-# Count ▮ and what follows for the budget line's own "~N✕5h left". Past
-# cells come from usage.jsonl; unknown and idle stay different glyphs
-# because drawing a gap in the record as an idle session is the one lie
-# this row must not tell. The prospective glance beside report's
+# The report draws every slot, so ▮ and what follows IS the budget line's
+# "~N✕5h left" laid out cell by cell; the live row folds that same tail and
+# prints the count instead. Past cells come from usage.jsonl; unknown and
+# idle stay different glyphs because drawing a gap in the record as an idle
+# session is the one lie this row must not tell. The prospective glance beside report's
# retrospective ledger; the same strip claude.py renders, so both
# surfaces tell one story. Reads usage.cache; stale data renders but says so.
# --- week row (the two windows as ledgers) ------------------------------------
@@ -2652,8 +2674,13 @@ run_session_summary() {
# ▮ the cell you are in now
# ▯ a cell still ahead of you (the hollow of ▮: an empty slot)
# × a cell the pool will not cover at the current pace
-# ...▯5h✕28 the folded future: 28 more 5h slots to the reset, all alike
-# (× red when the tail projects dry) — live 7d strip only.
+# ...▯(✕12) the folded future: 12 more 5h windows before the reset, all
+# alike (× red when the tail projects dry) — live 7d strip only.
+# The count is windows-to-reset, the same number the budget line
+# prices as "~12✕5h left" — one line, one arithmetic. (It is NOT
+# the hidden-cell count: the 34-cell grid spans 170h and the
+# period is 168h, and a row that says 10 beside a budget that
+# says 12 makes the reader arbitrate between its own halves.)
# ✕ is the operator, × is a cell: the two never mean the same.
# Unknown and idle are deliberately different glyphs: drawing a gap in the
# record as an idle session is the one lie this row must not tell.
@@ -2665,10 +2692,10 @@ FIVE_CELLS=5
FIVE_CELL_SECS=3600
WEEK_CACHE_TTL_SECS=300
# The live row compresses the 7d strip's future run: after the now-marker it
-# keeps WEEK_FUTURE_KEEP hollow cells, then folds the rest into `...▯5h✕28`
-# (count = slots to the reset; × red when the tail projects dry). History is
-# information, the future is all the same cell — but only fold when it hides
-# enough to matter, so a closing week still draws to its edge.
+# keeps WEEK_FUTURE_KEEP hollow cells, then folds the rest into `...▯(✕12)`
+# (count = 5h windows before the reset; × red when the tail projects dry).
+# History is information, the future is all the same cell — but only fold
+# when it hides enough to matter, so a closing week still draws to its edge.
WEEK_FUTURE_KEEP=2
WEEK_FUTURE_MIN_HIDE=10
@@ -2777,17 +2804,19 @@ five_history_cells() { week_scan "$1" "$2" | sed -n 2p; }
# The slot where burn exhausts the pool before reset (-1 = none). The learned
# walk speaks first (gap in HOURS before reset); with no trained forecast fall
# back to the linear projection claude.py's cap_eta uses, so a wall visible on
-# one surface is visible on the other.
+# one surface is visible on the other. Both wait out SEVEN_DAY_YOUNG_SECS: a
+# row of × drawn across a window that opened an hour ago is drawn from last
+# week's burn, and this row does not draw what it cannot see.
week_dry_slot() {
local seven_int="$1" seven_secs="$2" now="$3" period_start="$4"
local dry_epoch="" walk_gap walk_end elapsed7
read -r walk_gap walk_end <<<"$(_seven_day_walk "$seven_int" "$seven_secs")"
+ elapsed7=$(( SEVEN_DAY_WINDOW_SECS - seven_secs ))
if [ -n "$walk_gap" ] && [ "$walk_gap" != "-1" ] && [ "$walk_gap" -gt 0 ] 2>/dev/null; then
dry_epoch=$(( now + seven_secs - walk_gap * 3600 ))
- elif [ "$seven_int" -gt 0 ] 2>/dev/null; then
- elapsed7=$(( SEVEN_DAY_WINDOW_SECS - seven_secs ))
- [ "$elapsed7" -gt 0 ] && \
- dry_epoch=$(( now + elapsed7 * (100 - seven_int) / seven_int ))
+ elif [ "$seven_int" -gt 0 ] 2>/dev/null \
+ && [ "$elapsed7" -ge "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null; then
+ dry_epoch=$(( now + elapsed7 * (100 - seven_int) / seven_int ))
fi
if [ -n "$dry_epoch" ] && [ "$dry_epoch" -lt $(( now + seven_secs )) ] 2>/dev/null; then
echo $(( (dry_epoch - period_start) / 18000 ))
@@ -2799,10 +2828,14 @@ week_dry_slot() {
# The colored strip. Args: fill percent (for the pressure tint), now,
# period start, dry cell (-1 none), history line ("lo hi slot:cost,..."),
# cell count, cell seconds, day-gaps flag, future cells kept before folding
-# (0 = draw all), cell-unit label for the fold token. Cells and gaps come out
-# of one awk so the row is one string with a color run per role.
+# (0 = draw all), and the TRUE period length in seconds — the fold token
+# counts windows to the real reset, not cells on the grid, so it needs the
+# period, not the drawing. Defaults to the grid when a caller has nothing
+# truer to offer. Cells and gaps come out of one awk so the row is one string
+# with a color run per role.
build_ledger_strip() {
- local pct="$1" now="$2" ps="$3" dry="$4" hist="$5" cells_n="$6" cell_secs="$7" gaps="${8:-0}" fut="${9:-0}" unit="${10:-}"
+ local pct="$1" now="$2" ps="$3" dry="$4" hist="$5" cells_n="$6" cell_secs="$7" gaps="${8:-0}" fut="${9:-0}"
+ local plen="${10:-$(( cells_n * cell_secs ))}"
local span_lo="" span_hi="" cells=""
[ -n "$hist" ] && read -r span_lo span_hi cells <<<"$hist"
local fill_color tzoff_s
@@ -2810,7 +2843,7 @@ build_ledger_strip() {
tzoff_s=$(date +%z | awk '{ s=substr($0,1,1)=="-"?-1:1; h=substr($0,2,2)+0; m=substr($0,4,2)+0; print s*(h*3600+m*60) }')
awk -v w="$cells_n" -v cs="$cell_secs" -v ps="$ps" -v now="$now" -v dry="$dry" \
-v gaps="$gaps" -v tz="$tzoff_s" -v fut="$fut" \
- -v minhide="$WEEK_FUTURE_MIN_HIDE" -v unit="$unit" -v mult="$MULT_GLYPH" \
+ -v minhide="$WEEK_FUTURE_MIN_HIDE" -v plen="$plen" -v mult="$MULT_GLYPH" \
-v lo="${span_lo:--1}" -v hi="${span_hi:--1}" -v cells="$cells" \
-v C_FILL="$fill_color" -v C_DIM="$DIM" -v C_NOW="$BOLD" \
-v C_DRY="$RED" -v C_OFF="$RESET" '
@@ -2856,10 +2889,15 @@ build_ledger_strip() {
}
if (lim < w) {
# the fold: glyph = how the tail ends (× red when the pool
- # dries before the reset), count = slots left to the reset
+ # dries before the reset), count = 5h windows still to come
+ # before the reset — the same number the budget line prices,
+ # not the number of cells this fold happens to hide.
+ # No apostrophes in here: this comment lives inside a
+ # single-quoted awk program.
if (dry >= 0 && dry < w) { tg = "×"; tc = C_DRY }
else { tg = "▯"; tc = C_DIM }
- s = s C_OFF tc "..." tg unit mult (w - lim)
+ nleft = int((ps + plen - now + cs - 1) / cs)
+ s = s C_OFF tc "..." tg "(" mult nleft ")"
}
print s C_OFF
}'
@@ -2868,8 +2906,11 @@ build_ledger_strip() {
# 7d strip: 34 ✕ 5h cells from the period start, day-gapped. $5 is
# week_history_cells' line; $6 folds the future tail after that many kept
# cells (the live row passes WEEK_FUTURE_KEEP; the `week` report draws all).
+# The grid overshoots the period by design (34 ✕ 5h = 170h against a 168h
+# week), so the fold is told the period itself — its count ends at the reset,
+# where the week ends, not at the last cell it drew.
build_week_strip() {
- build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$WEEK_CELLS" 18000 1 "${6:-0}" 5h
+ build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$WEEK_CELLS" 18000 1 "${6:-0}" "$SEVEN_DAY_WINDOW_SECS"
}
# 5h strip: 5 ✕ 1h cells of the current window. $5 is
@@ -2920,15 +2961,19 @@ five_period_start() {
# Tail of a strip: pace and the axis label of its right end (the reset).
# pace = used / elapsed-fraction; >1✕ means the pool caps before the reset.
# Dim below 1✕, pressure-tinted from 1✕ (status lane), hidden while the
-# window is too young to judge (ADVISOR_PACE_MIN_ELAPSED). Reset is wall
+# window is too young to judge — and young is a FRACTION of the window, not a
+# clock reading: 5% of its own length, so 5h still waits 15m and 7d waits
+# ~8.4h (the same fraction seven_day_elapsed calls noisy). A flat 15m was
+# right for 5h and nonsense for a week: an hour into a fresh window, 2% of
+# the pool divided by 0.6% of the time printed 3✕ in red. Reset is wall
# clock: `@04:00` inside 24h, `@Wed 09:00` beyond — an axis label for a
# timeline that ends there. $5 = "hide" drops it: when the badge above
# already carries that reset, printing it again spends columns to say
# nothing (one badge per fact, in both directions).
strip_tail() {
local pct="$1" secs_left="$2" length="$3" now="$4" reset_mode="${5:-show}"
- local out="" elapsed=$(( length - secs_left ))
- if [ "$elapsed" -ge "$ADVISOR_PACE_MIN_ELAPSED" ] && [ "$pct" -gt 0 ] 2>/dev/null; then
+ local out="" elapsed=$(( length - secs_left )) min_elapsed=$(( length / 20 ))
+ if [ "$elapsed" -ge "$min_elapsed" ] && [ "$pct" -gt 0 ] 2>/dev/null; then
local pace tint band
pace=$(awk -v u="$pct" -v e="$elapsed" -v l="$length" 'BEGIN{ printf "%.1f", (u/100)/(e/l) }')
band=$(awk -v p="$pace" 'BEGIN{ print (p>=1.5?2:(p>=1.0?1:0)) }')
@@ -3476,6 +3521,8 @@ build_extra_usage_display() {
# opportunity "+ ..." cyan paid capacity is about to expire unused,
# or a sibling account is free while you're
# pinned — spend, don't conserve
+# budget no sigil, dim the week's resting reading; it interrupts
+# nothing, so it wears no mark
# Cyan is reserved for opportunity: it can never mean pressure, so the color
# alone carries the stance. Quiet = no row at all (Claude Code renders each
# stdout line as its own row; printing nothing costs nothing). All times are
@@ -3561,7 +3608,7 @@ build_advisor_fleet_hint() {
# rank voice scope key hl short long
#
# rank value order; the top surviving record owns row 2
-# voice !red / !yellow pressure · + opportunity · - budget (dim)
+# voice !red / !yellow pressure · + opportunity · - budget (dim, no sigil)
# scope 5h / 7d / fb / acct — one voice per scope per frame, so two
# clauses about the same window can never disagree
# key identity of the CONDITION, not of the text: row 3 shows a notice
@@ -3601,11 +3648,16 @@ notice_highlight() {
printf '%s%s%s%s%s%s' "${text%%"$tok"*}" "$BOLD" "$tok" "$NO_BOLD" "$color" "${text#*"$tok"}"
}
+# A sigil marks a voice that INTERRUPTS: `!` you are about to hit something,
+# `+` there is capacity to take. The budget voice interrupts nothing — it is
+# the week's resting reading — and a leading `-` on a dim line reads as a
+# bullet, which turned the pin into the first item of a list that had no
+# second item. Dim is the mark; the sentence carries itself.
notice_render() {
- local voice="$1" hl="$2" text="$3" color sigil
+ local voice="$1" hl="$2" text="$3" color sigil=''
color=$(notice_voice_color "$voice")
- case "$voice" in '!'*) sigil='!' ;; '+') sigil='+' ;; *) sigil='-' ;; esac
- printf '%s%s %s%s' "$color" "$sigil" "$(notice_highlight "$text" "$hl" "$color")" "$RESET"
+ case "$voice" in '!'*) sigil='! ' ;; '+') sigil='+ ' ;; esac
+ printf '%s%s%s%s' "$color" "$sigil" "$(notice_highlight "$text" "$hl" "$color")" "$RESET"
}
# First time THIS session saw a condition (stamping it if new). Wall-clock
@@ -3657,7 +3709,10 @@ notice_long_line() {
# Row 3: the same engine's long form, but only while the condition is new to
# this session. It says the part that does not fit beside the ledgers, then
-# disappears and leaves the pin. Never echoes the pin's own sentence.
+# disappears and leaves the pin. Never echoes the pin's own sentence: record
+# one IS the pin, so the flash starts at record two. A calm start has one
+# thing to say and says it once — two rows, not the same sentence twice at
+# two lengths.
notice_flash_line() {
local records="$1" state="$2" now="${3:-$(date +%s)}"
[ -n "$records" ] || return 0
@@ -3665,6 +3720,7 @@ notice_flash_line() {
while IFS="$NOTICE_FS" read -r rank voice scope key hl short long; do
[ -n "$key" ] || continue
if [ "$first" = 1 ]; then pin_key="$key"; first=0; fi
+ [ "$key" = "$pin_key" ] && continue
[ -n "$long" ] || continue
local seen age
seen=$(notice_first_seen "$key" "$state" "$now")
diff --git a/t/helpers.bash b/t/helpers.bash
index 0400cfc..66e8198 100644
--- a/t/helpers.bash
+++ b/t/helpers.bash
@@ -53,6 +53,7 @@ FIVE_HOUR_RECOVERY_SECS=1800
SEVEN_DAY_RECOVERY_SECS=43200
QUOTA_BUMP_NOTICE_SECS=60
SEVEN_DAY_WINDOW_SECS=604800
+SEVEN_DAY_YOUNG_SECS=86400
EXTRA_AUTO_UTIL_PCT=50
CACHE_BREAK_MIN_TOKENS=2000
CACHE_BREAK_DROP_PCT=5
diff --git a/t/statusline.bats b/t/statusline.bats
index 3ab6c56..65bb0ef 100644
--- a/t/statusline.bats
+++ b/t/statusline.bats
@@ -1259,6 +1259,19 @@ _seed_week_store() {
[[ "$plain" =~ @[A-Z][a-z][a-z]\ [0-9]{2}:[0-9]{2}$ ]]
}
+@test "strip_tail: young is a fraction of the window, not a clock reading" {
+ now=$(date +%s)
+ # 5h is unchanged: 15m is 5% of it, and the pace is legible from there
+ [[ "$(strip_ansi "$(strip_tail 5 $((18000 - 900)) 18000 "$now" hide)")" == *"✕"* ]]
+ [ -z "$(strip_tail 5 $((18000 - 899)) 18000 "$now" hide)" ]
+ # the same 15m is 0.1% of a week: 2% of the pool over 0.1% of the time
+ # printed 3.0✕ in red on an hour-old window. 7d now waits ~8.4h, the
+ # fraction seven_day_elapsed already calls the noise floor.
+ [ -z "$(strip_tail 2 $((604800 - 900)) 604800 "$now" hide)" ]
+ [ -z "$(strip_tail 2 $((604800 - 30239)) 604800 "$now" hide)" ]
+ [[ "$(strip_ansi "$(strip_tail 2 $((604800 - 30240)) 604800 "$now" hide)")" == *"✕"* ]]
+}
+
@test "build_week_row: each strip ends with its pace and reset" {
tmpdir=$(mktemp -d)
CLAUDE_ACCOUNT_DIR="$tmpdir"
@@ -1342,22 +1355,48 @@ _seed_week_store() {
plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//')
# everything from ▮ to the end is one contiguous run: kept hollow cells,
# then the folded tail (no gap ever lands right after the now-marker)
- [[ "${plain#*▮}" =~ ^▯+\.\.\.▯5h✕[0-9]+$ ]]
+ [[ "${plain#*▮}" =~ ^▯+\.\.\.▯\(✕[0-9]+\)$ ]]
rm -rf "$tmpdir"
}
-@test "build_week_row: the folded 7d tail still accounts for all 34 slots" {
+@test "build_week_row: the folded 7d tail counts windows to the reset, not cells hidden" {
tmpdir=$(mktemp -d)
CLAUDE_ACCOUNT_DIR="$tmpdir"
- reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ')
- usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d")
+ now=$(date +%s)
+ # not a whole number of 5h windows out: the grid rounds the period start
+ # to 5 min, and an exact multiple would sit on the ceil boundary
+ reset_epoch=$((now + 5 * 86400 + 3600))
+ reset_7d=$(date -u -d "@$reset_epoch" '+%Y-%m-%dT%H:%M:%SZ')
+ usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$now" "$reset_7d")
plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ')
strip="${plain#7d}"
- [[ "$strip" =~ ^(.*)\.\.\.▯5h✕([0-9]+)$ ]]
+ [[ "$strip" =~ ^(.*)\.\.\.▯\(✕([0-9]+)\)$ ]]
drawn=$(printf '%s' "${BASH_REMATCH[1]}" | grep -o . | wc -l)
- [ $((drawn + BASH_REMATCH[2])) -eq 34 ]
# exactly WEEK_FUTURE_KEEP hollow cells stay visible before the fold
[[ "${BASH_REMATCH[1]}" == *"▮▯▯" ]]
+ # the count is 5h windows before the reset — the number the budget line
+ # prices — and NOT the cells the fold hid: the 34-cell grid spans 170h
+ # against a 168h period, so the two disagree by design
+ [ "${BASH_REMATCH[2]}" -eq $(( (reset_epoch - now + 17999) / 18000 )) ]
+ [ "${BASH_REMATCH[2]}" -ne $((34 - drawn)) ]
+ rm -rf "$tmpdir"
+}
+
+@test "build_week_row: the fold token and the budget line count the same windows" {
+ # one line, one arithmetic: the row folded `...▯(✕N)` and the budget
+ # sentence beside it priced `~N✕5h left` from the same instant, and a
+ # reader must never have to arbitrate between the halves of one row
+ tmpdir=$(mktemp -d)
+ CLAUDE_ACCOUNT_DIR="$tmpdir"
+ now=$(date +%s)
+ reset_7d=$(date -u -d "@$((now + 3 * 86400 + 7000))" '+%Y-%m-%dT%H:%M:%SZ')
+ usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":30,"resets_at":"%s"}}' "$now" "$reset_7d")
+ row=$(strip_ansi "$(build_week_row "$usage" always)")
+ [[ "$row" =~ \(✕([0-9]+)\) ]]
+ fold="${BASH_REMATCH[1]}"
+ pin=$(strip_ansi "$(build_advisor_line "$usage" always)")
+ [[ "$pin" =~ ^([0-9]+)✕5h\ left ]]
+ [ "$fold" -eq "${BASH_REMATCH[1]}" ]
rm -rf "$tmpdir"
}
@@ -1368,10 +1407,10 @@ _seed_week_store() {
reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ')
usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":50,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d")
plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ')
- [[ "$plain" =~ \.\.\.×5h✕[0-9]+$ ]]
+ [[ "$plain" =~ \.\.\.×\(✕[0-9]+\)$ ]]
# the dry cell and the operator sit side by side in this token and must
# never be the same mark: × is a reading, ✕ is punctuation
- [[ "$plain" == *"×5h✕"* ]]
+ [[ "$plain" == *"×(✕"* ]]
rm -rf "$tmpdir"
}
@@ -1419,7 +1458,7 @@ _seed_week_store() {
usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":20,"resets_at":"%s"}}' "$(date +%s)" "$reset_7d")
[ -z "$(build_week_row "$usage" auto)" ]
plain=$(strip_ansi "$(build_week_row "$usage" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ')
- [[ "$plain" =~ ^7d░+▮▯+\.\.\.▯5h✕[0-9]+$ ]]
+ [[ "$plain" =~ ^7d░+▮▯+\.\.\.▯\(✕[0-9]+\)$ ]]
[ -z "$(build_week_row "$usage" off)" ]
# no live 7d window: nothing, even in always mode
[ -z "$(build_week_row '{"seven_day":{"utilization":20}}' always)" ]
@@ -1464,11 +1503,11 @@ _seed_week_store() {
[ "$(compact_text "$t" 200)" = "$t" ]
[ "$(compact_text "$t" 40)" = "! 5h caps ~05:18, 52m before reset" ]
[ "$(compact_text "$t" 20)" = "! 5h caps ~05:18" ]
- b="- budget ~3✕5h left · even 20%/win · heading ~52%"
- [ "$(compact_text "$b" 36)" = "- budget ~3✕5h left · even 20%/win" ]
- [ "$(compact_text "$b" 19)" = "- budget ~3✕5h left" ]
+ b="budget ~3✕5h left · even 20%/win · heading ~52%"
+ [ "$(compact_text "$b" 34)" = "budget ~3✕5h left · even 20%/win" ]
+ [ "$(compact_text "$b" 17)" = "budget ~3✕5h left" ]
# no joint left: hard cut with an ellipsis, never a mid-word lie
- [ "$(compact_text "- budget last window is long" 16)" = "- budget last w…" ]
+ [ "$(compact_text "budget last window is long" 16)" = "budget last win…" ]
# under 16 columns nothing honest fits
run compact_text "$b" 15
[ "$status" -eq 1 ]
@@ -1705,6 +1744,67 @@ EOF
rm -rf "$tmpdir"
}
+# --- the young-window guard: a 7d projection needs this window's evidence ---
+
+@test "young 7d window: nothing projects from last week onto a window hours old" {
+ tmpdir=$(mktemp -d)
+ CLAUDE_ACCOUNT_DIR="$tmpdir"
+ now=$(date +%s)
+ # A heavy trained profile (30%/day, 40% in the trailing 24h) and a window
+ # that opened 2.4h ago with 2% on it. Every point of that profile — and
+ # every hour of that trailing day — belongs to the window BEFORE this one.
+ _write_profile_cache "$tmpdir" 21 30 40
+ young=$((604800 - 8640))
+ [ -z "$(seven_day_forecast 2 $young)" ]
+ [ -z "$(_seven_day_walk 2 $young)" ]
+ ps=$(week_period_start "$now" "$young")
+ [ "$(week_dry_slot 2 "$young" "$now" "$ps")" = "-1" ]
+ reset_7d=$(date -u -d "@$((now + young))" '+%Y-%m-%dT%H:%M:%SZ')
+ usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":2,"resets_at":"%s"}}' "$now" "$reset_7d")
+ row=$(strip_ansi "$(build_week_row "$usage" always)")
+ [[ "$row" != *×* ]] # no wall of dry cells drawn from history
+ [[ "$row" != *"✕ @"* ]] # and no pace on a 2.4h-old week
+ records=$(notice_collect "$usage" always)
+ [ -z "$(printf '%s\n' "$records" | grep -E '7d\.(dry|caps)')" ]
+ long=$(strip_ansi "$(notice_long_line "$records")")
+ [[ "$long" == budget* ]]
+ [[ "$long" != *heading* ]] # nowhere to head yet
+ rm -rf "$tmpdir"
+}
+
+@test "young 7d window: a day in, the projections come back" {
+ tmpdir=$(mktemp -d)
+ CLAUDE_ACCOUNT_DIR="$tmpdir"
+ now=$(date +%s)
+ _write_profile_cache "$tmpdir" 21 30 40
+ aged=$((604800 - 90000)) # 25h elapsed: the window can speak
+ [ -n "$(seven_day_forecast 2 $aged)" ]
+ ps=$(week_period_start "$now" "$aged")
+ [ "$(week_dry_slot 2 "$aged" "$now" "$ps")" != "-1" ]
+ reset_7d=$(date -u -d "@$((now + aged))" '+%Y-%m-%dT%H:%M:%SZ')
+ usage=$(printf '{"fetched_at":%s,"five_hour":{"utilization":9},"seven_day":{"utilization":2,"resets_at":"%s"}}' "$now" "$reset_7d")
+ row=$(strip_ansi "$(build_week_row "$usage" always)")
+ [[ "$row" == *×* ]]
+ records=$(notice_collect "$usage" always)
+ [ -n "$(printf '%s\n' "$records" | grep -E '7d\.(dry|caps)')" ]
+ [[ "$(strip_ansi "$(notice_long_line "$records")")" == *"7d dry"* ]]
+ rm -rf "$tmpdir"
+}
+
+@test "seven_day_pace: a window younger than a day has no pace of its own" {
+ # 8% burned 12h into a fresh week extrapolates to 16%/day and calls the
+ # week at risk — on half a day of evidence, most of it one session.
+ read -r level runway hint <<<"$(seven_day_pace 8 $((604800 - 43200)))"
+ [ "$level" = "green" ]; [ "$hint" = "0" ]; [ "$runway" = "-1" ]
+ # a day in, the same arithmetic is allowed to speak
+ read -r level runway hint <<<"$(seven_day_pace 20 $((604800 - 90000)))"
+ [ "$level" = "yellow" ]; [ "$hint" = "1" ]
+ # ...and a young window that is genuinely spent still goes red on its own
+ # percentage: the guard mutes pace, never facts
+ read -r level runway hint <<<"$(seven_day_pace 88 $((604800 - 43200)))"
+ [ "$level" = "red" ]
+}
+
# --- is_1m_model ------------------------------------------------------------
@test "is_1m_model: ctx_size > 200k is sufficient regardless of exceeds_200k" {
@@ -3421,10 +3521,12 @@ JSON
reset_7d=$(date -u -d '+4 days 22 hours' '+%Y-%m-%dT%H:%M:%SZ')
usage="{\"five_hour\":{\"utilization\":20,\"resets_at\":\"$reset_5h\"},\"seven_day\":{\"utilization\":21,\"resets_at\":\"$reset_7d\"}}"
plain=$(strip_ansi "$(build_advisor_line "$usage" always)")
- [[ "$plain" =~ ^-\ 24✕5h\ left\ ·\ 3\.3%/win$ ]]
+ # the budget voice wears no sigil: it interrupts nothing, and a leading
+ # '-' on a dim line reads as a bullet in a list of one
+ [[ "$plain" =~ ^24✕5h\ left\ ·\ 3\.3%/win$ ]]
# the whole sentence is still there for surfaces with a line to spend
long=$(strip_ansi "$(notice_long_line "$(notice_collect "$usage" always)")")
- [[ "$long" =~ ^-\ budget\ ~24✕5h\ left\ ·\ even\ 3\.3%/win\ ·\ heading\ ~70%$ ]]
+ [[ "$long" =~ ^budget\ ~24✕5h\ left\ ·\ even\ 3\.3%/win\ ·\ heading\ ~70%$ ]]
}
@test "build_advisor_line: budget degrades to plain headroom in the last window" {
@@ -3438,10 +3540,10 @@ JSON
# 25% unused is under ADVISOR_SURPLUS_MIN_PCT, so the end-of-week voice
# stays quiet and the budget line owns the row
plain=$(strip_ansi "$(build_advisor_line "$usage" always)")
- [[ "$plain" =~ ^-\ last\ window\ ·\ 25%\ left$ ]]
+ [[ "$plain" =~ ^last\ window\ ·\ 25%\ left$ ]]
[[ "$plain" != *"/win"* ]]
long=$(strip_ansi "$(notice_long_line "$(notice_collect "$usage" always)")")
- [[ "$long" =~ ^-\ budget\ last\ window\ ·\ 25%\ left\ ·\ heading\ ~[0-9]+%$ ]]
+ [[ "$long" =~ ^budget\ last\ window\ ·\ 25%\ left\ ·\ heading\ ~[0-9]+%$ ]]
}
@test "build_advisor_line: hot 5h pace projects the cap wall-clock" {
@@ -3531,19 +3633,24 @@ JSON
NOTICE_RECS=()
}
-@test "notice_flash_line: explains a new condition, then fades and leaves the pin" {
+@test "notice_flash_line: the pin never flashes its own sentence; a second condition does" {
state=$(mktemp -d)/notice_seen
NOTICE_RECS=()
notice_add 90 '!yellow' 5h '5h.caps.05:18' '~05:18' '5h caps ~05:18' '5h caps ~05:18, 42m before reset'
records=$(notice_ranked)
now=$(date +%s)
+ # one condition, one row: row 3 would only restate row 2 at more length
+ [ -z "$(notice_flash_line "$records" "$state" "$now")" ]
+ [ "$(strip_ansi "$(notice_pin_line "$records")")" = "! 5h caps ~05:18" ]
+ # a second, lower-ranked condition has something the pin does not say
+ notice_add 65 '+' 7d '7d.surplus.9' '47%' '47% unused' '7d resets @07:00, 47% unused · spend it'
+ records=$(notice_ranked)
first=$(strip_ansi "$(notice_flash_line "$records" "$state" "$now")")
- [ "$first" = "! 5h caps ~05:18, 42m before reset" ]
+ [ "$first" = "+ 7d resets @07:00, 47% unused · spend it" ]
# same condition a minute later: still inside the flash window
[ -n "$(notice_flash_line "$records" "$state" $((now + 60)))" ]
# ...and past it the row is gone; row 2 keeps the pin
[ -z "$(notice_flash_line "$records" "$state" $((now + NOTICE_FLASH_SECS + 1)))" ]
- [ "$(strip_ansi "$(notice_pin_line "$records")")" = "! 5h caps ~05:18" ]
NOTICE_RECS=()
rm -rf "$(dirname "$state")"
}
@@ -3840,17 +3947,23 @@ _write_ppw_fixture() { # dir ppw
reset_5h=$(date -u -d '+3 hours' '+%Y-%m-%dT%H:%M:%SZ')
reset_7d=$(date -u -d '+5 days' '+%Y-%m-%dT%H:%M:%SZ')
_mk_input() {
- printf '{"session_id":"adv","model":{"id":"claude-opus-4-8","display_name":"Opus"},"cwd":"/t","workspace":{"current_dir":"/t"},"cost":{"total_cost_usd":0},"context_window":{"used_percentage":10,"context_window_size":200000},"rate_limits":{"five_hour":{"used_percentage":%s,"resets_at":"%s"},"seven_day":{"used_percentage":10,"resets_at":"%s"}}}' "$1" "$reset_5h" "$reset_7d"
+ printf '{"session_id":"adv","model":{"id":"claude-opus-4-8","display_name":"Opus"},"cwd":"/t","workspace":{"current_dir":"/t"},"cost":{"total_cost_usd":0},"context_window":{"used_percentage":10,"context_window_size":200000},"rate_limits":{"five_hour":{"used_percentage":%s,"resets_at":"%s"},"seven_day":{"used_percentage":%s,"resets_at":"%s"}}}' "$1" "$reset_5h" "${2:-10}" "$reset_7d"
}
hot=$(_mk_input 85 | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh" --notice off)
[ "$(printf '%s\n' "$hot" | wc -l)" -eq 2 ]
[[ "$(strip_ansi "$hot")" == *"5h caps ~"* ]]
- # a condition this session has not seen yet also explains itself on row 3
- fresh=$(_mk_input 85 | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh")
- [ "$(printf '%s\n' "$fresh" | wc -l)" -eq 3 ]
- [[ "$(strip_ansi "$fresh")" == *"before reset"* ]]
+ # one condition is one row: row 3 does not restate the pin above it
+ lone=$(_mk_input 85 | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh")
+ [ "$(printf '%s\n' "$lone" | wc -l)" -eq 2 ]
calm=$(_mk_input 20 | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh")
[ "$(printf '%s\n' "$calm" | wc -l)" -eq 1 ]
+ # a SECOND condition this session has not seen earns row 3 with its own
+ # sentence — the 7d wall the 5h pin says nothing about. Last in the
+ # sequence on purpose: a later render at a lower 7d would read this
+ # window as re-based and speak about that instead.
+ fresh=$(_mk_input 85 95 | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh")
+ [ "$(printf '%s\n' "$fresh" | wc -l)" -eq 3 ]
+ [[ "$(strip_ansi "$fresh")" == *"before reset"* ]]
rm -rf "$tmpdir"
}