usage: per-tank fuel from the vendor's usage endpoint; board dots and burn read it (#72) - #89
usage: per-tank fuel from the vendor's usage endpoint; board dots and burn read it (#72)#89cverorg wants to merge 19 commits into
Conversation
… burn read it (#72) `clikae usage [engine] [tank] [--json]` asks the same endpoint Claude Code's own /usage view uses (api.anthropic.com/api/oauth/usage with the tank's OAuth token and the oauth-2025-04-20 beta header) and prints the five-hour and seven-day utilization with their reset instants; source is vendor | transcript | unknown. The token is read inside the adapter and handed to curl through a config on stdin — never argv, never a log — and any HTTP or network failure is reported as unknown. Readings are cached under state/usage/<engine>/<tank>.json with a 120 s TTL (CLIKAE_USAGE_TTL, --fresh bypasses) so the board and burn share one call per tank: the board's fuel dot shows the real percentage when a vendor reading exists and keeps the transcript-derived dot (including #79's expired-reset state) otherwise; burn prefers the tank with the most headroom. codex reads the same two windows from its own status output; antigravity is unknown. Endpoint recipe taken from tony1223/better-agent-terminal (MIT). Smoke-tested against a real tank: window 5 %, weekly 91 %, resets match the app's own /usage view; no token in the cache file. Built by codex (gpt-6-astra) on tank crazy; report in the lane worktree.
8086e6e to
6126adb
Compare
Round-1 review, PR #89. `burn <engine> <tank>` had a pre-launch swap (burn.sh:2310-2324) that could pick a DIFFERENT tank than the one the caller named, before the first attempt was even tried: - P1-2: the swap fired even when the named tank had plenty of headroom, with no flag to turn it off short of --no-reroute (which also disables the real dry-tank reroute burn exists for). - P1-3: --to was never read by this block, so an explicit next hop was silently overridden by the unnamed heuristic. - P1-4: the swap happened before `$tried` existed, so `rerouted_from` and the human summary showed the tank that actually ran with zero reroutes recorded — a caller could not tell "I got what I asked for" from "I got silently rerouted". Fix: delete the pre-launch swap outright. Headroom preference now lives in exactly one place — _burn_next_same_engine's candidate ordering, which only runs once the named tank has gone dry (the existing mid-loop reroute path, which already honors --to outright and already appends to $tried/rerouted_from correctly; untouched by this commit). bats: burn claude a still launches a when a's headroom is worse than b's; --to claude/c lands on c; a reroute driven by headroom ordering appears in --json rerouted_from. (added in a follow-up commit, with the rest of this round's test additions, to keep this diff readable.)
…-7/P2-1) Round-1 review, PR #89. P2-2 — burn's candidate ranking (_burn_next_same_engine) called usage_read per candidate, which is a live vendor curl unless the cache is already warm (up to --max-time 8 EACH, serialized: measured 14s wall / 4 curl calls for 4 candidates). burn's launch/reroute path must never call the vendor. Fix: usage_cache_peek — cache file only, no adapter, no network. Stale is fine for ranking (stale headroom beats no headroom); missing/incomplete/ non-vendor = unknown. Fresh reads still happen, but only from `clikae usage` (and --fresh) — burn never triggers one. P2-7 — with P2-2 fixed, the macOS Keychain read (lib/adapters/claude.sh's adapter_usage) is off burn's hot path, but `clikae usage`/--fresh can still reach it, and it had no `command -v security` guard or time bound (unlike adapter_migrate_credentials:702, which mirrors both). A locked keychain or an ACL prompt with no `security` on PATH could otherwise hang a headless caller waiting on a GUI dialog nobody can answer. Fix: guard + a 5s timeout/gtimeout wrap when either is on PATH (best-effort unbounded fallback otherwise, matching the rest of the codebase's stance on missing timeout tooling). P2-1 — home.sh's own docstring already names this contract ("fork-free") from an earlier round on the codex side; this PR's usage_cached_fields broke it for every engine. _home_fuel_dotv is called once per tank PER SECTION it appears in within a single redraw (a live tank's Live row and its Tank row are the same tank) — up to 5 call sites — and each call forked `date` + `jq`: 4605µs/call measured vs 719µs on the pre-usage-cache path, 6.4x. Fix: usage_cached_fields takes an optional `now` (epoch seconds) so it doesn't have to fork `date` itself, and _home_fuel_dotv gained a per-redraw memo (_home_fuel_memo_reset, called once per render right next to the existing _home_cols_prime — the same "once per render, not once per row" shape that function already established) so a repeated (dry,cli,tank) lookup within one redraw is served from memory. Board redraws still never touch the network — this only removes forks, not I/O. Re-measured (see REPORT-usage72-fix1.md for the full before/after).
… (P2-3) Round-1 review, PR #89. The vendor's real reset instant is "2026-09-13T14:50:00.189940+00:00" — microseconds AND a numeric "+00:00" offset, never the bare "…Z" jq's fromdateiso8601 requires. The old guard (usage_cached_fields) fed the raw string straight in; the `sub` regex never matched, fromdateiso8601 threw, and `catch` reported "still valid" regardless of what the timestamp actually said — a guard that had never once fired against an actual vendor response. tests/bats/usage.bats's fixture used "2099-01-01T00:00:00Z", a shape the vendor never sends, so nothing caught it. Fix: norm_stamp — drop fractional seconds, then turn a UTC-zero offset into "Z" (any other offset still fails to parse and still fails open, unchanged; no real vendor sends one). usage.bats: usage_fixture's curl stub now returns the vendor's real shape (previously the unrealistic bare-Z one), and a new test proves red->green directly — a negative control shows the OLD regex reading a 2020 real-shape timestamp as "not yet expired", then the SAME timestamp through the FIXED usage_cached_fields is correctly rejected, with a future real-shape timestamp still accepted.
…s own time (P2-4) Round-1 review, PR #89. codex.sh's adapter_usage claimed `source:"vendor"` unconditionally — but no `codex` process ever runs for this: it reads the same rollout-transcript `rate_limits` evidence limit_codex_status already uses (`_limit_codex_rate_limits`, scanning rollouts modified in the last 7 days). #72's own acceptance criteria named `source` as `(vendor|transcript|unknown)`; nothing in the repo ever produced "transcript" (`grep '"transcript"'` was a zero-hit search across the whole tree). Fix: codex's adapter_usage now returns `source:"transcript"`. usage_read's whitelist and usage_cached_fields/usage_cache_peek's `select`s accept it alongside "vendor" (previously "vendor"-only, which would have silently made every codex reading "unknown" the moment its honest label changed). Compounding issue while here: _limit_codex_rate_limits computed the winning event's own timestamp (as its 7th field, `ts`) and then discarded it (`: "$ts"`) before its caller ever saw it — so even a correctly-labeled reading would have been stamped `cached_at: now`, "just read", regardless of whether the underlying rollout event was from a second ago or a week ago. Fixed: the 7th field now reaches the caller; usage_read pulls it out (as `event_epoch`, source:"transcript" only) before whitelisting and uses it as `cached_at` instead of `now`. limit_codex_status (the OTHER, older caller of _limit_codex_rate_limits) gained a matching 7th `read` variable so the added field doesn't silently glom onto its `sr` via plain `read`'s "extra fields append to the last variable" rule. docs/DESIGN-board-fuel-dots.md: rewrote the "Vendor usage cache" section — the 7-day window is now stated, source semantics are accurate, and the stale "burn also refreshes candidates" / "initial selection changes with known readings for both tanks" prose (true before commits b843c0a/23466b0 in this same review round, false now) is corrected to match. CHANGELOG's #72 entry updated to match. usage.bats: usage_fixture's codex test now asserts source:"transcript" (was "vendor") and that cached_at equals the rollout event's own epoch, not the time of the `clikae usage` call.
…ne (P2-9/P2-6) Round-1 review, PR #89. Rewrote _burn_next_same_engine's ranking. P2-9 — best_peak started at 101 and `fallback` (listing order) only won when NO candidate had any reading at all. So a tank we know nothing about always lost to any tank with a reading — including one the vendor just reported as 99% used. Fix: three tiers, best first — known headroom under 90% (sorted by lowest weekly_pct, then lowest window_pct) beats unknown, which beats a known reading >=90%. P2-6 — dried_accts only ever contained CONFIRMED-dry accounts, and even then only skipped a candidate, never collapsed two live candidates on the same account into one decision. Measured: two same-account tanks with different cached snapshots (a=95%, b=10%) made burn chase a phantom between them with zero real headroom gained. Fix, two parts: - never offer a tank as the very NEXT hop when it shares an account with the hop just left (reads $tried's last entry) — this is a real gap dried_accts can't cover, since the account isn't "confirmed dry" yet; - among the candidates that remain, tanks sharing an account collapse to ONE ranking slot, valued at the WORST (highest) reading any sibling reported this call — never letting one sibling's better-looking cache snapshot make the shared quota look healthier than it is. Both fixes read _limit_tank_account, which previously was only called when dried_accts was non-empty — now called for every candidate (gracefully empty, same as before, when limit.sh/adapter_loader.sh aren't sourced by a caller that never needed account data). bats: two new tests reproduce the review's exact P2-9 scenario (unknown vs. a 99%-used known reading) and P2-6's two halves (same-account collapse to the worst reading; a same-account sibling never chosen as the next hop). The existing "reserve ranks vendor headroom and skips solo tanks" test still passes unchanged (verified by hand under a temp HOME/CLIKAE_HOME before this commit — it doesn't source limit.sh, so the new _limit_tank_account calls degrade to empty exactly as the old dried_accts-gated calls did). docs/DESIGN-board-fuel-dots.md: the Vendor usage cache section (rewritten in cf7910d) said "ranked by lowest maximum utilization" — corrected to describe the actual three-tier, weekly-then-window ordering implemented here, plus the account-collapse and no-consecutive-hop rules.
Round-1 review, PR #89. CI's doc gate (bats (macos-latest)/(ubuntu-latest), `not ok 536 doc gate: passes on the repo as it stands`) was red on this PR on both platforms: docs/DESIGN-board-fuel-dots.md names five JSON field names from `clikae usage --json`'s result object — window_pct, weekly_pct, window_resets_at, weekly_resets_at, cached_at — and the doc gate treats every identifier a doc mentions as a function-name claim unless it's allow-listed. Precedent for exactly this shape already exists here (rerouted_from, run_id, used_percent, resets_at, …); this adds the five this PR introduced, each with a reason. `bash scripts/doc-names-exist.sh` rc=0 (was rc=1, 5 unclaimed names).
Round-1 review, PR #89. `clikae usage` had no detail page (`clikae help usage` printed only the one-line synopsis, unlike every other verb) and was absent from `clikae help`'s "Use & inspect" section, README, and docs/grammar.md's verb table — the only documented surface was a design doc's own appendix. - lib/commands/usage.sh: --help now prints a real detail page (mirrors settings.sh's `cat <<'HELP'` shape) — flags, both output forms, the reading fields including what `source` values mean, and the cache/TTL contract. - lib/commands/help.sh: added to "Use & inspect", next to `status`. - docs/grammar.md: added to the §3.3 management-verbs table, next to `status` (mirroring where and how #85 documented `settings`); also added `wait`, which turned out to be missing from this table entirely. - README.md: a bullet in "Wield the fleet, don't just switch", next to the other burn/conduct capability bullets. Verified end-to-end under a temp HOME/CLIKAE_HOME: `clikae help | grep -c '\busage\b'` is now 1 (was 0), `clikae help usage` prints the detail page.
Round-1 fix receipts (PR #89) — running `bats usage.bats home.bats burn.bats` under the suite lock surfaced three test-only bugs, none in lib/ code, in the tests added by this round: - "cached vendor thresholds and expired reading preserve unverified fallback" (pre-existing test, from the original PR): _home_fuel_dotv's new per-redraw memo (commit 23466b0) correctly caches within ONE redraw, but this test calls it three times in a loop while rewriting the SAME tank's cache file between calls to simulate the reading changing over time — a call pattern no real caller uses (production always calls _home_fuel_memo_reset once per redraw, right where _home_cols_prime already is). Fixed by resetting the memo each iteration, matching what a real "new redraw" would do. - P2-6b (this round's own test): the scenario it exercises legitimately skips a same-account candidate, which calls log_warn — but the test didn't source lib/core/log.sh, so bash's "command not found" for the undefined function landed in bats' combined stdout+stderr $output and broke the exact-match assertion. Fixed by sourcing log.sh (both P2-6 tests, for consistency) and using `run --separate-stderr` so a legitimate advisory on stderr can't corrupt an assertion about stdout — mirroring _burn_next_same_engine's own documented contract ("log_warn writes to stderr, so a skip notice can't corrupt this function's captured stdout" — true for `$(...)`, not for bats' `run`). - `bats_require_minimum_version 1.5.0` (existing precedent in switch.bats/scrollback.bats/antigravity_keychain_real.bats): silences the BW02 warning `run --separate-stderr` triggers without it. Receipts: `bats tests/bats/usage.bats tests/bats/home.bats tests/bats/burn.bats` under the suite lock, 215/215 ok, rc=0 (was 213/2 not ok before this commit).
…te_limits Found running the full suite in 3 chunks (PR #89 fix round 1). Same class of bug as 5fad1b6: commit cf7910d (P2-4) added a 7th field (the winning event's own timestamp) to _limit_codex_rate_limits's output; three pre-existing tests in this file still `read` exactly 6 named variables, so plain `read`'s "extra fields glom onto the last variable" rule silently appended the new field onto $sr. One of the three asserts $sr exactly and was failing: not ok 403 codex rate_limits: a healthy pair of windows reads back verbatim `[ "$su" = "5.0" ]; [ "$sw" = "10080" ]; [ "$sr" = "1791999999" ]' failed The other two don't assert $sr so they weren't red, but had the same latent bug. All three now name a 7th (unused) variable, matching how limit_codex_status in lib/core/limit.sh was already fixed in cf7910d. limit_codex_status_cached's own 6-field read (lib/core/limit.sh:1517) is NOT touched — it reads from _limit_codex_rate_limits_cached, a separate cache layer that has always explicitly constructed a 6-field string on its own cache-write path, unrelated to this change.
b843c0a's own commit message deferred these to "a follow-up commit, with the rest of this round's test additions" — this is that commit, added while running the full suite in chunks for this round's receipts. Three tests reproducing the round-1 review's exact scenarios against the now-deleted pre-launch swap: - `burn claude a` with a's headroom worse than b's still launches a, with an empty rerouted_from (P1-2: no pre-launch substitution at all). - `--to codex/c` wins outright over headroom ordering on a dry tank, even when a sibling (b) has an obviously better cached reading (P1-3) — a stronger regression lock than the existing "burn honours an explicit --to next hop on a dry tank" test, which has no competing headroom data to override. - A reroute actually driven by headroom ordering (no --to) shows up honestly in --json's rerouted_from (P1-4). Verified: `bats --filter 'P1-2|P1-3|P1-4' tests/bats/burn.bats`, all 9 matches ok (3 new + 6 pre-existing P1-2/P1-3-named tests from an earlier, unrelated review round on #44's infra-failure classification).
|
P1-1 (doc gate red on both CI platforms): docs/DESIGN-board-fuel-dots.md names five |
|
P1-2 (burn silently swapped the named tank before the first attempt): |
|
P1-3 ( |
|
P1-4 ( |
|
P2-1 (board redraw lost its fork-free contract): |
|
P2-2 (burn's candidate loop called the vendor): |
|
P2-3 (reset-expiry guard never fires against a real vendor timestamp): the vendor sends |
|
P2-4 (codex claimed |
|
P2-5 ( |
|
P2-6 (no same-account dedup — burn chased a phantom between two tanks sharing one quota): fixed in b1ea9a3, two parts — a tank is never offered as the very next hop when it shares an account with the hop just left (reads |
|
P2-7 (macOS Keychain read had no |
|
P2-8 (merge conflict with origin/main): rebased onto origin/main (CHANGELOG.md and bin/clikae's reserved-word list both kept both sides' entries) before any of the seven fixes, then force-with-lease pushed. |
|
P2-9 (a tank known to be ~99% used beat a tank we know nothing about): fixed in b1ea9a3 — ranking is now three tiers: known headroom under 90% (lowest weekly_pct, then window_pct) beats unknown, which beats a known reading ≥90%. New bats test reproduces the review's exact scenario. |
…invalid (P2-1)
Round-2 review, P2-1: nothing but a human running `clikae usage` ever
refreshed the on-disk vendor cache, so the board's percentages were
invisible almost all the time on a real machine (3 of 4 real tanks, hours
stale, showed nothing), and burn's reroute ranking could stay pinned to a
tank's last-known ≥90% reading long after that window had actually reset.
(a) burn refreshes the LAUNCHED tank's reading once, at run end — after
the run's own artifact check (so it can never delay judging that run's
outcome), never before launching (P1-2..P1-4's zero-cost launch is
unchanged).
(b) when the named tank is dry and burn must reroute, `_burn_next_same_
engine` refreshes each surviving CANDIDATE's reading once, right
before ranking — bounded to candidates only, reusing the adapter's
own existing --max-time. This is the one moment a stale number would
cost burn a wrong hop.
(c) the board still never fetches. `usage_board_fields` (new) reads
whatever is on disk, however old: silently within the TTL, WITH its
age alongside it ("window 44% · weekly 20% · 3h ago") once past the
TTL, and treated as unread once 24h or older. `_home_fuel_dotv_compute`
now calls this instead of the TTL-only `usage_cached_fields`.
(d) `usage_cache_peek` (burn's ranking) and `usage_board_fields` (the
board) both honour the reading's own window_resets_at/weekly_resets_at:
a window whose reset instant has passed reads as 0% used, never as a
stale ≥90%.
`usage_cache_peek`'s norm_stamp guard needed to parse an arbitrary ±HH:MM
offset (not just +00:00/-00:00) to do (d) correctly, and since that is the
exact same parser `usage_cached_fields` already had (round-1, narrower),
this commit generalizes it once, shared by both — folding in the P3-12
fix from the same review round, since splitting the parser's shape from
its use here would mean re-deriving the epoch-arithmetic twice.
`usage_read`'s cache-hit check also picks up its P3 fix in this commit for
the same reason (adjacent code, same "who reads what" story): a new
`scanned_at` field, separate from `cached_at` (the reading's own
evidentiary time), so a codex reading — whose `cached_at` is an event
timestamp, almost never "just now" — gets a real cache hit within the TTL
the same way claude's already did, instead of re-scanning its entire
rollout store on every `clikae usage codex` call.
docs/DESIGN-board-fuel-dots.md's "Vendor usage cache" section is rewritten
to state (a)-(d) as the doc, replacing the false "burn never calls the
vendor" claim; it also narrates the P2-2/P2-3 fixes from this same review
round in passing since they live in the same continuous section.
Bats: usage.bats and burn.bats gain the P2-1(a)-(d) coverage (run-end
refresh, candidate refresh via a shared live-fetch test harness, board age
display + 24h fallback, resets_at zeroing, stale-allowed vs TTL-gated
contrast) alongside this round's other new tests (P2-2/P2-3/P3), since the
suite files interleave edits throughout and do not split cleanly by item.
Round-2 review; part of #72.
…m the Keychain read too (P2-2) Round-2 review, P2-2: `lib/adapters/claude.sh`'s Keychain read rolled its own two-arm timeout resolver (`timeout` -> `gtimeout`, nothing else) right where the repo already had a three-arm one (`timeout` -> `gtimeout` -> `perl -e 'alarm …; exec …'` -> an honest warning and an unbounded call as a last resort) in `lib/commands/burn.sh`'s `_burn_timeout_bin`. Stock macOS — the ONE platform this Keychain branch runs on — ships neither `timeout` nor `gtimeout` by default, so the two-arm copy's bound was silently empty on an unmodified install: a locked Keychain or an ACL prompt with nobody there to answer it could hang a headless `clikae usage`/`--fresh` call indefinitely. `_burn_timeout_bin` moves to `lib/core/timeout_bin.sh`, sourced globally in `bin/clikae` like every other `lib/core/*.sh` file, so an adapter can call it without sourcing a command file for it. `lib/commands/burn.sh`'s own call sites are unchanged (same function name, now defined elsewhere). `lib/commands/conduct.sh`'s comment explaining why it sources burn.sh is corrected — it still needs `_burn_size`, but no longer this function. `lib/adapters/claude.sh`'s Keychain branch now calls `_burn_timeout_bin` and dispatches on all three arms it can return, instead of resolving `timeout`/`gtimeout` inline and falling straight to an unbounded call otherwise. Bats: a shadow-PATH test (usage.bats) proves the perl arm actually BOUNDS a real subprocess — a `security` stub that blocks 30s is cut off near 5s — not just that `_burn_timeout_bin` picks the right binary name. The stub uses `exec sleep 30` (replacing itself) rather than launching `sleep` as a child: a spawned child inherits the pipe's write end and keeps it open past its exec'd parent's SIGALRM death, which would fail this test even though the real `security` binary (a single process, same shape as the exec'd `sleep`) is genuinely bounded — a fixture artifact worth recording so the next person doesn't reintroduce it chasing the same red. Round-2 review; part of #72.
…t only the tie-break (P2-3) Round-2 review, P2-3: `_burn_next_same_engine`'s three-tier ranking already weighs the 5-hour window heavily when it CHOOSES a tier — tiering uses `peak = max(window_pct, weekly_pct)`, so a tank with plenty of weekly room but its window nearly spent still lands in the worst tier — but WITHIN a tier the round-1 shape sorted by lowest `weekly_pct` first, `window_pct` only as the tie-break. The two disagreed about which window matters: a burn is about to run NOW, against the 5-hour window, so a tank with a great weekly number but its window nearly gone was picked over one with hours of window left, purely because its weekly digit looked nicer — a real reroute to the worse choice for the run about to happen. Swapped: `window_pct` ascending first, `weekly_pct` only breaks a tie. Tiering itself, the reset-instant guard (P2-1(d)), and every other guard in this function are unchanged. Round-2 review; part of #72.
…c doc, offset parsing, third positional, changelog/grammar hygiene Round-2 review, the P3 list: - `_home_fuel_dotv`'s header promised "fork-free" outright; that was never fully true (the codex branch already carried its own caveat) and P2-1(c) makes it less true, not more — every tank not yet memoized this redraw now pays one `jq` fork to parse its cache file. Rewritten to say the real contract (at most one `date` fork per redraw, one `jq` fork per tank per redraw) and why a hand-rolled bash-only JSON reader was judged not worth it for a sub-millisecond, redraw-only cost. - The Tank row's `_fd="$(_home_fuel_dot …)"` was the one remaining call site using the ECHOING form through a `$( )` — a command substitution is a subshell, so the per-redraw fuel-dot memo it writes never reached the parent shell. Switched to `_home_fuel_dotv` + `$_FDOT`/`$_FNOTE`, matching every other call site. - codex's transcript readings now get a real cache hit within the TTL (see the P2-1 commit's `scanned_at` field) instead of re-scanning the whole rollout store on every `clikae usage codex` call. - `usage_cache_peek`'s doc said a miss returns "rc=1"; `jq -er`'s actual exit status for a pipeline that produces no output at all is 4. Comment fixed to match (no behavior change — every caller already tests truthiness, not a specific code). - `norm_stamp` (P2-1 commit) parses any `±HH:MM` offset, not only `+00:00`/`-00:00` — bats regression test added for `+09:00`. - `clikae usage <engine> <tank> <extra>` silently `return 1`'d on a third positional argument. Now refuses with a clear error naming the usage. `clikae usage --help` and `clikae usage` in docs/grammar.md are also corrected to state (a)-(d) from the P2-1 commit instead of "burn never calls the vendor". - The "stale allowed" design (a reading past its TTL still ranks, unlike `usage_cached_fields`' fresh-only gate) gets a bats test. - CHANGELOG.md: the #72 entry had a blank line before the next bullet, turning the whole Unreleased/Added list loose (extra paragraph spacing on every item when rendered) — removed, and the entry now also names this round's fixes. - docs/grammar.md's `clikae wait` row (added in f7eaec2, alongside the actual #72 `usage` row, unrelated to #72's own scope) is moved to sit next to `burn`, its actual semantic neighbor, instead of sitting between `usage` and the `to`/`auto`/`watch`/`burn` cluster. Bats: usage.bats gains coverage for the third-positional rejection, the codex cache-hit fix (delete the rollout after the first read, prove the second read within TTL still returns the same reading rather than finding nothing), the `+09:00` offset, and the stale-allowed contrast — alongside this round's other new tests, committed together with the P2-1 change since the suite files interleave edits throughout (see that commit's message). Round-2 review; part of #72.
# Conflicts: # CHANGELOG.md
… CI caught it, not bash -n CI's macOS job failed with `lib/adapters/claude.sh: line 781: unexpected EOF while looking for matching \`''` — a genuine bash 3.2 parser bug this lane's own `bash -n` (bash 5.x here) could not see. Reproduced directly against a real `bash:3.2` container (`docker run --rm bash:3.2 bash -n …`): the P2-2 commit's `case "$_tbin" in … esac` inside the nested `$( if … elif … fi )` — one branch with a `\` line-continuation immediately followed by a `|` pipe-continuation on the next line, plus a long prose comment block sitting inside that same nesting — is more than bash 3.2's parser can track through to a real EOF. Fix: no case statement (if/elif/else instead, matching the original pre-P2-2 shape more closely), no backslash line-continuation inside the nested subshell (the perl invocation is one line), and the long P2-7/P2-2 explanation comment moved above `adapter_usage() (` entirely, out of every level of nesting. Same three-arm dispatch, same behavior. Verified against real bash 3.2 (`docker run --rm -v "$PWD:/repo:ro" bash:3.2 bash -n /repo/<file>`), not just this lane's own bash -n, for every lib/bin/*.sh file (93 files, all pass) — this lane's own local `bash -n` checks throughout this round were bash 5.x and never would have caught this; recording it so the next round knows to reach for the container, not just the local interpreter, on anything darwin-flavored. Round-2 review; part of #72.
|
P2-1 (nothing ever refreshed the vendor cache — board mostly dark, burn's ranking could stay pinned to a stale ≥90% forever): fixed in a55b4f0. (a) burn refreshes the launched tank's reading once, at run end, after the artifact check, never before launching. (b) on a dry-tank reroute, burn refreshes each surviving candidate once, right before ranking, bounded to candidates only. (c) the board still never fetches — |
|
P2-2 (Keychain read's 5s timeout was empty on stock macOS — the one platform it runs on): fixed in 06d3d19. The claude adapter had rolled its own two-arm resolver ( |
|
P2-3 (intra-tier reroute ordering was weekly_pct first, window_pct only the tie-break): fixed in 990839b. A burn about to run NOW runs against the 5-hour window — sorting by weekly first could reroute to a tank with a great weekly number but its window nearly spent, over one with hours of window left. Swapped: window_pct ascending first, weekly_pct only breaks a tie. Tiering (peak = max(window,weekly)) and the reset-instant guard are unchanged. |
|
P3-1 ( |
|
P3-2 (Tank row's |
|
P3-3 ( |
|
P3-4/P3-5 (codex's cache-hit TTL almost never fired since cached_at is the underlying event's own timestamp, not the fetch time — every |
|
P3-6 ( |
|
P3-7 ( |
|
P3-8 ( |
|
P3-9 (no test for the 89.9/90.0 tier boundary or the "stale allowed" design): fixed in the P2-1/P2-3 test commits — new boundary test (89.9%peak beats unknown, 90.0%peak loses to unknown) and a dedicated "stale allowed" test contrasting |
|
P3-11 (docs/grammar.md's |
|
P3-12 ( |
…w, off its own hot path The round-2 P2-1 fix (a55b4f0) added exactly the two burn-time vendor calls this entry's original wording said didn't exist ("burn never calls the vendor to decide"). Missed when the round-2 summary was appended in c673adb — that commit added a clause about the new behavior without correcting the sentence stating the old one. Fixed. Round-2 review; part of #72.
Closes #72.
What
clikae usage [engine] [tank] [--json] [--fresh](lib/commands/usage.sh,lib/core/usage.sh): five-hour / seven-day utilization and reset instants per tank;source=vendor|transcript|unknown.adapter_usage:GET https://api.anthropic.com/api/oauth/usagewith the tank's OAuth access token (credentials file, or the tank-specific macOS Keychain item, guarded bycommand -v security+ a bounded timeout) andanthropic-beta: oauth-2025-04-20; the bearer header goes to curl via a config on stdin (never argv/log), tracing disabled, bounded timeouts; 401/network ⇒unknown. Recipe from tony1223/better-agent-terminal (MIT).rate_limitsevidencelimit_codex_statusalready uses (scanning rollouts modified in the last 7 days) — nocodexprocess is ever invoked, sosourceis honestly"transcript", andcached_atis the winning event's own timestamp, not the time it was read.state/usage/<engine>/<tank>.json, TTL 120 s (CLIKAE_USAGE_TTL),--freshbypasses; atomic private writes.usage_cached_fieldsreads the cache file directly and shares onedate/jqcall across a whole redraw instead of forking per row.--toalways wins outright, every hop recorded inrerouted_from. Burn never calls the vendor itself — it reads whatever is already cached (stale allowed); onlyclikae usage/--freshfetches.unknown(noadapter_usage).Round 1 fixes
Full findings and before/after evidence: the round-1 review and the per-finding comments on this PR. Commits, in order:
b843c0a— the named tank is always the launch target; the pre-launch headroom swap that ignored--toand hid itself fromrerouted_fromis deleted outright (P1-2/P1-3/P1-4).23466b0— burn's candidate ranking never calls the vendor (usage_cache_peek, cache-only); macOS Keychain reads gained the samecommand -v security+ timeout guardadapter_migrate_credentialsalready has; the board's per-redraw fork cost is back down near its pre-cache baseline via a per-redraw memo (P2-2/P2-7/P2-1).cb9c64a— the vendor's real reset-instant shape (microseconds + numeric UTC offset) is normalised before parsing; the old guard had never once fired against a real response (P2-3).cf7910d— codex'ssourceis honestly"transcript", never"vendor";cached_atis the evidence's own timestamp (P2-4).b1ea9a3— an unknown reading never loses to a known ≥90%-used one; same-account tanks rank as one and are never chosen as consecutive hops (P2-9/P2-6).2f0b609— doc gate fix: allow-list the five new JSON field names (P1-1).f7eaec2—clikae usagedocumented inclikae help, README, anddocs/grammar.md(P2-5).5fad1b6,3a77923— test-only fixes found while running the full suite (a pre-existing test's assumption broken by the P2-1 memo; two of this round's own tests neededlog.sh/a 7th field name).5b8f44d— the P1-2/P1-3/P1-4 bats coverage promised in commit 1's message.Validation
Full suite this round (not just the two files the original PR description named):
bats tests/bats/usage.bats tests/bats/home.bats tests/bats/burn.bats(under the repo's suite lock): 215/215 ok, rc=0.bats tests/bats(all 80 files, run in 3 chunks under the same lock — the machine runs other lanes concurrently): 1235/1235 ok across all three chunks, rc=0 each.shellcheck -S warning(bin/clikae, install.sh, scripts/test.sh, everylib//tests//scripts/.sh, 99 files): rc=0.bash scripts/doc-names-exist.sh: rc=0.git merge-tree --write-tree origin/main HEAD: rc=0 (rebased ontoorigin/mainfirst; settings: versioned permissions template applied to every tank — settings apply / --check / --dry-run, init seeds it, doctor reports drift (#76) #85'ssettingslanded there in the meantime)../bin/clikae usage claude wrasse --json):window_pct 44.0, weekly_pct 9.0,window_resets_at "2026-09-13T14:50:00.367830+00:00"— the exact real-vendor shape P2-3 fixes, verified end-to-end against a live response.usage_cached_fieldscorrectly reads it back (rc=0). No token in stdout/stderr/cache (grep, 0 hits); cache file mode 600; the four real tanks'settings.jsonfiles are byte-identical before/after (size/mtime/mode).Full write-up (Chinese, per-finding red→green, and what I did NOT verify):
REPORT-usage72-fix1.mdin the lane.🤖 Generated with Claude Code
Round 2 fixes
Full findings and before/after evidence: the round-2 review (
REVIEW-usage72-r2.md) and the per-finding comments on this PR. Commits, in order:a55b4f0— who writes the vendor cache, and when a stale reading is invalid (P2-1). Nothing but a human runningclikae usageever refreshed the on-disk cache, so the board's percentages were invisible almost all the time on a real machine and burn's ranking could stay pinned to a stale ≥90% reading long after that window had actually reset. Now: (a) burn refreshes the launched tank's reading once, at run end, after the artifact check, never before launching; (b) on a dry-tank reroute, burn refreshes each surviving candidate's reading once, right before ranking, bounded to candidates only; (c) the board still never fetches — it shows a stale-but-recent reading WITH its age instead of hiding it, and treats anything 24h+ old as unread; (d) a window whose ownresets_athas passed reads as 0% used, never a stale ≥90%. Folds in the P3-12 (norm_stampnow parses any±HH:MMoffset) and codex-TTL (scanned_at, separate from the reading's owncached_at) fixes since they touch the exact same functions.06d3d19—_burn_timeout_binmoves tolib/core/timeout_bin.sh;lib/adapters/claude.sh's Keychain read had its own two-arm copy (timeout/gtimeoutonly) on the ONE platform (stock macOS) that ships neither by default, so its 5s bound was silently empty (P2-2).990839b— intra-tier reroute ordering iswindow_pctascending first,weekly_pctonly the tie-break — swapped from round-1's weekly-first shape, which could reroute to a tank with a great weekly number but its 5-hour window nearly spent over one with hours of window left (P2-3).c673adb— the P3 list:_home_fuel_dotv's "fork-free" header corrected to its real contract; the Tank row's one remaining$( )call site (losing the per-redraw fuel-dot memo to a subshell) fixed;usage_cache_peek's rc doc corrected (4, not 1); a third positional argument toclikae usageis now refused with a message instead of silently doing nothing;clikae usage --help/docs/grammar.mdcorrected to state the real (a)-(d) contract; a "stale allowed" bats test;CHANGELOG.md's stray blank line (was turning the whole Added list loose) removed;docs/grammar.md's strayclikae waitrow (added alongside the actual Per-tank usage from the vendor's usage endpoint: clikae usage --json, real fuel dots on the board, burn prefers headroom #72usagerow, unrelated to Per-tank usage from the vendor's usage endpoint: clikae usage --json, real fuel dots on the board, burn prefers headroom #72) moved next toburn.34cd504— an empty commit to retrigger CI (the webhook did not fire forc673adb— a one-off delivery gap, not a CI config issue; confirmed by other lanes' PRs triggering normally in the same window).3b6a3e9— mergedorigin/main(which had moved: tmux: touch scrolling for terminals that send a swipe as a click pair (a-Shell on iPhone) #88 landed) — a real conflict inCHANGELOG.md's Added list (both sides added an entry next to each other), resolved by keeping both.da86fda— CI'spty smoke (macos-latest)/smoke test (macos-latest)caught a real bash 3.2 parse error in the P2-2 rewrite oflib/adapters/claude.sh(acaseinside a nested$( if … fi ), a\continuation immediately followed by a|continuation, plus a comment, stacked) — this lane's ownbash -n(bash 5.x) never saw it. Fixed (nocase, no backslash continuation inside the nesting, comment moved outside it) and reverified against a realbash:3.2Docker container.165dfeb— a follow-up correction to the CHANGELOG entryc673adbhad appended a round-2 summary to without fixing the sentence it was appending next to ("burn never calls the vendor" — no longer true after P2-1)._home_fuel_dotv's "fork-free" header corrected to its real contract; the Tank row's one remaining$( )call site (losing the per-redraw fuel-dot memo to a subshell) fixed;usage_cache_peek's rc doc corrected (4, not 1); a third positional argument toclikae usageis now refused with a message instead of silently doing nothing;clikae usage --help/docs/grammar.mdcorrected to state the real (a)-(d) contract; a "stale allowed" bats test;CHANGELOG.md's stray blank line (was turning the whole Added list loose) removed;docs/grammar.md's strayclikae waitrow (added alongside the actual Per-tank usage from the vendor's usage endpoint: clikae usage --json, real fuel dots on the board, burn prefers headroom #72usagerow, unrelated to Per-tank usage from the vendor's usage endpoint: clikae usage --json, real fuel dots on the board, burn prefers headroom #72) moved next toburn.Validation (round 2)
bats tests/bats/usage.bats tests/bats/home.bats tests/bats/burn.bats: 228/228 ok, rc=0.bats tests/bats(all 80 files, 4 chunks, partition verified exact againstls tests/bats/*.bats), re-run at the final HEAD after mergingorigin/main: 1245/1245 ok at the pre-merge state, 455 + 303 + 265 (1 pre-existing, unrelatedroam.batsflake — reproduced identically against a cleanorigin/mainworktree, not caused by this PR) + 225 ok at the final state. CI's ownbatsjobs (below) are the authoritative account and are unaffected by this host-local flake.shellcheck -S warning -x(lib/bin/scripts/hooks, 101 files): rc=0.bash scripts/doc-names-exist.sh: rc=0.lib/adapters/claude.sh's P2-2 rewrite (acaseinside a nested$( if … fi ), a\line-continuation immediately followed by a|continuation, plus a comment block, all stacked) parsed fine under this lane's own bash 5.x but hitunexpected EOF while looking for matching \''under real bash 3.2 — caught by CI's macOS jobs (pty smoke,smoke test), not by this lane. Fixed inda86fda(nocase, no backslash continuation inside the nesting, comment moved outside it) and reverified against a realbash:3.2` Docker container for every changed file, not just this lane's own interpreter.git merge-tree --write-tree origin/main HEAD: rc=0.git diff --check origin/main...HEAD: rc=0.securitystub near 5s, not just that the right binary name is picked.CI (final)
Head
165dfeb, run 34779802048: 9/9 jobs pass on all three platforms (bats/pty smoke/smoke testmacOS + ubuntu,shellcheck,signet,pesterwindows).gh pr view 89 --json mergeable,mergeStateStatus:MERGEABLE/CLEAN.Full write-up (Chinese, per-finding red→green, and what I did NOT verify):
REPORT-usage72-fix2.mdin the lane.🤖 Generated with Claude Code