Skip to content

usage: per-tank fuel from the vendor's usage endpoint; board dots and burn read it (#72) - #89

Open
cverorg wants to merge 19 commits into
mainfrom
feat/usage-endpoint-72
Open

usage: per-tank fuel from the vendor's usage endpoint; board dots and burn read it (#72)#89
cverorg wants to merge 19 commits into
mainfrom
feat/usage-endpoint-72

Conversation

@cverorg

@cverorg cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member

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.
  • claude adapter adapter_usage: GET https://api.anthropic.com/api/oauth/usage with the tank's OAuth access token (credentials file, or the tank-specific macOS Keychain item, guarded by command -v security + a bounded timeout) and anthropic-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).
  • codex adapter: reads the same rollout-transcript rate_limits evidence limit_codex_status already uses (scanning rollouts modified in the last 7 days) — no codex process is ever invoked, so source is honestly "transcript", and cached_at is the winning event's own timestamp, not the time it was read.
  • Cache state/usage/<engine>/<tank>.json, TTL 120 s (CLIKAE_USAGE_TTL), --fresh bypasses; atomic private writes.
  • Board fuel dots use the cached reading when present (≥90% red, ≥60% yellow) and fall back to the transcript dot, keeping board: a dry verdict expires when its own reset instant has passed (#75) #79's expired-reset state. usage_cached_fields reads the cache file directly and shares one date/jq call across a whole redraw instead of forking per row.
  • Burn's launch target is always the tank the caller named — no pre-launch substitution. Headroom preference only governs which tank a dry burn reroutes to next: three tiers (known headroom under 90%, sorted by lowest weekly_pct then window_pct → unknown → known ≥90%), same-account tanks ranked as one (using the worst shared reading, never offered as two consecutive hops), --to always wins outright, every hop recorded in rerouted_from. Burn never calls the vendor itself — it reads whatever is already cached (stale allowed); only clikae usage/--fresh fetches.
  • antigravity: unknown (no adapter_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:

  1. b843c0a — the named tank is always the launch target; the pre-launch headroom swap that ignored --to and hid itself from rerouted_from is deleted outright (P1-2/P1-3/P1-4).
  2. 23466b0 — burn's candidate ranking never calls the vendor (usage_cache_peek, cache-only); macOS Keychain reads gained the same command -v security + timeout guard adapter_migrate_credentials already 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).
  3. 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).
  4. cf7910d — codex's source is honestly "transcript", never "vendor"; cached_at is the evidence's own timestamp (P2-4).
  5. 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).
  6. 2f0b609 — doc gate fix: allow-list the five new JSON field names (P1-1).
  7. f7eaec2clikae usage documented in clikae help, README, and docs/grammar.md (P2-5).
  8. 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 needed log.sh/a 7th field name).
  9. 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, every lib//tests//scripts/ .sh, 99 files): rc=0.
  • bash scripts/doc-names-exist.sh: rc=0.
  • bash 3.2 compatibility grep on every changed file: no hits.
  • git merge-tree --write-tree origin/main HEAD: rc=0 (rebased onto origin/main first; settings: versioned permissions template applied to every tank — settings apply / --check / --dry-run, init seeds it, doctor reports drift (#76) #85's settings landed there in the meantime).
  • Real smoke, one live tank (./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_fields correctly reads it back (rc=0). No token in stdout/stderr/cache (grep, 0 hits); cache file mode 600; the four real tanks' settings.json files 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.md in 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:

  1. a55b4f0 — who writes the vendor cache, and when a stale reading is invalid (P2-1). Nothing but a human running clikae usage ever 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 own resets_at has passed reads as 0% used, never a stale ≥90%. Folds in the P3-12 (norm_stamp now parses any ±HH:MM offset) and codex-TTL (scanned_at, separate from the reading's own cached_at) fixes since they touch the exact same functions.
  2. 06d3d19_burn_timeout_bin moves to lib/core/timeout_bin.sh; lib/adapters/claude.sh's Keychain read had its own two-arm copy (timeout/gtimeout only) on the ONE platform (stock macOS) that ships neither by default, so its 5s bound was silently empty (P2-2).
  3. 990839b — intra-tier reroute ordering is window_pct ascending first, weekly_pct only 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).
  4. 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 to clikae usage is now refused with a message instead of silently doing nothing; clikae usage --help/docs/grammar.md corrected 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 stray clikae wait row (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 #72 usage row, 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 to burn.
  5. 34cd504 — an empty commit to retrigger CI (the webhook did not fire for c673adb — a one-off delivery gap, not a CI config issue; confirmed by other lanes' PRs triggering normally in the same window).
  6. 3b6a3e9 — merged origin/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 in CHANGELOG.md's Added list (both sides added an entry next to each other), resolved by keeping both.
  7. da86fda — CI's pty smoke (macos-latest)/smoke test (macos-latest) caught a real bash 3.2 parse error in the P2-2 rewrite of lib/adapters/claude.sh (a case inside a nested $( if … fi ), a \ continuation immediately followed by a | continuation, plus a comment, stacked) — this lane's own bash -n (bash 5.x) never saw it. Fixed (no case, no backslash continuation inside the nesting, comment moved outside it) and reverified against a real bash:3.2 Docker container.
  8. 165dfeb — a follow-up correction to the CHANGELOG entry c673adb had 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 to clikae usage is now refused with a message instead of silently doing nothing; clikae usage --help/docs/grammar.md corrected 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 stray clikae wait row (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 #72 usage row, 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 to burn.

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 against ls tests/bats/*.bats), re-run at the final HEAD after merging origin/main: 1245/1245 ok at the pre-merge state, 455 + 303 + 265 (1 pre-existing, unrelated roam.bats flake — reproduced identically against a clean origin/main worktree, not caused by this PR) + 225 ok at the final state. CI's own bats jobs (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.
  • bash 3.2 compatibility grep on every file changed this round: no hits — but this MISSED a real bug: lib/adapters/claude.sh's P2-2 rewrite (a case inside 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 hit unexpected 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 in da86fda(nocase, no backslash continuation inside the nesting, comment moved outside it) and reverified against a real bash: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.
  • P2-2's shadow-PATH bats test proves the perl arm actually bounds a real 30s-blocking security stub 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 test macOS + ubuntu, shellcheck, signet, pester windows). 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.md in the lane.

🤖 Generated with Claude Code

… 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.
@cverorg
cverorg force-pushed the feat/usage-endpoint-72 branch from 8086e6e to 6126adb Compare September 13, 2026 13:52
KITT added 10 commits September 13, 2026 14:01
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).
@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P1-1 (doc gate red on both CI platforms): docs/DESIGN-board-fuel-dots.md names five clikae usage --json field names (window_pct, weekly_pct, window_resets_at, weekly_resets_at, cached_at) that the doc gate reads as function-name claims. Fixed in 2f0b609 — allow-listed with reasons, matching the existing rerouted_from/run_id precedent. bash scripts/doc-names-exist.sh rc=0.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P1-2 (burn silently swapped the named tank before the first attempt): burn.sh:2310-2324's pre-launch headroom swap is deleted outright in b843c0a. The named tank is always the launch target; headroom preference now lives only in the existing mid-loop reroute path (which already ran once $tank itself went dry).

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P1-3 (--to ignored by the pre-launch swap): same fix as P1-2 (b843c0a) — the block that ignored --to is gone. --to was already honored outright in the real dry-reroute path below it; that path is unchanged.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P1-4 (rerouted_from/summary hid the pre-launch swap): same fix (b843c0a) — with the swap removed, the first attempt is always on the named tank, so $tried/rerouted_from accurately reflect every hop from the start. Added a bats test (usage.bats) asserting a headroom-driven reroute shows up in --json rerouted_from.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P2-1 (board redraw lost its fork-free contract): usage_cached_fields forked date+jq per call, and _home_fuel_dotv is called up to 5x per redraw for the same tank (Live row + Tank row, etc.) — measured 4605µs/call vs the pre-cache 719µs baseline. Fixed in 23466b0: usage_cached_fields takes an optional shared now, and _home_fuel_dotv gained a per-redraw memo (_home_fuel_memo_reset, called once per render next to the existing _home_cols_prime) so a repeated (dry,cli,tank) lookup in one redraw is served from memory.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P2-2 (burn's candidate loop called the vendor): _burn_next_same_engine called usage_read (a live vendor round-trip unless cached) per candidate — measured 14s wall / 4 curl calls for 4 candidates. Fixed in 23466b0: usage_cache_peek, a new cache-only reader (stale allowed, no network, no adapter fork). Fresh reads now happen only from clikae usage [--fresh].

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P2-3 (reset-expiry guard never fires against a real vendor timestamp): the vendor sends "2026-09-13T14:50:00.189940+00:00" (microseconds + numeric UTC offset) but the old regex only matched bare "...Z", so fromdateiso8601 always threw and the guard failed open on every real reading. Fixed in cb9c64a: norm_stamp normalizes before parsing; usage.bats's fixture now uses the real shape, plus a negative control proving the old regex read a 2020-dated real-shape timestamp as "not yet expired".

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P2-4 (codex claimed source:"vendor" though no codex process ever runs, and cached_at was wrong): fixed in cf7910d — codex's adapter_usage now returns source:"transcript" honestly (accepted by usage_read's whitelist and both cache selects alongside "vendor"), and cached_at is the winning rollout event's own timestamp (a 7th field _limit_codex_rate_limits was computing and discarding via : "$ts") rather than "now". docs/DESIGN-board-fuel-dots.md's 7-day scan window is now documented.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P2-5 (clikae usage undocumented): fixed in f7eaec2 — a real --help detail page (was a one-line synopsis), added to clikae help's "Use & inspect" section, docs/grammar.md's verb table (next to status; also added wait, which turned out to be missing entirely), and a README bullet.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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 $tried's last entry, independent of dried_accts), and same-account candidates collapse to ONE ranking slot valued at the worst (highest) reading any sibling reported. Two new bats tests reproduce both halves.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P2-7 (macOS Keychain read had no command -v security guard or timeout, unlike adapter_migrate_credentials's own): fixed in 23466b0 — same guard, plus a 5s timeout/gtimeout wrap when available. Also now off burn's hot path entirely (P2-2), reachable only from clikae usage/--fresh. I don't have a real macOS headless session to verify the lock/GUI-prompt scenario itself — see "what I didn't verify" in the report.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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. git merge-tree --write-tree origin/main HEAD rc=0.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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.

KITT added 5 commits September 13, 2026 18:21
…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.
KITT added 2 commits September 13, 2026 19:54
… 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.
@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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 — usage_board_fields (new) shows a stale-but-recent reading WITH its age ("3h ago") instead of hiding it, and treats 24h+ as unread. (d) usage_cache_peek/usage_board_fields both honour the reading's own resets_at: an expired window reads as 0%, never a stale ≥90%.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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 (timeout/gtimeout only); the repo already had a correct three-arm one (timeout -> gtimeout -> perl -e 'alarm …; exec …' -> honest warning) in burn.sh. Moved to lib/core/timeout_bin.sh, sourced globally, and the Keychain branch now calls it. Shadow-PATH bats test (only perl on PATH) proves a real 30s-blocking security stub is cut off near 5s, not just that the right binary name gets picked.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-1 (_home_fuel_dotv's header promised "fork-free" outright, which was never fully true and is less true after P2-1(c)): fixed in c673adb — rewritten to state 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.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-2 (Tank row's $( ) call site lost the per-redraw fuel-dot memo to a subshell): fixed in c673adb — switched to _home_fuel_dotv + $_FDOT/$_FNOTE, matching every other call site.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-3 (usage_cache_peek's doc said a miss is rc=1; jq -er's actual exit status for zero output is 4): fixed in c673adb, comment only — every caller already tests truthiness, not a specific code.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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 clikae usage codex re-scanned the whole rollout store): fixed in a55b4f0 — new scanned_at field, separate from cached_at, used for the TTL cache-hit check; cached_at keeps meaning "how old is this reading's own fact", unchanged.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-6 (clikae usage <engine> <tank> <extra> silently return 1'd on a third positional argument): fixed in c673adb — refuses with "Too many arguments: clikae usage [engine] [tank] [--json] [--fresh]".

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-7 (_limit_tank_account now calls unconditionally per candidate, ~4ms noise vs the round-1 14s): not addressed this round — review itself flagged this as noise ("跟第 1 輪的 14 秒比完全是雜訊"), and it isn't in the round-2 DESIGN DECISIONS list. Left as-is.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-8 (rerouted_from reroute test only had 2 tanks — deleting the ranking pass would have stayed green): fixed alongside P2-3 in 990839b's companion test commit — reserve ranks vendor headroom… (usage.bats) now has two tier-0 candidates with opposite window/weekly shapes, plus a dedicated hiwin/hiweek negative control, so removing the intra-tier sort turns both red.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

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 usage_cache_peek (a 3-day-old reading still ranks) against usage_cached_fields' fresh-only gate (refuses it).

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-10 (CHANGELOG.md's #72 entry had a stray blank line before the next bullet, turning the whole Added list loose): fixed in c673adb — blank line removed, and the entry now also names this round's fixes.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-11 (docs/grammar.md's clikae wait row, added alongside the actual #72 usage row in f7eaec2, unrelated to #72's own scope): fixed in c673adb — moved to sit next to burn, its actual semantic neighbor ("the composable half of burn's &/background pattern"), instead of between usage and the to/auto/watch/burn cluster.

@cverorg

cverorg commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

P3-12 (norm_stamp only handled +00:00/-00:00, not an arbitrary ±HH:MM offset): fixed in a55b4f0 — now parses any offset by epoch arithmetic (naive time as if UTC, shifted by the offset); bats tests +09:00 both expired and not-yet-expired.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-tank usage from the vendor's usage endpoint: clikae usage --json, real fuel dots on the board, burn prefers headroom

1 participant