Statusline 2.1.0 — one cost ledger, ten modules, width-aware rendering - #12
Merged
Conversation
…ne-costs.py statusline-costs.py summed every assistant line of every transcript, which over-counts spend ~2.2x: Claude Code re-logs one API response 2-3 times (streaming / tool continuation), so a correct total must deduplicate on message.id:requestId before pricing. Measured over 172 local transcripts: $3438.84 deduped vs $7645.04 raw. costs.sh replaces it and prices each session from its OWN transcript plus the full recursive <transcript>/subagents/ subtree, so Task, worktree-isolated and workflow agents are all billed — none of them appear in Claude Code's .cost.total_cost_usd, which covers the main thread only and additionally carries prior spend forward on a resumed session. pricing.json ships alongside: costs.sh resolves it next to itself, then falls back to ~/.claude/pricing.json, and refuses to run without it. Also fixes the six shellcheck findings the file carried, so it can join the CI shellcheck step rather than be excluded from it: three `local x="$(...)"` splits (SC2155 masks the substitution's exit status), one dead `log=` assignment (SC2034), and two scoped SC2016 disables on the jq programs, whose $models/$ml sigils are jq variables that must reach jq unexpanded. Verified behaviour-preserving against the unmodified script over the real ledger and a 531-line transcript: byte-identical ledger row ($10.178724), identical today/month totals, same three cache sidecars. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
… the dials The renderer had reached 1022 lines against the 500-line cap (rules/coding-standards.md §4.1). It is now a composition root plus ten modules under statusline-lib/, one concern per file, largest 249 lines: platform.sh BSD/GNU spelling differences (stat, date) palette.sh colour tokens, the heat-track bar fit.sh visible-width measurement and trimming severity.sh the one ok/warn/danger scale and its thresholds format.sh numbers and times as the reader sees them config.sh the two JSON config files gitctx.sh the repository facts session_state.sh cost ledger, transcript telemetry, subagent tracker layout.sh terminal width probe, verbosity preset render.sh one function per status line statusline-command.sh now only wires: it reads the statusLine JSON, asks each collector for its facts, asks each renderer for its line, and emits what fits. Modules resolve relative to the script's own path ($STATUSLINE_LIB overrides), and a missing one exits non-zero naming the file — a silently blank status bar would read as "nothing is happening". The split is behaviour-preserving: verified byte-identical against a 34-configuration preset x width golden render. One deliberate behaviour change rides along, isolated in its own golden diff (only identity lines move): the identity line renders model | dir | effort | thinking rather than putting dir last. Segment order IS priority order — fit_line drops from the tail — so the old order volunteered the working directory as the first thing to lose on a narrow terminal, while the effort and thinking dials, which the reader chose and already knows, survived. Confirmed against the old golden: at 50 columns the previous build rendered "model Opus 5 | effort high | thinking on" with no directory at all. Tests. The static checks had been reading a single file, so every one of them would have passed vacuously on a rule a module was the one breaking. They now sweep the whole source set (SOURCES_UNDER_TEST = the script plus its statusline-lib/*.sh), and five tests are added: the §4.1 size cap, every module loading and defining its exports, the loader's missing-module failure path, and the two identity-order regressions. Each was mutation-checked — cap raised past a padded module, token_color duplicated into a second module, a bare BSD-only stat call added to a module, the loader made to continue silently, and the identity order reverted: all six mutants killed. 22/22 and 35/35 pass, shellcheck clean on the renderer and every module. SC2034/SC2154 are disabled per file with a written rationale: shellcheck analyses one file at a time and cannot follow a sourced-module boundary, so it reads every cross-module constant as unused. Scoped to those two codes, never by excluding a file from the check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
…ut, gate in CI Install path. The renderer is no longer one file: the skill and the SessionStart auto-update hook now place costs.sh, pricing.json and the whole statusline-lib/ directory, and delete a leftover ~/.claude/statusline-costs.py. The lib is installed as a WHOLE directory — the renderer refuses to start on a missing module, so a half-updated module set is worse than an absent one. Post-install verification checks each of the ten modules and both execute bits. CI. It ran one of the two statusline suites and shellchecked one file, so nothing added in this series would have been exercised. It now runs both suites and shellchecks the renderer, every module, and costs.sh — which is why that file's six pre-existing findings were fixed in the ledger commit rather than excluded from the step. pricing.json joins the JSON validation. Docs. Both READMEs still described an emoji layout (🌿 branch, 💰 cost, 🎯 gauges) that the word-based design-system rendering had already replaced, a `grad_rgb` continuous interpolation that the discrete 4-palier heat track replaced, and statusline-costs.py. They now document what ships: the segment map, the module table, pace, and width fitting. plugin.json and the marketplace entry still said "Catppuccin Mocha". Versions: statusline plugin and skill 2.0.0 -> 2.1.0, marketplace metadata to match. CHANGELOG entry records the removal of statusline-costs.py and the module directory as the two breaking changes for anyone installing manually. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
…he suites
The completion ledger for this branch surfaced two load-bearing functions
introduced by it with no asserting test: probe_cols and resolve_preset. A
coverage gap in new code is blocking (rules/coding-standards.md §13.3), so it
is closed here rather than deferred.
probe_cols walks four sources and only the $STATUSLINE_COLS rung is reachable
as-is from a test process — the rest depend on whether the runner owns a
terminal, which differs between a developer's shell and CI. Each lower rung is
reached by shadowing the command it calls (stty, tput) with a failing stub in
the test's subshell. No production seam is added for the tests and no case
depends on the runner. The tput rung gets the negative assertion the bug
deserves: with stdout on a pipe the answer must be the wide fallback, never
terminfo's blind 80.
Writing those cases exposed a defect: probe_cols printed $STATUSLINE_COLS
straight through, so a non-numeric override escaped its own postcondition ("a
positive integer, always") and broke every arithmetic width comparison
downstream. An invalid override now falls through to the probes.
The four new cases were verified against injected mutants — unconditional
tput, an off-by-one width cap, the unvalidated override, and a config size
treated as a pin — each of which they kill.
Boy-scout (§14), in the touched test tree: SC1090 x3, SC2004 x4, SC2034 x4,
SC2153 and SC2329 x9 fixed or disabled per site with a written rationale, and
CI extended to shellcheck tests/statusline/*.sh — it checked only the assets,
which is how those findings survived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
…plicit if CI's shellcheck flags three SC2015 sites that the local 0.11.0 suppresses when C is `true` — a version skew that makes a local run greener than the gate. The findings are real regardless of which version reports them (§14), so the three sites are rewritten rather than silenced. The weekday counter is the one with teeth: `(( count++ ))` evaluates to 0 — FALSE — on the first weekday of the month, which is precisely why the `|| true` was needed to keep the loop alive. An assignment has no such trap. A/B on the same ledger store, before and after: `today`, `month` and `info` all byte-identical ($291.29 today, $3528.10 month, $196.01 avg/weekday). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
…codes The gate ran whatever apt shipped, which is several releases behind a current local install. The two versions do not report the same findings — the older one raises SC2317 where 0.11.0 raises SC2329, and SC2015 on `A && B || true` which 0.11.0 no longer flags. That skew is how three costs.sh findings and five test findings reached CI unseen by a clean local run: an unpinned linter is not a gate, it is a lottery. CI now installs 0.11.0 from the upstream release, matching a local run. The four stub directives list both codes so the suites stay clean under either version — verified against 0.9.0 in a container and 0.11.0 locally, both exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
The §8 citation on FIT_RATIO pointed at pictet-tech claude-statusline assets/statusline.sh:98-108. That plugin has been uninstalled, so the cited source can no longer be consulted, and its claim (a line near the full width is truncated with "…" and costs the block its second row) was never reproduced in this repo. A citation nobody can follow is not a source. Replaces it with what is actually established — code.claude.com/docs/en/ statusline, read 2026-07-26 against Claude Code 2.1.220: the reserve the host takes is additive (statusLine.padding, "extra horizontal spacing (in characters)", default 0, added to the interface's built-in spacing, whose column count is unpublished), not proportional. Records that 85 is an inherited working default rather than a measurement, and states the known cost of keeping it: measure_widths.sh puts preset l's widest line at 89 columns, so at 85% l is untrimmed only from 104 columns up, while SIZE_L_MIN_COLS selects l from 90 — l is trimmed on every refresh between those two widths. Comment-only; FIT_RATIO stays 85 and no rendered output changes. Verified: shellcheck 0.11.0 clean, 46 + 22 tests pass, smoke render exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Statusline only. No change to
context-guardorrefine-gate.Six commits:
b7b269a—costs.shbecomes the one cost ledger,statusline-costs.pyis removed.b7c3509— the renderer is split into ten modules; the identity line putsdirbefore the session dials.8af5a74— install path, docs, and the CI gate follow the new directory.bf85315— tests for the width probe and preset resolution; the lint gate extended to the suites.58ab8eb— theA && B || trueidiom incosts.shrewritten as explicitif.600e2db— shellcheck pinned to 0.11.0 in CI.The first three were each verified green before the gate was widened.
bf85315and
58ab8ebeach failed the widened gate on findings the local (newer)shellcheck did not report — that skew is what
600e2dbcloses. CI is green onthe merge head (
600e2db).Why
One cost ledger.
statusline-costs.pysummed every assistant line of everytranscript and over-counted ~2.2x, because Claude Code re-logs one API response
2–3 times (streaming / tool continuation). A correct total deduplicates on
message.id:requestIdbefore pricing. Measured over 172 local transcripts:$3438.84 deduped vs $7645.04 raw.
costs.shalso prices the full recursive<transcript>/subagents/subtree, so Task, worktree-isolated and workflow agentsare billed — none of them appear in Claude Code's own
.cost.total_cost_usd.Ten modules. The single file had reached 1022 lines, over the 500-line cap
(
rules/coding-standards.md§4.1).statusline-command.shis now a 173-linecomposition root sourcing
statusline-lib/*.sh; the largest module isrender.shat 260. A missing module is a hard failure that names the filerather than a silently partial statusline.
$STATUSLINE_LIBoverrides thelocation.
dirbefore the dials. Segment order is priority order under widthfitting. The previous order put
dirlast, which made the working directory thefirst thing dropped on a narrow terminal.
BREAKING
statusline-costs.pyis removed. Installs carrying a stale~/.claude/statusline-costs.pyshould delete it; the SessionStart hook nowdoes so.
statusline-lib/must be installed nextto
statusline-command.sh.Full detail in the CHANGELOG 2.1.0 entry.
Completion Ledger
Paths introduced by the diff
test_modules_all_load_and_define_their_functionstest_missing_module_fails_loudly(asserts the emitted message names the file, not a downstream effect)$STATUSLINE_LIBoverridetest_missing_module_fails_loudlydrives the loader through a relocated$STATUSLINE_LIBfit_line— fits, drops trailing segments, invalid budget passes throughtest_fit_line_*(incl.test_fit_line_invalid_budget_passes_through)$STATUSLINE_COLS, invalid override,$COLUMNS,tputonly when stdout is a tty, wide fallbacktest_probe_cols_*(5 cases). The tput rung carries a negative assertion: the answer must be the wide fallback, never terminfo's blind 80test_resolve_preset_*(6 cases)test_pace_*casesused_percentagenon-numeric ("n/a") rejected before awktest_quota_reading_rejects_absent_window,test_pace_ratio_rejects_bad_input(previously rendered as a healthy 0%)file_mtime— existing file, missing file → 0test_file_mtime_reads_an_existing_file,test_file_mtime_missing_file_is_zero;test_static_no_bare_stat_callsweeps every source for a rawstattest_identity_puts_dir_before_the_session_dialstest_heat_rgb.sh(22 cases)costs.shdedup + subagent subtree pricing$10.178724, identical totals, same 3 sidecars§13.1 checklist
Total: 22 — Echecs: 0/Total: 46 — Echecs: 0file_mtimemissing → 0; invalid budget → pass-throughused_percentageand width values validated before awk/arithmeticcosts.shwalks the transcript tree once per cache miss; results are mtime-cachedDevice not configured)hooks/hooks.json,skills/statusline/SKILL.md, both READMEs,marketplace.json,plugin.json,ci.yml, and both suites — all updated in this PR. The local~/.claudeinstall was re-synced and verified byte-identical to the assetsstatusline-costs.jsonlformat unchanged; the stale.pyis deleted by the SessionStart hook (one-shot, no shim)stat -fBSD-only bug is the fix in this PR.file_mtimetries the BSD then the GNU spelling; only the runner's own spelling is exercised per platform, sotest_static_no_bare_stat_callis what holds the contract on both — it sweeps every source for a rawstatcall bypassing the helper. The suites run on macOS's stock bash 3.2 and on CI's GNU bashtest_missing_module_fails_loudlystat -f,"n/a"percentage,tput colsunder a pipe, the non-numeric$STATUSLINE_COLSoverride, and the vacuous-static-test defect each have a case. The tty-probe stderr leak is a redirection shape, asserted by review rather than a test — it has no observable output to assert on beyond the absence CI cannot seetest_static_no_lerp_or_gradrgb_residueand the other absence checks now sweep aSOURCES_UNDER_TESTarray rather than one filestatusline-costs.pydeleted rather than kept; one deadlog=incosts.shremoved600e2db. Two intermediate commits went red on the widened gate and are fixed by the two that follow — see the commit listcosts.shcarried 6 pre-existing shellcheck findings (3× SC2155, 1 dead assignment, 2 SC2016 on jq programs) — all fixed here and A/B-verified byte-identical output.shellcheckcannot follow a sourced-module boundary, so SC2034 (libs, main) and SC2154/SC2153 (render.sh) are disabled per file with a written rationale; no file is excluded from the check. The test tree carried its own SC1090/SC2004/SC2034/SC2153/SC2329 findings, invisible because CI checked only the assets — fixed or disabled per site with a rationale, and the gate widened so they cannot come backThe lint gate was not a gate
Widening shellcheck to the test tree exposed that the gate itself was
unreliable: CI installed whatever
aptshipped, several releases behind acurrent local install, and the two versions disagree — the older raises SC2317
where 0.11.0 raises SC2329, and SC2015 on
A && B || truewhich 0.11.0 nolonger flags. A clean local run was therefore not evidence of a clean gate, and
two commits here went red proving it.
Fixed at the source: CI installs a pinned 0.11.0 from the upstream release, and
the four stub directives name both codes so the suites are clean under either
version — verified against 0.9.0 in a container and 0.11.0 locally, both exit 0.
The three
costs.shfindings this surfaced are real regardless of reporter andwere rewritten, not silenced. The weekday counter is the one with teeth:
(( count++ ))evaluates to 0 — FALSE — on the first weekday of the month,which is exactly why the
|| truewas needed to keep the loop alive. A/B on thesame store, before and after:
today,monthandinfobyte-identical($291.29 / $3528.10 / $196.01 avg-weekday).
Mutation check (§12)
The five new
probe_cols/resolve_presetcases were verified against fourinjected mutants rather than trusted for being green — each is killed:
tputconsulted unconditionally (drop the[ -t 1 ]guard)test_probe_cols_ignores_tput_when_stdout_is_not_a_terminal— got 80, expected 200-lt→-le)test_resolve_preset_width_cap_boundary— got s at the thresholdtest_probe_cols_rejects_a_non_numeric_override— gotabctest_resolve_preset_config_size_is_a_preference_capped_by_width+ the boundary caseBehaviour preservation
The split is behaviour-preserving, verified by a golden render across a
34-configuration preset × width matrix, byte-identical before and after
(the countdown field is masked because
resets inmoves between runs).Test output
🤖 Generated with Claude Code
https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4