diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b356e..6585138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # Changelog +## v0.37.0 — 2026-09-01 — what a point buys + +**Two pools drain toward one wall and nothing said which one binds.** The +live reading that started this: the account at 81%, the model-scoped +weekly at 63%, both resetting at the same instant. Line 1 has printed the +two numbers for months and never their relation — and the relation is the +decision, because 22 of the model's remaining points are already +unreachable at the rate this week is mixing them. + +**The ratio is the mix, and it needs no history.** Both counters are +cumulative from one reset instant, so `scope / seven` IS this week's rate +of scoped points per account point. `reachable = round((100 - seven) * +scope / seven)` is what the account's remaining points can still buy in +the scoped pool, and `strand = (100 - scope) - reachable` is the rest of +it. At 81/63: mix 0.78, reachable 15, strand 22 — against 0.77 mined from +the corpus at week level. One division, and the strand is its remainder, +because both numbers print beside the total they have to add to. No new +`forecast.cache` field and no schema concern: the inputs are already on +screen. + +**A new `+` notice at rank 58.** `fb ~22% expires at this mix`, and in +full: `7d caps before fb: this mix reaches ~15% of its 37% left · run fb +heavier to extract more`. Above the 5h tail, below the fleet hint, and it +can never outrank the 7d pressure that causes it — the pairing it was +built for is the dry warning pinned on row 2 with the strand flashing on +row 3. It is the mirror of `fb 91% vs 7d 55%` and the two cannot coexist: +that one needs the model ahead of the account, this one needs the account +ahead of the model. + +**Gates, because a ratio is easy to say and easy to get wrong.** One wall +(`SCOPE_SAME_WALL_SEC = 120`: the pools have always reset together to the +microsecond, but Anthropic could split them, and a ratio across two +different weeks is a fluent lie); the account at 60% or deeper, where +"which cap binds" is a question at all; the model at 5% or more, since a +model untouched this week belongs to the underuse voice; both pools under +100; ten stranded points or more; and the 7d window past the young guard +every projection shares. Two mutes on top, statusline-only: a projected +scoped cap, because "fb caps first" and "fb strands" cannot both be true +in one frame, and a live re-base, which just moved one of the two counters +for a reason the ratio cannot see. + +**`report` prices the same thing in the other currency.** One line after +the price and spend block: `fb mix: ~0.78 fb-pt per 7d-pt this week · +7d's 19% left carries ~15% of fb's 37%`. No session runs there to name a +model, so it takes the binding scoped limit, else the deepest. + +The four constants are shared rules, not local thresholds: +`SCOPE_STRAND_MIN_PCT`, `SCOPE_MIX_MIN_7D`, `SCOPE_MIX_MIN_SCOPE` and +`SCOPE_SAME_WALL_SEC` are documented in `docs/api/state-dir.md` as +READING RULES computed live rather than cache fields, and ccpace v0.5.0 +ships the same four with the same closed forms. The coupling in the other +direction — what a scoped point costs the account — stays unpublished: +n=22 in a 0.2-0.5 band is fluent enough to be believed and too noisy to +be true. + ## v0.36.0 — 2026-09-01 — the night is not runway **The forecast learns when you work, not just how much.** Claude Code can diff --git a/DESIGN.md b/DESIGN.md index 9fff311..b57eeb9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -161,6 +161,9 @@ fb capped ~Thu 07:00 the running model is gone until then last 5h of the week no later window exists to spend the remainder through fb 91% vs 7d 55% the MODEL caps first, not the account: switch, and the week's remaining capacity comes back +fb ~22% expires the ACCOUNT caps first: at this week's mix the 7d pool + runs out before the model's does, and what it cannot + reach expires unspent 5h ~40m left throughput you cannot bank — said only while the week has slack, since an unspent 5h window is otherwise headroom, not waste diff --git a/README.md b/README.md index d398507..fe5da9b 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,7 @@ clauses about one window can never disagree. | `! 5h caps ~05:18` | Linear projection off this window's own pace: you hit the wall before the reset. Long form adds how long you'd sit blocked. | | `! 7d dry ~Thu 09:00 · hard stop` | The learned per-weekday burn profile (EWMA over your own history), not a straight line — your heavy Tuesday counts more than an average, and the learned hour-of-day shape puts the dry point where you are awake to act on it instead of at 03:00. Tail says what happens at 100%: extra billing, or a stop. | | `! fb 91% vs 7d 55% · go op` | **The relation** between two badges: the *model* caps before the account does. Switching models buys the week's remaining capacity back; the roomiest other `weekly_scoped` limit in the payload gets named. | +| `+ fb ~22% expires at this mix` | **The mirror of the row above**: the *account* caps first, so the model's remaining pool cannot all be spent. Both counters run from one reset instant, so their live ratio is this week's mix rate: at 81% / 63% the 19 account points left carry ~15 of the model's 37, and the other 22 expire where nothing can reach them. Running the scoped model heavier is what moves the mix. | | `+ 7d rebased 53%→12%` | Utilization fell *inside* one window instance: burn never runs backwards, so this is a plan change or an out-of-band reset. The projections restart; the ledger still draws the old period. | | `+ last 5h of the week · 47% unused` | The 7d window resets inside this 5h window: no later window exists to spend the remainder through. | | `+ 47% unused · spend it` (or `· ~33% expires even at full burn`) | Expiring surplus, feasibility-checked against your learned `pct_per_window`: "spend it" only when full-tilt burn can actually consume it. | diff --git a/docs/api/state-dir.md b/docs/api/state-dir.md index 3e98a36..2e69ae1 100644 --- a/docs/api/state-dir.md +++ b/docs/api/state-dir.md @@ -7,7 +7,7 @@ read concurrently. - Contract version: **2** (bump on any breaking layout/field change; this file is the changelog) -- Synced with: statusline.sh v0.36.0 +- Synced with: statusline.sh v0.37.0 - Permissions: the script runs under `umask 077` — files are owner-only. Caches hold account PII (email, uuid, org names). @@ -100,6 +100,49 @@ when idle); a `usage.err` JSON file appears during fetch-failure cooldowns Treat `fetched_at` as authoritative staleness; anything older than ~1h means no active session is feeding this account. +#### Two pools, one wall + +The account `seven_day` limit and a `weekly_scoped` limit are two counters +over one week: the same reset instant, both cumulative from it. Their live +ratio is therefore this week's MIX RATE, and no history is needed to read +it. Everything in this section is COMPUTED LIVE from the two numbers above +— nothing here is a stored field, and no cache changes when it changes. + +``` +mix = scope / seven scoped points per 7d point +reachable = round((100 - seven) * scope / seven) of the scoped pool +strand = (100 - scope) - reachable = round(100 * (seven - scope) / seven) +``` + +`reachable` is what the account's remaining points can still buy in the +scoped pool; `strand` is the rest of that pool, which expires because +nothing can reach it. Round ONCE and take the strand as the remainder: the +two numbers are printed beside the total they must add to. + +Live check: `seven = 81`, `scope = 63` gives mix 0.78, reachable 15, strand +22 — against 0.77 mined from the corpus at week level. + +The reading speaks only when ALL of these hold: + +| Rule | Value | Why | +|------|-------|-----| +| `SCOPE_SAME_WALL_SEC` | 120 | Resets no further apart than this are one wall. The two have always reset together to the microsecond, but Anthropic could split them, and a ratio taken across two different weeks is a fluent lie. | +| `SCOPE_MIX_MIN_7D` | 60 | "Which cap binds" is only a question once the account's own end is in sight, and the ratio is well established by then. | +| `SCOPE_MIX_MIN_SCOPE` | 5 | A model untouched this week belongs to the underuse reading, not to a mix. | +| `SCOPE_STRAND_MIN_PCT` | 10 | Under ten points there is nothing to act on. | + +Plus: both pools under 100 (a capped pool is its own message), and the 7d +window past the young guard every projection shares (a day of this +window's own evidence). + +Selecting the scoped limit where no running model names one (`report`, +ccpace): prefer `is_active == true`, else the deepest `weekly_scoped`. A +surface that knows which model the session runs matches on that instead. + +The coupling in the other direction — what a scoped point costs the +account — is deliberately NOT published. Measured at n=22 it sits in a +0.2-0.5 band: fluent enough to be believed, too noisy to be true. + ### profile.cache Raw `/api/oauth/profile` response: `account.{uuid,email,display_name,…}`, diff --git a/statusline.sh b/statusline.sh index f20d368..a361fc8 100755 --- a/statusline.sh +++ b/statusline.sh @@ -2096,6 +2096,20 @@ FORECAST_SCHEMA=2 # that disagree about which hours are spendable disagree about the ration. REST_MULT_MAX=0.25 +# The account 7d pool and the model-scoped weekly pool drain at different +# speeds toward the SAME wall. Both counters are cumulative from one reset +# instant, so their live ratio IS this week's mix — `scope / seven` is +# scoped points per account point — and what the account cannot reach before +# the wall arrives strands in the scoped pool. These are READING RULES, not +# cache fields: nothing is stored, the numbers are computed from usage.cache +# on every frame. They live here beside the cache contract because ccpace +# says the same thing from the same numbers (docs/api/state-dir.md), and two +# tools that disagree about what strands disagree about the week. +SCOPE_STRAND_MIN_PCT=10 # scoped points the mix strands before it is worth a row +SCOPE_MIX_MIN_7D=60 # ...and only once the account's own end is in sight +SCOPE_MIX_MIN_SCOPE=5 # a model untouched this week is the underuse voice's job +SCOPE_SAME_WALL_SEC=120 # resets this close are one wall; further apart, no ratio + # Rotate usage.jsonl when it exceeds the cap. Single .1 backup; the profile # builder reads .1 + current, so rotation never costs learned history. rotate_usage_log() { @@ -2757,6 +2771,51 @@ run_usage_report() { awk -v a="${usd24:-0}" -v b="${usd7d:-0}" \ 'BEGIN{ if (a > 0 || b > 0) printf "spent: $%.2f in 24h · $%.2f in 7d\n", a, b }' + # What a 7d point buys in the pool beside it. The account pool and the + # model-scoped pool are cumulative from one reset instant, so their live + # ratio is this week's mix rate, and the account reaching its wall first + # is what strands the rest of the model's pool. No history is needed and + # nothing is cached — the reading rules are the SCOPE_* constants, shared + # with ccpace (docs/api/state-dir.md). No session runs here, so there is + # no model to match: the binding scoped limit speaks, else the deepest. + local mix_cache="$CLAUDE_ACCOUNT_DIR/usage.cache" mix_tsv="" + if [ -f "$mix_cache" ]; then + mix_tsv=$(jq -r '. as $u + | [$u.limits[]? | select(.kind == "weekly_scoped" + and (.scope.model.display_name // "") != "")] + | ((map(select(.is_active == true)) | first) // (sort_by(.percent // 0) | last) // empty) + | [(.scope.model.display_name | ascii_downcase), (.percent // 0), (.resets_at // ""), + ($u.seven_day.utilization // 0), ($u.seven_day.resets_at // "")] | @tsv' \ + "$mix_cache" 2>/dev/null) || true + fi + if [ -n "$mix_tsv" ]; then + local mx_name mx_pct mx_reset mx_su mx_sreset + IFS=$'\t' read -r mx_name mx_pct mx_reset mx_su mx_sreset <<<"$mix_tsv" + local mx_int mx_seven mx_secs mx_ssecs + mx_int=$(printf '%.0f' "$mx_pct" 2>/dev/null || echo 0) + mx_seven=$(printf '%.0f' "$mx_su" 2>/dev/null || echo 0) + mx_secs=$(get_reset_seconds "$mx_reset") + mx_ssecs=$(get_reset_seconds "$mx_sreset") + if [ -n "$mx_secs" ] && [ -n "$mx_ssecs" ] && [ "$mx_ssecs" -gt 0 ] 2>/dev/null \ + && [ "$mx_int" -ge "$SCOPE_MIX_MIN_SCOPE" ] && [ "$mx_int" -lt 100 ] 2>/dev/null \ + && [ "$mx_seven" -ge "$SCOPE_MIX_MIN_7D" ] && [ "$mx_seven" -lt 100 ] 2>/dev/null \ + && [ $((SEVEN_DAY_WINDOW_SECS - mx_ssecs)) -ge "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null; then + local mx_wall=$((mx_secs - mx_ssecs)) + [ "$mx_wall" -lt 0 ] && mx_wall=$((-mx_wall)) + local mx_reach=$(( ((100 - mx_seven) * mx_int + mx_seven / 2) / mx_seven )) + local mx_strand=$((100 - mx_int - mx_reach)) + if [ "$mx_wall" -le "$SCOPE_SAME_WALL_SEC" ] \ + && [ "$mx_strand" -ge "$SCOPE_STRAND_MIN_PCT" ]; then + local mx_ab + mx_ab=$(model_scope_abbrev "$mx_name") + printf "%s mix: ~%s %s-pt per 7d-pt this week · 7d's %s%% left carries ~%s%% of %s's %s%%\n" \ + "$mx_ab" \ + "$(awk -v s="$mx_int" -v v="$mx_seven" 'BEGIN{printf "%.2f", s / v}')" \ + "$mx_ab" "$((100 - mx_seven))" "$mx_reach" "$mx_ab" "$((100 - mx_int))" + fi + fi + fi + # The rhythm the walk below is now shaped by. Stated because it changes # every projection in this report and is the one learned fact the reader # can check against their own week. @@ -4342,6 +4401,44 @@ notice_collect() { pressure=1 fi + # The other way round: the ACCOUNT caps first, and the model's remaining + # points expire in a pool nothing can reach them from. Both counters run + # from the same reset instant, so their live ratio is this week's mix + # rate — no history, no cache field, just the two numbers line 1 already + # shows. Measured at 81/63: mix 0.78 against 0.77 mined from the corpus. + # It is the mirror of the steering notice above and cannot coexist with + # it — that one needs the model ahead of the account, this one needs the + # account ahead of the model. + # + # Rank 58: above the 5h tail, below the fleet hint, and never above the + # 7d pressure that CAUSES it. The pairing this is built for is the dry + # warning pinned on row 2 with the strand flashing on row 3. + if [ -z "$sc_str" ] && [ -z "$rebase_at" ] \ + && [ -n "$scope_int" ] && [ "$scope_int" -ge "$SCOPE_MIX_MIN_SCOPE" ] 2>/dev/null \ + && [ "$scope_int" -lt 100 ] 2>/dev/null \ + && [ "$seven_int" -ge "$SCOPE_MIX_MIN_7D" ] && [ "$seven_int" -lt 100 ] \ + && [ -n "$scope_secs" ] && [ -n "$seven_secs" ] && [ "$seven_secs" -gt 0 ] 2>/dev/null \ + && [ $((SEVEN_DAY_WINDOW_SECS - seven_secs)) -ge "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null; then + # One wall, or no ratio: the two pools have always reset together to + # the microsecond, but Anthropic could split them someday, and a mix + # taken across two different weeks is a fluent lie. + local wall_gap=$((scope_secs - seven_secs)) + [ "$wall_gap" -lt 0 ] && wall_gap=$((-wall_gap)) + if [ "$wall_gap" -le "$SCOPE_SAME_WALL_SEC" ]; then + # One division, not two. Both numbers land on one line beside the + # total they must add to, so the strand is the REMAINDER of what + # the mix reaches, never its own rounding. + local mix_left=$((100 - scope_int)) + local mix_reach=$(( ((100 - seven_int) * scope_int + seven_int / 2) / seven_int )) + local mix_strand=$((mix_left - mix_reach)) + if [ "$mix_strand" -ge "$SCOPE_STRAND_MIN_PCT" ] 2>/dev/null; then + notice_add 58 '+' fb "fb.strand.$((mix_strand / 5))" "~${mix_strand}%" \ + "${scope_ab} ~${mix_strand}% expires at this mix" \ + "7d caps before ${scope_ab}: this mix reaches ~${mix_reach}% of its ${mix_left}% left · run ${scope_ab} heavier to extract more" + fi + fi + fi + # Expiring surplus — use it or lose it. Inside the last day of the 7d # window a big green remainder is forfeiture, not headroom. The tail is # feasibility-checked against the learned pct_per_window ratio: burning diff --git a/t/helpers.bash b/t/helpers.bash index 39b2ca2..57902d9 100644 --- a/t/helpers.bash +++ b/t/helpers.bash @@ -64,6 +64,10 @@ MULT_GLYPH="✕" LEDGER_BASE_GLYPH="▁" FORECAST_SCHEMA=2 REST_MULT_MAX=0.25 +SCOPE_STRAND_MIN_PCT=10 +SCOPE_MIX_MIN_7D=60 +SCOPE_MIX_MIN_SCOPE=5 +SCOPE_SAME_WALL_SEC=120 ADVISOR_FLEET_FRESH_SECS=3600 ADVISOR_FLEET_FREE_PCT=40 ADVISOR_EXPIRY_HORIZON_SECS=86400 diff --git a/t/statusline.bats b/t/statusline.bats index 2184e98..95bfa83 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -1306,6 +1306,42 @@ _write_ledger_fixture() { # dir rm -rf "$tmpdir" } +@test "run_usage_report: what a 7d point buys in the pool beside it" { + tmpdir=$(mktemp -d) + CLAUDE_ACCOUNT_DIR="$tmpdir" + _write_ledger_fixture "$tmpdir" + reset=$(date -u -d '+2 days' '+%Y-%m-%dT%H:%M:%SZ') + _uc() { # seven, then each scoped limit as pct:model:is_active + local seven="$1" lim="" t p n a + shift + for t in "$@"; do + IFS=: read -r p n a <<<"$t" + lim="${lim:+$lim,}$(printf '{"kind":"weekly_scoped","percent":%s,"resets_at":"%s","scope":{"model":{"display_name":"%s"}},"is_active":%s}' \ + "$p" "$reset" "$n" "$a")" + done + printf '{"seven_day":{"utilization":%s,"resets_at":"%s"},"limits":[%s]}' \ + "$seven" "$reset" "$lim" > "$tmpdir/usage.cache" + } + # nothing scoped in the payload: no second pool, no ratio + _uc 81 + run run_usage_report 28 + [[ "$output" != *"mix:"* ]] + # 81 against 63: 19 account points left carry 15 of the 37 the model has + _uc 81 "63:Fable:false" "20:Opus:false" + run run_usage_report 28 + [[ "$output" == *"fb mix: ~0.78 fb-pt per 7d-pt this week · 7d's 19% left carries ~15% of fb's 37%"* ]] + # no session runs here, so no model to match: the binding limit wins over + # the deeper one + _uc 81 "63:Fable:false" "20:Opus:true" + run run_usage_report 28 + [[ "$output" == *"op mix: ~0.25 op-pt per 7d-pt this week · 7d's 19% left carries ~5% of op's 80%"* ]] + # the account has to be deep enough for "which cap binds" to be a question + _uc 50 "30:Fable:false" + run run_usage_report 28 + [[ "$output" != *"mix:"* ]] + rm -rf "$tmpdir" +} + @test "run_usage_report: --days cutoff hides older closes; no history exits 1" { tmpdir=$(mktemp -d) CLAUDE_ACCOUNT_DIR="$tmpdir" @@ -4199,6 +4235,98 @@ JSON [ -z "$(notice_collect "$usage" auto "claude-fable-5" | awk -F$'\037' '$3 == "fb"')" ] } +# One wall, two pools. Default: five days into the week, both counters +# resetting at the same instant — the shape the mix reading is only ever +# allowed to speak in. +_mix_reset() { date -u -d "${1:-+2 days}" '+%Y-%m-%dT%H:%M:%SZ'; } +_mix_usage() { # seven scope [reset-iso] [scope-wall-skew-secs] + local seven="$1" scope="$2" r7="${3:-}" skew="${4:-0}" rfb + [ -n "$r7" ] || r7=$(_mix_reset) + rfb=$(date -u -d "$r7 + $skew seconds" '+%Y-%m-%dT%H:%M:%SZ') + printf '{"five_hour":{"utilization":40},"seven_day":{"utilization":%s,"resets_at":"%s"},"limits":[{"kind":"weekly_scoped","percent":%s,"resets_at":"%s","scope":{"model":{"display_name":"Fable"}}}]}' \ + "$seven" "$r7" "$scope" "$rfb" +} +_strand_recs() { notice_collect "$(_mix_usage "$1" "$2" "${3:-}" "${4:-0}")" auto claude-fable-5; } +_strand_key() { _strand_recs "$@" | awk -F$'\037' '$4 ~ /strand/ { print $4 }'; } + +@test "notice_collect: the account caps first, so the model's own pool strands" { + # 81 against 63 — the live reading the model was built from. Both + # counters run from one reset instant, so the ratio IS this week's mix + # (0.78, against 0.77 mined from the corpus): the 19 account points left + # carry 15 of the 37 the model still has, and 22 expire unreachable. + recs=$(_strand_recs 81 63) + _fb() { printf '%s\n' "$recs" | awk -F$'\037' -v n="$1" '$3 == "fb" { print $n }'; } + [ "$(_fb 1)" = "58" ] + [ "$(_fb 2)" = "+" ] + [ "$(_fb 4)" = "fb.strand.4" ] + [ "$(_fb 5)" = "~22%" ] + [ "$(_fb 6)" = "fb ~22% expires at this mix" ] + [ "$(_fb 7)" = "7d caps before fb: this mix reaches ~15% of its 37% left · run fb heavier to extract more" ] + # the key re-flashes as the strand grows, like the surplus voice + [ "$(_strand_key 81 59)" = "fb.strand.5" ] +} + +@test "notice_collect: every gate the mix reading stands on" { + # a week that has barely started: the ratio is two days of noise + [ -z "$(_strand_key 81 63 "$(_mix_reset '+6 days 12 hours')")" ] + # two walls 5 minutes apart would be a ratio of two different weeks; + # the observed jitter between them is seconds + [ -z "$(_strand_key 81 63 "" 300)" ] + [ -n "$(_strand_key 81 63 "" 60)" ] + # a model untouched this week belongs to the underuse voice, not here + [ -z "$(_strand_key 81 4)" ] + [ -n "$(_strand_key 81 5)" ] + # ...and the question only exists once the account's own end is in sight + [ -z "$(_strand_key 59 30)" ] + [ -n "$(_strand_key 60 30)" ] + # under 10 points there is nothing to act on + [ -z "$(_strand_key 70 64)" ] + [ -n "$(_strand_key 70 63)" ] + # either pool at 100 is its own notice, and neither is a mix + [ -z "$(_strand_key 81 100)" ] + [ -z "$(_strand_key 100 63)" ] +} + +@test "notice_collect: a model projected to cap first cannot also be stranding" { + # 85 against 95 five days in: the linear scoped pace hits the model's own + # cap a day before its reset, so the frame already says fb runs out + r=$(_mix_reset) + [[ "$(notice_collect "$(_mix_usage 95 85 "$r")" auto claude-fable-5 \ + | awk -F$'\037' '$3 == "fb" { print $4 }')" == fb.caps.* ]] + [ -z "$(_strand_key 95 85 "$r")" ] + # the same two numbers with the wall 20h out: the projection no longer + # reaches the cap, and what the account cannot spend strands again + [ "$(_strand_key 95 85 "$(_mix_reset '+20 hours')")" = "fb.strand.2" ] +} + +@test "notice_collect: the mix stays quiet while the denominator is suspect" { + tmpdir=$(mktemp -d) + export CLAUDE_ACCOUNT_DIR="$tmpdir" + r=$(_mix_reset) + # first sight of the window, then 78 -> 65 inside it. A plan change moved + # one of the two counters for a reason the ratio cannot see. + notice_collect "$(_mix_usage 78 40 "$r")" auto claude-fable-5 >/dev/null + recs=$(notice_collect "$(_mix_usage 65 40 "$r")" auto claude-fable-5) + [ -n "$(printf '%s\n' "$recs" | awk -F$'\037' '$4 ~ /rebase/')" ] + [ -z "$(printf '%s\n' "$recs" | awk -F$'\037' '$4 ~ /strand/')" ] + # once the re-base is no longer newsworthy the same frame speaks + rm -f "$tmpdir/seven_seen" + [ "$(_strand_key 65 40 "$r")" = "fb.strand.7" ] + unset CLAUDE_ACCOUNT_DIR + rm -rf "$tmpdir" +} + +@test "notice_collect: the wall pins the row, the strand it causes flashes" { + state=$(mktemp -d)/notice_seen + recs=$(_strand_recs 81 63) + # the account wall is the pressure and owns row 2; the strand is its + # consequence, and a consequence never outranks its cause + [[ "$(strip_ansi "$(notice_pin_line "$recs")")" == "! 7d caps ~"* ]] + flash=$(strip_ansi "$(notice_flash_line "$recs" "$state" "$(date +%s)")") + [[ "$flash" == "+ 7d caps before fb: this mix reaches ~15%"* ]] + rm -rf "$(dirname "$state")" +} + @test "notice_collect: 7d falling inside one window reads as a re-base, not as burn" { tmpdir=$(mktemp -d) export CLAUDE_ACCOUNT_DIR="$tmpdir"