Conversation
The Session bar and the monitor title averaged each bucket independently, so a slot sitting at the 100% Week cap contributed its idle 5h reading to the pool as headroom. A three-slot pool with two slots refusing prompts rendered a 33% Session bar, and the aggregate contradicted the rotation engine, which already scores such a row at 100 through Get-RowMaxUtilization and refuses to enter it. A 5h window is nested inside the 7d one, so while the week is capped none of that slot's session capacity is reachable and the pool has none of it left to offer. The rule runs one way only: a capped 5h window costs the week at most 5h of 168, so the Week mean keeps each row's own number. The README watch and monitor examples carry a 'limited 7d' row, so their Session bar moves from 22% to 40% and the two SVGs are re-rendered.
…mbers The bare -Watch section printed 22% | 62% as its terminal-title example, which are the pool-aggregate bar numbers. Invoke-UsageWatch passes -Aggregate only under -Auto, so a bare -Watch title carries the active slot, exactly as the note directly beneath the example already says. The example's active row is 'work' at 18% / 42%.
…S.md Claude Code 2.1.277 reads AGENTS.md directly in a project that has no CLAUDE.md, so the one-line @import shim no longer carries anything. The feature's Bedrock / Vertex / Foundry carve-out does not reach this repo, which manages claude.ai subscription OAuth slots. The cost is that a client below 2.1.277 now loads no project instructions at all, silently and with no error, including the security rules governing live credentials. Accepted deliberately.
2063582 scored a week-capped slot as 100% on the Session bar, which answers 'of nominal pool capacity, how much is gone'. That is the Week bar's job, and it left the bar contradicting the Session cells printed underneath it. The Session bar answers a different question, 'of the session capacity I can still reach, how much is spent', so a slot the week has capped now leaves that average entirely, denominator included. The cells keep reporting what Anthropic measured, including the idle 0% on a capped slot, so no rendered number asserts a reading the API did not give. The Week bar keeps a week-capped row at its real 100%: dropping it there would hide the exhaustion the bar exists to show. The rule stays one-way, since a capped 5h window costs the week at most 5h of 168. Accepted cost: the Session average improves as slots fall out of the pool. The shrinking pool is signalled by the Week bar and the red rows instead. When the week has capped every measurable slot the average would have no rows left, so it returns 100 rather than blanking the bar and the title at the moment they matter most. The README example's Session bar moves from 40% to 25% over its four reachable slots, and the two SVGs are re-rendered.
`sca warmup` and `sca monitor -KeepWarm` refused while Claude Code was running. Both make every slot active in turn, and the concern was that the `claude -p` a warm pass spawns would race the live client for a cold slot's grant, leaving one of them holding a refresh token the server had already rotated. It cannot, and claude.exe 2.1.278 says why. The OAuth refresh has no timer behind it, only request paths: 401 recovery, the bearer-attribution preflight, the request-header build and a poll auth check. An idle client never refreshes on its own. The refresh itself then takes a cross-process lock, re-reads the credentials both before and under it, and returns "refreshed" when the access token moved, adopting a peer's result rather than racing it. Anthropic instruments that path as tengu_oauth_token_refresh_race_resolved. Two Claude Code processes on one account cannot both rotate. The loss that did exist was sca's own, and it needed no live client at all. Invoke-WarmAllSlots mirrored .credentials.json back into the slot file only after an 'ok' activation, but claude can refresh a grant and only then be turned away, hitting the 5h limit being the common case. The next iteration's swap overwrote those tokens and left the slot on a rotated refresh token. The mirror now runs after every activation; it costs nothing when nothing moved, because the swap stamped state.last_sync_hash with the bytes it wrote. That same reconcile now gates the auth verdict. A verdict asserts claude proved a grant dead, which holds only if claude wrote nothing: had its refresh gone through, the grant is alive and the refusal was about something else. Recording one then stranded a working slot behind a verdict that outlived the run. What remains is a prompt sent mid-pass billing whichever slot is mounted, a surprise rather than a loss, so `sca warmup` warns instead of refusing. `save` still refuses, for a reason the round-robin never shared: it pairs tokens with an identity read from a different file, and nothing later corrects a mislabel.
Scratch artifacts belong under .tmp/sessions/<session-id>/, but nothing here supplied an id, so rule 3 fell through to its minted YYYYMMDD-HHMMSS-<random6> fallback on every run and a resumed session opened a second directory instead of reusing its own. Both clients can hand one over, by different routes. OpenCode gets a plugin on the shell.env hook, putting SESSION_ID into the environment of every command; Claude Code gets a SessionStart hook whose stdout joins the conversation ahead of the first prompt. Neither puts the value in the system prompt, which is the one block every turn and every session share and which a prompt cache keys on: a per-session value there leaves no two sessions a reusable prefix and has the whole block reprocessed at each session start. The plugin comment carries that. The Claude Code hook shells out to jq, which this repo otherwise does not depend on. A pwsh equivalent costs a process start per session and reads far worse, and an absent jq degrades to the minted fallback rather than breaking a session.
…verdict Both files handed an agent the run command and nothing about reading it. The suite prints a line per test under -Output Detailed, so the summary and the coverage line sit at the bottom of roughly a thousand, and the obvious reflex is to pipe the run through a filter sized for a terminal. That cuts exactly the two lines worth keeping, and the only way back to them is a second full run of a suite that takes minutes. It cost one here. The runner has answered this since it was written: it exits 1 if any test failed or coverage missed the gate, 0 otherwise. Asking for $LASTEXITCODE in the same command is one run, and it survives any truncation a harness applies, which writes the full output to a file anyway. Stated as the recipe first and the prohibition second, because a command gets copied and a rule gets skimmed.
…tring PowerShell's binder converts $null to '' on the way into a [string] parameter, AllowNull or not, so the guard could never be entered and no caller could ask for a JSON null. Verified against 7.6.6. The reason it cannot happen now sits on the function instead, where the next reader tempted to re-add it will look. Test-ClaudeNodeProcess's docblock claimed the opposite of what it should: that a Get-Process mock cannot reach a dot-sourced function. It can, against Pester 5.7.1. What actually kept Test-ClaudeRunning unexecuted is the suite's own blanket mock of it, and the real reason for the split is that the caller reaches this probe only on Unix.
… call Exit-WatchTerminal wrote [Console]::CursorVisible unguarded, the only System.Console call in the file that was. docs/architecture.md -> Console APIs already documented the invariant it broke: every one of them is wrapped at its call site, because off an attached console the setter throws SetValueInvocationException on Windows and the whole API is unavailable elsewhere. The write is reached only where the capture succeeded, so in practice it ran on a real Windows console where it works. What it cost was the failure mode that call exists to avoid: this runs inside the caller's finally, so a throw there unwinds the one path that leaves the alt buffer and shows the cursor again, stranding the terminal in exactly the state the restore is for. The VT sequence above it is what the cursor actually depends on; the API call is belt-and-suspenders for the .NET-side state and not worth taking the restore down with it.
Command coverage went from 96.1% to 98.6% (2475 of 2509 instructions), 981 tests passing. The gaps closed are the ones that were untested rather than untestable: Invoke-Main's whole action-dispatch switch and the dot-source guard, save's snapshot and rollback diagnostics, reconcile's degraded-input paths, the auth-verdict read and write failures, the activator's timeout and subtype classification, the load-time behavior with no resolvable home, and Test-ClaudeRunning, which no test had ever executed. The gate lands at 97 rather than 98, because coverage is measured on one OS and the headroom above it is what the next platform-conditional branch spends. Raising it to the number a run reaches would fail the build for a branch that is tested, just not on this leg. The 34 instructions that remain are enumerated in docs/testing.md -> The ceiling so the next person can tell a real gap from a structural one: 25 are Unix-only arms covered on the other legs, 5 need failures the test host cannot provoke, and 4 are defense-in-depth arms reachable only by mocking their immediate caller, which would pin the mock. Common.ps1 mocks Test-ClaudeRunning for the whole suite and a mock cannot be lifted, so the new file opts out through a flag read before the dot-source and mocks Get-Process instead.
Dropping the refusal was right: Claude Code refreshes only when a request
needs it and serializes refreshes across processes behind a lock file, so
a warm pass cannot race a live client for a grant. Two things it left
open.
The first is sca's own loss, and it is the one no later pass repairs. The
round-robin overwrites .credentials.json once per slot, so every slot
depends on the reconcile after `claude -p` both running and capturing.
Three paths left that unchecked, each ending with a slot holding a
refresh token the server has already rotated:
* The reconcile was sequential. Invoke-SlotActivator can throw after
claude has already refreshed, reading its output files or reaching
for its exit code, and the loop-body catch swallowed that without
reconciling. It runs in a finally now.
* Its result was never read. Invoke-Reconcile answers Captured = $false
for identity-unresolved and credentials-changed-mid-probe, both of
which saw changed bytes and deliberately wrote nothing. Its docblock
makes reading the field mandatory and every other call site does.
* The mirror writes through Set-CredentialFileAtomic, which throws.
That proves nothing either way, so it is as unsafe to write over as
an explicit refusal.
The pass stops on any of the three, and the restore in the finally is
skipped with it, because that restore is one more overwrite of exactly
the bytes the stop exists to protect. A swap failure still fails only its
own slot: that write is an atomic rename, so a throw leaves the file as
the previous slot's mirror captured it.
Naming the slot the user is left on could not have reached them before.
The restore-failure advisory was written as `Write-Color ... 6>$null`,
the only such line in the file, so it went to the information-stream sink
a test mock could still see and a user could not. Both watch call sites
suppress that same stream, so it rides back on the snapshot instead and
each caller renders it on the surface it owns.
The second is the user's. `sca warmup` warned about a live client and
then started immediately, which is a label rather than a decision, since
the first billable `claude -p` follows it by milliseconds. The warning
now precedes a five-second pause that makes the implied Ctrl-C reachable.
A prompt would be the stronger gate and was rejected: it needs a rule for
a redirected stdin, and this is a command people put in a scheduler.
`monitor -KeepWarm` removed the refusal without adding even the warning,
and it is the repeating one of the two. It cannot pause, so it carries
the same sentence in the footer latch, at startup and again at every
re-warm: a client opened at hour three of a watch is dragged across every
account by the very next pass.
The bar was documented as reaching 100% "only once the week has capped every one of them", which is false and reads as a stronger signal than the number carries. Get-PoolMeanUtilization averages the session utilization of every slot still in the pool, so two slots at 5h = 100%, 7d = 50% read 100% with the week nowhere near capped. The all-capped case is a second route to the same number, through the empty-pool early return, not the only one.
MINOR rather than PATCH on two user-visible reversals this cycle carries: the warm round-robin of `sca warmup` and `monitor -KeepWarm` now runs beside an open Claude Code instead of refusing, and the `Session` aggregate bar drops a week-capped slot from the pool entirely, so it reports a different number wherever one exists. The two Session-bar commits are folded into one entry. They are successive refinements of the same rule, first scoring a capped slot at 100 and then dropping it, and only the net change is a fact about the shipped version.
Write-Color took PowerShell color names, so any theme mapping 'DarkYellow' to blue would have turned every call site into a lie. Rename the seven tokens to the semantic roles its docblock already described (Heading, Warning, Success, Danger, Muted, Neutral), drop the dead Cyan arm, and move the role -> SGR mapping out of the function into $Script:ThemePalettes so a palette swap touches no call site. $env:SCA_THEME picks a palette, resolved once per run in Invoke-Main. Precedence is -NoColor > NO_COLOR > SCA_THEME > default: naming a theme says which colors, not whether, and PlainText strips truecolor by the same regex it already used for named SGR, so no-color mode needed no theme-specific handling. An unknown name falls back quietly rather than warning, because a typo lives in a shell profile and would otherwise print on every invocation for as long as it sat there. The default palette stays palette-relative, spelling roles as named ANSI 30-37/90-97 so it keeps following the user's own terminal scheme and stays legible on any background; only an explicitly named theme burns in truecolor. Nothing probes for truecolor support, because COLORTERM and TERM are both unset in a Windows truecolor terminal and the probe would answer wrong on the primary platform. A theme may also declare Background + Foreground, applied only inside the alternate screen that usage -Watch and monitor own. Scrollback is excluded deliberately: a background there would leave ragged colored bars in the user's shell history for good. Chrome is re-asserted after every ESC[0m, since Write-Color's reset clears background along with foreground, and asserted before each erase so those fill through back_color_erase; repeated runs are collapsed so a 1 Hz repaint carries no redundant bytes. Get-WatchChrome checks OutputRendering by hand because Write-VTSequence bypasses the StringDecorated filter that gives every other color path no-color mode for free. Neutral stays absent from truecolor themes on purpose: it inherits the chrome foreground inside a frame and the terminal's foreground outside one, and a fixed hex would be wrong on a light or a dark background.
Each theme spelled out its own $PSStyle calls, so every theme added was a
fresh chance to wire a role differently from its siblings. State the
role-to-slot rule once in New-ThemePalette and reduce a theme to a row of
seven hex values in $Script:Base16Schemes, transcribed from the base16
definitions in tinted-theming/schemes. Adding a theme is now a data change
and the integrity tests pick it up with no edit of their own.
Ports dracula, everforest, flexoki, gruvbox, kanagawa, monokai, nord and
onedark alongside the existing material, whose values are unchanged.
github is deliberately absent despite having an upstream. base16 slots
carry syntax-highlighting meaning, which usually but not always coincides
with the ANSI meaning a status table needs: all three github dark variants
put orange in base08 and pale blue in base0B, so Danger would have
rendered orange and Success blue and a glance at the table would have
misread which slots were healthy. The suite now hue-checks both slots on
every scheme so a theme added later cannot reintroduce that.
Muted keeps base03 ("Comments") rather than base04 ("status bars"), even
though a status table is what it renders: base04 sits close enough to
base05 that the row stops reading as de-emphasized, and being dimmer than
the body text is the whole job. The resulting contrast runs 1.7:1 to 3.8:1
across these schemes, which is each theme's own comment contrast rather
than something to correct here.
Dark variants only. A light scheme is legible, the chrome path handles one
correctly, but shipping both doubles a list that is read at a glance.
The help screen wraps the names into the description gutter instead of
joining them onto one line, which had already reached 124 columns.
Ten palettes were named in `sca help` and the README and none of them was shown, so choosing one meant setting SCA_THEME, re-running something and repeating. Adds docs/themes.md built around a single rendered gallery, and surfaces the same image in the README where the decision is made. One combined SVG rather than one per theme. The four existing images run 368-370 KB apiece despite holding different amounts of text, so embedded font data dominates and nine separate files would cost roughly 3.3 MB against about 375 KB for one. Side by side is also what "see them all at once" actually asks for. The rows are generated by dot-sourcing switch_claude_account.ps1 and looping its own $Script:Base16Schemes rather than transcribing the hexes a second time, so the picture cannot drift from the palettes that ship. That dot-source is read-only: the dispatcher is guarded by an InvocationName check and nothing at load time touches a file. The hex table in docs/themes.md is hand-maintained and was checked against the scheme data across all nine themes and six columns. `default` appears in the gallery with an explanation rather than being omitted. It emits named ANSI and so renders in whatever the terminal's own palette resolves those to, which no single image can be honest about; the row is drawn in Campbell and the page says so. Considered and rejected a `sca theme` action that would have previewed the palettes and set the variable for the session. It works, because the installed alias invokes the script with the call operator and so runs it in the caller's process, but it would have added a tenth entry to a ValidateSet otherwise entirely about credentials, plus a dispatch arm, a help row, a test file and permanent maintenance, to serve a decision made once.
Every palette so far came from tinted-theming; this one has no upstream. It is an original set in the same seven-slot shape, keyed to the warm accent and near-black of the Claude Code interface this tool manages logins for, so the tool can look like the thing it sits beside. Its Danger is the one deliberate departure. The accent that makes the theme recognizable sits at hue 15, and a true red at hue 0 would land within 15 degrees of the heading, close enough for the two to blur in a table that is scanned rather than read. That is the same glance ambiguity that kept github out. Danger is pulled to hue 349 instead, holding the pair 26 degrees apart at a contrast of 3.6:1 against the background, which is where nord already sits. Monokai is the precedent for a rose-leaning Danger reading correctly. Muted lands at 3.1:1 over the canvas, better separated than material, nord or onedark manage, so the footer stays legible without competing with the body. No test changes: the scheme table drives the integrity tests, so the hue and slot guards picked the new row up on their own.
The gallery showed a one-line swatch per theme, which answers "what colors does this use" when the question a reader actually brings is "what will this look like". Each theme now gets the entire monitor scene -- aggregate bars, the five-row slot table, both footer lines -- painted on its own canvas, so the page shows the view someone will sit in front of instead of a strip they have to imagine applied. Also removes a second, duplicate gallery block that 9697be1 shipped by mistake. Both assigned $themeGalleryLines and the later one silently won, so the committed image came from one block while the other's output was discarded. It went unnoticed because that commit was verified by inspecting the rendered SVG rather than re-reading the file around the insertion point. The scene is now built by New-HeroLines from a role-to-SGR palette rather than spelled out per palette, because it is rendered once in Campbell for monitor.svg and again for each of the eleven themes; two copies of eighteen hand-aligned columns would drift apart on the first edit. monitor.svg, usage-watch.svg, usage-table.svg and usage-verbose.svg were hash-compared before and after and are byte-identical, so the refactor changed nothing that ships. ConvertTo-ThemedPanel re-asserts the canvas after every ESC[0m, since a reset drops the background along with the foreground and would otherwise punch a hole to the end of each colored line -- the same rule ConvertTo-WatchFrameSequence follows at runtime. Panel width is measured from the scene rather than hardcoded, so widening a table column cannot leave the canvas too narrow for it. One image rather than eleven: the four existing SVGs run 368-370 KB apiece because embedded font data dominates, so separate files would have cost roughly 4 MB against 453 KB for one, and stacking them is what comparing them asks for anyway.
The gallery was one tall image with each theme labelled inside it, which left nothing to link to: a reader told "use nord" had to scroll and count. Each theme now gets its own file and its own `## <name>` heading, so docs/themes.md#nord goes straight there. The name moved out of the panel and into the heading, since repeating it in both would just be two places to keep in step. Splitting one image into eleven would normally multiply its weight, and here it did the opposite. Measured: a rendered SVG is 367 KB of which 970 bytes is the drawing, all the rest being one base64 woff2 -- 99.7% font. The theme panels drop the embedded face for a monospace fallback chain, which is safe because they are column-aligned text and every viewer resolves the chain to some monospace: a substituted face changes glyph shapes without disturbing the alignment the scene depends on. Eleven panels cost 92 KB against the 453 KB the single combined image took, so docs/images/ fell from about 1.93 MB to 1.57 MB while going from one gallery to eleven. The four README scenes keep their embedded copy. They are the front door, there are only four of them, and pixel-identical rendering is worth 370 KB there in a way it is not eleven times over. All four were hash-compared before and after and are byte-identical. docs/themes.md now carries each theme's hexes on its own line beneath its heading rather than in one shared table, so a deep link lands on the image and the values together. Verified against the scheme table: eleven headings, eleven image references, eleven files on disk and sixty hex cells.
Two defects in the per-theme gallery, and the page trimmed to what it is for. The usage bars stopped filling their cells. 02114b4 dropped the embedded font for a fallback chain on the grounds that the panels are column-aligned text and any monospace would do. That was wrong: freeze emits no per-glyph positions and no textLength, so the advance of every line comes from the font, and a substituted face moves the text off the rect geometry freeze computed from JetBrains Mono metrics. The block glyphs U+2588 and U+2593 are where it shows first, because their job is to fill a cell exactly. Embedding is restored and the rationale recorded so it is not traded away again; correct rendering costs ~365 KB per image and the theme panels now total 4.1 MB. The canvas only covered the text. Each row carried its own background SGR, which left freeze's 30px window padding showing the default terminal black around every panel, so a theme read as a rectangle floating on somebody else's background. The color now goes to freeze's own --background, which fills the entire window face. That also removes the per-row paint, the pad-to-width that kept those rows flush, and the re-assert-after-reset dance the background needed: 292 rects per panel become 2, and ConvertTo-ThemedPanel is left doing only the foreground. docs/themes.md is now a heading and an image per theme, nothing else. The hex lines and per-theme prose repeated what the picture already says and what switch_claude_account.ps1 already holds; the shared explanations move below the gallery, and `default` keeps its caveat in the intro because no image can be honest about a palette-relative theme.
The uncaptured-credentials abort breaks out of the round-robin, which left every row behind it at the 'warming-up' the pass seeds them with. Two readers take that for a result. Invoke-KeepWarmStep counts any outcome that is not 'ok' as a failed warm, so each untried slot was stamped with a cooldown and charged a failure, and Get-WarmupCooldownMinutes doubles on that count: a slot the pass never entered could be held off for up to 160 minutes, and only a successful warm clears the counter that the inflated cooldown itself delays. Invoke-WarmupAction then renders those rows through Get-UsageStatusLabel as 'warming up', in the attention-yellow Get-StatusColor gives the transients, in the final table of a command that has already exited. Give them a terminal 'skipped' instead, painted Muted because the abort advisory beside the table is what the user has to read, and let the keep-warm accounting leave a skipped slot exactly as it found it.
The repaint is the one statement in the round-robin's loop body that sits outside a catch, and the watch startup pass is the caller that hands it a real renderer writing to the console. A throw there unwound straight to the finally with $uncaptured still false, so the restore ran and overwrote the credentials nothing had captured: precisely the loss the abort exists to prevent, reached through the guard's own blind spot. Set $uncaptured, the advisory and the skipped rows immediately after the mirror settles them, and leave the break after the repaint so the frame still shows the row the pass stopped on.
The notice describes what the round-robin is about to cost, and the five-second pause after it offers a window to call that off. Both ran before anything had established that the pass had a slot to act on, so `sca warmup` on a fresh install, or with a -Name matching nothing, warned about billing and blocked for five seconds beside an open Claude Code before reporting that it had nothing to activate. A false alarm on the only interlock the tool puts in front of money. Split the pass's slot filter out as Get-WarmupSlotSet so the question can be asked before the notice rather than only by the pass. The watch's equivalent latch is cleared on the same discovery, where it had been left claiming a live session would be dragged across accounts that do not exist. Resolving Get-SafeName once in Invoke-WarmupAction falls out of the same change: the name now reaches the preflight, the pass and the no-slots message already sanitized, so a name that needs sanitizing is reported once instead of twice.
The 4.2.0 entry stopped at the warm-pass and aggregate-bar changes, but seven commits after it added the whole SCA_THEME palette system: ten themes, the themed alt-screen canvas, the rendered gallery and the help screen's new ENVIRONMENT section. A user-facing Added with no release record, in an entry that has never shipped (main is still 4.1.0 and no v4.2.0 tag exists), so it folds in here rather than claiming a version of its own. The image rule in AGENTS.md was one short for the same reason: the renderer emits a panel for `default` as well as for every entry in $Script:Base16Schemes, which is eleven files rather than ten.
Both Unix twins chmod a pre-existing slot pair to 000 so the snapshot read in Invoke-SaveAction fails, then chmod it back in a finally. Mode 000 denies the read but not the unlink, which the parent directory's permissions govern, so the save they drive to a warning went on to delete that pair as an obsolete sibling: the re-save carries a different email, the write lands on a new path, and the old one is cleaned up behind it. The finally then handed SetUnixFileMode a path that no longer existed and threw FileNotFoundException before either assertion ran. The Windows twins need no such guard because FileShare::None blocks the delete along with the read. That is why this failed only on the Linux and macOS legs, and why no Windows run could see it: both cases are -Skip:$IsWindows, and the workflow's push trigger is main-only, so the branch reached a release pull request before either leg had ever executed them.
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.
This release lets the warm round-robin run beside an open Claude Code, closes the paths where that round-robin could lose a token it had just caused to rotate, and adds
SCA_THEME. Upgrading is replacing one file, and nothing here is marked breaking.Two things change what you see without you asking for them.
sca warmupandsca monitor -KeepWarmno longer refuse whileclaudeis open: the first warns and gives you a five-second Ctrl-C window before it starts spending money, the second carries the same sentence in its footer and cannot pause at all. And theSessionaggregate bar now drops any slot whose week has capped, denominator included, so on a pool with a week-capped slot it prints a different number than 4.1.0 did. Colors are unchanged unless you setSCA_THEMEyourself.4.2.0
Running beside a live client
sca warmupandsca monitor -KeepWarmto run while Claude Code is open. Reading claude.exe 2.1.278 is what settled it: the OAuth refresh sits behind request paths only, never a timer, so an idle client never refreshes on its own, and the refresh itself takes a cross-process lock, re-reads the credentials before and under it, and returns "refreshed" when the access token moved. Two processes on one account adopt a result rather than race for it.sca warmupfor five seconds before the first billable activation when it finds a live client, because what remains is a prompt sent mid-pass billing whichever slot is mounted. The warning alone was a label rather than a decision, since the firstclaude -pfollowed it by milliseconds.sca warmupon a fresh install, or with a-Namematching nothing, no longer warns about billing and blocks for five seconds before reporting that it has nothing to activate.monitor -KeepWarmas a footer latch at startup and again at every re-warm, since a client opened at hour three of a watch is dragged across every account by the very next pass.sca saveas the one action that still refuses, for a reason the round-robin never shared: it pairs tokens from.credentials.jsonwith an identity read from~/.claude.json, and nothing later corrects a mislabel.The loss that needed no live client
.credentials.jsonback into the slot after every activation rather than only anokone.claudecan refresh a grant and only then be turned away, hitting the 5h limit being the common case, and the next iteration's swap overwrote those tokens and left the slot on a rotated refresh token.finally, so an activator that throws afterclaude -phas already refreshed still files what it landed instead of having it swallowed by the loop-body catch.Capturedfield and stop when it is false, and skipped the restore along with it, because that restore is one more overwrite of exactly the bytes the stop exists to protect.claudeproved a grant dead, which holds only ifclaudewrote nothing; recording one otherwise stranded a working slot behind a verdict that outlived the run.catch, and a throw there unwound straight to thefinallywith the guard still unset, so the restore ran and overwrote the credentials nothing had captured: the guard's own blind spot reaching the exact loss it exists to prevent.skippedinstead of leaving them at the seededwarming-up.Invoke-KeepWarmStepcounted that as a failed warm andGet-WarmupCooldownMinutesdoubled on the count, so a slot the pass never entered could be held off for up to 160 minutes, and the final table rendered it as an in-progress transient in a command that had already exited.6>$null, the only such line in the file, which sent it to a sink a test mock could still see and a user could not. It rides back on the snapshot now, and each caller renders it on the surface it owns.The Session aggregate
Weekcap out of theSessionaggregate bar entirely, denominator included. A 5h window is nested inside the 7d one, so while the week is capped none of that slot's session capacity is reachable, and its idleSessioncell was being counted into the pool as headroom.Weekbar at its real 100%, because dropping it there would hide the exhaustion the bar exists to show. The rule runs one way only: a capped 5h window costs the week at most 5h of 168.Sessionbar means the week has capped every slot, and re-rendered the two SVGs and the watch-title example whose numbers moved.Theming
SCA_THEME, resolved once per run, which pins output to an exact palette instead of the terminal's own ANSI colors. Precedence is-NoColor>NO_COLOR>SCA_THEME> default: naming a theme says which colors, not whether.Write-Color's seven color tokens to the semantic roles its docblock already described and moved the role-to-SGR mapping out of the function, so a palette swap touches no call site. Taking PowerShell color names would have made every call site a lie the moment a theme mappedDarkYellowto blue.claude. The role-to-slot rule is stated once, so a theme is a row of seven hex values and adding one is a data change the integrity tests pick up with no edit of their own.usage -Watchandmonitorown in the theme's background, re-asserted after every reset and before each erase so it fills through back_color_erase, with repeated runs collapsed so a 1 Hz repaint carries no redundant bytes. Scrollback is excluded deliberately: a background there would leave ragged colored bars in the shell history for good.docs/themes.md, one heading and one full renderedsca monitorview per theme, and a README section pointing at each anchor.sca helpunder a newENVIRONMENTsection.Elsewhere
System.Consolecall, so a failure there cannot unwind the terminal restore that follows it.CLAUDE.mdimport shim, now that Claude Code 2.1.277 readsAGENTS.mddirectly in a project without one.Shortcomings
sca warmupblocks briefly instead of asking. Anyone running it non-interactively gets the warning and no way to act on it.monitor -KeepWarmcannot pause at all. It carries the sentence in the footer latch and nothing more, which is the weaker half of the interlock on the repeating one of the two commands.Sessionbar now reads better as the pool gets worse. Shrinking the denominator is the correct answer to "of the capacity I can still reach, how much is spent", but it means the number improves as slots fall out. TheWeekbar and the red rows are what signal that instead, and a reader has to know the indirection is there.textLength: a substituted face moves the text off the rect geometry freeze computed, which shows first on the block glyphs the usage bars are made of. There is no cheaper correct option in the current renderer.Mutedcontrast is not corrected.Mutedkeeps base16'sbase03("Comments") rather thanbase04("status bars"), becausebase04sits close enough tobase05that the row stops reading as de-emphasized. The resulting contrast runs 1.7:1 to 3.8:1 across these schemes, which fails WCAG at the low end.COLORTERMandTERMare both unset in a Windows truecolor terminal, so a probe would answer wrong on the primary platform. A named theme therefore emits truecolor unconditionally and degrades to approximation on a 256-color terminal.SCA_THEMEfalls back silently. A typo lives in a shell profile, so a warning would print on every invocation for as long as it sat there. The trade is a discoverable error for a quiet wrong answer.sca usage, which CI cannot run because it needs real credentials.Feedback I want
Invoke-WarmAllSlots. The guard now settles its state immediately after the mirror and leaves thebreakafter the repaint, so the frame still shows the row the pass stopped on. Confirm there is nothing left between the mirror and the flag that can throw, because that gap is exactly the bug this fixes.Sessiondenominator. It is the one number in the tool that now improves as the pool degrades, and it reverses a decision made earlier in this same cycle rather than an old one. If scoring it 100 was right, the revert is small.SCA_THEME. I chose quiet over a warning that repeats forever. The opposite case is that a theme which never applies and never says why is worse than a repeated line.4.2.0. No commit carries a breaking marker, butwarmupandmonitor -KeepWarmlose a refusal, so a wrapper that treated their non-zero exit as "Claude Code is open" now gets a run it did not expect.What is not done
release-assets.ymlfires on publish, so the standalone.ps1asset appears only once this merges andv4.2.0is created.mainintodevelopafterwards.tests.ymlrestrictspushtomainon purpose, so a develop push with no open pull request runs nothing, and none of these commits had been through CI when the pull request opened. The first run failed both Unix legs on two tests this range added, and finding that is the whole argument for the trigger staying as it is rather than the gap being widened. Now: Windows 1033 passed at 98.7% coverage against the 97% gate, Linux and macOS 1042 passed, 0 failed anywhere.96b7500. Both Unix twins chmod a slot pair to000to force the snapshot read to fail, then chmod it back in afinally. Mode000denies the read but not the unlink, so the save deleted the pair as an obsolete sibling and thefinallythrewFileNotFoundExceptionbefore either assertion ran. The Windows twins hold aFileShare::Nonehandle, which blocks the delete too, so no Windows run could ever have seen it. The restore is now guarded on the path surviving, and both assertions execute for the first time.~/.claude.json.lockis still not taken, so the compare-and-swap in the substitution path stays narrowed rather than closed. Carried over from 4.1.0 and still the top follow-up.skippedcarve-out added here does not change where the status comes from);Test-TokenEndpointThrottledanswers from a field two endpoints stamp; andauth_verdictsentries are never pruned when a slot is removed.githubis deliberately absent from the theme list despite having an upstream base16 definition. All three dark variants put orange inbase08and pale blue inbase0B, soDangerwould have rendered orange andSuccessblue and a glance at the status table would have misread which slots were healthy. The suite now hue-checks both slots on every scheme so a theme added later cannot reintroduce it.