Skip to content
Changelog

What Claude Code Tray shipped.

Every entry in CHANGELOG.md, read at bbe2cd2 and dated by the commit that wrote it.

Atom feed
  1. The refusal check's skip is listed, with the sentence that says which machine is expected to produce it: elevation satisfies the emitted preflight on its own, so the branch cannot be reached at all - expected on CI, whose Windows runner is an administrator, and never on a developer's machine, where it means the suite ran from an elevated shell and the one assertion this project has that is weaker on CI went unchecked. Found before it reached CI rather than by a red build: an unlisted skip exits 1 by T218's rule, and the check had been written one task earlier with no entry. Both directions were driven - forcing the elevated branch shows the skip printed with its allowance and no UNEXPECTED line, and removing the entry while it fires reports 1 UNEXPECTED and exits 1. The mirror of the entry above it, which is why these carry a sentence about a machine rather than a restatement of the condition.
    T387 · c9e643e
  2. The refusal fires in a run now: the plan carries the registry key the emitted preflight reads, so the check composes a script that cannot pass its own preflight, runs it, and asserts what the branch actually promises - an untouched tree. Not an exit code, because a run that relinked three entries and then threw also exits non-zero. Its own fresh pair, since "nothing moved" cannot be read over directories an earlier half of the section already linked and unlinked. Skipped rather than failed when the process is elevated, the condition's other half, which a check must not grant itself. The premise was wrong and the CI log said so: the branch was believed covered by hosted runners having no Developer Mode, and they allow an unprivileged mklink too - so both machines took the linking path and the refusal fired nowhere. Neutralising the seam turns five assertions red and shows the counterfactual in full: done, 4 changes, the copies moved aside, the file replaced. Unlinking is unconditional after that run left a scratch tree behind.
    T386 · 037a572
  3. update-release ... upload now closes by naming the half it did not do, with the wingetcreate submit command and the SHA-matching reason it is safe: the manifests were bumped and committed, which looks exactly like they were sent, and the only thing that opens the PR to microsoft/winget-pkgs is a build.yml step that is workflow_dispatch only and so does not fire on a tag push. Found by publishing v1.7.2 and then asking where the winget PR was. The notice also states the trap, because reaching for the workflow afterwards is the obvious move and the wrong one: it recompiles the installer - Inno Setup stamps a time, so a different binary - and gh release upload --clobber swaps the asset out from under anybody who already has it, leaving the committed manifest describing a file the release no longer carries. A release has one binary. The release discipline carries the same two paragraphs, and the cmd escaping of the token placeholder was rendered rather than assumed.
    T385 · 4fe0c9a
  4. The paragraph closes on what a reader does with the list - if something you keep in one of those folders matters to you, that line is where you will see it - instead of on what the developers' own list had been missing and for how long. The sentence was off-register in a section where every other line says what the feature does, and its "four releases" was not even true: nothing shipped between those tasks. Left alone deliberately: the two older "used to" sentences elsewhere in the README, which describe a change in product behaviour a returning user would notice and are a defensible call, not the same thing as an omission in our own list. The reusable part is how it got there. Five tasks each appended a bolded paragraph to one section without re-reading the four above it; the result reads coherently, and the last one drifted because there was nothing left to say about the feature and something left to say about the work.
    T384 · b6e1b84
  5. The claim goes through Repo like the twenty-eight beside it, so an installed copy reports 0 failed and 30 skipped where it reported 1 failed and 29 - and a half-missing checkout stays red, which is Repo's needs argument and the distinction that made hand-rolling the guard look reasonable. Inside a checkout it is unchanged: 7 files, both the precondition and the result printed. The interesting half is why nobody could have seen it. build.yml publishes into the checkout and runs the exe from there, so RepoFile walks up, finds AGENTS.md, and the whole repository family runs; running it from the repo root does the same; and every developer command is inside the tree. It takes copying the single file somewhere else - which is exactly what the installer does - and nothing in the loop did that. So the release discipline now says to, with the command, because the gap was the loop rather than the guard: one publish a release already pays for, and the only path that exercises what an installed copy actually is.
    T383 · cd5c43f
  6. Acquire-Main hands the shared window back to the state --main opens in before the next case reads it, and asserts that it did - so -Case All reports DEGRADED for two machine-state preconditions where it reported FAILED with five assertions, and every case still passes alone on its own process. Resetting in one place rather than restoring in five is what stops the sixth case forgetting, which is how this happened: T195's comment said the three cases it gave one window to left it in a state the next would accept, and that was true of those three until Sessions drilled into a row and Link navigated to a settings panel. The window turned out to carry two independent pieces of state, and fixing only the shell destination left three of Names' assertions failing - the Settings page keeps its own sidebar panel, and a collapsed page's children are not in the automation tree at all, so the reset visits it blind and says so rather than leaving the gap for somebody to rediscover. Breaking the reset puts the named cause in front of every symptom it used to produce.
    T382 · 47ed971
  7. The suite is seven partial files of one class, the largest 2,204 lines where it was 9,330: Run and the vocabulary in the original file, then Usage (the quota arithmetic, pure over synthetic readings), Transcripts (the byte cursor, the grid, sessions - it builds a projects tree and walks it), Docs, Lang, Ui and Profiles. Verified three ways rather than by reading the diff: it compiled first try, the member multiset before and after is identical - 205 to 205, nothing lost - and the run reports the same 1,051 passed and 0 failed at every step. Moving it also found a latent defect, which is the argument for having done it: two source scans skip the suite because a scanner naming the string it looks for reports itself, both keyed on the filename SelfTestCli.cs, and one went red on the split. They key on the stem now, the rule the file map already uses to resolve a partial to its row, so a seventh file costs nothing - and the claim that the scan reaches the end of the largest file is over all seven rather than whichever happens to be biggest.
    T381 · 71b7bfd
  8. Six pairs of config dirs, one script composed per branch, and eight invariants run over all of them - each reported once, naming the plans that broke it. The coverage is asserted rather than hoped for: every verdict and per-entry state is enumerated from the enums, so a fifth verdict demands a fixture the day it is added, and a branch no fixture produced is named. Removing one fixture reports "Withheld/same" by name, which is the point. It found a defect on its first run, and not the one expected: the plural refusal fired on Bash(a) and Bash(b) - the user's own permission rules, echoed verbatim into the widening report, which are exactly that shape and are not ours to rewrite. So the script now marks a quoted rule as quoted, both scans skip those lines through the one constant that prints the marker, and a parenthesised plural in our own heading is still red. Reading better for a person too: the rules are visibly theirs. Seven junctions are unlinked before the temp tree goes, which the run before the fix reported it could not do.
    T380 · e537830
  9. The three shapes every entry's work is one of - MergeEntries, MergeLines and LinkEntry - are emitted once with their explanations and called per entry, so a plan with nine acting entries went from 485 lines and 311 of code to 290 and 156. The two sentences that cost a real run to learn survive once each rather than nine times: why mklink and not New-Item, and why the result is verified by a reparse-point attribute. What stays inline is the guard, two lines per entry, because folding it in would hide the most reassuring thing in the file - that a second run does nothing. Each function writes $script:Acted, since PowerShell reads an outer scope and writes a local copy, and that number is what the footer and the trap both report; breaking one of them to the bare name is what the new assertion caught. Verified by running it: nine links, nine originals kept, the union landed and readable through the link, 20 changes counted, and a second -Apply reporting none.
    T379 · 5bd72b0
  10. --selftest refuses a parenthesised plural in any of the five language files, so the pair convention is a rule rather than a cleanup that happens twice. Matched by shape and not by a list of three: a suffix of one to three letters glued to a word, because the four this repository actually used were four of a family and a check naming only them is one the fifth walks past; a parenthetical with a space in front of it is prose and is left alone. Swept over every language and not just en, which is the half nothing else could see - a Portuguese-only regra(s) is neither a missing key nor a missing hole, so the key-set and placeholder comparisons both pass on it, and breaking exactly that case is what proved the sweep earns its place. No allowlist: none of the 300-odd strings needs a literal one, and inventing the escape hatch for a case that has never occurred is how a check becomes something people route around - the day a string needs one, the red build is that conversation. AGENTS.md's string rule now names the pair and this refusal in the four lines it already had.
    T378 · 3da1bd6
  11. The script counts things by naming them: ProfileLink.Noun carries both words and Counted picks by the number, so a user reads "1 entry sits at the top level", "1 command on the side that keeps its files" and "1 project to copy over" instead of a parenthesised plural in a file they are about to run. Both halves are covered, and the second is the one a person actually sees: five of the counts are composed by PowerShell at run time, so the script now carries its own Count function taking both words, and the catalogue's unit is a Noun rather than a string. The verb and the pronoun go with the noun where they have to agree - "1 entry sits ... in none of the rows above ... nothing here has an opinion about it" - which is the part (s) could never have reached. Noun.Of is the +s case and sits at the declaration, where an author looking at Of("entry") can see it is wrong; entry/entries is written out. Two guards, and finding the second is the lesson: the whole-script scan ran over a plan whose two settings files were identical, so the line it would have caught was never emitted.
    T377 · ffc30b5
  12. L.N(stem, count, ...) picks <stem>.one or <stem>.many by the number and passes it as {0} through Nums, so the three counted strings on the linking card read "1 outro item fica ao lado destes ... continua separado" in Portuguese instead of "1 item(ns)" - agreement on the verb and the pronoun, which (s) could never carry. Two forms and not three: all five shipped languages have two, Slavic rules are real and not a thing here, and a plural library is a NuGet package the single-exe rule forbids. The convention turned out to predate the helper: three pairs were already written this way and three call sites had each hand-rolled the same == 1 conditional, one of them passing a raw int so its number went through the current culture instead of Nums. Two of those now go through L.N; the method note keeps its deferred fragment. --selftest sweeps for the pair and fails a stem with one half, or a .many with no {0} - the singular needs none, because "1 minute" is prose and several languages drop the digit. Both seen red before shipping.
    T376 · 6f0cafd
  13. --capture-settings takes card=<x:Name> and answers where that is: it scrolls the element's top into view, prints the offset it used, the element's height, the viewport's, and WHOLE or PARTIAL with the fraction that made it into the picture. The card this was built for reports PARTIAL 78% on the first run, which is what four of the five discarded captures were spent discovering. An unknown name writes no file and exits 1, listing the page's own named frames - template parts excluded by TemplatedParent rather than by a list of ids, after a first pass offered PART_ScrollContentPresenter, ContentBorder and four repeats of Desc. Any x:Name resolves and only those over 32dip are listed, because a section header is a 17dip TextBlock and is exactly what a caller wants at the top of a shot. scroll= and card= together are refused: two answers about one frame. The published shot now names LinkHeader instead of 1152, a number that meant the top of a card only until the card above it gained a line.
    T375 · 2cb7851
  14. Three rows the catalogue was missing - agents, commands and output-styles, Merge by entry name exactly like skills - and, more to the point, a plan that reports its own edge: every top-level entry in either config dir that no row covers is named in the script and counted on the page, with per-machine caches and snapshots counted rather than named, so an absence of opinion is never confused for one. Scratch is a short declared set for that reason: anything not certainly a cache belongs in the report where the reader decides, and a generous list would turn one silence into another. This script's own .pre-link- copies are excluded, or a second run reports the first. The rule it settles is why the two rows went missing for four tasks with nothing saying so - neither folder existed on the machine this was built against: a fixed catalogue must report its own edge, because a row can be added later and being told nothing is missing cannot be undone. Seen to fail by sweeping one side only, which the two-sided assertion caught.
    T374 · f286936
  15. SettingsUnion reads both sides and reports what the union would add, so the one decision the script refuses to make is made on evidence: how many entries arrive on each side, which permission list each lands in, and the first few by name. Three things it keeps apart, each of them a way a report about risk stops describing risk - the two directions, because a union adds to both; narrowing counted apart from granting, or entries arriving in deny read as new capability; and hooks per event with a count each side, because a hook is a command line that runs and no total answers that. Error means nothing was measured and never that nothing would change: a file only one profile has is the widest form of this decision rather than the emptiest, an unparseable one is an absence, and two files that agree say so. Read once in For and rendered by three surfaces, so no two of them can disagree about a figure somebody is about to act on. The verdict stays Withheld and no command is emitted. AccountFixture now writes two settings files that genuinely disagree, since two copies of {} render the one branch nobody needs a picture of.
    T373 · f554673
  16. --selftest now runs the script it emits, which is the one thing thirty-one assertions over its text could not: it writes the plan to a .ps1, launches Windows PowerShell 5.1 against a throwaway pair of config dirs, and asserts the resulting tree - four links resolving into the primary, four originals kept beside them under .pre-link-, the union landed before the link and readable through it, and a second -Apply a no-op rather than a relink. Split in two so it holds on any machine: a directories-only plan is all junctions and needs no privilege, and a plan carrying a file asserts the pair - it links, or the preflight refuses with nothing moved, which is the CI branch. Both halves were seen to fail: reintroducing the ResolveLinkTarget call turned seven assertions red where the text checks had all stayed green, and pointing the preflight at a registry key that does not exist exercised the refusal against an untouched tree. powershell.exe by absolute path, never pwsh: 7.x reproduces neither defect. Third claim kind - a program this program writes, executed.
    T372 · a7668c3
  17. Three surfaces now say what a shared transcripts folder costs, and the first is the only one that says it before the decision: the script's header names both profiles and states that Follow the active profile stops moving the icon between them, emitted only when projects is actually in the plan, so it disappears once that entry is already linked. The toggle's own description gains a sentence when every followable profile shares a tree - composed onto the sentence explaining the setting rather than replacing it, and a description rather than a disabled control, because a third profile arriving makes it work again with nothing changed. And --profiles no longer offers --link-profiles 0 1 for a pair it has just reported as sharing its transcripts. ProfileActivity.CanFollow is the reading all three go through, resolving links and walking nothing, since a settings page must not pay for a transcript sweep to write one sentence. Followable is part of the question rather than a filter on the answer, which is the assertion that went red when it was dropped.
    T371 · 23f6c64
  18. Settings > Claude Code > One setup across profiles picks which profile keeps its files and shows, per entry, what sharing it would mean - merged with the number the union would copy over, adopted whole, not offered, or never - with the four refusals dimmed and still listed, since an entry missing from the list reads as one nobody had an opinion about. Write script writes the PowerShell where the user points and reveals it; the app still has no write path into a config dir. Two surfaces the width fight decided: both blocks are label-above-control, because a SettingsRow's header is one unwrapped line and French clipped it mid-word, and the detail column never renders the catalogue's unit noun, because "union by session uuid" is half-translated in four languages while a count needs no noun. AccountFixture now builds the entries, so a published shot renders every branch but already-linked. -Case Link is the check a capture cannot be: both combos open seeded, so a page ignoring them photographs correctly - it swaps the sides and asserts the plan moved, then that one profile twice is refused with the button disabled.
    T370 · 7c64645
  19. The app composes the linking script and hands it over, never runs it: a fixed catalogue of what a config dir holds and what each entry needs before a link is safe - a union by session uuid, project or skill folder for the directories and by timestamped line for history.jsonl; plugins and CLAUDE.md adopted whole, because a per-entry merge leaves absolute installPaths behind and half a set of instructions is not instructions; settings.json emitted commented out, because its union widens the other account's permission allowlist and that is a decision; and .claude.json and .credentials.json named only to say they are never linked. The script prints its plan and needs -Apply to act, never elevates, never deletes - every original moves to <name>.pre-link-<stamp> - and refuses before the first move on a machine with no Developer Mode. It links with mklink and not New-Item, because PowerShell 5.1's New-Item does not pass the unprivileged-create flag and fails on the very machine the preflight has just cleared: found by running it, along with ResolveLinkTarget being absent from .NET Framework. 31 assertions hold the emitted text rather than the plan behind it, because the script is what leaves the app - no command in it so much as names the token.
    T367 · 3333336
  20. A three-layer burn-up band closes the hero and opens the footer: bars scaling from a floor while the row drifts, the even-pace line ruled inside the same svg so a crossing is true in every frame, and two bars in the icon's own red and clay. Upright in the footer, and standing on a baseline rule.
    T369 · 637144a
  21. The site is a Vite + React workspace under site/, prerendered to six routes each with a Markdown twin, a manifest, a generated sitemap and a rasterised social card; the theme follows the OS and no font is fetched; docs/ is gone and Pages publishes from Actions.
    T368 · c127144
  22. The Profile submenu says why the icon is not following. Where a junction puts two profiles behind one projects tree, auto-follow is on, both entries read the same and nothing moves - the state T365's refusal produces, and until now only --profiles explained it. A disabled line under the list names the directory the profiles share, and the entries stop carrying the active-now suffix: a reading taken from a directory another profile is also reading is not this profile being worked in, and that suffix beside a note saying nobody can be told apart was the half that was not true. It reports and stops there - the junction was made on purpose and the app has no write path into ~\.claude (I.4). Five languages, and both halves are asserted by Check-Interaction.ps1 -Case Menu: it reads the line back through UI Automation against the directory --profiles names, fails a note drawn where no two profiles resolve to one tree, and is Unchecked rather than quietly passed where no junction exists to render it. Seen in English and in pt-BR on a machine that has one.
    T366 · 7041f6e
  23. The icon stops being pushed off the profile you chose. Auto-follow's evidence is the newest projects\**\*.jsonl write under each config dir, and a junction can put two profiles behind one tree - here ~\.claude-pessoal\projects pointed at ~\.claude\projects, so both read the same last turn to the second. Worse than a tie: Read walks the profiles in order with the monitored one first, so a turn landing in the ~30ms between two walks made whichever was scanned *second* look newer, which is never the profile the icon is on - with nine sessions appending about once a second the icon left the chosen profile roughly hourly, and a later poll raced it back. ResolvedProjectsDir follows a link on projects or on the config dir above it, MarkShared takes every profile in a sharing group out of the running, and Pick then leaves the icon where the user put it. A margin on the comparison was the other route and was rejected: with a shared tree the newest write can be minutes old on one reading and current on the next, and both still describe one file, so no threshold supplies evidence nobody measured. --profiles now names the shared directory on each row and says the icon stays put, which is how this was diagnosed and is how it reads back. Twelve assertions in --selftest: eight over synthetic readings - the tie, the between-walks race, a third profile with its own tree still followed, two spellings of one path - and four over a junction mklink /J actually makes, skipped rather than failed where one cannot be created.
    T365 · 2430e7e
  24. Check-Interaction.ps1 now documents itself, and the check that reads it no longer reads only the other file. Its header opened with 'Three cases, runnable separately' and listed six, while the ValidateSet accepted seven: Sessions had no bullet anywhere in the comment-based help, which is what Get-Help prints and the first thing a reader of the file meets. That is T360's own defect one file over - the check exists because a case in the script and not the document is one nobody runs, it compares the ValidateSet against AGENTS.md in both directions, and it was green, because T359 updated AGENTS.md and left the script's own description behind. The count came out of the sentence rather than being corrected to seven: a figure in prose that nothing derives is wrong one case later, which is precisely how this arrived. Two assertions replace it - every name in the ValidateSet has a -Case bullet in the help, both directions, and the help spells out no count at all. Not how much prose each case gets, which is the line T360 already drew when it let Panes and Names share a bullet. Verified against history rather than against a synthetic edit: with the new assertions built and the script checked out as it stood at 4c370ab, the run goes red on both, naming Sessions as the missing bullet and quoting 'Three cases' back. The repaired script parses with no errors and Get-Help renders all seven.
    T364 · 2ae3904
  25. The mojibake check no longer calls correct arithmetic prose damage, and it now reads the five localization files. T362's predicate accepted any UTF-8 lead byte in C2..F4, which makes 0.31x-1.81x - a multiplication sign is D7, an en dash is 96 - decode to the Hebrew letter zayin. It had never fired because the one line in this repository carrying that pair, CHANGELOG.md:244, was outside the scope the check was given; the demonstration is that reverting only the lead guard and rebuilding turns the run red on two lines, one of them the comment added here to describe the problem. Narrowing to the four leads this project's characters actually use - C2, C3, E2 and F0 - costs no detection: replayed against both files as they stood at d83ddd3, before the repair, it still reports all 37 damaged lines, and over every text file in the tree it reports nothing. T362 also argued for scanning only src on the grounds that a false positive would live in the accented lang files; measured, that was wrong in both halves - zero hits across those five files and 3333 non-ASCII lines, while the one false positive sat in Markdown the exclusion never covered. So the five files join the scan, and a count assertion beside it fails rather than quietly narrowing if one of them stops being found. Verified by three injections: a mojibake'd pt-BR.json goes red naming it, a correct multiplication-sign span in src stays green, and moving es.json aside fails the count.
    T363 · 4c370ab
  26. The four cards under Projeto & links draw an octopus, a package, a bug and a star again, and a check now says why they cannot silently break twice. Reported from the running app; the bytes had been wrong in the file since T310, when an editor read SettingsPage.xaml as CP1252 and wrote it back - every non-ASCII character in it, and in StatisticsPage.xaml, took the same trip. 37 lines were repaired, five of them user-visible literals and the rest em-dashes and arrows in comments. Nothing was going to catch it: T259 asks whether a .ps1 carries a mark and T347 whether a source holds a NUL, both about a decoder failing loudly, and double-encoded text decodes perfectly - it is well-formed UTF-8 that spells the wrong character. The new assertion scans runs rather than lines, deliberately: asking whether a whole line round-trips would miss any line that also carries one correctly-encoded dash, since that dash maps to a byte no valid sequence can start. Its CP1252 table is written as escapes, because five of the thirty-two entries are invisible C1 controls and because a table spelled literally is one this check could not survive being applied to. Verified by injecting both shapes - the original defect and a mojibake octopus beside a good em dash - and confirming the run goes red naming line and intended character for each, then green over 96 files once restored; the repaired About page was captured in pt-BR and looked at. The warning glyph on the System page is the one repair no capture here can show: its card renders only when a profile authenticates off-subscription.
    T362 · 1e868c1
  27. The Sessions case finds its note through Label, like every other expected string in this file, instead of the English words it was typed with. It matched "list prices", so in the other four languages it found nothing and reported the note unreadable - and CI runs English only, so the case read as covered while four fifths of it did nothing. The string interpolates the date the rates were read, which is why it had been skipped: what identifies the note is the fixed part before the placeholder, and the date is what the assertion is about and was already checked as a pattern. Run in all five shipped languages it now passes in all five. Verified as a comparison and not as a coincidence: the script reads lang/<code>.json from disk while the app carries the embedded copy, so diverging the Spanish string without rebuilding makes the two disagree - and the assertion goes red, which is what proves it is reading the window rather than agreeing with itself.
    T361 · 7a2b262
  28. The rule AGENTS.md states about itself is now a check: the case set is read out of the script's ValidateSet and compared, both ways, against the names its usage line lists, and every one of them has to be introduced in bold in the prose below. A case in the script and not the document is one nobody runs, which is the defect T201 named and then left to prose; a name in the document and not the script sends a reader looking for a flag that is gone. What is deliberately not asserted is how much prose each case gets - Panes and Names share a bullet on purpose, and the file sits one line under its own ceiling, so a check demanding a paragraph each would one day demand a line the budget refuses. Verified by dropping Sessions from the usage line and un-bolding Switch, which produced one failure each naming exactly what had drifted. The evidence that this was needed is the round before it: T359 added the seventh case and the whole suite stayed green with the document still saying six, which it was only saved from by somebody reading the rule.
    T360 · 9575976
  29. The Sessions pane is driven: a case selects it, waits out the transcript scan, reads a row as a screen reader would, clicks it and asserts the call tree arrives in the accessibility tree, then opens the list-price note and asserts it carries the date the rates were read. That last one is the case for this file existing - the note lives in a Popup, its own top-level window, so no capture can photograph it and no screenshot ever showed whether it was readable at all. Measured on this machine: 124 conversations, a row exposing ten readable fields including the money column, and the click taking it from ten to fifty-eight with ten task lines. Two defects were injected and each was named by its own assertion. The case degraded on half its early runs and that was mine, not the machine: a SelectionItemPattern.Select that does not land leaves WPF never building the tab's content, and the case then blamed a forty-second scan for a pane it had never opened - so the selection is confirmed and retried, and the two waits poll for what they expect instead of sleeping a number somebody guessed. Nine runs clean after that. What it still does not drive is the sort headers, and AGENTS.md gained the seventh case at one line under its ceiling.
    T359 · e041c0e
  30. The panes case reads its tab set out of en.json instead of naming three keys, so it asserts four headers today and covers the next pane without an edit here. It had reported "all three tab headers read" against a four-tab window since T328, which is the failure mode the automation-id check already avoids by deriving its types - the Sessions pane, the one whose body a capture can show but whose click paths it cannot, had never been asked whether it was in the accessibility tree at all. Derived from the strings and not from the tree, deliberately: the tree is the thing under assertion, so an expected set read out of it could never notice a header that had gone missing. Verified by adding a fifth stats.tab.* string on disk and watching the case fail naming it, which shows both halves at once - a new tab is covered with no edit to the script, and a header that is not in the tree is a red run. What this does not do is drive the pane: the row click that unfolds a call tree, the two sort headers and the information popup a capture provably cannot see are still undriven, and that half stays open.
    T358 · 744f744
  31. Migrating the store is asked for rather than assumed: SetMonitored takes mayMigrate, false by default, and the two callers that pass true are the tray's own startup refresh and its profile switch. The gate could never have covered this - the profile is adopted above every flag Main dispatches, so Observing was still false when a dev flag moved the user's files, and ObservingTray could not see it because it fingerprints the tree after the write has landed. An argument rather than a second guess at what kind of process this is: at that line the dispatch has not happened, so asking "am I the tray" would need a list of every flag or args.Length > 0, and the latter is wrong for a tray started with --lang. Real users migrate at the same moment in practice, because RefreshWatched is the first statement of the tray's constructor, ahead of every poll and every store read. Asserted on the source and not at runtime, deliberately: on a machine that has already migrated there is nothing left to move, so a counter would read zero whether the argument were honoured or not - what is checkable is who asks, and exactly one call site does. Verified by putting the migration back on the startup path and watching the check name both lines.
    T357 · 1db3e28
  32. The promise is counted where it is made instead of inferred from a tree two processes share. StoreFile is now the one way this app mutates its own store — twelve files, thirty-eight call sites, effective mutations only, so creating a directory that exists and deleting a file that does not are not writes — and ObservingTray asserts the count is zero after driving every writer it knows. A diff could never answer that question: when the gate holds, every change the tree shows belongs to somebody else by definition, which is why four runs in one afternoon went red on the resident tray's poll and green on the next attempt. The tree comparison stays as corroboration for the write that bypasses the layer entirely, and stands down by name when another tray is alive, declared in AllowedSkips with the note that it must not fire on CI. Two assertions keep the counter honest: every store mutation goes through StoreFile, and every one of those methods still consults the gate. Verified by injecting both defects — a raw File.Delete, which the layer check named by line, and a writer with its gate removed, which the counter caught at one while the tree diff was skipped, which is the whole property. The CI branch was exercised too, by posing as a machine with no resident tray: 913 passed with the diff running.
    T356 · 6a7dcca
  33. The rule asks what the property actually is: not "which container is this in" but "is this element free to size to its own content", which a horizontal StackPanel does by measuring at infinite width and a non-Stretch HorizontalAlignment does by opting out of the parent's — so T338's case is now one instance rather than the whole rule, and T346's Grid-column heading is the other. The Width/MaxWidth exemption carries over unchanged, and vertical alignment is deliberately not asked about, since trimming is horizontal. Both halves were injected and both went red, which is how a second defect surfaced: Element() found its element by matching the line's text, and TextTrimming="CharacterEllipsis" sits alone on four identical lines of one file, so a scan of the fourth reconstructed the first and answered about a tag nobody was looking at. The widened check was green against the injected defect until that was fixed to take the index — and the same bug could have granted the Width exemption from an unrelated element.
    T355 · 56d577e
  34. The reset log is written where the store list said it belonged all along: LogResetEvent routes through ProfileStore.PathFor with the key it already had as its first argument, instead of composing %LocalAppData%\ClaudeTray itself. The flat log left behind is renamed once to reset-events.pre-profiles.log rather than merged or deleted — the user's call, and the honest one, since merging would rewrite somebody's log to save them opening a second file. Verified on this machine, though not on purpose: a run of the new binary performed the rename, and the file is there with its 1,793 bytes and its mtime intact and the flat name gone. Two shared caches were re-spelling the store root as well and now build it from Settings.DataDir, which leaves exactly one place that joins %LocalAppData% to the store folder — asserted, and confirmed by putting the second speller back and watching the check name its line. What the rename also exposed is not this task's: SetMonitored runs before any flag is dispatched, so Migrate touched the store from a --selftest run, long before the observing gate T344 put on it was thrown.
    T354 · 408f376
  35. The Sessions pane carries an x:Name and its index is asked of the TabControl that holds it, so the two constants that both said 3 are gone rather than reduced to one. The section proposed keeping the one whose doc already read "named once rather than counted at each call site"; deriving goes one step further and closes the failure the section argued from, because a pane inserted above Sessions now moves both call sites at once instead of moving one and leaving the other on whatever sits at 3. Verified positively — the captured pane is the Sessions pane with its first conversation opened — but not by injection: moving the name to another pane produces a byte-identical capture, because the wait and the open run at the wrong index and their effect persists into the later tab, so no off-screen capture can tell the two apart. The click path that jumps from a live spike to its conversation is the observable that would, and no interaction case drives it.
    T353 · 2d23cce
  36. Nine of the ten cited designs that shipped, so each now names the task whose changelog entry holds the claim — §XVI.2, §XVI.3 and §XVI.4 became T172, T173 and T174, §XVIII.1 and §XVIII.6 became T179 and T184 — which is how this repository refers to shipped work everywhere else, and is the correction the section asked for over repointing at a nearest surviving ancestor. §XVIII.9 was never a heading in any commit — §XVIII only ever carried .1 through .6 — so the two sentences leaning on it were corrected rather than repointed: §XXIII.4 states the rule it was citing, and §XXIII.5 cites §XXIII.4, which is the live section that actually declines. The tenth is not a citation to fix: STRATEGY.md's §S:V is a live anchor that roadkeep truncates to §S, filed upstream as RK1111 with the repro, so lint reports one finding on a known cause instead of nothing on a reworded sentence. Verified by putting one citation back and watching lint name it again.
    T352 · bb096aa
  37. ReleaseDate now comes from the commit being packaged rather than from the clock - the tag for that version, the HEAD behind it, then the clock, with the run naming which answered - so regenerating v1.6.2's manifests today reproduces the four published files byte for byte instead of redating them to the day the job ran.
    T351 · e3dab43
  38. Measured, and the pin holds through both compilers: two clean Release publishes on the pinned SDK produced one .exe (75,882,190 bytes, SHA-256 C0D8C245...969D), and two Inno Setup 6.7.3 compiles of that .exe produced one installer (70,589,578 bytes, SHA-256 5E7E1577...A41F) - so the InstallerSha256 update-winget.ps1 writes into every manifest is reproducible from a tag, and T349 is closed rather than assumed.
    T350 · 73858f2
  39. The SDK patch is named in global.json and nowhere else: all three setup-dotnet steps resolve through it, so a tag rebuilds to the same .exe, and --selftest reads the workflows and fails a fourth step that names its own version.
    T349 · 86d67c3
  40. A conversation's tokens now carry a list-price equivalent beside them, priced per model: the session cache splits the six token counts by the model that answered (schema 9), and a compiled-in rate card turns them into dollars — input and output at each model's own rate, a cache read at 0.1× that rate, a five-minute write at 1.25×, a one-hour write at 2×, and a write with no TTL stated at the dearer of the two. The column reads "at list prices" and never "cost", the rate card carries the date it was read so a stale one is visible rather than silent, and a model the table has no row for is reported as unpriced instead of folded in at a guess. The Sessions pane got its own ⓘ for the method rather than a paragraph in the footer note, which stood at 1,302 of its 1,350-character budget in French — 48 characters of room, measured before deciding rather than after the check refused it. The published shot could not show the feature until the fixture stopped inventing its model id: a rate card that refuses an unknown model refuses a fake one too.
    T346 · 3936972
  41. Measured before deciding, then re-measured on a quiet machine, and the honest answer is that the premise cannot be quoted as a figure at all. SLXXIII's four minutes is the Release build; the loop it describes was timed at 39s, 47s, 85s, 96s while another project compiled in parallel and at 329s, 367s and 413s an hour later with the machine idle - the same edit, the same tree. A v1.5.3 worktree with half the code took 409s, 456s and 742s for it, so the repository has not regressed and a stopwatch here measures the machine. What is stable is the no-op at 1-3s in either configuration, the suite at 8s, and a change to a document costing nothing at all, because the repo-reading checks open their files at run time. AGENTS.md now says which configuration a check is worked on in and refuses to quote a single rebuild figure, and the second target stays unbuilt.
    T345 · d2c7298
  42. Three more writers consult the gate - the reset log, the profile directory that every read used to create, and the migration that moved the user's files - and the list of call sites has an owner instead of a fourth fix: --selftest derives them from the source, failing any method that mutates a file under the store root without consulting ProfileStore.Observing unless a table beside the check names it and says what makes it safe, with an exemption that matches nothing as red as an ungated write.
    T344 · df3ebad
  43. Both waits now answer the same question and only the caller's reach differs: a transcript scan still showing its reading line at the 30-second deadline omits -sessions.png rather than photographing the placeholder, names the file it did not write on stderr and exits 1, while the three panes that did render are written and announced - so the wrote line lists the files that exist instead of a fixed four, and refresh buys no exemption here, because that modifier asks for a mid-recompute pace page and not for an unfinished scan of the transcripts.
    T343 · 8f0ed53
  44. AGENTS.md holds 323 lines and 25500 bytes, twelve lines of headroom at its prose width, so the next rule earns its place on the test the file states rather than on whatever happens to fit.
    T342 · 5f2ae79
  45. The four single-file properties are Release-only, so a no-op Debug build fell from 81-395s to 1.2s and bin\Debug from 155.6 MB to 1.9 MB, with the published .exe byte-identical either way.
    T341 · 67977a0
  46. SessionIndex.cs is a text file again, and a check keeps every source file that way. The separator it joins its cache key on was right as a value - a byte no path can contain is what an unambiguous key separator should be - and wrong as a way to write it: the source carried the literal byte instead of the C# escape that produces one, so the string is unchanged at run time and the file is no longer binary to git. What that byte cost was entirely outside the running program. Git classifies a file containing a NUL as binary, so it had no line diff, ever: T330's nine-line edit to it was reported as 1141 +++---, the sum of both versions, and a reviewer saw a file replaced rather than what changed in it. blame and add -p degrade the same way, and this repository's own grep answered binary file matches and nothing else, which cost two detours in the session that found it. It was the only one of the 92 source files under src that carried one. The check is byte-wise and deliberately not text, for the same reason the .ps1 encoding check reads bytes: the question is what a decoder does with the file, and reading it as text first answers with whichever decoder the check happened to use. Putting the byte back was watched fail before the green was trusted, and it named the file and the count. One artefact to expect once: this commit's own diff of that file still reads as a whole-file rewrite, because the blob it is compared against is the binary one; from the next change onward it diffs by line like its neighbours.
    T347 · 6497ac5
  47. Every state that has news now says it in every shipped language, and one assertion over the cross product holds that instead of three tests each written for one cell of it. The tooltip has 127 characters and Fit takes the first rung that fits or none of them, silently, and that silence had already cost the same thing three times: T222 lost the paying sentence in all five languages, T302 lost the spell line in both French billing states, T288 lost the blocked sentence in es and fr. Each was then locked shut by a test naming that one state and that one key, so the check that would have caught the next one never existed until the next one had happened. The check asks the composer rather than re-deriving the rule - Compose now reports which optional sentences no rung of it fitted, because walking the rungs in a test would be the first-that-fits walk written a second time, three lines from the method it duplicates, which is exactly the shape T307 removed from that file. On its first run it found seven live losses in seventy combinations, all of them the projection sentence and none of them in English: three in fr, two in es, one each in pt-BR and pt-PT. Six were closed by giving the projection a third rung with no word in it, following T302's argument exactly - a glyph and the figures costs the same in every language and cannot be spent away by a future translation, because there is nothing in it to translate. The seventh was the French billing state, where the compact rung was already shorter than the English one and still did not fit, so the rest of that composition is simply longer in French; it took a shorter compact form, and no allowance was declared for it. The at-limit branch deliberately gets no bare rung: "you are blocked" carries no number, and a lone glyph would be a claim rather than a shorthand, which is T288's own reasoning left standing. There was no need to stage a failure - the check was red on real state four times, at seven losses, then four, then one, and green only when the last was fixed - and the French tooltip that used to say nothing now reads "usage payant" at 106 of its 127 characters.
    T340 · 297ef7c
  48. roadkeep lint exits clean here for the first time, so a finding it reports from now on is a finding somebody has to look at rather than one more line in a wall of expected noise. Of the two this task owned, one had already closed itself: T301's why went over on a dep annotation it never typed and now measures exactly 200 of 200, because the line it depended on shipped and the annotation shortened again. The other was §I.8 spelling T85, an id no line carries. The honest reading is that T85 is not an illustration at all - gaps resolves it against the history, dropped in 31f9fa6 on 2026-08-02, with T101 dropped the same way in 6e2f55c - and this repository declares marker = false, so the ledger cannot spell either of them as a retired line. They are ids that are genuinely spent with nothing carrying them, which is exactly what reserved_ids is for, so both are declared there with the commits that spent them written beside the declaration. The section keeps its sentence: what T85 parked, and why the signal was looked for instead of waited on, is the part worth having. Commenting the declaration out was watched put the finding straight back, and next-id answers T348 over a floor that now skips them. Two things left as they are, deliberately. gaps also reports T325 as never carried anywhere in the history, and no section spells it, so nothing is claiming an id it does not have and there is nothing to declare. And the four section.ambiguous findings this task explicitly did not own are already gone - T315 took the anchors apart - which is why clean was reachable at all. Worth recording for whoever reads the CI next: roadkeep.yml runs the published action with no baseline, so it lints the standing backlog rather than what a branch added, and that job cannot have been passing while any of these findings stood.
    T339 · b2eb49c
  49. The row's title and prompt lines end in an ellipsis instead of being cut mid-word under the next column. TextTrimming was set on all three lines and did nothing, and the reason was two elements up rather than on the TextBlock: the chevron and the text stack were children of a horizontal StackPanel, which measures its children at infinite available width, so each line reported the width of its longest form, never discovered it was too wide, never trimmed, and the Grid column clipped whatever did not fit. The chevron and the stack are now the Auto and star columns of a Grid, so the text measures at the width it actually has. A fixed Width or MaxWidth was refused for this row on purpose: the window is resizable and the column is proportional, so a number chosen once is wrong at every other size, and wrong silently. It shipped with T334 and survived unseen because the published capture of that pane predates the line it broke, which is the argument for a scan rather than an eye - this defect is invisible in every screenshot that was not taken. So a check walks the markup tracking nesting depth, because the trimmed line sat two levels below the horizontal panel where a reviewer stops looking. On its first run it found two more instances that no task had asked about, and they turned out to be different from each other: the Context page's project title beside its grade chip is the same defect and is fixed here, while the Settings row it also flagged carries its own MaxWidth, which does constrain the measure and does make trimming fire - so the check learned that an explicit width is the exception, the weaker fix that is correct only where the width is not proportional. Putting the horizontal panel back was watched go red naming the file, and the capture shows the ellipsis landing where the text ends.
    T338 · 5203c2b
  50. A click on the live strip now names what is burning: it moves to the Sessions pane and opens the conversation that is running. T329 ended with the opposite sentence - a selected task cross-highlighting the strip - and this ships the same join in the direction that has data behind it. The measurement is why: LiveRate keeps a 300-second ring and the Sessions list opens on seven days, so a task lighting up the chart is correct and blank for every row except the one running right now, while a spike is by definition inside the strip's window and therefore always has a conversation to name. The rule is a pure function over the rows and a clock, so it can be asserted rather than clicked: the newest conversation inside the window wins, a conversation exactly at the far end of the ring is still inside it, an empty list is no conversation rather than a crash, and - the part that matters - a list with nothing recent names nothing at all. Removing the window was watched go red on exactly that assertion, reporting the conversation from an hour earlier, which is what a click on an empty strip would otherwise have opened. What is not verified here is the click itself. A capture cannot show a cursor, a tooltip or a tab switching, so the handler that wires the chart to the rule, moves to the Sessions tab and brings the row into view was compiled and read but never driven; only the rule it calls is held up by assertions. The strip carries a hand cursor and a tooltip in five languages saying what a click does, and the README and the published page say why the join runs this way rather than the other. The chart over a session's own timeline that would make T329's original sentence real is a different chart and a bigger task, and the reason it was not attempted is recorded rather than left to be re-derived.
    T337 · 482ac11
  51. The Sessions row is named by the title Claude Code generated for the conversation, and the search that concluded none existed looked for type:summary while the key is ai-title — present in 529 of this machine's 664 transcripts. The index takes the LAST one, because 322 of those 529 had the title rewritten as the conversation turned out to be about something else, so keeping the first would label a third of them by what they started as. The opening prompt stays: the label for the 135 transcripts carrying no title, and a dimmer third line under one, since a title and a prompt answer different questions. Both capped at 200 characters before storage, on that one screen and nowhere else.
    T336 · a09e15a
  52. A published shot of the Sessions pane now comes from an invented tree, and the shot in the repository has been retaken from it. --capture-stats renders that pane from the monitored profile, and since T334 the pane carries the opening prompt of every conversation, so the one command that makes a README screenshot made one holding this machine's real prompts - into a public repository, where a screenshot cannot be un-published. The fixture follows AccountFixture, which exists for the same reason on the System page: several conversations so the list is worth sorting, a fan-out under one of them so a row folds its agents in, both task kinds so the by-kind table has more than one row, and one prompt past the 200-character cap so the truncation is visible in the picture rather than taken on trust. It is reached as --stats sessions and --capture-stats <out> sessions, one row of the table both flags already read, because the fixture has to be the easy path rather than the careful one - nothing can check whether somebody committed a PNG anyway. Five assertions hold what is checkable: that the preview exists and is capturable, that it reads as a list, that a fan-out folds in, that the table has a command and a typed prompt to compare, that a truncated prompt is on screen, and that every row is invented. Two of them failed first and both were the fixture being wrong about the app rather than the other way round: a slash command is not text beginning with a slash but the harness's own command-name markup, so the by-kind table came back with no command row in it, and the stored prompt sits at the cap plus the mark that says it was cut. Looking at the capture then found what no assertion was pointed at: the shot came out reading ago. 11 - a Portuguese month in an English window - because two date formats went through the machine's culture instead of the UI language's. Both now pass L.DateCulture, one of them a line this task did not write, and a source scan over src/Ui keeps every month and weekday name that way; putting a bare format back was watched name the file and the call. The published PNG is retaken in English from the fixture, and the three other published shots it would otherwise have overwritten were restored rather than left in Portuguese.
    T335 · a4248d6
  53. The Sessions pane now says which kind of work ate the range, not just which repo: one row per task kind - slash commands, typed prompts, inherited turns - and one row per named command, with tasks, median, total and share. On this machine over a week the reading is the one the design was filed on: /loop, 89 tasks, 50% of the range, at a median of 567k tokens against 28k for a typed prompt, which is twenty times per task. A per-project breakdown cannot see that, because the expensive command and the cheap prompt land in the same repo. The table costs no transcript reads. The index stores the tasks each transcript was cut into, by the one cutter that owns the rules about what a person-ask is, and the fan-out is joined on at group time where every one of a session's files is already in hand - the same attachment rule the on-demand reader uses, replayed over stored rows rather than re-read files, so the two cannot disagree. Cache generation 8. The median rather than the mean, because one overnight run among short ones moves a mean and says nothing about a usual task; commands split by name rather than lumped, because the finding was about one command and not about automation in general; shares taken against what the table itself sums to, so they add to 100% and never to a figure that is not on screen. A command's name is a name, on the footing the model id and the tool names already have, and the prompt that followed it is not read. It reports and does not advise: the table says a command was half the range and never says run fewer of them. Three injections were watched go red - the mean, every command in one row, and a task that got no answer counted as a task - and the second of them found a defect in the check itself, which used First() on a row the defect deletes and so brought the whole run down instead of failing; it looks the row up now and fails with a message. Two more came from checks this repo already had: the key check caught a fragment concatenated inside L.T where a key belongs, and the doc-comment check caught a record inserted into the middle of another type's documentation. The capture found the last one: four unlabelled numeric columns nobody could read, so the table has a header row in all five languages.
    T333 · 41dadb9
  54. The Sessions pane now reports the heaviest five hours in the range on screen - when that stretch began and what went through it - beside the median active day's own busiest five hours. Every other five-hour window in this app is the one the API anchors, running from the reset the headers report, which is the right frame for how much is left and says nothing about a window that has already closed; this is the other one, swept out of the index, and it is the first reading here that looks backwards across days. Measured on the way in over 83,581 turns: the busiest five hours hold a median 72% of a day's work across 34 active days, so a daily total understates what the meter was pointed at. The resolution was the open question and it was settled by measurement rather than taste - per-minute buckets find 23,430,121 tokens against the exact per-turn 23,434,297, which is 0.018% low and right to the minute on when it started, while storing 12,147 buckets instead of 83,581 rows; the cheap hour-aligned alternative was 2.6% low with a start time up to an hour out, a caveat the reading would then have had to carry on screen. It is a measurement and never a prediction: nothing here knows the plan's allowance, so it says what the window held and stops, which is the same line the projection verdict already holds. Five assertions hold the sweep up and every one was watched go red - a closed frame instead of a half-open one, the frame's left edge reported instead of the first minute that carried work, and a median over calendar days instead of active ones - and one of them failed honestly on its first run, reporting 1000 where the fixture claimed 600, because two buckets I had not considered were a heavier frame than the one I meant; the arithmetic was the check's and the mistake was mine. Looking at the capture found what no assertion did: a seven-day range reported nine active days, because a conversation is listed when its last turn falls inside the range and a long one carries minutes from before it, so the sweep is clamped to the range and not merely to the sessions in it. The session cache moved to generation 7 for the series.
    T332 · 20a2271
  55. Every conversation now names the effort its calls ran at, and names it as a mix rather than a winner. The field was on 133,546 of this machine's 147,324 assistant lines and no reader here had ever opened it - measured on the way in, and the one detail about reading it that is not guessable is that it sits at the line's root rather than inside message beside the model id, which is where a reader would look first and find nothing. It is a flag and not content, so it stands on the same footing as the model id under the privacy constraint. What it buys is the answer to a question the list could not previously answer: effort is the largest lever on what a conversation costs and it buys more calls rather than longer answers, so two rows of the same length can differ several-fold with nothing on screen saying why. The mix is kept per call and never voted on, because the dear level is the minority here - 127,292 calls at high against 6,254 at xhigh - and a majority would round away exactly the part worth seeing; a session that ran at one level shows that level's name and one that ran at several shows each with its share. The ladder is spelled in one place because its order is not the alphabet: xhigh sits between high and max, and three of the five levels have never run on this machine, so a first sighting is rendered in its right place rather than blank, and a level the ladder does not name is printed as the transcript spelled it instead of being dropped. It carries no advice. The app cannot see whether an answer was right, and effort traded for a wrong answer is not a saving, so it reports what ran and stops - the same line the projection verdict already holds. Five assertions hold it up and four were watched go red first: reading the field from message instead of the root, sorting the ladder alphabetically, dropping a level the ladder does not name, and rendering a majority instead of the mix. The Sessions pane and the per-task drill-down both show it, the five language files carry the ladder, and the capture that proves it is in Portuguese. The session cache moved to generation 6, since no earlier entry recorded an effort at all.
    T331 · 09c8628
  56. A cache write is now read at the TTL the transcript stated it at: TokenBits carries CacheCreate1h and CacheCreate5m as a split of the total rather than additions to it, so Total counts the write once and no token figure in the app moved. Measured over this machine's transcripts on the first reading - 146,428 lines carrying a write, every one of them carrying the object beside the total, every one summing to it exactly, 804.5M tokens at the one-hour TTL against 37.7M at five minutes. That is 95.5% of the write at the rate that costs 2x a model's base input against the 1.25x the five-minute rate costs, which is the blend the old single number was hiding. The split is threaded through every fold the compiler could find rather than stopping at the parser - LiveRate's ring and window, SessionTasks' subtree and builder, SessionIndex's per-file accumulator and row - and the session cache's schema generation moved 4 to 5 with it, because the totals were already right and an untouched row would otherwise have gone on serving an answer with no split in it, which is the exact trap that file's own comment describes. An absent split is derived and reported rather than guessed: CacheCreateUnattributed is the remainder, because folding it into five minutes prices the most expensive component at the cheapest number and folding it into one hour does the reverse, both without saying so. Three synthetic line shapes and a fold hold it up, and all three were watched go red before being trusted - the parser ignoring the object, the subtree dropping the split, and the tempting wrong answer of treating a line with no object as all five-minute, which failed on exactly the one case it changes. The pricing half of the line this shipped from is T346: the rates are public and the arithmetic is over tokens already counted, but a session's row aggregates the models that answered, so per-model attribution comes before any figure with a dollar sign on it.
    T330 · 1947a40
  57. STRATEGY.md has an address namespace of its own and nothing was renumbered: one line, [refs] strategy = "S", and the file still opens at "## §I - What this is" because the prefix rides on the pointer and never on the heading. roadkeep lint went from 5 problems to 1 - the four section.ambiguous this repository has reported on every run since adoption are gone, and the one left is T339's body.promise - and commenting the line out brings all four straight back, which is how it was checked rather than assumed. anchors now reports two sets where there was one flat one: S:I through S:VII with their own free top-level §S:VIII, while §I and §I.1-§I.8 name IMPROVEMENTS alone, so the roadmap's ten non-goal citations are unambiguous exactly where they already stood and did not move. A namespaced role answers only prefixed addresses - section show I --role strategy is now refused with "no §I section in STRATEGY.md" - so the four citations that meant the strategy file took the prefix in the same commit: two in the roadmap-docs skill, one in §LXIX's body, one inside §S:VII itself. The route §L proposed was the other one, giving STRATEGY addresses this project has never declared, which anchors puts at §LXXIV: the strategy document would have opened at "§LXXIV - What this is". Upstream had already settled it - RK340 shipped this namespace and RK378, filed a day later for the renumbering route, was retired as superseded by it, and RK340's own test docstring is a description of this repository. One sentence of §L did not reproduce and is worth not repeating: section show I.5 printed the house constraints before this change as well as after, because that verb resolves by role. What was ambiguous was the address space and the gate reading it, not that lookup.
    T315 · dd542d9
  58. Every key the sources name is now held against the table that has to hold it, which is the one direction the parity check cannot look: it loads en and compares the other four to it, so en is the source of truth for what exists and every failure it can find is a translation gap - while an English gap has the fallback behind it and nothing else. Measured on the tree as it stands: 564 distinct keys, 488 named in code and 231 in markup. Injected before being trusted - a misspelled ternary branch in StatisticsPage and a misspelled {local:Loc} in ContextPage.xaml made it red and named both keys with the file each came from, and the five-language parity check stayed green through that same run, which is exactly the gap the task was filed on. The scan reads the whole first argument of L.T rather than the literal after the opening paren, because 67 of the 564 are written as a ternary branch or a switch arm - the legend's among them, which is the shape T313 introduced - and a regex anchored on the opening literal reaches only 497 of them; it stops at the first comma, so a format argument is not mistaken for a key. Two limits are numbers in its own summary rather than a later surprise: 7 of 447 calls build their key at run time and are invisible here, and it is a subset check in one direction only, since a key in en that no call site names is dead weight rather than a defect on screen. The walk itself is asserted against nine synthetic argument forms, each followed by a literal that must not be collected - a terminator read wrong runs past the closing paren and reports every string after it - and breaking the verbatim terminator, then the markup pattern, was watched go red before either was put back.
    T314 · 43d1e9d
  59. Every doc comment is checked against the member under it, in one file walk with no compiler: a run of /// lines may hold at most one summary, and must be followed by a declaration rather than by a blank line. Both directions of the same edit. It found seven live instances on its first run, all fixed here - MeasuredProfile had lost its summary to a record, one Discover overload carried the other one text, and four checks in --selftest had swapped theirs. Two of the seven were written in this session by the author of the check: T291 put a new member above Overflow and took its documentation, and T312 did the same to ObservingTray while adding this. That rate is the argument the design made, demonstrated on itself. One hole is named rather than closed: an UNDOCUMENTED member inserted directly under a comment adopts it silently - one summary, no blank line - and telling that apart needs a parser, which this deliberately does not use.
    T312 · 2de25fd
  60. One clay, declared once. Brand.cs holds the three bytes and one conversion per edge - GDI+ Color for the tray icon, a frozen WPF Brush for the charts and the markup through x:Static, the band alpha, and the hex string the toast palette takes - because a WinForms icon and a WPF chart can share a value and never a brush. The count in the design was eight and the reading found twelve, one of them added by T288 four days after the line was written, which is the drift arriving while the task about it sat open. The check moved onto the value rather than onto whichever surface happened to be assertable: it used to pin the toast palette to a literal hex, holding one of twelve still while eleven were free to move. Now it asserts both conversions against the bytes and fails a second spelling anywhere under src - and on its first run it reported itself, because the needles were literals; they come from Brand too now.
    T310 · 38c8e49
  61. HeaderProbe.Record consults the observing gate, so a second tray no longer appends to header-probe.jsonl - nor lets Trim rewrite it, which was the same promise broken twice. Written in the order the design demanded: the drive went into --selftest ObservingTray first and the run went red twice over, once on Record answering true and once on the store-tree comparison naming header-probe.jsonl as moved. That second failure is the evidence the check was already sound and only the drive was missing, which is what the doc comment on ProfileStore.Observing had said would happen - the call sites are a list with no owner, and the one it forgets is the one that keeps writing. Record answers false to an observing caller, which is what nothing was recorded already meant to every caller of it.
    T305 · 151691e
  62. The state T299 draws for can be looked at: --stats ghost-pieces hands the page a ghost built the wrong way round, bits from the clock and a curve that finishes at 80%, which is what a fold that lost a delta at a reset leaves behind. Every other Demo mode derives the spans from the curve precisely so the two cannot disagree, which is why the pinned mark and the floor sentence had been asserted in code and never seen. Looking answered the question the assertion could not: the clay mark reads clearly as a mark at the ceiling and its tie to the ghost line a centimetre below it is NOT visually obvious - the legend entry names it and the floor sentence is on the hover, so a still capture cannot carry the explanation. The preview is asserted rather than trusted: --selftest checks that this row still produces a shaded stretch above a curve short of the ceiling, since a row that quietly stopped would go on printing its description and capturing something else.
    T301 · 6be3790
  63. The in-app capture refuses rather than photographing the placeholder: --capture-stats waits for the report to arrive and, when it does not, writes no file, names why and exits 1. The settle was a fixed 2.5s against a pace computed on a task, which is how it came back holding the heading, the subtitle and the computing line while printing four PNG names - twice in five runs while T295 was being shipped, caught by looking at the picture. The wait is the same pump T328 gave the sessions pane, with the opposite answer at the deadline: there an in-progress list is still a picture of the pane, here the placeholder is the whole page. The refresh modifier is exempt, because snapshotting mid-recompute is exactly what it exists for (T118) - the one flag that asks for the in-progress page is the one this does not refuse. Verified by making the wait fail: no file was written, exit 1, and refresh still captured.
    T298 · 51814f7
  64. The fixtures that need a folded week the fold never wrote now write through the fold own writer: HourlyUsage.WriteAll is internal - the seam T189 opened FillCurve with - and SelfTestCli.WriteStore hands it a dictionary instead of composing the line a second time. What a fixture still decides is whether a day carries the x column, because absence is a reading here and one worth testing, and that choice now lives on the same HourlyDay.Over the real writer branches on. Locked shut in T293 idiom rather than left to care: --selftest reads every .cs under src and fails a second place that appends the day key. Verified by putting one back, which named SelfTestCli.cs beside HourlyUsage.cs and went red - the drift T287 caused could not have been noticed any other way, since a fixture missing a column means folded before it existed, which is legitimate.
    T297 · 1c94b43
  65. The path that changes which account the icon follows is driven and read back, by Check-Interaction.ps1 -Case Switch: it opens the Profile submenu, activates the entry for the other profile and asserts the check mark moved to the one picked and to no other. -Case Profiles walks a different control - that one only chooses whose numbers a page draws - so until now AdoptMonitored, which rewrites the setting, re-keys the stores and takes the incoming account token, ran under nothing. The case runs inside the T239 store comparison, which makes this the one place an observing tray writes nothing is asserted across the path that writes. Verified by removing the Enter keystroke: the run went red with the switch picked VILT Group and the check mark is on Pessoal. That injection also caught a defect in the check itself - the icon accessible name concatenates the name it was REGISTERED with, so a test for is there a reading yet matched connecting on every run and would have turned a wrong-profile failure into NOT CHECKED.
    T294 · d9e32e4
  66. Every toast card is now asked whether it drew a quota meter exactly where its row carries a quantity, by --check-toasts, which CI already runs once per language on every push. The row knows the answer because the row chose the reading, and the window is what answers: QuotaBlock.Visibility after layout, not ExtraUsageBar(0) being null a layer above any window. Verified by putting the defect back - a null coalesced away at the call site, which is one of the two ways T277 said it could return - and the run went red naming extra-bare and the 33.3px of bar it drew. The two profile cards have been bar-less since T174 and had never once been asked; they are two of the three rows that now say so.
    T291 · 37145cb
  67. A blocked account says so on whichever window is being looked at. T274 rescoped the state and fixed the billing half; this is the other half, where the at-limit sentence kept gating on the window the icon shows - so a session rejected at 102% behind a week at 47% drew the ordinary on-track projection while nothing would run. Scoped where the metric is the window that crossed, unscoped and figure-less where it is not, in two distinct rungs because the natural sentence costs 42 characters in es and 51 in fr where that reading leaves 37 and 33: the first draft rendered no sentence at all in both, which is as mute about being blocked as the wrong one was. The Statistics page carries the same fix and needed the check placed before the shape branch - the pane with room is by definition the pane that has an activity-aware projection, and the first capture read as closing the week around 65% under a green On track chip. That chip goes red and says Blocked, for T323 reason one window over.
    T288 · 58a8624
  68. Capture-Window.ps1 reads the accessibility tree of the window it is about to copy and refuses, naming the string it found and the key and language it came from, when the page is still showing its own loading text. The strings are resolved from all five lang files before the app is launched, so the check holds in whatever language the run resolves and a renamed key fails in under a second rather than matching nothing quietly; a key carrying a placeholder is refused for the same reason, since an exact-name read can never match one. Measured here: the Statistics page shows its computing line for about 1.1s and clears at 1.7s, either side of the 1500ms default, which is the coin-flip that produced a PNG of the spinner and called it Captured.
    T286 · 73586f6
  69. Claude Code stores no title - measured, zero summary lines across 664 transcripts, and what /resume shows as one is history.jsonl display field, the prompt as typed - so the only thing that makes a row recognisable is the text that opened it. §I.1 is amended rather than bent: one bounded exception, the opening prompt truncated to SessionIndex.PromptChars, drawn under the project it was typed in, on the Sessions list and nowhere else, and the constraint, the roadmap non-goal, STRATEGY, the README, the published page and the row own doc comment all moved with it before the column shipped. The cap is applied in the reader, so the cache holds no more than the row may show and what is not stored cannot leak from the store. TryParseSample is untouched and still returns no content: the one content reader in this app is a separate method named for what it does, so an audit has one place to look. Two things the first capture taught. Nine rows in ten read <command-message> or <ide_opened_file>, because a user line is often the harness writing about itself - a slash command is now reassembled from its name and arguments and reads as typed, and anything else opening with a tag is declined so the scan keeps looking. And the fix was invisible on every file that had not changed, because a size-plus-mtime key cannot notice that a field changed meaning; the cache carries a schema generation now, and bumping it is what retired the bad rows.
    T334 · 84cee8f
  70. A conversation now opens into the tasks that produced it, and under each the workflows and agents it fanned out to, every node carrying its own cost beside its subtree - which is the reading a total cannot give: a coordinator that spent 300k under a fleet that spent 900k is a different afternoon from one that spent it all itself. A task is cut at each person-ask, and telling one from the many user lines that are the harness writing about itself is the shared reader the list already uses, so the two cannot disagree. Turns predating the first ask are their own node, or a resumed conversation spend vanishes. Two things the transcripts taught rather than the design. A message typed while a turn is running is not a user line at all: Claude Code files it as an attachment of type queued_command, and a reader that only knew user lines cut this repository own 6-ask session into 1 task. And a task that got no answer has no clock, which made it match every agent and collect the whole fan-out of a task an hour earlier - it inherits the previous task last turn now, which puts it back in the sequence it is in. What is not built is the cross-highlight on the Throughput strip that the design ends with: that strip is a 300-second ring of live data and a conversation in this list is typically hours old, so the two overlap only for a task running right now. Filed rather than approximated.
    T329 · be75b48
  71. A fourth pane on the Statistics window lists the profile conversations, newest first: project, the clock of the last turn, how long it ran, turns and tokens, sortable by clock or by tokens and bounded to the last 7 days by default, with 30 days and everything-on-disk in the picker. A fan-out is one row and not twelve. The columns are the design and not an omission - no prompt, no title, no summary - because a list of conversations is exactly where a subject line is wanted, which makes this the surface that would erode the promise; the session id rides on the hover instead, where it is an identifier and what --resume takes. Three things were found by looking rather than by reasoning. The first capture photographed "Reading your transcripts..." and announced a PNG, which is T286 and T298 defect arriving in a third place, so the capture path now pumps until the scan lands. The second showed every heading one scrollbar left of its values, because this theme scrollbar is an overlay and takes no width from the content, so both the header and the list inset by SystemParameters.VerticalScrollBarWidth. The third read "1,2 h" in the English screenshot, which is exactly what T216 rule exists to stop, so the duration is a static formatter over Nums and the type is in the format sweep. Also: --capture-stats writes a fourth file now, and the loop that stops at the shorter of tabs and suffixes is why a new pane could have gone uncaptured in silence.
    T328 · 7f47cbd
  72. SessionIndex is one pass over a profile transcripts producing one row per conversation - slug and display name, first and last turn, calls, the four token classes, every model that answered, and how many transcripts the fan-out wrote - which is the unit every other reader here aggregates away. Parsing is TryParseSample, so the promise is the parser and not a second reading of a line; the model was already read there to test it against synthetic, and is now returned rather than re-opened. Two things were measured rather than assumed. Deduplication is per file and that is exact, not an approximation: across 664 transcripts, 144,930 assistant lines carried 80,836 distinct responses - 44% repeats, every one inside the file that wrote it, and not a single response appearing in two files - which is what lets the cache hold totals instead of an id per turn. And the cost is 4.5s cold over 1.2 GB against 117ms warm on T92 path-plus-size-plus-mtime key, in a shared session-index.json for the reason the hour sweep is shared. The scan also found a defect in T324 that no fixture could have: SafeWalk resolves a reparse point to its target, and this machine monitored profile keeps 55 junctions into another tree, so every transcript arrived under a root nobody passed and the whole tree read as one project called ? with each agent a session of its own. Locate now re-anchors on the root own leaf when the path escaped it, which repairs the live chart on such a profile as well.
    T327 · fb7be1b
  73. The count is now over conversations that had something *written* to them in the last two minutes, and both halves of that are the repair. A workflow of eleven agents is the one session that spawned it, because T324 made the parent session an identifier the path carries; and a tab waiting on a three-minute build or on the user typing is still working, because the transcript keeps taking user lines and tool results that no assistant turn covers. TranscriptTail already holds the FileInfo it is deciding whether to open, so the broader reading costs no second scan of the tree. What was refused is the third option: widening the 120 seconds until the two readings agreed, which would have counted a terminal left open all week - a transcript past the sweep freshness floor is never opened and never remembered, so no window a caller widens to can bring it back.
    T326 · 1764aa0
  74. A turn is filed under the first path segment beneath the tail root, so a workflow's agents now land on the repository they ran in rather than drawing a legend row called 315 - the digits ProjectSlug.Tail found after the last dash of wf_475bc61a-315 - on a colour slot taken from that repository. TailSample carries the three identifiers the path already spelled beside the project: the session an agent belongs to (the folder, not its own file stem), and the workflow and agent ids where there are any, which is what a fan-out needs to be counted once and drawn as a call tree. Nothing new is read from a line. The fixture is the layout that hid it: every tail check before this was one file in one folder, which is exactly the shape the old rule was right for.
    T324 · 279ffd7
  75. The chart's chip states the account's state before the window's pace. Both panes wear the clay Extra usage badge instead of the red At limit whenever their own readings say the spell is still running — chip and sentence reading one predicate, which also stops a stale overage figure from being read out in the present tense.
    T323 · 6b215f6
  76. The number's colour is now readable at the size the tray draws it, and it states the account rather than a menu pick. Both inks are resaturated after being chosen on an 8x sheet that now carries a real-16px band beside it, and orange means billing whichever window the figure is about — stopped keeps the window's own colour, because orange says paying.
    T322 · 365efaf
  77. The digits now say which window their number is about. White for the 5h session, a light yellow for the 7d week and orange for extra usage — the digits' fill being the one attribute on the tile that §XI.3's rule had never spent — judged on --render's new scope sheet at 16px over every bar colour, with --selftest holding the three apart and an unspelled scope falling back to cream.
    T321 · c7fc019
  78. The number on the icon now belongs to the account, not to a menu pick. Past the included quota it reports the window that crossed — 0% left in remaining mode, 100% used in the other — with the tooltip's caption, the projection and the near-limit flash all moved onto that same window, and Show-on-icon Extra exempt as the one metric the state exists for.
    T320 · b4749d4
  79. An open report follows the icon. The tray rebuilds the picker where it already detects a switch, and the page carries a hand pick across by store key rather than by position — the reorder IS the switch — so a work account's 86% can no longer be drawn under a personal account's name, and the account being worked in no longer reads as the empty one. --selftest gained 12 checks: the index decision is pure over keys, and the call site is asserted to sit in RefreshWatched above the return only a switch passes; both were confirmed to fail against a deliberately un-fixed build.
    T319 · aa36e99
  80. The overage axis keeps its scale and says so: in remaining mode both the rotated axis caption and the legend entry read "extra usage spent", chosen by one pair of readers so neither can word it alone, because the API sends a share of the allowance spent and never its size — there is no remaining figure to flip to.
    T318 · 6f83fd9
  81. The bridge is a required argument, so the fixed floor can no longer be reached by omitting it — a compile error where it was a silent comb instead of a stretch — and the suite now carries the difference the argument exists for: one stretch on the measured bridge, five on the constant. The one other default in the file was read and is legitimate.
    T317 · 556d3c6
  82. The ceiling edge is one fact read once: the mark offset and the swatch bar alignment both derive from CeilingAtTop, so remaining mode flips the legend with the chart instead of leaving a bar at the top of its box while the mark sat above the 0% line, and a check pins the two together rather than each alone.
    T316 · 4b28536
  83. Each legend entry has its own sentence in all five languages, naming its shape and where on the chart to find it, so the chart hit targets keep the deixis that only works under a cursor on the stretch — asserted by key, so borrowing one back is a red build, and the method now takes the element type that has the property instead of testing for it.
    T313 · 27699d3
  84. The clay entry describes only what its chart drew: swatch halves and tooltip come per render from OverQuotaMarks, so the ghost-only case that is most weeks names the ceiling mark alone, both tabs decide it through one reader, and a new ghost-over preview finally puts that state on screen.
    T311 · 7beee4f
  85. OverQuotaMarks is the one reader: the band loop and the ghost loop draw what it yields, filtered by shape so the z-order stays where the drawing is, and the legend counts the same list — with the fraction test as the only claim and the plot minimum applied at paint, where a mark the legend names is widened rather than dropped.
    T309 · adf8091
  86. Being past the included quota is a fact about the account, so both charts draw the band and the second axis and each legend names what its own chart carried: the 5-hour tab gets the two entries it never had, band-only where the week carries a mark it cannot draw, and both overage previews now put the session past its quota so either is lookable at.
    T308 · 11ae054
  87. The fitter is asked instead of predicted. The projection line is committed before the shed decision, so SpellSurvives measures the room actually left on the lines and the duplicated "first rung that fits" walk is gone — a third projection rung can no longer leave a confident wrong answer behind. Verified by diffing all fifteen billing compositions in five languages: byte-identical. The residual is stated rather than left: the projection now picks its rung before a shed frees room, so a translation whose full form straddled that room would keep the unscoped sentence.
    T307 · 53f3f08
  88. All ten billing states now say how long, and the measurement chose the rule. Where the duration would otherwise be silent, the bounded window the icon is not about — the one T215 already sheds, and the one the display setting did not choose — is given up for it. Both brakes are asserted: never where a rung already fitted, and never to buy the wordless form. en and fr each traded a percentage and got the full sentence; the two states that showed the glyph kept both readings.
    T306 · d8ba2b0
  89. One poll per account, and the gate lives on the account. A tick arriving mid-poll is skipped, a click is remembered and run when the poll lands, and a switch needs no rule at all — the incoming account arrives with the flag clear, because T293 replaces the whole object. So the refresh interval is a ceiling on API calls rather than an average, and one reading can no longer be appended twice seconds apart into the store that folds into a permanent aggregate.
    T304 · fbede8b
  90. A poll now attributes its reading to the account it started on. The account is captured before the fetch await and compared after it: on a mismatch the screen, projection, alarm and toasts are refused, and the reading is filed under its own key rather than dropped — it cost quota, and usage-history.jsonl cannot be rebuilt. RecordReading makes that key a parameter for all three callers, and --selftest fails both a missing guard and any line keyed on the live monitored profile.
    T303 · 02d9b8f
  91. A third rung, and it has no words in it. ← 3h 20m costs 8 characters in every language because there is nothing in it to translate, so fr now says how long in the state that is actually spending and es in the one that is not — five of five languages reach it somewhere, against three before. Two of ten states still drop it, both with under six characters free. The arrow is asserted never to be ⟳, which means time remaining.
    T302 · 67097c7
  92. The clay pair has one legend entry — past your plan quota — shown exactly when this week shaded stretch or last week mark at the ceiling is actually drawn, from the one predicate the marks are drawn under, with the week left to a hover the entry now makes findable.
    T300 · 66e6bb7
  93. A switch is one assignment now. Everything held about the followed account lives on MonitoredAccount, replaced where RefreshWatched already detects the icon changed hands — so a route that switches without a click gets it too. Building it found three fields nobody had listed, two of them live: a leftover error count drew a red icon on an account whose polls had never failed, and a leftover auth latch suppressed the sign-in prompt on the account you switched to in order to sign in. --selftest reads the source and fails a second assigner.
    T293 · 27520bc
  94. The outage threshold and the overage bridge are one derivation: FindGaps reads BridgeSeconds over its own points, so the median-and-floor rule has one name, one place a change lands, and no way for the two elements on one chart to part without a check noticing.
    T289 · d1dd212
  95. The tooltip now dates the spell, not just the state. While extra usage is paying it reads "paying since 3h 20m", measured from the first reading past the included quota — and says nothing at all when the crossing is not on file, because the store's own beginning is not an event. Last rung of the fitting ladder, so it is the first line dropped: at the 127-character cap pt-BR gets it in both billing states, en and es in one, fr in neither.
    T280 · 31d7376
  96. The stretch is marked where the header's claim lives, at 100%, and a week whose line reads lower under it is told to be a floor — two honest halves of one fold, no longer one contradictory picture.
    T299 · 6f6da75
  97. Coverage and the spell are counted for every reading the fold sees, its oldest included, so an hour observed once reads as observed rather than unknown; only spend still waits for a pair.
    T296 · a693f0f
  98. The ghost carries the stretches that week was over and the chart draws them in clay on the ghost's own line, clear of the ceiling they always sit on; a week with no column says unknown.
    T295 · 4360863
  99. The extra-usage alarm is rebuilt from the incoming account's own history when the icon changes hands, and it carries the profile its readings belong to — so a reading from another account re-baselines quietly instead of being compared into a start nobody witnessed.
    T292 · 29ad4be
  100. The extra-usage transition takes the reading that preceded the process as an input instead of fetching one the poll had already overwritten, so a crossing caught on the first poll after launch is announced, and a spell the tray only walked in on still is not.
    T290 · 113500d
  101. The fold keeps a bit an hour for the spell, written even where nothing was over so its absence still means a day folded before it existed, and a pruned week is no longer a ceiling.
    T287 · 4671b0b
  102. Every check that reads this repository sources now takes them through one lexical reader that removes comments and keeps literals, which caught the flag catalogue counting a flag named only in a paragraph, and its own answer is held against synthetic source in every literal form here.
    T285 · b50dbd6
  103. Every header name ApiClient spells is held against the list the parser derives by enumerating itself, both directions, so a read placed behind a branch fails the build instead of being reported unread by the instrument built to prove otherwise.
    T284 · 23a072a
  104. The console goes to UTF-8 once where the flags are dispatched, so every read-out prints its dashes and its section pointers, and a source scan fails the build if a second file sets it.
    T283 · eb909f8
  105. The readership block and the --all sentence are returned as values built from one derived count, so --selftest holds each summary against the marked lines it is a summary of rather than against the classifier both came from.
    T282 · 25f4c0d
  106. The tray now flashes on the threshold the API named, not the one this repository picked. 5h-surpassed-threshold reaches a field, and QuotaStates.Warns prefers it wherever it is sent — falling back to the 0.90 constant on the four readings in six that carry none.
    T281 · f6affdc
  107. Measured before deciding, and the premise did not survive. tengu_usage_overage_included_models is a cached GrowthBook flag reading ["Fable", "Fable 5"]; 8 days of transcripts here are 34,595 Opus turns, 195 Sonnet, 0 Fable. Now a non-goal.
    T279 · 7d56423
  108. Every rate-limit header --probe prints is marked read or unread from the parser enumerating itself, each profile opens with the count, and --selftest holds the two against each other.
    T278 · 02492c7
  109. The extra-usage card draws its bar only from a figure the reading carries, so a spell reported with none arrives as the sentence and its reset rather than behind a meter showing a full allowance.
    T277 · 0d219ca
  110. The extra-usage notification arms on the overage-in-use header as well as on the figure, so an account whose overage utilization reads zero straight through the crossing is still told, once per spell, that money has started.
    T276 · 952d3e7
  111. A spell with no figure now leaves a record. usage-history.jsonl carries ix beside ux, so the week shades the stretch the API said the account was over — behind the usage line, with no second axis, because no header states an amount.
    T275 · 22947fd
  112. Whether the account is paying no longer depends on a menu click. The verdict resolves from the worst bounded window while the caption stays with the metric, so a rejected session behind a week at 47% says so — unscoped, since 47% is not what crossed.
    T274 · dd06731
  113. The affirmative nothing could believe now reaches the screen. overage-in-use is parsed into UsageData and ranked in Resolve and the poll's idle above the local flag and below a measured refusal, so an account past 100% reads as billing on the API's own statement.
    T273 · 7dcc03a
  114. Moved the Empty declaration above the All array that reads it, which took the build from three CS8602 warnings to zero with --tooltip all byte-identical across the move, and put the invariant in a doc comment: static initialisers run in textual order, so the three rows survived only by deferral inside a Func. Gated it in check.yml with -warnaserror on both build steps rather than TreatWarningsAsErrors in the csproj, so a future SDK analyser bills a merge decision instead of whoever is mid-task, and asserted the flag in --selftest because dropping one word from a YAML file nothing reads is exactly how the protection would go missing while the run stayed green. Proved both directions: the check goes red naming the ungated step, and the old order rebuilt with the flag fails as error CS8602 rather than warning.
    T272 · 3f43fa1
  115. The read-out named a heaviest and a lightest hour that cannot be found in the only picture it draws. Measured, not eyeballed: the grid Sunday row was 13 light cells then a run of mid cells, with 16:00 and 22:00 both the same glyph, while the line beneath called one the heaviest Sunday hour (2.00x) and the other the lightest (0.79x). Not a wrong number - the grid draws P, the share of weeks an hour was active at all, and the line draws I, how hard it is worked when it is. The control that proves the gap is real is the busiest-bucket line, which IS on the grid axis and does point at its darkest cell (Monday 22:00, the only full-black glyph). Of the three shapes the task allowed - a second grid, a legend, or one clause - this is the cheapest that closes it: the line now opens with intensity, then names the axis it is not. Extracted as the pure ActivityCli.IntensityLine so a check reads both branches without a profile store; three assertions, and the axis one was seen to fail with the clause removed.
    T271 · 74a3814
  116. The two build failures that are not your code are named where the command is, both measured: MSB1011 is a killed build leaving a temp project, and a missing generated file is the WPF markup pass racing the IDE design-time builds over one obj directory - three dotnet worker nodes and two IDE language servers were sharing it, which is what T258 and T269 had guessed was a parallel session. Both mitigations were tried and rejected by measurement: Directory.Build.rsp is not honoured by dotnet build and MSBUILDDISABLENODEREUSE leaves nodes alive, and node reuse buys no measurable time here anyway - 9s either way. So the deliverable is the sentence plus a non-goal, and never a retry loop.
    T270 · 1a6cd39
  117. A completed build now sweeps the temp projects the WPF SDK leaves behind, so the folder stops drifting into the state where the documented dotnet build refuses with MSB1011 - one had sat in this root for four hours, gitignored and therefore never mentioned. Age-guarded on purpose: parallel sessions share this tree, a temp project younger than ten minutes may belong to a build running now, and an unguarded delete would have made the missing-generated-file failures worse rather than better. What the sweep cannot do is recover from the state, because MSB1011 is raised before the project is loaded, so that one line went to AGENTS.md instead.
    T269 · 2d3ef2b
  118. The word Measured names one quantity in that document again: the summary row that meant sources the scan kept is now two rows, Files walked and Sources kept, which also gives the capped-walk caveat above the table the two figures that show its shape - 1021 against 881 on the machine that produced the task. The scan header says files walked, so both artifacts of one scan share a vocabulary. The check names the word rather than sweeping for collisions, because the first draft of it fired on Findings, which is the same quantity at two scopes and misleads nobody - same word for the same thing is fine, and no regex tells that from the defect.
    T268 · 2c7cf9d
  119. The caveat travels with the text now: a capped scan says inside the prompt that the list is a floor, and tells the reader to say it is partial before starting - an instruction rather than a note, because the rest of that section instructs and the reader is something that will act. The report already carried the same fact inside itself; the prompt printed it in the scan header, above the hash, which is outside what a person copies. Absent when the scan is whole, since a caveat that is always there is one nobody reads. Checked in both directions, alongside the two promises the prompt exists under.
    T267 · af08bf3
  120. The publish refuses when something is running from the folder it would overwrite, naming the pid and the path, instead of failing inside the SDK bundler with UnauthorizedAccessException and four frames of MSBuild - which is what the release path did for a state that is not a bug, since whoever installed by building runs the tray from exactly that folder. Matched on the path and not the process name, so a Debug tray does not block a Release publish. It never stops that process: the tray is the user own running app, and a build script that kills applications is worse than one that asks. Demonstrated on the real tray, both directions.
    T266 · e6a4165
  121. The question IconRenderer names as the only one worth asking of this colour now has a sheet to ask it with: billing_sheet.png pairs stopped against paying at 16, 20 and 32, adjacent because the claim is about two fills a glance must not confuse, magnified 8x with the real pixels and drawn on both a light and a dark strip. Its own file rather than a row on the accent sheet, since one image carrying two claims cannot say which of them failed. Answered while building it: at 16px the red and the clay are tellable apart and neither reads as an alarm.
    T265 · 6f8da13
  122. The overage preview produces the state it exists for again, and the screenshot it publishes can be re-taken. The seam was gated on the curve being empty, and it is never empty on a machine whose history holds a measured zero for overage - which is most of them - so the demo declined, the series scaled to nothing, and the flag drew an ordinary week with no second axis. Gated on there being nothing to scale instead, and the zeros are replaced rather than drawn under the climb. Checked on the seam over the three states it has to tell apart, the one every machine is in included.
    T264 · 4f9298b
  123. A date takes the order of its fields from the culture and not from a pattern in the source, through Dates - the other half of the rule Nums states for numbers. The Statistics axis put the month first in French, where all four non-English cultures put the day first, and its day dividers read 3/8 for 3 August to an American whose short date is month-first, on the chart the published screenshots are of. Checked over every shipped language against that culture own MonthDayPattern and ShortDatePattern, with the precondition that they disagree, so the sweep is not vacuous. The three published stats captures are regenerated.
    T263 · 491af9a
  124. A page or destination name this build does not have prints the ones it does and exits 1, at all four entry points that read one - so a capture no longer lands a picture of General under the name the caller chose while saying wrote about it, and the file is not created at all. Naming nothing still opens the default, because that is a request and not a typo. The names are declared once each and the checks sweep those declarations in both directions, plus the other end nobody had asserted: every page name is a tag the sidebar actually carries, or it would select nothing.
    T262 · db2883f
  125. A read-out that could not start now says so and exits 1, through one helper that prints the sentence and sets the code together, so no call site can do one without the other - four of them printed error: and returned 0, and --root exists for the fixture loop, where the caller is a script. How wide the rule goes was measured rather than argued: a scan has no partial form, since its Error is only ever set on one that returned immediately, while a scan that finished with holes carries Truncated and no error - and UsageEvidence, the one type where partial is representable, says so with Complete beside its Error and stays a note. Both halves are asserted, boundary included.
    T261 · 5c005a7
  126. A code this build does not ship is refused with the catalogue and exit 1, and so is the flag with no code after it - the same rule and the same shape as --sample-env ten lines below it, which has always had it. It is the flag the i18n verification loop rests on, so falling through to the machine language meant a capture of the wrong one under the name the caller gave, with nothing saying so. Matched exactly, so EN and pt_BR are refusals; auto is accepted, because asking for the OS language out loud is a request. The setting is untouched: an unknown saved preference still falls back, which is what IsValidPreference guards.
    T260 · e2de5a6
  127. The five scripts carrying prose now carry a UTF-8 mark, so PowerShell 5.1 reads them as written instead of in the ANSI code page - where a dash inside a string arrives as a smart quote that closes it and the script does not parse at all. The rule is a check rather than a memory, and it is stated as the property and not as the fix: ASCII, or marked, which accepts a pure-ASCII script needing nothing and rejects the state that produced the defect - five of six files, every one with prose in it and none of them marked.
    T259 · 7a99468
  128. Every process the script starts goes through one launcher that remembers it, and a finally over the whole run stops whatever a case did not - named, because a tray left behind is a lock on the next build and that is a reading about the run. And the consequence the leak had is now its own reading: an -Exe older than the newest source under src is Unchecked, so a run that would report on the build BEFORE the edit comes out DEGRADED saying which file and when, instead of passing about code that is not in the tree. That is word for word what T236 refuses by flag, arrived at by accident.
    T258 · df434c9
  129. Every card is asked whether it fits, by --check-toasts, and check.yml runs it in all five languages on every push - twenty seconds for forty questions that until now only a hand-typed loop asked. It reads a laid-out window rather than a picture, so there is no settle wait, and no card is ever activated, which keeps it out of the way of the interaction check beside it. One command per language because the display language is fixed per process; a command that spawned itself five times was the alternative.
    T257 · 186a0ca
  130. A busy desktop is now a precondition the Keyboard case asks about and reports as Unchecked, so the run comes out DEGRADED naming the window and pid that holds the foreground instead of FAILED naming a control it could not reach. Deliberately not a retry: a case that passes on the second attempt cannot tell a busy desktop from a broken build. A SetFocus that throws mid-run asks the same question, and owning the foreground while still being refused stays a failure. What CI sees is unmeasured and said so: a hosted runner has no competing foreground, so this either changes nothing there or reports a condition that was being suffered quietly.
    T256 · 9f77718
  131. The door retire cannot open here is written down before somebody walks into it, with the sentence that replaces it: a task decided against ships with a why that opens on the decision, carries the measurement that settled it, and names the non-goal the conclusion moved to - T253 as the worked example. Two claims about the other doors were corrected while in there: record amend does rewrite an entry where it stands, which the skill had said was impossible, and it is for a sentence wrong about the repository rather than a second draft.
    T255 · f4043ce
  132. The roadkeep gate is green on a bare checkout again. T151's sentence named the built .exe while explaining why the job builds rather than publishes, bin/ is git-ignored and that workflow checks out without building, so lint had been exiting 1 on every push and 0 for anyone who had just compiled. Corrected through record amend - the ledger's own door for a word, which leaves the entry where it stands - keeping every fact and losing one path a reader can reconstruct from the sentence. The asymmetry that hid it is upstream as RK213: local lint still passes where CI would fail, because bin/ is there.
    T254 · d2f959e
  133. Measured before deciding, and the premise did not survive: a clean Release build is 10s against Debug's 11s, so what a prose commit pays is the runner and the restore, not the configuration. The one saving left - moving the document checks out of the binary - buys that 10s with a second reader of the very thing they exist to compare, so it is a non-goal now rather than an idea somebody re-files. The guard keeps running on every commit, which is what T252 bought and this measurement says is affordable.
    T253 · 17eb4e9
  134. The guard has no paths-ignore at all now. Twice in two tasks a hand-kept list of what nothing reads was wrong - T249 removed markdown and kept docs on the same argument, T250 shipped the check that reads docs next - and being wrong there does not fail a run, it stops one from happening, which is the quietest way this repository loses a check. What the list bought was one build on a commit touching LICENSE alone; what those minutes should cost instead is T253.
    T252 · 943fbb5
  135. The precondition takes the paths the check reads, so the seven guards that opened each body are one assertion that names which path is missing instead of seven that pass every run and have never been seen to fail. A checkout lacking a governed file is red, not a skip - that is a repository somebody is halfway through changing, which is not the same fact as an installed copy having none, and standing a check down for it loses coverage to a state nobody meant to leave behind.
    T251 · d9cbf71
  136. The last surface pair on trust is checked: every image the README or the published page points at exists, every screenshot in docs is shown by one of them - a shot nobody displays is one nobody re-takes when the window moves - and the winget id a user types is spelled one way across both files and the manifest that publishes it. The wording is deliberately left alone: the site is marketing copy and the README is not, so a check demanding they agree would fail on every rewrite and be turned off.
    T250 · dc5bff2
  137. The fourth file describing the suite says what the other three now say, and the sentence that had become a rule went with it: check.yml ignored every markdown-only commit on the argument that prose cannot move arithmetic, which stopped being true the day four checks began reading CHANGELOG.md, ROADMAP.md, the source tree and two skills - so the commits those checks exist to catch were the ones being waved through. What is left ignored is read by nothing: the published site and the licence.
    T249 · f694195
  138. The flag scan reads a comparison, not a literal: a paragraph may quote a flag all it likes, and what counts is a literal standing beside Contains, IndexOf, Equals, an equality operator or an is pattern. That retires the // stripper, which was right about comments and approximate about strings - a // inside a literal ended the line and took any real flag after it, silently, which is a check that asserts less and stays green. Narrowing costs a pattern of shapes, and what happens when a flag is read a way it does not know is now stated and demonstrated: the flag drops out of accepted and the opposite assertion fails on it by name, so an unlearned shape is a red build naming which flag. Both failure modes are asserted against assembled synthetic source, because a written-out sample would be a flag this very scan accepts.
    T248 · a7e83c9
  139. The seven checks that read the repository share one named precondition now: it hands over the root nothing had, and the allowance travels with the skip instead of being remembered in a second list where a name typed differently is a skip nobody can act on. Five of the seven sentences are gone and a missing file in a checkout is a red precondition rather than a skip, so the quiet-run report went from ten names to three. The allowance is for an installed copy only - under CI the checkout is here, so a repository skip there is five checks lost in a green run, and it exits 1.
    T247 · 1d7cfe1
  140. The routing sentence in the file loaded every turn now names the two kinds of claim the suite holds - an invariant over synthetic inputs, and a document against the thing it documents - so a consistency rule has somewhere to be filed instead of becoming a paragraph in the one file that is zero-sum. The section is renamed off arithmetic, the synthetic-inputs promise says written rather than touched now that four checks read repository files, and the trim of one discovery story paid for all but 143 bytes of it.
    T246 · 980e672
  141. What --probe was asked to do is a value now - ProbePlan, decided from the arguments and nothing else - so both rules the flag carries are asserted instead of being held up by whoever last ran it: --recorded takes no reading, and --live --recorded refuses without doing either half. Swept over all eight combinations of the three switches rather than tabulated, in both directions, plus six spellings and casings of --recorded that must none of them ask for a call. Where it stops is stated: the plan is pure, so the run honouring it rests on the single if in front of the one call site, which is why the decision came out of the method at all.
    T245 · 18264f1
  142. A row's (active - see ROADMAP) is now derived rather than remembered: it must be present exactly when the roadmap carries a task line under that block's heading, which was wrong on six of thirty-six - five blocks marked with nothing open, and Localization open with no mark. Required in both directions, because unlike the entries under a heading the marker is not frozen prose about what shipped but a live reading of another file, and every drifted row could be corrected here.
    T244 · 64610aa
  143. Every "--x" literal the sources compare against is now held to the catalogue and back, so the half of the flag surface with no variant table in code is checked rather than remembered - --raw, unlisted since --live shipped, is the one it found. The second direction is read at declaration position only, left of the # inside a fence: over the whole document claude auth status --json reads as this app promising a flag it has never had, and a check that cries wolf is a check somebody turns off.
    T243 · bb19273
  144. The map is asserted against the tree it copies: every source file is named by a row or by its stem's, so one class in six files stays one row, and every path a row names is on disk. Eight files had no row and one sat under the wrong folder heading; AGENTS.md's folder table is held to the same both-ways rule, because a new subsystem folder is exactly when placement matters.
    T242 · d6574dd
  145. The check fingerprints the whole of %LocalAppData%\ClaudeTray around the trays it launches, so a writer nobody listed is caught by the directory rather than by a list of methods - and because the check's own writes always show, no change observed is a sound pass even beside a resident tray.
    T241 · 17cea4a
  146. The check always launches with --second-tray, so whether a tray was resident a second ago decides nothing: the flag is free when the mutex is, the icon tag and the observer promise are unconditional, and the race that blamed the menu for a tray that never started is gone with the branch.
    T240 · fd95a56
  147. A tray launched with --second-tray reads normally and persists nothing - no store append, no cache, no settings save, and its environment reconcile lands on a sampled copy - asserted over the whole of %LocalAppData%\ClaudeTray.
    T239 · 77537f7
  148. The Menu case sweeps the sampled environments itself, one tray per mode, so T172's mark and its outside-the-profiles line are asserted on a run with no flag typed instead of waiting for somebody to remember one.
    T238 · c02189e
  149. --second-tray runs a tray beside the resident one, so the menu case launches the build under -Exe instead of refusing - the check covering the menu is runnable on the machine it is developed on, without quitting the app.
    T237 · d83174a
  150. -UseRunning compares the resident binary to -Exe by version and, when those agree, by build time, so driving a tray that is not the build under test comes out DEGRADED instead of clean.
    T236 · 6c583dd
  151. The panel list is derived from lang\en.json by name rather than from whichever table -Lang happened to load, and a list that resolves to nothing fails the run instead of being walked in zero steps - 6 panels and 23 row controls now checked under a non-English -Lang where 0 were.
    T235 · be9576a
  152. The Profile submenu says out loud how far a pick reaches: the tooltip carrying T171 scope sentence and T172 running-session line reaches no accessibility property at all, so an accessible object supplies it as Help - the one property a client can read, measured after AccessibleDescription turned out to reach only LegacyIAccessible, which the managed client cannot name a pattern for. That object costs the frameworks TogglePattern, so a position is announced as a word in front of the sentence, which makes OFF a reading for the first time rather than the same silence as not-a-switch. Check-Interaction asserts all three, and the role-based repair was tried first and rejected: it re-types the entry as a check box and the entries vanish from the menu tree.
    T234 · 4ca5965
  153. The left-click and the submenu expand each retry a bounded three times and name the attempt that worked, so a gesture the shell dropped no longer wears the wording of the defect the check exists to catch - and a gesture that stays broken still goes red.
    T233 · 9878296
  154. --profiles discovers through the same registered-profile list the tray does, so the name it prints for a profile is the name every menu and picker shows.
    T232 · d90fd54
  155. --sample-env puts the process on a CLAUDE_CONFIG_DIR named rather than written, so the menu's environment mark, the outside-the-profiles line and the whole read-back path can be looked at and asserted without touching the machine's own registry.
    T231 · f7ff918
  156. The Menu case walks the Profile submenu and compares every entry to what --profiles says it should be: one entry per profile by name, the check mark on the icon's profile and no other, the pinned marker paired with its undo, and both toggles reading the position the app reports.
    T230 · 4694aac
  157. Save merges by which write is newer instead of by ownership: any field the page left at the value it opened with takes the live one, so a toggle flipped in the tray menu with Settings open is no longer written back over - and with it T171 machine-wide environment write, which reconciles off the value Save had been restoring. It removes a category rather than adding one: T162 attribute could not mark FollowActiveProfile or SyncEnvironmentProfile, which have a control on the page and a toggle in the menu, and a field with no control on any page simply never differs from the snapshot, so nothing has to be declared and T126 and T155 missing-from-a-list defect has no list left to be missing from.
    T229 · 7d3fae4
  158. A capture now refuses rather than writing a picture of a clipped card: after the settle timer each text block is asked where it landed relative to the card, and the flag exits 1 naming the block and the rectangle. It found one on its first run - the French extra-usage caption, 7.6px under the bottom edge and clipped away by the inner grid, so the PNG showed nothing wrong - and every card now grows for its own wording, with a MinHeight of the designed height so all seven published screenshots come back at exactly the pixels they already are. Forty combinations, eight cards in five languages, fit.
    T228 · c594f26
  159. Measured before fixing, and the premise did not survive: naming a colour font changes nothing, because WPF text and GDI+ both draw Segoe UI Emoji monochrome base layer - no coloured pixel in six glyphs under either, and a real captured card with the font named is the same black popper. What shipped is the half that is true: BodyFont and EmojiFont are fields the window binds with x:Static, so the glyph comes from a font carrying every codepoint the cards use instead of whatever font linking reached first, and --selftest asks the typeface for each one so a card that would draw a tofu box is a red build. Colour is now a non-goal with the measurement behind it.
    T227 · 644c8fa
  160. The probe grew the half that skips the call: --recorded reads the log only, so reviewing what was captured costs nothing against the account being measured - not the default, because a stale log read as current is how a wrong reading gets quoted, and passing it with --live refuses instead of picking one. Two things that sat with it are said out loud now: --all names the single profile its live call refreshes beside each column's last recorded time, and a kept reading points at --recorded, since the summaries printed above it were derived before it joined the log.
    T226 · ae96ad6
  161. The project count counts directories, which is what the row claims: the keys are folded with the comparison the filesystem uses, so one folder Claude Code recorded as d:/Git/x and D:/Git/x is one project instead of two - on the file that produced the task, 39 keys and 37 folders. The probe list is folded by the same rule, so its cap is a budget of folders. Case only, deliberately: a junction or a substituted drive reaches the same directory and nothing measured says which.
    T225 · d6885c0
  162. A measured refusal now outranks the local flag: overage-status rejected, or the overage-disabled-reason header that only exists because something said no, resolves to Stopped where hasExtraUsageEnabled alone said Billing, and the poll idles instead of spending a call an account cannot use. The System page states the cause in five languages, and week=refused is the fixture branch no local file can produce. An account observed spending still outranks both.
    T224 · 9ac3ed8
  163. The ledger's index of blocks is asserted against the ledger's own headings, both directions and the anchor with them, so a block that ships under a letter the index never learned is a red build - the two that had drifted, P and AG, are listed, and AG's five shipped tasks are findable from the table again. The roadmap-docs theme table is checked one way only, since it lists the themes meant to be reused rather than every letter that ever shipped.
    T223 · 7312616
  164. The overage reading carries T182's sentence as its own label, so the one state where extra usage is paying now says so in all five languages instead of being priced out by the very reading it describes - and billing before a cent is spent, which has no reading to merge into, keeps the standalone sentence.
    T222 · 608baf1
  165. The capture asks whether anything covers the region it is about to copy, by walking the Z order above the window and intersecting each frame, so a foreign window between the old sample points is named and the file is not written.
    T221 · 5aa8c41
  166. The Menu case matches the tray's labels against the language that tray actually resolved when -UseRunning attached to it, and refuses a -Lang that was typed alongside instead of quietly checking a different one.
    T220 · 54131e5
  167. AGENTS.md now states the test it is kept by - a rule earns its bytes if getting it wrong has produced a defect and --selftest cannot assert it instead - and the per-file map left on that test for the new file-map skill, reference material consulted rather than read. 40,981 bytes against a 41,000 ceiling became 23,359 against 24,200, so the next rule is written instead of negotiated against whichever paragraph was least recently needed.
    T219 · 7938dc6
  168. An unexpected skip is now a red run: --selftest carries a named list of the skips it is allowed to have, each with what allowing it costs, so losing a check in CI costs the exit code instead of nothing - and a skip that stops happening is reported too, which a count could never say.
    T218 · 6288f32
  169. A capture can be asked to prove it contains the surface it was taken for: the preview prints what it drew and where, -Expect makes Capture-Window.ps1 demand that line and check the rectangle is inside the copy, and neither a surface that went unreported nor one that landed outside the window writes a file - while holding preview popups open moved from one page's call site to the host every preview shares.
    T217 · 276e2e3
  170. Numbers are invariant everywhere and dates follow the display language, stated once in Nums and named into L.DateCulture; the split between chart and prose was refused because one context row carries a size beside a token estimate, so wherever its line fell it cut through a sentence. The format sweep now runs every static formatter on all six surfaces, not twelve on one page.
    T216 · f50931e
  171. The tooltip's 127-character budget is asserted across all five languages and every state - 50 combinations, plus a profile label of any length - after measuring found French with an overage line composing 129 characters, which Windows was truncating from the end where the reading's own time sits; an over-budget reading now sheds the bounded window the icon is not about, and the metric labels stopped being frozen in whichever language the process started in.
    T215 · 6d7243b
  172. The tooltip has a read-out and a render: --tooltip prints its composed text for ten states with each one's character count against the 127 cap, in any language, and --capture-tooltip draws the same text into the shell's card - so docs/tooltip.png stopped being a hand-taken photograph of one machine's notification area and became a picture anybody can regenerate, which is why it had gone a release out of date saying a line the app no longer produces.
    T214 · e121efa
  173. The tooltip's last line is about the window the icon is about: the metric's own status, under a label that names it, so watching the week no longer reports the session — and --selftest holds the pairing on a static, the tray around it never having been constructible headlessly.
    T213 · 4c38629
  174. The live reading is kept: --probe records what it fetched, against the profile whose log it printed and under that profile's key, so a reading survives the console scrolling — and an empty log now says nothing has polled this profile rather than blaming a change that has not come.
    T212 · 5005fc2
  175. A header's absence is now a reading the instrument states: every name says on how many readings it arrived, and --probe --all ends with which names each account is sent and which it never is. That comparison measured the premise false — the fallback pair is not on every response. fallback-percentage is 0.5 on two accounts in opposite overage states, while unified-fallback is sent only where the overage window exists, so what it announces is conditional and still unexplained by anything on screen.
    T211 · 36507b1
  176. --probe opens each profile's log with every value its categorical headers have taken, counted and dated, so the second reading the three unparsed headers are blocked on announces itself instead of being eyeballed across five hundred readings of fourteen headers.
    T210 · ed32691
  177. Every surface that shows the extra figure names what it is a percentage of — your extra-usage allowance, a window with its own limit and its own reset — in all five languages, and the chart hover no longer points at a method-note paragraph that was never written. The weekly chart past the quota is published rather than described.
    T209 · 690b1a3
  178. The overage window's own status reaches code: a stated 'allowed' is a third reason the poll never idles, while the display goes on inferring — one reading cannot tell a permission apart from a value every response carries, so only the direction that costs an API call may believe it.
    T208 · 0db80a6
  179. The preview and capture surface has 25 assertions of its own: every variant and modifier the two tables declare resolves, every refusal still refuses instead of rendering the default, and the dev-flags catalogue is checked against the tables in both directions - so writing a flag down is now a build rule rather than a convention.
    T207 · b473d42
  180. The screen copy takes the window's painted frame from DWM instead of GetWindowRect, so a capture no longer carries the strip of desktop the invisible resize border and drop shadow put down its left, right and bottom edges - measured at 200% as L11 T0 R11 B11, printed on every run, and the mid-edge pixels it exposes are now checked for ownership too.
    T206 · 68720f7
  181. A capture of the System information page is refused without --sample and writes no file, so the one page whose content is an account can no longer be published off a real holder and organization by forgetting a flag - while interactive --settings System still shows the real login, which is the only place it belongs.
    T205 · daeb7a4
  182. Every settings row is a named group in the automation tree, so a screen reader reads label-to-control pairs instead of a flat run of controls, and the sweep now checks each control against its OWN row's header on all five panels that carry one - a rule handing out the wrong header used to pass it.
    T204 · fa241a7
  183. The ids the interaction check drives are now read out of the script itself, in the three shapes that carry a literal one, so a lookup added later is covered with no edit here - it already found three the hand-written list had missed - and the six assembled per pane at runtime stay explicit and say which kind they are.
    T203 · c40e875
  184. A tray already running makes the menu case Unchecked and the whole run DEGRADED, so exit 1 from a full run means a defect again on the one machine the loop is actually run from.
    T202 · e032dee
  185. AGENTS.md's interaction section lists all five cases and says which need no second profile, so Panes and Names are findable without reading the script — and the per-case prose that had drifted is now the script header's job, paid for out of the same line budget.
    T201 · 924804a
  186. week= beside --sample chooses which stored week the personal fixture profile gets, so all three extra-usage branches render — including the measured-zero 'Enabled — not in use' that nothing could produce — and a --sample that cannot be honoured now stops the run instead of quietly rendering this machine's real account.
    T200 · bf5471b
  187. The screen-copy capture asserts that the handle, and the pixels sampled across its rectangle, belong to the process it launched — retrying the raise and refusing while another ClaudeTray window is open — and its success line names the window title and pid, so a wrong capture reports itself instead of being caught by a reader.
    T199 · ce0d311
  188. The toast variants are one table both flags read, so an unknown name prints the catalogue instead of the early weekly reset, and --capture-toast requires its output path — with --render and --context-report moved under git-ignored docs\_preview\, no capture flag writes a file the caller never named.
    T198 · d970c73
  189. The Statistics picker is filled from whatever the command is about: --sample from AccountFixture, the default variant from this machine, and a synthetic variant on its own gets no picker — so a published fixture week carries no real account name, and profile= is refused where there is nothing to walk.
    T197 · 9cd9637
  190. -Case Names now opens every panel the page declares - the list derived from the settings.nav.* labels, so a panel added later is swept with no edit - and reads every control the row rule is responsible for, identified by control type plus TogglePattern so a Slider's repeat buttons and a ScrollBar's page buttons are excluded by what they are rather than by a list of ids. Coverage went from 3 controls on 1 panel to 23 across 5, plus a four-control witness for the branch that must not fire. Both branches were seen to fail.
    T196 · 4bc6af8
  191. Panes, Profiles and Names now take the --main window from Acquire-Main and hand it back with Release-Main, so a full run launches it once instead of three times: measured 3 launches and 21s before, 1 launch and ~15s after. A case run alone still owns its process, because -Case Names being ten seconds is part of its value; sharing is opted into per invocation and the window is closed before the menu case, which refuses to run while any ClaudeTray is alive.
    T195 · b719909
  192. check.yml gained an interaction job running -Case Keyboard on windows-latest, bounded at 10 minutes, with exit 2 (DEGRADED) treated as a failure because nobody is watching a CI run. Measured on a hosted runner before being committed to: synthesised mouse and keyboard input does reach a WPF window under the WinForms pump there, all four assertions ran and passed in ~11s, and that answer is written into the job's own comment.
    T194 · 243b51a
  193. Combo-Select now anchors the keyboard fallback on Home/End so it reaches its target in one selection change, which makes T166's switch-back timing observable on either route — verified with the UIA route forced dead. The eight downgrades that were Info lines are now Unchecked, counted and named, and the summary has three outcomes: exit 0 every assertion ran and passed, 2 DEGRADED, 1 failed.
    T193 · 5541333
  194. Each named control now says which page it is on (StatsProfileCombo / CcProfileCombo / SysProfileCombo), and --selftest reflects over every IComponentConnector type to assert no x:Name is carried by two controls, plus that the fifteen ids Check-Interaction.ps1 drives still exist — the rename found the StatusText lookup behind T166's assertion, which a missing control would have made pass by seeing nothing.
    T192 · f0096ce
  195. A reading from a real account settles it: overage sends the same triple as 5h and 7d — utilization, reset, status — so the figure is a window's own utilization, and its reset falls on a calendar-month boundary while the other two roll. The probe now keys on every categorical header rather than 5h's status alone.
    T181 · 3d16d24
  196. A machine-wide profile write now confirms itself, on the read-back rather than on the click: a slate card naming the profile, the directory and the one claim the non-goal permits - sessions from now on, never one already running. No confetti and, deliberately, no quota bar: animating one account's remaining quota into another's would say switch accounts because this one has room. The failure arm names what CLAUDE_CONFIG_DIR still reads.
    T174 · 2df56b4
  197. A queued environment write now reports how it went: the value is read back off the registry once the queue reaches it, and the outcome - landed, did not land, or threw with the reason - is raised per write and kept as the last one, so a write that was merely accepted is no longer indistinguishable from one that took. The click still returns at once and nothing shows a dialog.
    T173 · e48ffcf
  198. The Profile submenu now reads CLAUDE_CONFIG_DIR back and marks the profile it names when that is not the one the icon draws - or names the folder outright when it covers no registered profile - so whose numbers these are and which profile a new session starts in stop being one answer. Agreement passes without ceremony; the divergence is the signal.
    T172 · 2415f16
  199. Picking a profile now states how far the pick reaches - the tray only, or the whole Windows user environment - and the machine-wide switch sits in the Profile submenu itself, so the half that did not happen is legible at the moment of the choice instead of buried in Settings.
    T171 · 1c7f99c
  200. The method note reads as three headed sections — the numbers and the curve, the weekly projection, the Throughput charts — in the order the window shows them, so the paragraph about this machine's own evidence is found rather than read past, and a character budget asserted in all five languages keeps the next paragraph a decision.
    T170 · 7cab286
  201. The two probe assertions over the slug encoding now run wherever --selftest runs: the temp root is resolved to the filesystem's own spelling, so a CI runner's 8.3 alias no longer skips them forever, and every skip that does happen is named beside the counts where the exit code is read.
    T169 · d30dbe4
  202. Which paragraphs the method note yields is a pure function of the report now, so Block Z's rules are twelve assertions instead of two screenshots — including the T163 one whose failure is a plausible sentence: telling somebody to keep the tray running another week when their projection is unshaped because they are at the limit.
    T168 · 81eefde
  203. Every number the Statistics page writes now goes through one invariant formatter — the method note's five interpolations included — and --selftest runs each of the page's twelve formatters under two cultures, so a sixth that reads the OS is a red build rather than something to be noticed in a screenshot.
    T167 · b8b6589
  204. The map is complete again and the ceiling came down with it: the flag catalogue — reference material, consulted rather than read — is the dev-flags skill, complete and current, and the room that freed went to the five files Blocks AE and AF had left unmapped (HeaderProbe, QuotaState, ProbeCli, StatsPreviews, OutFile) rather than back into prose. AGENTS.md sits at 360 lines against a budget lowered from 400/42 KB to 370/41 KB.
    T191 · 9d3ce12
  205. AccountFixture writes the personal profile a week of stored readings — through UsageHistory.Append itself, not a private copy of the line format — so --settings System --sample renders the branch that reports the allowance in use, 'Enabled — in use now (42%)', checked at that width in English and in French. The series runs from readings with no overage figure through one measured zero to a figure that climbs, so it is also the store's absent-is-not-zero rule made visible.
    T190 · ed44ff0
  206. The chart's series-building is asserted where it is made: --selftest hands FillCurve a list of readings and a window and checks that an absent overage figure is skipped while a measured zero is kept, that ExtraMax is the peak of the kept points, which of the two shaping paths ran, and that a hole in the readings becomes exactly one gap span — none of it needing a profile, a chart or a screen.
    T189 · 2cc9202
  207. The early-reset toast is rose and the extra-usage one keeps the clay: clay now means one thing across the icon's bar, the chart's second axis and the notifications — past the included quota — and the good news that quota came back early no longer wears the colour of having started to pay. The palette is a table with no fallback arm, so a new toast cannot inherit another's colour, and --selftest asserts every theme's row is its own.
    T188 · 9ebca18
  208. Every capture flag creates the directory it was told to write into: the three SaveSnapshot bodies, the icon writers and --social share one OutFile.Create, so a capture into a new folder writes the file instead of throwing DirectoryNotFoundException from a timer tick after the window has already rendered — and --selftest holds it.
    T187 · 7c3b9a6
  209. --stats <variant> and --capture-stats <out> <variant> read one table of previews (src/Cli/StatsPreviews.cs), so a preview added is a preview both flags gain, and a name neither knows is refused with the catalog printed instead of rendering the default sample as if it were the thing asked for.
    T186 · b11e93d
  210. --selftest compares the five string tables against en: every key en has reaches all five files, no key exists in one file alone, and every {0}-style hole survives translation — and each failure names the offending keys rather than counting them.
    T185 · 20a849a
  211. The first reading that finds the account spending past its included quota raises one clay toast, on by default and off in Settings, that states the fact and the reset and names no account, no alternative and no action.
    T184 · 7e5011a
  212. The weekly chart draws the overage series in clay on a right-hand scale of its own, with the legend and the projection sentence saying so, rather than laying an uncapped figure over the axis that means 'of your included quota'.
    T183 · 2dad2b0
  213. Reaching 100% is drawn as one of three things: the bar goes clay rather than the alarming red when extra usage is paying, the tooltip says which kind of maxed, and the System row says whether the allowance is in use or merely enabled.
    T182 · 862bf94
  214. The idle is gated on its own premise: an account that can still spend past its included quota keeps polling, so the stretch that costs money is sampled instead of slept through.
    T180 · 680122e
  215. UsageSample and PaceSnapshot carry four numbers and none is overage, so every task downstream waits on a store with no column the figure could go in.
    T179 · 689d9bc
  216. A timed-out read says what it last saw: 'working' when the status line was up and names it with the poll count, 'blank' when nothing was ever in the tree, and either way it prints the control view — so the dump that diagnosed T165 is the check's own output instead of the next throwaway script.
    T178 · 7761b3a
  217. The round trip asserts T166's timing where it is made: on the way back to a profile seen seconds ago the status line must never be observed at all, watched continuously from the instant the selection changes rather than sampled once the switch has settled — and stated as a sighting, not a deadline, so a slow machine cannot make it red for a correct reason.
    T177 · def928e
  218. The three tab headers and the selected pane's body — a used %, a reset caption, a live headline — are asserted by a case of their own that needs no second profile, so the property T165 restored is checked on the single-profile machines where -Case Profiles only ever skipped, and that skip now costs the switch alone.
    T176 · 3971be6
  219. The Statistics picker announces the label beside it and the method-note button announces the note's own title, and every settings row hands its header to a trailing control that would otherwise announce nothing — so the two controls the T165 tree dump found unnamed, and the thirty-odd rows carrying the same shape, all read.
    T175 · 874068b
  220. Coming back to a profile is instant, and says when it was measured. T164 clears the last-rendered pace on the way out of a profile — right, since leaving it up puts the previous account's curves under this account's name — and the cost was that coming back blanked the whole pane, Throughput tab included, for the length of a transcript scan: the exact blink T118 removed for the poll refresh, reintroduced by the switch. The window now keeps each profile's own last report, keyed by profile, and puts it straight back while the recompute runs. Measured on the same machine, same two profiles: without it, switching back showed *"Computing your consumption pace…"* at 162 ms and the panes returned after 961 ms; with it the status line is never shown at all and the panes are readable after 12 ms. Three design questions §XIV.2 posed, answered: what invalidates an entry — nothing has to, because every successful render overwrites its profile's entry and every switch starts a recompute that lands on top within a second, so an entry is at worst superseded, never wrong; is the footer enough — yes, and by construction, since Render stamps *"Atualizado …"* from the report's own ComputedLocal, so a re-shown view states when it was measured instead of implying now; how old is too old — one minute, because the two countdown-shaped figures ("até o reset", "meta de ritmo equilibrado agora") drift against a frozen report and Dur renders them to the minute, which is also the footer's own resolution and the same tolerance T165's check already treats as one reading. Past that the switch blanks as before. What comes back is the pace panes only: the entry is read after the live tail has been reset, never restoring the drawn live history, which appends by construction (T119) and is the third defect T164 fixed. A cache keyed by profile cannot show one account under another's name — and the path it changes is precisely the one T165 asserts, so -Case Profiles re-run against it still walks *Pessoal → Trabalho → Pessoal* and reads 28% both times. Closes Block AA; --selftest unaffected at 134/134, no new strings, no UI change beyond the blink being gone.
    T166 · 7c580b2
  221. The picker is driven, and the round trip is asserted. Every --capture-stats renders *a* profile and the check is a person comparing two PNGs, which is structurally incapable of seeing what T164 fixed: all three defects need a second switch, two of them only when it goes back where it started. Check-Interaction.ps1 gains a third case — `-Case Profiles` — that launches --main (the real shell, the tray's WinForms pump, the real discovered profiles, and a synthetic monitored reading so index 0 is a fixed number), walks the real ComboBox 0 → 1 → 0 through UI Automation, and reads the report back at each stop: the used %, the reset caption and the live headline. It asserts the picker actually moved and came back (a check that compares a profile with itself passes everything), that the middle stop's report is not the first one's, that the used % is identical across the round trip, that the reset caption is within one minute of itself — a caption counting down in real time must not make a red build, but an hour of drift is another profile's window — and that the live headline is never *"unavailable"* at a settled stop, the one defect a percentage cannot see. Reading neither the panes nor the status line is a FAIL, and one profile registered is a stated skip that says what went unchecked (T161's rule). Confirmed to fail first, twice: restoring _snapshot from itself reproduces the field report exactly (picker on *Pessoal*, numbers 0% / 31m — the other account's), and dropping the StartLive() leaves the headline reading *"Live throughput unavailable"* at two of the three stops. Writing it found a defect nothing else could have: the segmented-tab ControlTemplate never named its content host, and WPF finds it by GetTemplateChild("PART_SelectedContentHost") — so the entire selected pane, every number, legend, projection sentence and the live headline, was absent from the UI Automation tree. A screen reader read three tab headers over an empty body; the window looked perfect in every screenshot ever taken of it, because a picture does not use the accessibility tree. One x:Name on the ContentPresenter fixes it, with no visual change (captured before and after). Dev-only surface otherwise: no new strings, no README/site change, --selftest unaffected at 134/134, and the Keyboard case still passes in en and the Profiles case in pt-BR.
    T165 · 670a75b
  222. Coming back to a profile shows that profile again. Reported from use: *"se eu visualizo o gráfico de consumo de um perfil, mudo para outro perfil no combobox e volto para o perfil anterior, o gráfico muda — parece que quando mudamos o combobox ele não zera corretamente os dados"*. Exactly right, and three times over — the switch carried three pieces of the previous profile with it, each with a different lifetime bug behind it. The live reading: _snapshot is whichever profile is on screen, so leaving the monitored profile overwrote it with the other account's numbers, and the branch that was supposed to restore it read _snapshot itself — finding what the profile being left behind had just put there. The window then drew this profile's transcripts scaled to the other profile's utilization, under this profile's name, until the next poll happened to correct it (five minutes by default). It is held aside now (_monitoredSnapshot), and kept fresh even while another profile is shown, because the tray goes on polling it for the icon regardless — coming back must not wait for the poll after that. The error banner goes with it: "the live API is unavailable" is a fact about the polled profile, and carrying it over a stored reading of another account blames that account for an outage nothing tried to reach on its behalf. The live tail: StopLive(dispose: true); StartLive(); was a one-way trip — StartLive guarded on _chartProjects is not null, which is true forever after the first build, so the tail was disposed and nothing ever replaced it. One profile switch killed the Throughput strip for the rest of the window's life, on every profile including the one it started on. The two lifetimes are separated now: the charts are built once (they own the Border's child and its hover handlers, so rebuilding them would stack a second set), the tail restarts per profile. The drawn history: LiveChart only ever *appends* to what it has drawn — deliberately, since T119, so a late-reported turn cannot rewrite a point already on screen — which is right while the past belongs to the same source and wrong the moment the source changes; the by-type chart, whose three series never change, would have spliced the new profile's first second onto three minutes of the previous account's line with nothing to force a re-adopt. A new LiveChart.Clear() drops it, and the cached series, legends and cache-read note go with the tail. Also cleared: the last-rendered pace for both windows, which a resize would otherwise redraw and the Throughput tab reads for its two averages. Verified as the round trip, because that is the only sequence it appears in: --capture-stats … profile=1,0 now walks the picker through a list of indices one full settle apart, and against a deliberately un-fixed build it reproduces the report in full — the picker reads "Trabalho" while the numbers are Pessoal's 31% / 34% / reset ago. 7, over a curve built from Trabalho's transcripts, with *"Vazão ao vivo indisponível"* underneath. With the fix, 0 → 1 → 0 lands back on the baseline capture exactly: 74% / 57% / 2d 23h, reset ago. 5, and the strip alive at 131 tok/s over five sessions. No new strings, no UI change beyond the window being right; --selftest unaffected (119/119 — the switch is UI state, and asserting it is T165).
    T164 · 2f46aed
  223. A straight-line projection says which line it is, and that it is a matter of weeks. When
    T163 · d825800
  224. The Settings carry-over list becomes a declaration on the fields it names. T141 made the
    T162 · fcf3bdb
  225. `ProjectSlug` gets the assertions its defect record earned it. It is the one reader *and*
    T161 · fda5914
  226. The measured away-week gate opens on a machine the tray doesn't watch all week. T152
    T160 · 5b5cd4e
  227. The method note stops counting the weeks the grid threw away. The note said "{0} weeks
    T159 · 193989c
  228. **Statistics, Context and Settings stop being three windows and become three destinations
    T158 · b1d6d1d
  229. Follow the active profile explains itself before it defends itself. Reported as *"o texto está bem complicado de entender"*, and reading it back that is exactly what it is: two long sentences whose first one spends its second half on the reading mechanism (*"from transcript timestamps read on each refresh — no transcript is ever opened"*) before the reader has been told what the switch does at all. That clause is a privacy guarantee, and it is worth keeping — but it is an answer to a question that only arises once the feature is understood, so it now comes last, in four words. Four short sentences replace the two long ones, in the order a person asks: what the icon can show (one profile), what the switch changes (it shows the most recent one), what happens if you pick by hand anyway (it holds, until Resume following — named exactly as the menu item names it, which the old text did not do), and only then what is read (file dates, never a transcript). The tray menu's own tooltip for the same toggle got the same treatment: it opened with *"whichever profile a turn just landed in"*, and "a turn" is API vocabulary that no part of the UI defines. Both strings rewritten in all five languages; no code change, and the line still wraps to four lines in the row at the width it had. Verified by capture in English and pt-BR.
    T157 · 33f06ec
  230. Follow the active profile and Use the chosen profile everywhere in Windows move out of the profile editor into a section of their own, All profiles. Reported as a scope error, not a layout one: *"usar o perfil em todo o windows deve ser uma configuração global do Claude Tray e não uma configuração específica do perfil"* — and it always was one. Both are single bools on Settings (FollowActiveProfile, SyncEnvironmentProfile), neither has ever been stored per profile, but they sat in the same card as Name, Working directory and Configuration folder, four rows under the combo that says which profile those fields edit. A row that re-renders when you change the combo and a row that does not are indistinguishable when they share a card, so the page was inviting the reading that you were arming the environment for *that* profile — the more so because the toggle's own value line shows a config dir. The per-profile card now ends at Terminal default, the note about a fresh profile starting empty stays with it, and the two app-wide switches sit under their own header below. Their one-profile gate is unchanged in effect and simpler in code: header and card hide together on one gate (they had one each, and the divider above the first one used to be drawn on a one-profile machine with nothing under it — that bug is gone with the divider). One new string in five languages (settings.cc.profilesGlobal), no behaviour change, no settings-file change: an existing settings.json reads identically. README's two "Settings → Claude Code → Profiles" pointers at these toggles now say All profiles. Verified by capture in English and pt-BR: the editor card ends at Configuration folder for the sample's default profile, the note follows it, and the All profiles card holds exactly the two toggles with the live CLAUDE_CONFIG_DIR beside the second.
    T156 · 548ad41
  231. The Icon profile row is gone from Settings → Claude Code. It arrived as a question — *"não entendi o que é perfil de ícone"* — and the answer was that it is the same setting as the tray's Profile submenu, written to the same MonitoredConfigDir, only slower: select the profile in one combo, press a button in a row below it, then Save. T138 added it because three controls on that page said "profile" and none of them switched the icon; the fix that mattered was renaming the other two ("Viewing", "Editing"), and the fourth control was one switch too many — a second way to do a thing is a second thing to explain, and the page's own description had to spend a clause pointing at it. The tray submenu keeps it: one click, the other profiles' readings beside it, no Save. Removing the row also removes a defect it left behind. ApplySettings treated a changed MonitoredConfigDir as a hand pick and pinned the icon to it (T139) — but the window's copy of the settings is taken when it *opens*, so with no control writing that field, a Save could only ever write back a stale value, moving and pinning the icon to whatever profile was current when the window was opened. It now joins Metric and the two environment fields on T141's exception list: fields the page does not edit are carried over from the live model rather than from the buffer, so Save cannot move the icon at all. Four strings dropped from all five languages, the "Editing" row's description reworded in all five to point only at the tray menu, and the divider that preceded the removed row now hides with the auto-follow row it sits above — on a one-profile machine that divider used to be drawn with nothing under it. Verified by capture at three scroll positions in English and pt-BR (--capture-settings ClaudeCode --sample): the profile section reads Editing → Name → Working directory → Configuration folder → Follow the active profile → Use the chosen profile everywhere, with no gap and no orphan rule; --selftest 58/58.
    T155 · b356ea4
  232. The Throughput legend names a project by its last two path segments — turing/2026.3, not 2026.3. Reported from use with the screenshot to prove it: *"em vazão estou normalmente usando a pasta com a versão 2026.3 tanto no shio quanto no turing … aparece o mesmo nome 2026.3 e não dá pra distinguir qual é o projeto"*. Two lines, two colours, one label — and the tooltip that carries the full slug only answers on hover, one line at a time. The suggested fix was the right one and it was already in the app: the Context Load Inspector's project list has shown parent/leaf since it shipped, for exactly this machine's reason (a release folder is checked out once per client, so several projects are called 2026.3). What shipped is therefore the *shared* rule rather than a second copy of it — ProjectSlug.ShortName, in the class that has been the app's only reader of the projects/<slug> encoding since T105, now also the only writer of the name that comes out of it. ContextProject.ShortPath is four lines that call it, TranscriptTail resolves through ShortNameFor (same exact RootFor walk as before — the name still comes from the session's root, never from a cwd that a cd moved), and the two screens can no longer disagree about what to call the same directory. Three consequences of a longer label, each dealt with rather than discovered: the project legend is a WrapPanel now, so five long names fold to a second row instead of running off the plate (identical to the StackPanel it replaces while they fit); --live's column widened 18 → 24 and its ellipsis moved to the middle, since cutting the head is cutting the half that disambiguates; and ThroughputFixture's four repos carry their parent segment (acme/web-console), so the published screenshot goes on showing what the app really draws. Verified against both ends: --live on this machine now prints turing/2026.3 beside alegauss/claude-tray and alegauss/roadkeep, and the re-captured Throughput tab shows the legend and the hover readout naming all five series in full. No new strings — the label was never a translated one; docs/statistics-throughput.png regenerated (the accent colour differs from the older shots because this machine's Windows accent has changed since, not because anything in the app did).
    T154 · 18e85c6
  233. The two properties --selftest was partly built for, and did not assert, are asserted. Both were named in §IV.12 as reasons to write the checks at all, and both were unreachable for a mechanical reason rather than a hard one. The tail's primed offset: every transcript fixture is a few hundred bytes, so fi.Length - PrimeBytes was always ≤ 0, the first sweep always started at offset 0, and NeedsAlign — the branch that skips a resumed cursor forward to a line boundary — had never once executed inside a check. The new fixture is a 317 KB transcript built by byte offset rather than by line (\n only; WriteAllLines would write CRLF and the arithmetic would describe a different file from the one on disk): 200 old turns as bulk, then enough fresh ones to overfill the 256 KB window by 4 KB, so the prime offset lands *inside a fresh turn* — which is what makes the assertions sharp rather than decorative. Each turn carries its index as its input_tokens, so a reported sample says which line it came from; the assertions are on the min and the max of the reported set rather than on its first and last element, because the sweep sorts by timestamp and these turns share a second, so their order is not defined. Six checks: the straddled turn is dropped and the next one is not, every turn after it arrives exactly once, nothing behind the window arrives at all, the sweep reads exactly PrimeBytes of a 317 KB file, and — the one the flag alone can break — an append after a primed read is not re-aligned away, which is what asserts NeedsAlign is cleared. The rate's zero-fill: LiveRate.Add was private and reachable only by standing up a real TranscriptTail and waiting for a sweep, so "a paused caller resumes with a truthful gap rather than a stale plateau" could not be stated. Add is internal now — one method on an already-internal class, the same visibility the fixtures rely on — so a burst goes straight in and the clock moves by hand: the burst lands at its full weight, a caller paused past the 60s window comes back to a true zero on both the smoothed and the raw reading and reports itself Quiet, the burst is still in the 5-minute strip (the strip is not the window), and a caller gone for an hour comes back to an empty strip and no project series at all — the cap on the zero-fill loop, asserted. Each new check was confirmed to fail before it passed, against three deliberately broken builds: priming removed (from = 0) fails four of them, NeedsAlign left set fails the append pair (889 samples where 890 were expected), and the zero-fill loop commented out fails the hour-long-gap pair (1 of 300 buckets still holding tokens). 58 checks at the start of the block, 80 now, 2.7s. Also fixed two stale cross-references while in the files: the project-display-name rule is T154, and two doc comments credited it to T150 — a number that now means something else entirely. Internal: no user-facing change.
    T153 · 69fc126
  234. A week away no longer teaches the measured grid that nobody works those hours. T95 gave the transcript grid an away-week test; the folded store (T88), which T93 blends in bucket by bucket as it earns coverage, still counted every hour of a holiday — so the same week that was dropped from one vote was cast in full in the other, and as the measured share grows that goes from softening the grid to *being* it. T95 deferred it for a stated reason rather than an oversight: a quiet folded week is ambiguous between "the user was away" and "the tray was not running to see it". The store is what resolves it — an hour with no reading is *unknown* and already outside the denominator — so the test is two conditions where the transcript one needed one: a week is judged only once at least half its 168 hours carry a reading (MinAwayWeekCoverage), and it is dropped only if, being judged, its active-hour count falls under AwayFraction (25%) of the median judged week's. A week that fails the coverage test is not evidence either way: it is never dropped, never in the median, and goes on contributing exactly what it contributed before. MeasuredProfile therefore reads the store into weeks before it reads it into buckets — an away test is a statement about a whole week and cannot be made an hour at a time — and a dropped week leaves no weight behind it, the same way T95's does, so removing evidence weakens the bucket instead of handing its vote to the weeks that remain. EffectiveWeeks subtracts the measured exclusions too, so a holiday inside the folded history can no longer talk the confidence gate into shaping a projection. --activity --measured prints the verdict per week with its two markers and, deliberately, the coverage it was judged on: 50h/67c? — 50 active hours, 67 of 168 with a reading, ? for not judged, * for excluded. Printing the coverage is what makes "too little coverage to judge" checkable instead of asserted, and on the dev machine it immediately showed the gate's real reach: with the tray running ~10h a day, weeks land at 67c and 71c and are never judged at all. That is the safe direction (an unjudged week keeps its old behaviour exactly) but it does mean the measured exclusion only fires for a machine the tray watches most of the week. Seven new --selftest assertions over a six-week synthetic store, 65/65: the well-covered holiday is dropped, the thinly-covered quiet week is not, the median judged week is never dropped, an excluded week's weight is gone from Observed to the ninth decimal while an unjudged one's is still there, under four judged weeks nothing is excluded, and the measured span is subtracted from the confidence gate. Internal: no new user-visible string, no UI change.
    T152 · eb01d1e
  235. The self-check runs on every push and pull request, in a check workflow of its own. build.yml had been the only place --selftest ran, and it is workflow_dispatch only, so the 58 assertions that guard the pacing and rate arithmetic were evaluated at the moment somebody decided to publish — with an installer half-built behind them. A broken invariant surfaced as *"the release is blocked"* rather than *"that commit is red"*, which is the opposite of why the properties were written down. The new workflow is build, run, done: checkout, .NET 10, dotnet build -c Release, then the same Start-Process-and-check-ExitCode idiom build.yml uses (the app is a Windows-subsystem .exe, so its output has to be redirected to be read and its exit code waited for). No secrets, no signing, no Inno Setup, contents: read. A separate workflow rather than a job added to the existing one, because the trigger belongs to the workflow and not to the job: build.yml must stay manual — cutting a release is a decision somebody makes, never something a push does — and it still runs the check a second time against the packaged single-file .exe, which is a different artifact from this one. dotnet build rather than dotnet publish here on purpose: the csproj is SelfContained for win-x64 either way, so the plain build output is already a runnable .exe, and skipping the single-file bundling and its compression is most of the difference in cost (measured on this machine against that exact output: 6.4s build, 4.1s wall for the check, of which 2.3s is the 58 assertions). Two economies: concurrency cancels a run that a newer push has superseded, and paths-ignore (**.md, docs/**, LICENSE) skips prose-only commits — it skips only when *every* changed file matches, so the repo's usual code-plus-docs commit still runs the check. branches: ['**'] keeps tag pushes out of it, since a tag is a release and build.yml owns that path. Internal engineering: no README or site change, and the two paths-ignore lists are duplicated rather than shared because GitHub Actions does not support YAML anchors — a shared list would silently not be shared.
    T151 · b98bee8
  236. The live tokens/second reads what it measures. The age-weighted window divided its weighted sum by W/2 — the integral of the triangular kernel, ∫₀^W (1 − t/W) dt — while the kernel it actually evaluates is a sum over discrete seconds, whose weights come to Σᵢ₌₀^{W−1} (1 − i/W) = (W+1)/2: 30.5, not 30. Sustained work at R tok/s therefore displayed as R × (1 + 1/60), 1.7% high, since T98 and in every place the rate is shown — the headline, the strip, the per-project lines, the per-second hover readout. Nobody would have reported it (nobody decides anything on the second decimal of a tok/s reading) and nobody could have seen it on a chart; it was found by T96 having to *state* what a sustained rate should read as, and the check pinned the wrong value as a closed form on purpose so that the day the divisor moved, the assertion had to move with it. The fix is one number, now named once as LiveRate.KernelSum instead of open-coded at the four sites that evaluate the kernel (Tick, Series, RateFrom, WeightedRate) — the class doc's claim that "the weights sum to what it is divided by" is true again, and it is a const a future reader can't retype wrong. Two --selftest assertions moved with it: a sustained rate now reads as itself (1000 → 1000.000000, not 1016.67), and the burst's opening value is derived from KernelSum rather than restating a divisor. Nothing else in the kernel changes — the decay is still exactly linear to a true zero at 60s, and per-project rates still sum to the total, because scaling a linear operator preserves both. 58/58, 2.3s. Not a user-facing change beyond the number being right: no new strings, no layout, no README/site claim was quoting a rate.
    T150 · 5550b0f
  237. --selftest: the pacing and live-rate arithmetic asserted over synthetic inputs, exiting non-zero on failure. Both blocks introduced properties that are cheap to assert and expensive to lose, and every one of them had been verified exactly once, by a human reading a chart or a CLI dump — nothing stopped an edit from breaking one silently. 58 checks, 2.3s, in five sections: the staircase (a flat profile reproduces the straight average-pace line to the last bit, at three different p — which is also T94's degradation claim, since a flat intensity grid must make every factor it introduces exactly 1; Σ p over a week equals the sum of the grid; Σ p·i equals the dot product; a thin profile refuses to shape anything; weeks away are subtracted from the confidence gate), the advice (never proposes an hour whose projected close exceeds AdviceTarget, and never proposes an hour the user does not work), the stores (folding the same readings twice spends the same, a day's spend is the sum of its positive deltas, the ghost stays hidden under both of its gates, a ~50× incident is capped at the clamp rather than believed, a near-zero hour is shrunk back toward ordinary rather than clamped, an hour observed but never active stays exactly 1.00×), the sweep (two away weeks out of [40, 40, 40, 5, 0] active hours are excluded against a median of 40, the median week is never excluded, at least half always survive, and under 4 weeks nothing ever is), and the tail's byte cursor (a half-written line is held until its newline, completing it releases exactly one turn, an append costs only the appended turns, and a truncated file restarts without re-reporting what it already had). The rate kernel is pinned to its closed form rather than to "about right": a sustained R reads as R × (1 + 1/60) — the triangular window summed over discrete seconds has gain (W+1)/2, not W/2, so the 1.7% is arithmetic and not drift — a burst starts at its full weight, decays exactly along the line, and reaches a true zero at exactly 60s (the smoothing is attack-only, so a fall passes through untouched), and two projects' rates sum to the rate of both together. No test project and so no third-party framework, which the zero-dependency rule (§I.3) forbids for a repo whose single self-contained .exe is a feature: this ships inside the same binary, costs nothing at runtime, and the build workflow now runs it against the very .exe it is about to package — a failed check stops the build before an installer exists. Everything it touches is synthetic and removed afterwards: a temp transcript tree and a selftest profile directory, never a real profile's stores (T128's rule). Verified to actually bite by mutating AwayFraction to 0.001 and watching it fail with the weeks it dropped printed beside the median it measured them against. --selftest --quick skips the tail section, which waits on real sweeps. Internal: no user-facing surface.
    T96 · 595b637
  238. A week away stops teaching the model. Every observed week voted with equal weight (times recency decay), so a holiday voted "these hours are idle" exactly as confidently as a working week voted the opposite — and the flat prior does not address that, because the prior is about *thin* evidence, not *unrepresentative* evidence. WeeksAway now measures each week's active hours against the median week's and drops anything under 25% of it from both numerator and denominator, in the same currency the grid votes in (presence, never request counts — a quiet week is still a working week). Because the fraction is below 1 the median week can never be excluded, so at least half the weeks always survive; below 4 whole weeks nothing is ever excluded, since with two weeks the median is one of them and "drop the quieter half of what little you have" is how a model convinces itself of a shape it has not seen. The shrink toward the prior now divides by the *voting* weeks rather than every week in range: evidence removed has to weaken the grid, not be quietly replaced by the weeks that remain. EffectiveWeeks subtracts the exclusions too, so the confidence gate can't keep drawing a staircase on a sample that no longer supports it. Measured on the dev machine — 6 whole weeks of 71h / 66h / 60h / 71h / 15h / 0h active: the last two were excluded (median 63h, floor 15.75h), including a week with *zero* active hours that had been voting every one of its 168 buckets idle at full weight. The grid moved from "33% of all hours active, 55.5h in a typical week" to 40% / 67.5h, and on the --stats shape preview the projected landing moved five hours earlier (Aug 4 01:06 → Aug 3 20:03) — the sixth of the shape a fortnight off had been silently subtracting. --activity prints the exclusion whether or not it fired, with the median it was measured against, and a fresh scan also prints the per-week active hours with the dropped ones starred — "2 weeks excluded" is unfalsifiable without the weeks it dropped (the standard T92 set for its sweep cache). Grid cache bumped to v2: a v1 grid was built without the exclusion and would report "0 weeks excluded", which is a claim about the evidence rather than a missing decoration — the sweep cache survives the bump, so the forced rebuild costs only the sessions that changed. Deliberately not extended to the measured grid (T88/T93): its weeks have unequal coverage, so a low-activity week there is ambiguous between "away" and "the app was closed", and guessing would destroy the very distinction that store exists to preserve. Internal: no user-facing surface.
    T95 · a914e68
  239. The projection stops treating every active hour as costing the same. p says *whether* an hour is worked; nothing said what it costs, so a morning of agent work and an evening of one-line questions were paced identically — a bounded error (the rate is calibrated on this window's own average) but a systematic one, and it under-projects exactly the week whose remaining hours are the heavy kind. HourlyUsage.MeasuredProfile now also returns the mean spend per *active* hour per bucket, divided by the mean over every active hour, so it is unitless and a week that simply spends more moves no bucket; ActivityProfile.I carries it and ExpectedIntensityHours sums Σ p·i beside the old Σ p. Only the folded store can supply this — the transcript sweep deliberately records presence and never reads token counts — which is also why intensity is applied on every read rather than cached: it moves with the store, not with the transcripts. The unit of the projection becomes an *ordinary* active hour, so the measured rate is divided by the elapsed span's own mean intensity (RatePerOrdinaryHour) before being multiplied back into Σ p·i — calibrating in one unit and spending in another would have introduced a bias larger than the effect. It degrades exactly: with a flat grid every factor is 1.0, which is exact in IEEE 754, so the arithmetic is the T87 one unchanged — including on a machine with nothing folded yet, where HasIntensity is false. The same two anti-overfitting guards the grid itself uses, and they do visible work: shrunk toward 1 by 2.0 weeks of prior and clamped to [0.5, 2.0], the dev machine's single folded day turns a raw 0.31×–1.81× spread (six hours, one observation each) into 0.77×–1.27× — a single 3am incident cannot create a "3am is 4× heavy" bucket. Sequenced after T93 on purpose: intensity is only meaningful once the measured grid is trusted, and two model changes at once would make a regression impossible to attribute. --activity prints the heaviest and lightest buckets with the guards that produced them, and --activity --measured prints the intensity grid with its own around-1 legend, blank where a bucket has never been observed active — no evidence is not "an ordinary hour", even though the projection has to treat it as one. Internal: no user-facing surface — the same sentence, a better number behind it.
    T94 · 3ca764a
  240. Picking Profile ▸ … or pressing Save no longer freezes the tray for seconds. Reported from use: *"quando mudo de perfil no tray a tela trava e não consigo fazer nada"*, and the same on saving an edited profile — a few seconds each time, then normal. The suggested fixes were a loading screen or a different way of writing the variable; neither was needed once the cost was located. Block T's EnvironmentProfile.Write ran on the UI thread, and both of its halves broadcast WM_SETTINGCHANGE to every top-level window on the machine: Environment.SetEnvironmentVariable(…, User) sends one itself before returning (no flags, so it waits out a hung window in full) and Broadcast() sent a second with a five-second per-window budget. Each sweep waits on every window in turn — measured on this machine, idle, at 129 ms to set and 486 ms to remove, and seconds with a browser and an editor open. A tray whose UI thread is inside a broadcast repaints nothing and answers nothing, which is exactly what was reported. So the write is now queued to the thread pool and the caller returns at once; the writes are chained, so two quick picks land in the order they were clicked rather than racing for the last word. Measured against the built assembly, driving Write directly and timing the *calling* thread: 0.95 ms for an unchanged Save, 0.01 ms for a real switch — against 1,212 ms the queue then spent broadcasting, which is precisely the time that used to be the frozen tray. That harness earned itself immediately by catching a defect in the first cut of this fix: the short-circuit compared the wanted value against the registry, and a queued write has not landed yet, so picking the other profile and picking this one straight back left the machine on the other one — the second click compared against a value that was already stale and dropped itself. It now compares against where the queue is *heading*, falling back to the registry only when nothing is in flight, which is also how a value changed behind the tray's back (System Properties) is still picked up. Two more things fall out of the move off-thread. Adopt/Restore now return "accepted" rather than "written" — there is no result to await without re-freezing the click, so the ownership bookkeeping is recorded up front, and the worst case if a registry write ever did fail is a settings line claiming a variable the tray does not own, whose only effect is to restore the value already there. And the Save path gets a real short-circuit: Write compares against the live value first, so the reconcile ApplySettings performs on every press — whether or not the profile row was touched — costs one registry read (~0.3 ms) instead of two broadcast sweeps. The tray's own broadcast keeps its SMTO_ABORTIFHUNG (that is what .NET's own does not do) with the budget cut 5 s → 2 s, since a slow sweep now delays the *next* switch's rather than the user. ExitApp drains the queue, bounded, so switching profile and quitting in the same second cannot lose the variable. No user-visible strings — the fix is that the freeze stops happening.
    T149 · 8f0bc4b
  241. The screenshot fixture moves in with the fixtures (src/Usage/ThroughputFixture.cs). The deterministic three minutes behind --stats live was hand-shaped inline in StatisticsWindow.RenderDemoLive, where it had accreted next to the rendering it feeds while three tasks came to depend on it (T110's ceiling, T112's outlier, T104's pinned reading). It is now one Build() returning a ThroughputDemo record — series, raw buckets, headline, sessions, cache-read rate and the second the readout is posed on — and the code-behind keeps only what a code-behind should: assign the fields, set the text, render once, hold the pose. Same numbers, same constants, same shaping through the real LiveRate.RateFrom kernel. Pure housekeeping, and proved to be: capturing the Throughput tab before and after and diffing the two PNGs pixel by pixel gives 77 differing pixels, all inside x 202–210, y 1311–1323 — the minute digits in the footer's "Updated …" line. Both charts, both readout cards, the legends and the cache-read line are pixel-identical. Block K's last task, so the block leaves ROADMAP.md and its design section (§V) leaves IMPROVEMENTS.md; three TOC rows there that pointed at sections already pruned went with it, and the roadmap's tray-icon non-goal now cites this changelog instead of a section that no longer exists.
    T108 · 682b47b
  242. The cache re-read is said out loud, where it is excluded. LiveRate has separated cache reads from real work since T98 and showed the result to nobody, and the result is the startling part: measured on this machine's ordinary traffic, ~30,000 tok/s of cache read against ~150 tok/s of work. Keeping it out of every rate on the tab is right — it barely weighs on the rate limit and would drown the signal — but an exclusion two orders of magnitude larger than what *is* drawn has to be stated, in the place where it is being made. One line now sits under both charts: the rate, its ratio to the rates above (34× in the fixture, rounded like a magnitude — "200×", never "197.4×"), and what it is: your context, re-sent with every turn. Deliberately a sentence and not a fourth line, which is what §V.12 warned against: a fourth entry in a legend of three reads as a series that was drawn, and the number is only meaningful as a ratio anyway. The ratio is dropped entirely when the work rate falls below 1 tok/s — through a pause the denominator decays toward zero and "∞× the rate above" is a division artefact, not a reading. The rest of the argument — why it is excluded, and that this is the per-turn price of a large eager context, which Context load measures statically — is on hover, because it did not fit: the first version was a two-line paragraph, and the screenshot showed it pushing the window averages below the fold, which is exactly the defect T111 fixed once already. Fixing it properly took both halves: the copy shrank to one line, and the window grew 920→948px (the "Window averages" heading's margin gave up 4 more), because in the *real* window — not the smaller capture frame — the last row was still being cut in half. Verified by looking at both: the capture path for the published image and the real window driven at its default size. Three keys across all five lang/*.json; the fixture reports the measured 30k tok/s against its own 870 tok/s of work, so the published screenshot carries a real order of magnitude rather than an invented one. README, site and llms.txt updated; docs/statistics-*.png regenerated.
    T107 · 048ad9b
  243. One reader of the slug encoding (src/Core/ProjectSlug.cs). A projects/<slug> name is the session's root path with every non-alphanumeric character replaced by `-`, which is lossy in the way that matters: a separator and a literal hyphen encode identically, so claude-tray and claude\tray produce the same slug and …-shio-2026-3 naïvely reads as the folder "3". §V.10 counted two readers of that encoding; the code had three, and the third is the reason this is more than tidying — ContextFixture replaced only the colon and the separators, an approximation that agrees with the real encoding right up until a fixture path carries a dot or a space, at which point the fixture writes a directory name Claude Code would never write and the project silently fails to resolve. All three now go through one class: Encode (the fixture included), RootFor/NameFor (exact — walk a recorded cwd up to the ancestor whose encoding *equals* the slug, which verifies instead of guessing, needs no filesystem so it still answers for a directory that has been deleted, and is immune to the cd-inside-a-session trap T100 found), TryProbe (the filesystem guess, for when no cwd was ever recorded), and Literal/Tail for reporting an unresolvable one. The scanner's order changed as a consequence, and that is the substance: it used to prefer the probe and fall back to the raw cwd, so where the two disagreed the *guess* won — now the verified answer comes first, the probe second, the raw cwd third. Measured on this machine's 38 projects, before and after, both fresh scans with the cache cleared: every project resolves to exactly the same path, so the reordering costs nothing where the two agreed — and the fresh scan goes from ~163ms to ~235ms, because a cwd is now read for every project rather than only for the ones the probe could not resolve. That cost is accepted rather than optimised away: the scan is cached and computed off-thread, and buying exactness for the disagreeing case with 70ms on a background rebuild is the trade §V.10 asked for. (An earlier measurement of 2467ms was a cold disk, not this change.) Also verified on the fixture (--context --sample, same seven projects, same three unresolved states, rules unchanged) and on the live path (--live still names claude-tray and roadkeep, not tray and 3). Internal: no user-facing surface, no new strings.
    T105 · fc03b41
  244. A second you can point at. T110 gave the charts a magnitude, T111 the height to use it and T116 lines you can follow, which left the one interaction a time-series should not ship without: they answered only in shapes. Pointing at a plot now snaps a crosshair to the nearest second, puts a dot on every line where it was in that second, and opens a readout naming the time and, per series, the rate the chart drew *and* the tokens that actually landed there — the distinction the rolling rate necessarily smooths over, and the one a shape can never carry. Verified on the real interaction rather than by eye: at 126s back a project reads 167 tok/s having landed 0 tokens, which is exactly the fact the two numbers exist to explain (that height is earlier work still ageing out of the trailing minute), and the readout says so in a line of its own when *nothing* landed in the second. Four decisions the implementation is: the reading comes from the chart's drawn history, never from a recomputation, so the number under the pointer is the number on the line — the same rule T119 settled for the line itself; the mapping from pointer to sample includes the slide's current offset, or the crosshair would name the second next to the one it stands on for most of every second; it is drawn inside the plot, not as a tooltip, because a tooltip is a top-level popup that flickers when reassigned under the pointer and cannot appear in a screenshot; and the scale sentence T112 requires appears in the card only while something is actually clipped — then it is a disclosure, otherwise it would restate the labelled axis at the cost of covering the chart (and if the card still doesn't fit, that footer is what gets dropped, never a reading). The card also flips to the far side of the crosshair and to the opposite half from the pointer. LiveChart stays geometry-only: the window supplies the copy, the localization and the raw counts (LiveRate.Strip() for the type chart, ProjectSlice.PerSecond for projects) through one Readout callback. Five keys across all five lang/*.json; the fixture pins the reading on its deliberate cache write (47k tokens landed in one second, drawn as a rate that spreads them over the following minute) so the published screenshot shows the interaction a capture has no pointer for, and --stats live now opens on the Throughput tab — the same reasoning --stats shape uses to open on the weekly one. Verified in English and pt-BR, and with a real cursor moved over the window: the hovered chart re-reads to the second under the pointer while the other keeps its pinned one. README, site and llms.txt updated; docs/statistics-*.png regenerated.
    T104 · 0102652
  245. The projection prefers the *measured* week once there is enough of it. The transcript grid has a structural blind spot the UI had to apologise for — usage from another machine or from claude.ai counts against the same limit and leaves no transcript here — while the folded aggregate (T88) is built from the rate-limit utilization itself and counts every request whoever made it and wherever. So the measured grid stops being a second opinion to validate against and becomes the better source. Blended per bucket, never switched wholesale: HourlyUsage.MeasuredProfile now also returns the decayed weeks of observation *per hour*, and each bucket takes w = min(1, observed / 3) from the store and the rest from the transcripts. Hours the store knows well come from the store; hours it barely saw — and every hour on a machine that has just started folding — stay with the transcripts, which keeps their two remaining jobs (bootstrapping the first weeks, and covering hours the app wasn't running to observe). A linear ramp rather than a cutover at three weeks, which would visibly jump the projection on an arbitrary day. The blend is applied on every read and deliberately not cached: what is cached is the transcript grid, which only changes when the transcripts do, while the measured store changes every day the tray runs — so WriteCache happens first and BlendMeasured second. Confident now reads the *larger* of the two coverages, since a machine with thin transcripts but weeks of folded readings knows its own week perfectly well. Two consequences handled rather than discovered: --activity --measured now diffs the measurement against the inferred grid rather than the blended one (comparing it with a grid it is already part of would be marking its own homework), and the method note stops disclaiming the blind spot once the grid is mostly measured — a stale disclaimer is its own kind of wrong — via a new stats.methodNote.shapeMeasured across all five lang/*.json, plus the matching README paragraph. Verified numerically against one folded day (Friday, covered 15:00–21:00): exactly those seven buckets moved, each to ⅓·measured + ⅔·inferred (71→47 where the hour was covered but idle, 71→81 and 57→71 where it was active), and every uncovered hour was left untouched — an unobserved hour must never drag the grid toward zero. --activity prints the blend share, the folded days behind it, and the per-bucket weight at the widest gap.
    T93 · 1e4a3bb
  246. The transcript sweep is incremental. Rebuilding the grid re-read every *.jsonl in the 12-week window although a session untouched since the last sweep produces exactly the counts it produced then; each file is now reduced once to the local hours it touched and cached on path+size+mtime (SweepEntry, activity-sweep.json), the way ContextUsage already caches its counts. Measured on the dev machine: 541 transcripts / 860 MB / 1,669ms cold → 3 files / 5.7 MB / 123ms warm, the three being the sessions actually being written. The design point is what is *not* cached: a week here is the rolling window [now−7(w+1)d, now−7w d), so the week a timestamp belongs to depends on when the sweep runs — caching a week index would cache an answer that rots by one every seven days. The entry stores the absolute local hour (local.Ticks / TicksPerHour, converted at scan time so the historical UTC offset and DST are the ones that applied then) plus a count per hour, and the week is derived at aggregation time in the one place both paths share, so cached and full-sweep results cannot drift. Nor is the 12-week cutoff applied to individual lines any more: the entry describes the file rather than the moment it was scanned, so it stays valid as the window slides, while the file-level mtime check keeps out-of-window transcripts from being opened at all and bounds the cache to the current window. The cache is shared across profiles rather than stored per profile, for the same reason context-usage.json is — entries are keyed by absolute path plus fingerprint, so two profiles pointing at one tree share the work instead of repeating it. Three modes keep it honest: --activity --refresh re-reads every transcript (without which a cache bug is unfalsifiable) while still leaving the cache warm, and a --root fixture tree neither reads nor writes it (T128's rule). --activity now prints what the sweep cost — transcripts in window, how many were actually opened, MB read — so the cache is visible rather than merely fast. Verified by diffing the printed grid across cold, cached and --refresh runs: identical in all three, and a synthetic fixture lighting only local hours 09/10/14 comes back as exactly those buckets. Internal: no user-facing surface.
    T92 · 3775745
  247. The tray keeps the weekly shape warm. ActivityProfile.Load had exactly one caller — UsageReport.ComputePace, which runs only while the Statistics window is open — so the grid was refreshed as a side effect of somebody looking at it. Two consequences nobody chose: a machine whose owner never opens the window projects along a shape that ages indefinitely (and the tray icon's verdict rests on that projection), and the first open on a fresh install waits on the full ~15s cold sweep in front of the chart. The 6h background timer that already samples context load (T79) now warms the profile too — renamed _backgroundSample, since it owns both weekly-scale samplers — plus once on launch, the same shape as ScanContextInBackground. Nothing but the caller changes: Load already returns the cached grid immediately and recomputes behind it once past RefreshHours, so a warm pass is a file read and the window keeps reading whatever is on disk. It runs on the pool because the fresh-install path has no cache to return and computes synchronously — verified by clearing the cache and launching: the grid was rebuilt within seconds while the tray stayed responsive. Only the *monitored* profile is warmed, matching how context is sampled. Internal: no user-facing surface — the change is that a number already on screen is less stale.
    T91 · 5b0ce63
  248. The Profile submenu opens from the keyboard. Found while driving T145's verification through the real menu: every other submenu expands with Right, and this one *closed the menu* instead. The cause was the population point, not the key handling — _profileMenu filled its items in DropDownOpening, so before the first open DropDownItems was empty, and an empty ToolStripMenuItem is not a submenu to WinForms: no ExpandCollapse pattern, no arrow, and Right handled as "activate a plain command", which dismisses everything. A mouse hover always worked, because that path opens the dropdown (raising DropDownOpening, which fills it) before anything asks whether it has items — which is exactly why a mouse-tested menu hid this indefinitely. It is now filled while the menu is being built and again in ContextMenuStrip.Opening, which already re-read every profile on each open (T137), so the readings are the same poll's and nothing is staler for the move; the settings-save path fills it too, since adding or removing a profile changes its contents. The second reading of the same bug is the automation one: a submenu that is empty at menu-open time cannot be expanded by Check-Interaction.ps1 either, which is why T142's menu case covered Open Claude Code and not this one. That gap is now closed — the harness expands Profile the way a keyboard user does (Down to it, Right) and asserts one entry per profile plus the auto-follow toggle, and it was confirmed to *fail* against the pre-fix binary before it passed against the fixed one.
    T148 · 0f4851a
  249. The tray icon says whose number it is: with more than one profile registered it wears a thin per-profile colour band along its top edge. Reported from use and independent of the toggle — the number, the fill and the projection colour are identical whichever profile the icon follows, so only the tooltip named it, and with the profile now global "which one am I in?" is the question a glance should answer. §XI.3 left the mark undecided between three candidates, and building settled it against the front-runner: the profile's initial was implemented first and rendered at the real 16px, where "P" against "54" or "100" blurs both — the second glyph and the number cannot share the only legible area, exactly as the design section feared, and the number is the thing the app exists to show. The band costs the digits nothing because it lands inside the margin FitToTile already leaves above them, so the number is drawn identically marked or not. It is a colour, which §XI.3 flagged as the real risk since colour here means *projection*: what keeps them apart is that they share neither a hue — Accents (violet, pink, periwinkle) is disjoint from the green/orange/red of the verdict, the cyan of "no projection yet", the amber of an API error and the slate of the tile — nor a form, the verdict being a level that rises from the bottom and identity a fixed 2px rule at the top, drawn after the bevel so it survives a 100% tile. The slot is the profile's position in the watch list ordered by key, not the list's own order: _watched[0] is whichever profile the icon currently follows, so indexing that directly would hand every profile the same band the moment it took the icon over — the exact opposite of an identity. Two profiles therefore always differ; a third can re-deal the hues, which is the price of not tying identity to a hash that could collide. One profile gets no band at all (the Profile submenu's own gate), since there is no question to answer and an unexplained stripe would be noise. TrayContext.AccentSlotFor is asked by both the icon and --profiles, which now prints the band per profile so the mapping is checkable without driving the taskbar. Verified at both ends: --render gained an 8×-magnified contact sheet of the real 16px pixels over a light and a dark backdrop (every case, unbanded and once per accent), and the live taskbar icon was screenshotted through UI Automation — a violet band over "76" on the orange fill, the digits untouched.
    T147 · 19be344
  250. With Use the chosen profile everywhere in Windows on, Open Claude Code is a plain command again. The per-profile submenu exists (T123) because a launch used to be the only moment the tray could choose a profile; once T145 puts that choice in the user environment, every entry starts the same session and the menu level asks a question the user has already answered elsewhere. The collapse is a consequence of the new mode, not a feature removed from anyone who did not opt in: with the toggle off the submenu is byte-for-byte what it was, which is half of what the verification checks. What had to survive the collapse is T137's affordance — a profile with no credentials on disk still reads "— sign in" and still runs claude auth login — so the collapsed command is aimed at the deliberate profile rather than left to inherit; that is also required for correctness, since the tray's own environment block was built at launch and does not see a user-scope variable written after it (Environment.SetEnvironmentVariable(..., User) writes the registry, not the running process). It aims at DeliberateProfile(), which is null while auto-follow is the thing driving — nothing has been chosen, the tray owns no variable, and inheriting is then the honest launch. The shape decision is one predicate (TrayContext.LaunchIsSubmenu) asked by both the tray and --profiles, keeping the property T136/T144 established that the two cannot drift about what the menu looks like — which matters here because Check-Interaction.ps1 reads --profiles to know what it should find. That harness was taught the second reason a plain command is correct (it used to report "one profile" for both) and to prefix-match the label, so the "— sign in" variant is not a false failure. Verified by driving the real tray's menu with UI Automation in both modes on a live two-profile machine: toggle on reads nine entries with Open Claude Code flat, toggle off expands it to Pessoal / Trabalho as before. docs/llms.txt also lost a claim that T145 had already made false ("the variable is set on the launched process only — never machine-wide").
    T146 · 3af8aab
  251. Use the chosen profile everywhere in Windows — the tray now writes the user-scope CLAUDE_CONFIG_DIR itself, which is a deliberate reversal of T143. That task refused to (shipping a copyable setx instead) because the variable reaches every Claude Code session on the machine and keeps applying after the tray is uninstalled; both are still true, and both are now the requirement — *"preciso que quando eu mudar para um dos perfis, o Windows use este perfil em todo o ambiente"*, VS Code from the Start menu included. Nothing the tray does to a process it launches can reach that, and the three alternatives were dropped for cause (env.CLAUDE_CONFIG_DIR in ~/.claude/settings.json is editing a file inside ~/.claude — §I.4, no exception; a claude.cmd shim on PATH reaches *less* and shadows another tool's executable; per-workspace editor settings are not "the whole environment"). What answers T143's objection instead of ignoring it: what the tray sets, the tray removes — the previous value is remembered in the tray's own settings and put back when the toggle goes off or the pin is released. Off by default, because an update must never silently rewrite somebody's environment. It follows the pin, not the icon: T139 already made a manual pick a distinct, persistent state ("· pinned" + "Resume following"), and auto-follow moves the icon by *observation* — on a machine where a profile is more or less always active, syncing to every icon movement would rewrite a machine-wide setting continuously, which is the very observation that produced T139. The write goes through the same three-way as T144, so choosing the ~/.claude profile removes the variable rather than setting it to a value that would make Claude Code read the wrong .claude.json (T136). WM_SETTINGCHANGE is broadcast (timed out, so one hung app cannot freeze a menu click) or Explorer keeps the block it built at logon and nothing launched from the Start menu would see the change until sign-out; the row states plainly that programs already running keep the profile they started with, which leaves the "no switching the account of a running session" non-goal untouched. The row also shows the live reading of the variable rather than the toggle's intent, so it still tells the truth when something else on the machine set it. Two fields of tray-owned bookkeeping (EnvironmentProfileOwned, EnvironmentProfileRestore) are exempted from the whole-model write-back for the same reason Metric is (T141): a Settings window left open while a menu pick took the variable over would otherwise save "not owned" and strand a value the app no longer knew it had replaced. Verified end-to-end by driving the real tray's menu with UI Automation — the harness T142 shipped — through all three transitions on a live machine: picking Trabalho (~/.claude) leaves the variable absent, picking Pessoal writes …\.claude-pessoal into HKCU\Environment, and "Resume following" puts it back to absent.
    T145 · 62197a4
  252. Which profile a launch selects is now decided against what the child process will inherit, not against ~/.claude. Reported as *"Abrir Claude Code → Trabalho abre no Pessoal"* on a machine with a machine-wide CLAUDE_CONFIG_DIR pointing at the personal profile, and the old rule made that unavoidable: NeedsConfigDirOverride answered "no override needed" for ~/.claude — true only while nothing sets the variable, because the tray then passed nothing and the child inherited the *other* profile's dir. The fix is not "set it anyway": T136 measured that pointing CLAUDE_CONFIG_DIR at ~/.claude makes Claude Code read ~/.claude/.claude.json instead of the real ~/.claude.json, so that session runs with none of the user's project history and leaves a stub that then misdescribes the profile to this app. ~/.claude is therefore selectable only by removing the variable, which is a third case the old boolean could not express — ClaudeAccount.ActionFor now returns Inherit/Set/Unset and ApplyConfigDir applies it (and flips UseShellExecute off only when it has something to do). The launch path, QueryAuthStatusAsync and --profiles all ask through it, keeping the property T136 established that they cannot drift; --profiles prints the three cases apart, because "nothing to do" and "remove the inherited one" look identical from the outside and mean opposite things. The Settings page's Terminal default row picks up the same rule: it was hidden for the *default* profile, and is now also hidden for ~/.claude whichever profile happens to be default, since the setx form it prints is exactly the footgun there. Prerequisite for the rest of Block T: the moment the tray writes a user-wide variable itself, every profile launch is in the situation this fixes. Verified against a live machine-wide CLAUDE_CONFIG_DIR: --profiles reports CLAUDE_CONFIG_DIR removed — inherited …\.claude-pessoal would select another profile for the home profile and a plain set for the other, ProcessStartInfo.Environment.Remove confirmed to leave the child with the variable genuinely undefined, and a UI-Automation-driven screenshot with the profile combo on Trabalho confirming the terminal row is gone.
    T144 · 4b6d719
  253. "Make this profile the default for terminals I open myself" — diagnosed exactly right by the reporter (*"the variable isn't persisting globally, only in the session"*): the tray passes CLAUDE_CONFIG_DIR to the session it launches and deliberately nothing else. Making a profile the default for terminals opened by hand means a user-level environment variable, and that is a promise the app has never made — it would reach every Claude Code session on the machine, including ones started by other tools, CI shims or an IDE, none of which the tray knows about, and it would keep applying after the tray is uninstalled. IMPROVEMENTS.md §X.4 weighed that against the cheap alternative and the cheap one shipped: the Claude Code settings page grows a Terminal default row, beside the profile's configuration folder, showing the exact command (setx CLAUDE_CONFIG_DIR "<dir>") with a Copy button and one sentence stating the consequence. The tray copies the command to the clipboard; it never runs it — the property that keeps it a reader, not a writer, of anything outside its own settings file. Hidden for the default profile specifically, not merely discouraged: T136 already found that pointing CLAUDE_CONFIG_DIR *at* ~/.claude makes Claude Code read the wrong .claude.json, so offering that command for the default profile would hand out a footgun rather than a convenience. Verified by build and a screenshot confirming the row is absent for the default profile (Pessoal (padrão)) on the real two-profile machine; the combo-driven check that the command and Copy button render correctly for a *non-default* selection awaits the interaction harness T142 is for, rather than another ad-hoc script.
    T143 · bf7a0e8
  254. Every verification loop in this repo was a picture, and T135 is what that costs: the WPF windows accepted no keyboard input at all — no typing, no Tab, no arrow key on a slider — from the day the first one shipped, while every screenshot ever taken of them looked perfect. --settings-tray closed the *reproduction* gap; scripts\Check-Interaction.ps1 closes the *checking* one. Two cases, runnable separately: Keyboard launches --settings-tray, navigates to a page by clicking the sidebar, types into DirectoryBox and reads the value back through ValuePattern, Tabs out of it, and drives RetrySlider with an arrow key; Menu launches the real tray, opens the notification icon's menu, reads its entries and expands *Open Claude Code* to read the per-profile ones (what verified T137). Labels come from lang\<code>.json, so the checks are not pinned to English and cannot drift from the shipped strings. The three traps from writing those checks by hand are encoded rather than rediscovered: the icon has no clickable point (GetClickablePoint() throws — use BoundingRectangle) and may be inside the overflow flyout, which the script opens; a collapsed WPF pane is absent from the UIA tree, so a page has to be navigated to before its controls exist, and the sidebar items are bare Borders with no automation peer (match the TextBlock inside, leftmost, because the page title carries the same words); and the menu does not always open, so it retries — and reading zero entries is a FAIL, because the first hand-written version of that check reported a pass having read nothing, which is worse than no check at all. Two findings from building it, both in the script: on Windows 11's XAML taskbar a *synthesised right-click* on the icon does not open the menu at all — the route that works is UIA SetFocus() plus the Application key — and a submenu is opened the way a keyboard user opens it (Down, then Right), never Invoke(), which on *Open Claude Code* would launch a terminal. Refuses to run the menu case while another tray is alive rather than silently reading the *other* tray's menu (-UseRunning drives it deliberately, printing which binary that is). Validated by putting the bug back: with WpfInputBridge.Install() commented out, the keyboard case fails all three checks; restored, all pass, and the menu case reads both profiles from a tray it launched itself.
    T142 · e0fbf68
  255. A field missing from the Settings copy is a field reset, and the copy is now total by construction. SettingsWindow built its edit buffer with a hand-written field list (so Cancel leaves the caller's model alone) and ApplySettings copied the whole model back the same way — which means any field absent from that list started at its default and was written straight over the user's value on Save. Two instances, both found by accident rather than by looking: MonitoredConfigDir (fixed in T126 — every visit to Settings sent the tray icon back to the default profile) and NotifyOnContextGrowth + ContextNudgeTokens, still live until now: the context-growth toggle opened *off* no matter what was on disk, and saving the page turned it off. Adding the two missing lines was not the fix. Settings.Clone() is a JSON round-trip through the same `System.Text.Json` serializer that already writes the file (zero new dependencies), so any property that persists is copied and a field added later cannot be forgotten — including a deep copy of Profiles, which the editor mutates in place. The window clones what it is given; ApplySettings takes the edited model whole (_settings = updated.Clone()) and keeps only the genuine side effects as explicit follow-up calls: clearing an in-progress flash frame, pinning the icon when the chosen profile changed, dropping the follow state, rebuilding the profile menu. One field is carried past the copy on purpose — Metric is the tray's alone (the icon's chosen window, set from the menu, with no control on the page), so a value the menu changed while the window sat open is not undone by the buffer's older snapshot. Verified end to end rather than by reading the diff: seeded settings.json with distinctive values, then a UI-Automation check under --settings-tray read the toggle and slider back as On / 33000 — and the same check against the pre-fix constructor reads Off / 15000, so it is a real pass and not a green that could never go red; then the full round trip through the real tray (right-click the notification icon, open Settings, click Salvar) left all 19 fields intact on disk, Metric and MonitoredConfigDir included. This retires the hand-maintained AGENTS.md rule T126 had to add ("every field must also be copied in SettingsWindow's constructor") — a structural fix replacing a warning nobody would remember. No user-visible strings and no new feature: the fix is that a setting you turned on stays on.
    T141 · 8933b08
  256. Two defects in one derivation. Anthropic names a personal account's organization <email address>'s Organization, so the tray's derived label for that account *was* an email address — reaching the tray menu, the tooltip and every screenshot of them, which §I.1 is explicit about. An organization name matching that pattern (ends with 's Organization, the part before it containing an @) is now treated as no organization at all, falling through to "Personal" like an account that never had one. Separately, two profiles can still end up with the same label — two derived "Personal"s, or a typed name reused, which is exactly what made the T136 incident unreadable (both profiles read "Pessoal", nothing in the menu told them apart). A collision now gets the config dir's leaf folder name appended, for display only — the stored label, typed or derived, is never rewritten, and Discover dedupes once, after any typed-name override, so a rename that resolves a collision doesn't leave a stale suffix behind. Verified against two synthetic profiles built for exactly this shape (one with an Anthropic-style personal org name, one with none): both now read "Personal", collide with the real machine's own "Pessoal" profile, and all three come back labelled Personal (t140fix1) / Personal (t140fix2) / Pessoal (.claude-pessoal) — while an unrelated real organization ("Trabalho") is left untouched.
    T140 · 729304c
  257. A hand-picked profile now pins the icon there until you say otherwise, instead of holding only until the next turn lands elsewhere. T126's rule was that a manual choice wins until a turn lands in a *different* profile — sound reasoning for a machine where attention moves *between* profiles, and wrong on one where a profile is more or less continuously active: observed live, a pick was undone within seconds of the click, because an assistant session kept writing turns into the work profile the whole time it was open. From the outside that reads as the setting being ignored. Of the three candidates weighed in IMPROVEMENTS.md §X.2 (turn auto-follow off silently, pin for a quiet period, pin until unpinned), the third shipped: the click is the strongest signal the app gets, so undoing it now also takes a click. The pinned profile's entry in the Profile submenu reads "· pinned" in place of the usual "active …ago", and a Resume following item appears under the toggle — visible only while something is actually pinned, so it can't be a confusing no-op. The pin lives in the tray's running state, not in settings.json: like the floor it replaces (T126), it is scoped to the session, and a restart is a fresh start. Both places that count as "picking a profile by hand" now pin identically — the tray's own submenu and the Claude Code settings page's Icon profile button from T138. Verified by tracing every path that can set or clear the flag (SetMonitoredProfile, ApplySettings, UnpinProfile, FollowActiveProfileAsync's gate) against a clean build; the live menu interaction itself awaits the harness T142 is for.
    T139 · a45367a
  258. Three controls in Settings were labelled "profile" and only one of them switched the profile the tray icon follows (the tray's own Profile submenu). Reported as *"I selected Pessoal but I'm still using .claude"* — a correct reading of what the UI offered, since the System information picker only re-rendered that read-only page and the Claude Code page's combo only picked which profile the fields below edit. The Claude Code page now carries the real switch: an Icon profile row with a button that reads "Active" (disabled) for whichever profile currently drives the icon, or "Use for icon" for any other — applied like every other field on the page, held until Save, so Cancel leaves the running tray untouched. The other two controls are reworded to say what they actually do: the System page's picker is now "Viewing", the Claude Code combo is now "Editing". Verified with --settings ClaudeCode against a real two-profile machine: the row is hidden with one profile (nothing for the icon to point at, same gate as T126's auto-follow toggle) and shows "Active" disabled for the profile the icon was already following.
    T138 · f6085f1
  259. The menu stopped lying about who is signed in. Reported from the first real second login: *"I clicked Pessoal — sign in, was taken to the browser, logged in, and it still said Pessoal — sign in. I clicked again and it sent me to the browser again."* The login had worked both times — the credentials were on disk — but the Open Claude Code submenu was built from a discovery that only ever ran at startup and on a settings save, so the entry kept offering the one action that was no longer needed, and taking it launched a second login. Discovery now re-runs on ContextMenuStrip.Opening, which is the moment the answer is about to be read; it costs 5–6ms with two profiles (timed, and --profiles prints it) and nothing at all while the menu is closed. Two things fell out of doing it there. RefreshProfileMenu had been running a second discovery sweep of its own, separate from the one behind the icon's own profile list — it now reads the list RefreshWatched just built, so one menu open is one sweep and the two menus cannot disagree about who needs a sign-in. And RefreshWatched used to clear the other profiles' cached readings unconditionally, which at startup was free and on every menu open would have blanked the Profile submenu's numbers to "…"; it now clears only when the icon actually changes hands, which is the only case the cache is wrong for. Verified with UI Automation against the running tray: with the profile's credentials moved away the entry reads "Pessoal — entrar", and with them restored without restarting the tray the next menu open reads plain "Pessoal".
    T137 · d0326fb
  260. The tray was corrupting its own reading of the default profile, and the report was that two profiles had the same name. They did: a Max 5x / Trabalho / 39-project profile was being read as "Claude Team", no organization, 0 projects — so its *derived* label collapsed to "Personal", which is exactly what the other profile was called. Two bugs, one incident, fixed together (the precedent is T124, which fixed the same pair for the auth check). The tray was creating the file that misled it: OpenClaudeCode set CLAUDE_CONFIG_DIR for *every* profile including the default one, and pointing it at ~/.claude makes Claude Code use ~/.claude/.claude.json instead of the real ~/.claude.json — so that session ran with none of the user's project history, and left a stub behind. T124 had already established this for claude auth status; the launch path had not followed, and now both ask ClaudeAccount.NeedsConfigDirOverride so they cannot drift again. And the reader had started believing the stub: T124's tie-break was "prefer whichever candidate names an account", which was enough while the stub was empty — then Claude Code filled in an oauthAccount on it, both candidates qualified, and the stub won on candidate order while carrying no userRateLimitTier, no organization and no projects. Candidates are now scored by how much identity they actually carry (account block, then the tier the plan is read from, then the organization the label derives from, then project history), with one rule above the score: a file naming a *different* account never wins, because the first candidate is the one sitting beside the .credentials.json the profile is polled with — if a second account was logged into ~/.claude, that login is what the dir *is*. Verified against the live broken state on the reporter's machine: with the stub in place, the profile reads Max 5x / Trabalho / 39 projects again. --profiles now also prints whether a launch overrides the config dir at all.
    T136 · 6054d1b
  261. The WPF windows had no keyboard input at all, in every window, since the first one shipped. Reported as *"I can't type anything in this field"* while naming a profile, then correctly widened by the reporter: *"I get the impression the keyboard doesn't work in any text field"* — it didn't, and not one of them was the bug. The tray is a WinForms app (Application.Run(new TrayContext())) that shows WPF windows, and WPF keyboard input does not arrive through WndProc alone: HwndSource subscribes to ComponentDispatcher and expects the pump to offer it every thread message *before* TranslateMessage. WPF's own Dispatcher.PushFrame loop does exactly that; WinForms' loop knows nothing about it. So mouse input — which *is* WndProc-driven — worked perfectly, and every key press was dropped: no typing, no Tab, no Esc, no arrow keys on a slider. WpfInputBridge is a WinForms IMessageFilter that forwards each message through ComponentDispatcher.RaiseThreadMessage, honouring the same "handled means don't translate/dispatch" contract WPF's loop uses, so a consumed keystroke cannot also be delivered a second time; messages belonging to the tray icon and its menu match no HwndSource and pass straight through. Why it survived every screenshot is the part worth keeping: --settings (and every --capture-* flag) runs a WPF Application.Run, which is a different input environment from the tray's — the previews were never able to see this. --settings-tray now opens the window under the tray's own hosting, and it is the flag to use for anything keyboard; the trap is written down in [AGENTS.md](AGENTS.md). Reproduced and fixed against that flag — a UI-Automation drive typed into the Name box and read the value back: blocked before, accepted after, including after a WinForms folder dialog had been opened from the same window.
    T135 · 3496a33
  262. SettingsWindow's code-behind is now one file per page: 929 → 286 lines, with SettingsWindow.General.cs (the interval slider and the per-interval cost estimate), .Notifications.cs (the three threshold sliders), .ClaudeCode.cs (the working directory and the whole profile editor) and .System.cs (the read-only system page and its formatting). The shell keeps what is genuinely shared: the constructor, sidebar navigation, links, Save/Cancel and the capture helpers. The markup was deliberately not touched, which is the decision this task was waiting on. §VIII.6 had listed three options and the reason it stayed an idea: a UserControl per page reads best but puts each page's styles *out of scope* unless the ~170 lines of shared row/value styles are hoisted into a ResourceDictionary — a second file to keep in sync, and a Source= path URI, which is the one thing the T130 experience says ties markup to its folder. A ResourceDictionary per page has the same URI cost with a worse home for page layout. Splitting only the code-behind removes most of the same pain at no styling risk, so SettingsWindow.xaml stays 1,019 lines and every style stays in scope for its users. Verbatim again: zero lines removed across the five files, only the per-file scaffolding added. Gate: all six pages captured and read (in pt-BR, so the non-English requirement is covered by the default), plus Check-Interaction.ps1 -Case Keyboard, which now spans two of the new files — DirectoryBox/BrowseButton live in .ClaudeCode.cs and RetrySlider in .General.cs — and passes every assertion under the tray's own pump.
    T134 · 88898b7
  263. The two big code-behinds now put each independent surface in its own file, one class and one XAML throughout. StatisticsWindow.xaml.cs went 1,269 → 405 lines, with StatisticsWindow.Throughput.cs (the live tab: the two charts, the legends, the 1 Hz clock and the demo strip), .Chart.cs (the burn-up drawing the session and week tabs share), .Profiles.cs (the profile selector) and .Format.cs (the number/time words). ContextWindow.xaml.cs went 1,369 → 708, with ContextWindow.Gauge.cs taking the session-zero gauge and its what-if simulation, and the four helper types that were never the window becoming their own files beside it — ProjectRow.cs, SourceRows.cs (SourceGroup + SourceRow), RowStyle.cs (SourceSort + the brushes) and ContextText.cs. partial class rather than new types is what makes this free: no XAML changed, no member changed accessibility, and not one call site moved — the multiset diff of every non-blank line across both windows comes back with zero lines removed and only the per-file scaffolding (usings, namespace, class header) added, which is a stronger statement than T132 could make. The gate was the regression screenshot: Settings captured byte-for-byte identical to its pre-split capture, and the Statistics 5h/7d/Throughput tabs plus the Context window over the --sample fixture all render exactly as before, down to the same fixture numbers.
    T133 · 6c1c386
  264. Program.cs was two unrelated programs in 2,955 lines, and is now 507: Main and the arg dispatch that routes every flag, ArgValue, and the two small types that belong to starting up (StartupManager, WpfInputBridge). The ~1,000 lines of headless printers became src/Cli/, one file per flag family with each family's formatting helpers beside their only users — ContextCli (--context, --context-report, and Kb/Clip/Mode/StateWord), ActivityCli (--activity, Shade), LiveCli (--tail/--live, Trim/Spark), ProfilesCli (--profiles) and PreviewCli (the fixture/capture/render entry points that produce the published images). The 1,401-line TrayContext — menu, four timers, poll, tooltip, icon render, the watched-profile list — became src/Tray/TrayContext.cs. Moved verbatim, and that was checked rather than asserted: a multiset diff of every non-blank line between the old file and the six new ones comes back with exactly 22 differences, all of them intended — the 11 call sites in Main gaining a class qualifier (PrintContext(…) → ContextCli.PrintContext(…)) and the same 11 members going private static → internal static so Main can still reach them. Not one line of a printer or of TrayContext changed. Every moved flag was then run: --context, --context --findings, --context-report, --activity, --profiles, --tail, --live, --render, --makeicon, --capture-toast. TrayContext itself is covered by the diff rather than by a launch: a second tray exits immediately by design (the Local\ClaudeTray.SingleInstance mutex), and the developer's tray was running.
    T132 · 409138e
  265. The seven root entries that exist to *ship* the app moved into build/: installer.iss, build.cmd, build-installer.cmd, update-release.cmd/.ps1, update-winget.ps1 and winget/ (the manifests are a byte-for-byte rename). Each script now resolves the repo root itself rather than assuming it is in it — the .cmds cd /d "%~dp0.." (which also fixes build.cmd's *_wpftmp.csproj sweep, whose target is the *project* folder), and the two PowerShell scripts split what they were conflating: Split-Path $PSScriptRoot -Parent for ClaudeTray.csproj and dist\, plain $PSScriptRoot for the winget\ that travelled with them. installer.iss needed three ..\ because Inno resolves every relative path against the script's own directory: the publish dir, SetupIconFile and OutputDir. CI was the reference that could not be fixed by reading a local error, so it moved in the same commit — ISCC.exe build\installer.iss, ./build/update-winget.ps1 and wingetcreate submit build/winget. Verified by running the moved chain from an unrelated working directory (d:\tmp), which is the only way to prove the paths are script-relative and not cwd-relative: ISCC produced dist\ClaudeTray-Setup.exe with the icon and the publish output found through ..\, update-winget.ps1 read v1.5.3 out of the csproj above it and rewrote the manifests beside it, and update-release.ps1 -Version 1.5.3 (a deliberate no-op bump) touched the csproj and all four manifests with a zero-byte diff. scripts/ deliberately stayed at the root: those are the screenshot/capture dev tools, not the release path. No C# was touched.
    T131 · 467fc1f
  266. The four windows moved into src/Ui/ — the 4 .xaml, their code-behind and SettingsRow.cs — which finishes the move and empties the root of code. Held apart from T129 because the failure mode is different and a green build does not cover it: a .xaml compiled as a Page gets a generated InitializeComponent whose Uri is derived from the file's path, so moving markup rewrites generated code, and anything hardcoding a pack://application:,,,/… URI would break at *runtime*. The repo hardcodes none (re-checked before the move, along with ResourceDictionary Source=), so the generated code absorbed it — but that was a prediction, so the gate was the picture. All four windows were captured before the move and again after, through the deterministic off-screen renderers (--capture-settings, --capture-stats, --capture-toast) plus a screen copy of the Context window over the --sample fixture: settings.png came back byte-for-byte identical to its pre-move capture, and the other four differ only where they carry live data (the burn-up curve, the throughput lines, the toast's confetti). The Settings *System information* page was captured separately, since the pages are what T134 will eventually split. One check beyond the stated gate, because a screenshot proves layout and not input (T135): Check-Interaction.ps1 -Case Keyboard was run against the moved windows and passes all four assertions under the tray's own WinForms pump — typing, Tab and an arrow key on a slider all still reach a WPF window whose markup now lives one folder down.
    T130 · f5111f6
  267. The 30 non-UI sources moved out of the repo root into src/, five folders on the seams the code already had: src/Context/ (9 — scanner, usage, rules, report, prompt, history, nudges, fixture, token estimate), src/Usage/ (11 — API client, reports, histories, burn, live rate/chart, insights, activity, tail), src/Profiles/ (5 — account, store, activity, settings, environment), src/Core/ (2 — localization, safe walk) and src/Tray/ (3 — program, icon renderer, updater). A pure git mv: the namespace stays flat `ClaudeTray`, so not one .cs file's *contents* changed — folder-derived namespaces would have added a using to most of the 30 files and churned every cross-type reference, turning a reviewable set of renames into a diff nobody reads. The csproj needed no edit either, because the SDK globs **/*.cs; lang/ deliberately stayed where it is, since the embedded resource name is derived from its path (ClaudeTray.lang.en.json is the exact string Localization matches at startup) and a move would have been a runtime failure a green build cannot catch. The point of the move is that the listing now *says* what the app is made of, which the hand-maintained file map in [AGENTS.md](AGENTS.md) could only approximate — that map is now grouped by folder and carries the rule that keeps the root from re-filling ("a new file goes in the folder of the subsystem it belongs to; the root is for the project, not for code"), plus the four files it had drifted past (UsageReport, UsageHistory, EnvironmentProfile, Localization). README's Structure table moved with it. Gate as specified: a green build plus the four headless paths that touch every moved family — --context, --activity, --profiles, --tail — all four unchanged.
    T129 · 4d35a8f
  268. The Statistics window reports on a profile, not on "the machine". A ProfileRef (store key + config dir) now travels into UsageReport.ComputePace, the transcript scan and ActivityProfile, and the window grew a picker that recomputes everything from the chosen profile's own stores and its own transcripts — including the Throughput tab, whose TranscriptTail is pointed at that profile's projects dir instead of the default one. For a profile the tray isn't polling for the icon there is no live reading in hand, so it is drawn from that profile's last stored reading (T125 gave it one) with the footer timestamp saying when that was; a profile with nothing stored yet says exactly that instead of borrowing someone else's numbers. This fixes a bug T127 introduced: UsageReport and ActivityProfile read ~/.claude/projects from a machine-wide constant, so monitoring a non-default profile would have drawn *that* account's quota curve shaped by the *default* account's transcripts — the constant is gone. A pushed reading from the tray's poll is refused while the window is showing a non-monitored profile, since a snapshot is one account's utilization. And one race the capture caught: Reload stamps each background computation with a generation and drops stale results, but switching profiles didn't bump it — so the previous profile's report landed after the switch and repainted the window with another account's charts under this account's name. The screenshot showed the picker reading "Pessoal" over the other profile's 74% curve; switching now invalidates the in-flight run. --capture-stats … profile=<n> renders the window as another profile, which is how both of those were seen.
    T128 · a2311b2
  269. The tray watches every profile, not just the first. One heartbeat per profile per interval, each with *that* account's token (ApiClient took a config dir in T125) and each appended to *that* profile's own series, so a profile the icon isn't showing still accumulates the history it would need if it ever became the one. A profile off the subscription is not polled at all: an API key has no quota window to read, and calling it would spend money to learn nothing (T124 is what makes that knowable). The icon is one number and cannot be two, so one profile owns it — Settings.MonitoredConfigDir, defaulting to the profile a bare claude uses — and switching it clears the burn history first, because a slope fitted across two accounts' readings would project about neither and would read the jump between them as a phantom reset. Where the other profiles' numbers went is a finding, not a preference: the design said "the tooltip and Statistics carry the rest", but the Windows tray tooltip is capped at 127 characters (NOTIFYICONDATA.szTip) and BuildTooltip already compacts its projection sentence to fit — there is no room for a second account. So they live in a new Profile submenu (hidden with one profile): each profile with its current reading beside it, checkmark on the one the icon follows, click to switch. The tooltip gains one short line naming the monitored profile, ahead of the projection in the character budget, because a percentage without an owner is a lie once there are two owners. Each profile's failure is its own: "sign-in needed" beside the profile that needs it, never a generic prompt that would send somebody to re-login on the wrong account. The Settings cost estimate now multiplies by the number of *polled* profiles and says so in a line of its own. --profiles reports the tray's own decisions — which profile the icon follows, and how many of them are polled — through ClaudeAccount.PickMonitored, the single implementation both the tray and the CLI use, so they cannot disagree. The Statistics window is still one profile's (the monitored one); a per-profile view there is T128.
    T127 · a2311b2
  270. The icon follows the profile you are working in, so switching accounts needs no click. The design opened with TranscriptTail — it already sees which config dir a turn landed in — and that is the one thing this task does *not* do: T101 was dropped precisely because an ambient tray nicety must not keep a tail running all session, and the tail is window-owned so a closed Statistics window watches nothing. Following a profile needs the *fact* that a turn landed, not its tokens, so ProfileActivity probes the newest projects\**\*.jsonl write timestamp per config dir on the poll the tray already pays for: directory entries only, no file opened, nothing resident between polls, ~20ms per config dir over ~600 transcripts (measured, printed by --profiles). The probe runs off the UI thread and *before* the fetch, so the number the icon draws already belongs to the profile it just moved to instead of showing one account's percentage under another's name for a whole interval. Three refusals keep it honest: a profile is followed only if the icon could say something about it (on the subscription, credentials on disk — T124 is what makes that knowable); only a turn inside the last 30 minutes counts, so yesterday's profile can't take today's icon; and a timestamp more than two minutes in the future is discarded as a copied file or a skewed clock rather than believed, which without the guard would have out-ranked every genuine turn and pinned the icon for as long as the file existed. A manual choice still wins: picking a profile stamps a floor with the moment of the click, and auto-follow resumes only once a turn lands elsewhere *after* it — an icon that overrules the user is worse than one that never moves. Off by default, toggled from the Profile submenu (where the switching already happens) or Settings → Claude Code, both hidden on a one-profile machine; the submenu also says how long ago each profile was active, from the last probe's cache, so opening the menu scans nothing. The per-profile colour dot the design floated was not built: the icon's colour already means the projection verdict and its corner already carries the API-error badge, so a third meaning would land on top of one of them — the profile reads from the tooltip and the menu, as T127 left it. One bug this uncovered, fixed here: SettingsWindow copies the settings model field by field and never carried MonitoredConfigDir, so ApplySettings wrote back an empty one — any visit to Settings silently sent the icon back to the default profile. Verified end to end: the tray launched with the icon pointed at an idle fixture profile, and the first poll moved it to the profile turns were actually landing in and persisted the choice. --profiles prints each profile's last turn, which of them are candidates, and where the icon would move to.
    T126 · 28beece
  271. Every store the tray derives from a profile is now keyed by that profile, which is the prerequisite the design put first: fed two accounts, one usage-history.jsonl sums both into a single burn projection, and the result is arithmetic over usage that was never one account's — a wrong chart that reads like a bad projection rather than like a bug. ProfileStore owns the layout (%LocalAppData%\ClaudeTray\profiles\<key>\), where the key is the account when there is one (two config dirs holding the same login are one series) and a digest of the config dir when there isn't (an unauthenticated profile still accumulates context history). UsageHistory, HourlyUsage, ContextHistory, ContextNudges and the ActivityProfile cache all take the key explicitly rather than reading an ambient "current profile" — polling several profiles at once then means passing different keys, not mutating shared state, so nothing built here has to be torn out for T127. Two caches stay shared on purpose (context-cache.json, context-usage.json): both are keyed by absolute path plus a size+mtime fingerprint, so entries cannot be confused between profiles and sharing them avoids rescanning what two profiles have in common. ApiClient takes a config dir, because a rate limit belongs to an account and reading two means two clients with two tokens. The migration is the delicate part: every installation that exists has one profile, and its accumulated history — up to eight days of readings plus the *permanent* hourly aggregate folded out of them, which cannot be rebuilt — belongs to it, so the flat files are moved (not copied, which would double the hourly store on the next fold; not left, which would make two builds disagree about which file is authoritative) into that profile's directory on first run, per file, best-effort, never overwriting. Verified against the real store: 1,604 readings and 11 folded days moved intact, the activity profile still read from its cache, then restored to the pre-test layout so the running release keeps its data until it is actually upgraded. No user-visible change — this is the plumbing under T127.
    T125 · 4f8668b
  272. Say which credentials the sessions actually use, because the tray was quietly reporting the wrong thing. CLAUDE_CONFIG_DIR isolates *files*, not *authentication*: on a machine with a machine-level ANTHROPIC_API_KEY, a config dir with no completed OAuth login answers {"loggedIn":true,"authMethod":"api_key"} — and that work bills to the Console and never touches the 5h/7d windows, while the tray keeps reading the OAuth token and drawing a near-idle subscription. The icon could honestly say 5% while somebody worked all day. ClaudeAccount now resolves each profile's effective auth from files and the environment (AuthMethod: subscription / API key / Bedrock / Vertex / none) following Claude Code's measured precedence — a completed OAuth login in the dir wins, otherwise an env key takes over — reading env vars, the profile's settings.json env block and apiKeyHelper. Only the presence of a key is ever read: no value is parsed, stored, logged or shown, including from a settings file where a key would sit in plain text. The System information page grows an Authentication row (method + where it came from + whether it was inferred or confirmed) and, when a profile is not on the subscription, an accent-bordered card that says it plainly: the plan and percentages above — and the tray icon — do not describe what Claude Code is spending here. Check asks the CLI itself (claude auth status --json, which also answers for API-key/Bedrock/Vertex setups where no oauthAccount exists), off the UI thread, and on failure leaves the inferred reading standing rather than blanking it. Local inference and the CLI agreed on both a subscription profile and an API-key one. Two bugs the verification turned up, both fixed here: the reader took the first parseable settings file, so a near-empty ~/.claude/.claude.json (anything that runs with CLAUDE_CONFIG_DIR=~/.claude creates one) shadowed the real ~/.claude.json and the page fell back to a coarse "Claude Team" with no holder, org or dates — it now prefers whichever file actually names an account; and the check itself was *creating* that litter by overriding the config dir even for the default profile, which it no longer does. --profiles [--check] prints the auth reading, whether it counts against the subscription, and the CLI's confirmation beside it.
    T124 · d93d67d
  273. Switching profiles, and the list that makes it possible. Settings.Profiles is the tray's own record — {Label, ConfigDir, WorkDir} and nothing else: no address, no token, no plan, so the tray never becomes a second store of somebody's credentials and a /login that changes the account inside a dir needs no update here. The editor lives on the Claude Code page (that page is about launching Claude Code, which is exactly what a profile decides — and it already has the Save button, which the read-only System page deliberately hides): add / rename / remove-from-list, where rename is just a text box (no dialog) and typing into a discovered profile is what registers it, since a name only persists if the tray remembers it. Remove never deletes the folder — it holds transcripts, memory and settings — and if discovery can still see the dir it comes straight back as a discovered profile, which is the honest meaning of "remove from list". Then the tray's Open Claude Code becomes a submenu, one entry per profile, launching with that profile's CLAUDE_CONFIG_DIR and its own working directory (without which one click would open the work account inside a personal repo). Not a single write into a config dir: ProcessStartInfo.Environment carries the dir (which forces UseShellExecute = false — noted in a comment, since Environment is silently ignored otherwise), and Claude Code creates a missing dir itself. A profile with no credentials file is marked *"— entrar"* and its entry runs claude auth login, with --email prefilled only from an oauthAccount the config already names — the one case where the tray knows the address without ever having asked for it, and it is passed only when it is unambiguously an address, since it lands on a cmd.exe command line and comes off disk. The separate per-profile sign-in item the task sketched was folded into this: the only profile that needs one is a profile without a login, and a second submenu of the same names is menu bloat. The submenu is rebuilt in place when the list is saved, so it never waits for a restart. Verified against a synthetic second profile: --profiles now prints each entry's menu label, resolved command, CLAUDE_CONFIG_DIR and working directory — the same helpers the click path uses, so the decision is checkable without a tray. One efficiency fix the work exposed: the window was running two full discovery sweeps (the editor's and the System page's) — now one, invalidated when the list is edited.
    T123 · 6f31d1e
  274. A profile is a config dir, and the tray can now see all of them. ClaudeAccount.Read takes a directory (it read one global path before) and Discover returns every profile on the machine, default first, from six sources with no guessing: the dir this process would use, ~/.claude, an env.CLAUDE_CONFIG_DIR in either one's settings.json, the same key in each registered project's <project>/.claude/settings.json / settings.local.json (files ContextScanner already opens, so the source is free), the ~/.claude-* convention, and a caller-supplied registered list (which T123's editor will feed — --profiles [dir...] exercises it today). A candidate qualifies on .credentials.json, a .claude.json naming an account, or explicit registration — that last clause is what will cover a dir that exists but has never been logged into, and it is why an *un*registered empty dir is correctly skipped rather than listed as a profile with no plan. The bug this task existed to avoid: the old reader fell back to ~/.claude.json whenever <dir>/.claude.json was missing, which for a second profile would have read the default account's identity into it — the fallback is now offered *only* for the home dir, verified against a synthetic second profile that came back with its own account uuid, its own plan and its own project count. Identity is the accountUuid, so one account reached through two paths (a copied config, a junction) is listed once, under the first path. Labels are derived, never typed: the organization name, else "Personal" for an account without one, else the folder name. The page grew a picker rather than one stacked card per profile — six rows times N profiles is a scroll wall, and repeating a card per profile means building layout in code-behind, which this repo deliberately doesn't do; the picker keeps the page one screen and switching it re-renders every row from that profile's own files. Hidden entirely when there is one profile, which is the normal case. UNC project paths are skipped and the probe is capped at 200, because a dead share can block a stat for seconds and this runs while the window is being built.
    T122 · 6576dc1
  275. A fixture account, so the page can finally be published. Every other window in this repo has a screenshot in the README and on the site; this one had none, because the only account on a real machine is the developer's and masking is not enough — it hides the holder's name and the local part of the address, but the organization name and the mail domain are the reading, and here the organization is a client's. AccountFixture.cs builds a throwaway pair of config dirs — a personal Max 20x (no organization, so that row collapses) and a Team seat (organization, type and role) — and hands them back through ClaudeAccount.Read, so the fixture is parsed by the very code the page uses rather than by a stub that could drift from it. The third case, no OAuth account at all, needed no fixture: an empty directory produces it. Behind --settings System --sample (plus --reveal, which opens with the holder unmasked — safe only over the fixture), reaching the window through one seam, DiscoverProfiles(), so no page has to know whether it is looking at this machine; --capture-settings and --settings-tray take the same flags, and page/flag arguments are now order-independent (PageArg). Nothing is written near the real configuration, and the credentials file carries no token even in the fixture — only the three fields ClaudeAccount actually reads (expiresAt, subscriptionType, the length of scopes), because a stand-in token would be a secret-shaped string on disk for no benefit. ContextFixture's "somewhere the user's name isn't in the path" rule moved to SampleRoot and is now shared by both. Published docs/system.png (as the page opens: profile picker, plan, credentials in use, masked holder) and docs/system-account.png (the same page after Show: name, email, organization and role) into the README section and the site block, both captioned as a sample account. No new user-visible strings — the fixture is a dev surface.
    T121 · dac2543
  276. A System information page in Settings, answering "which plan is this login actually on?" from what Claude Code already keeps on disk. ClaudeAccount.cs reads .claude.json and ~/.claude/.credentials.json into one nullable-everywhere reading: the plan named the way a person says it (userRateLimitTier → Claude Max 5x, with the raw tier, seat tier and billing type as its qualifier line, and an unmapped tier prettified verbatim rather than dropped — a plan we haven't seen yet should still read, and be recognizable in a bug report), the holder, the organization and role, extra usage, the account/first-token dates, and the stored sign-in's own expiry; then the install (CLI version from the native installer's version store, install method, auto-update, project count, the config folder with Open, CLAUDE_CONFIG_DIR honoured) and the machine (tray version and location, Windows build, .NET runtime, architecture, the app's data folder). Read-only by construction — it opens files, never writes one, touches no transcript (§I.1 untouched: this is configuration, not conversation), and reads the credentials file only for expiresAt, subscriptionType and the *length* of scopes, so no token is ever in reach of the UI. Two things it deliberately does *not* do: spawn claude --version (seconds of Node startup to fill in a label; the installer's version store answers it for free, and an npm install honestly reads "—"), and show the identity by default — the holder, the email and every absolute path are masked (initials, a•••••@example.com, paths folded back to ~ / %LocalAppData%) because this is precisely the page that ends up in a screenshot attached to an issue; one button reveals and re-hides them, and Copy for a bug report copies the page as plain text *exactly as displayed*, so a masked holder stays masked in the clipboard. A personal account with no organization collapses that row instead of dashing it, and a machine with no OAuth account at all (API key, Bedrock, Vertex) gets one explanatory card instead of eight rows of "—". 51 new keys across all five lang/*.json; --settings System opens it standalone for the preview loop. Two bugs the screenshots caught before shipping: the native installer's version entries are files, not directories (GetDirectories found nothing, so the version read "—"), and installMethod was being title-cased into "Native" when it is a literal token Claude Code's own config calls native.
    T120 · 8c7b00a
  277. The sweep stops walking all of history every 3 seconds. TranscriptTail enumerated every *.jsonl under ~/.claude/projects on every sweep — metadata-only and individually cheap, but O(all history), running twenty times a minute for as long as the window is open, and growing forever. §V.8 offered two fixes and named the directory-mtime one as the cheaper to try first; measuring it killed it: on NTFS a directory's mtime moves when an entry is added, renamed or removed and not when a file inside it is appended, which is precisely the case that matters — a live session appends to a transcript that already exists, so the "changed directories only" walk would have gone blind exactly while somebody was working. (Recorded in [AGENTS.md](AGENTS.md), since the idea is an inviting one to have twice.) So the other fix: the FileSystemWatcher already knows *which* file changed and was throwing that away, and its paths are now the sweep's work list — a cheap sweep looks at those and nothing else. The full walk survives as reconciliation every ReconcileMs (30s), which is the whole safety argument: a dropped event costs up to 30s of latency and can never strand a file, a watcher that died (_watching == false) forces *every* sweep to be full again, and a renamed file is announced under both names so the old cursor is retired rather than left pointing at a path nobody will visit. The same reconciling pass now prunes cursors for files untouched since the freshness cutoff — they accumulated one per transcript ever seen, and re-priming one later is safe because everything the prime reads back is already past the 300s floor (and _seen covers the clock-skew edge). Measured against the real tree, 544 transcripts: a full sweep 69.31ms, a cheap sweep 0.05ms — 19 sweeps in 40s of which 2 were full, so the walking drops from ~1.3s to ~139ms per 40s, and the per-sweep cost stops tracking the size of the history except twice a minute. Verified on synthetic roots for the three things this could have broken: a project directory and transcript created *after* the tail started are still found (600-file tree: 18 sweeps, 2 full, every turn reported), a partial last line is still held back and reported whole when its newline lands, and with no watcher at all every sweep goes full — 8 of 8 — and finds a file nothing announced. --tail and --live now print full vs cheap sweeps and both timings, so the claim is checkable rather than asserted. Internal: no user-facing surface, no new strings.
    T103 · b45d71c
  278. One API response, counted once, in every local scan. Claude Code writes one `assistant` line per content block — a thinking block and two tool calls are three lines — and each repeats that response's message.usage verbatim under the same requestId. T97 found this while building the tail and de-duplicated there; the two older sweeps that predate it still summed per line, so every number derived from the transcripts charged a turn once per block. Measured on a real week of this machine's transcripts before touching anything: 10,749 of 26,061 assistant lines (41.2%) are repeats, inflating tokens 1.63×, and — the part that matters — the inflation is *not* uniform, since it tracks how many blocks a turn had, which tracks heavy tool use. The burn-up curve hid it because it is rescaled to the live utilization, which fixes the endpoint and not the shape: hour-by-hour, the share of the window each hour carried moved by up to 0.64pp (4.06% total variation) once counted correctly — the same samples WindowPace.MeasuredActiveHours and the projection are calibrated against. UsageReport.ScanTokens now keys on the id the parser already emits (requestId, falling back to message.id), with the set global rather than per file, which also drops the copies a forked or resumed session inherits from its parent transcript; a line carrying neither id is still counted, since it cannot be told apart from a genuine second response and over-counting once beats dropping real spend. UsageInsights had the same defect and is fixed the same way — before/after on the same 24 hours: 5,763 → 3,370 requests (−41.5%, matching the line measurement) and the ">150k context" share moving 75% → 77%, a user-visible number that was skewed by exactly the mechanism above. ActivityProfile is the one reader that needed no change and now says so in a comment: its grid records *presence* ("this hour saw work"), so a repeated line sets a bit that is already set — only its Samples counter saw them, which is why --activity now reports "assistant lines" instead of miscalling them requests. Internal: no user-facing surface, no new strings — the visible effect is that existing numbers are right.
    T102 · ba5a4f7
  279. Drawn is drawn: the right-hand end stops rewriting itself. Reported from watching it live: *"at the end of the chart, when a value rises, the last couple of seconds that were already drawn get remade and behave oddly — what is done shouldn't change"*. Exactly the right principle, and the cause is reporting latency rather than arithmetic: a turn carries the timestamp of the second it finished, and the tail reports it up to a few seconds later (its watcher is fast; its floor sweep is 3s). So a late arrival lands in a bucket that is already *behind* the head, and because the chart recomputed the whole series from the buckets every second, the recomputation raised points that were on screen a moment ago — the shape of the last two or three seconds kept being rebuilt. LiveChart now keeps its own history and only ever appends the newest sample to it: a point takes whatever the rate was when its second was drawn and never changes again. The full series is adopted wholesale only when there is nothing to append to — first render, a changed series set, or a gap in the clock (a hidden window stops it) — which is also what fills the chart with the three minutes that preceded the window being opened, so nothing was lost by freezing the rest. A re-render of the *same* second (a resize, a tab switch) redraws the frozen history rather than importing the recomputation, or a window resize would have pulled the rewritten past back in. The cost is stated instead of hidden: tokens whose report arrives late are counted from the moment the app learned about them, not moved backwards into a second already on screen — which is what every append-only monitor does, and the alternative is a chart that edits its own history. LiveRate deliberately stays a faithful recomputation, so --live's worst redraw drift still measures the underlying instability: 40.67 tok/s against live traffic on this machine, all of it now absorbed by the chart layer.
    T119 · 31fdb29
  280. A poll no longer blanks the window. Reported plainly: *"the consumption screen refreshes every few minutes, and the Throughput tab doesn't need refreshing — the problem is the whole screen blinks when it does"*. It did, and the cause was one unconditional line: Reload() opened with ShowStatus("computing…"), which collapses the entire pane and shows a status line, then restores it when the background pace computation returns. On the first open that is right — there is nothing to show yet. On the tray's poll (every 5 minutes by default) it threw away a perfectly good screen for a few hundred milliseconds, and on the Throughput tab it was worse than cosmetic: that tab reads nothing from the API, so its live charts were being torn down and rebuilt for a recomputation that belongs to the other two tabs. The status now appears only when there is no rendered report to keep (_session is null); a refresh leaves the last good view up and swaps in the new one when it arrives, which is also the honest reading — nothing on screen is stale until its replacement exists, and the footer timestamp says when that was. Error states are untouched: a failed build still takes the pane, because then there really is nothing to trust. Verified by capturing the window *mid-refresh* — a fourth --capture-stats … refresh argument feeds a fresh reading in through the same UpdateSnapshot the tray calls and snapshots while the recomputation is in flight: before, all three tabs captured as an empty pane with "Calculando seu ritmo de consumo…"; after, all three capture their content.
    T118 · 9dcd52f
  281. The left edge holds still. Reported as *"the start of the chart moves back and forth to a beat, and the tip also drifts up and down — the rest of the chart stays constant"*, which is two defects with the same symptom and both were real. Horizontally: the figure ended at the oldest *visible* sample, one sample-width inside the plot, and the 1s slide moved that endpoint across a visible column every second — the newest end never showed it because it starts off the right edge and slides in, so the oldest end needed the same treatment on the other side. The charts now fetch and draw Overscan = 2 samples past the left edge, so the endpoint is always outside the clip. The same pass fixed the *hitch*: a slide that lands on exactly zero waits there for a rebuild delivered by a background-priority 1s timer, which never arrives exactly on time, so the scroll stalled and then jumped a whole sample-width; the animation is now 1.05s, and the rebuild always interrupts a still-moving slide (0.2px of lag). Vertically: Smooth started the attack lag *at the oldest reported point*, so a second's value depended on where the window began — as the chart scrolled, the left-hand end kept being recomputed with a colder filter and re-settled toward its neighbours. The series is now computed and smoothed over SmoothWarmUp = 3τ = 24s before the first reported point, which is the only place it can be fixed: the kernel is boundary-independent (a point depends only on the 60s behind it), the EWMA is not. Measured with a new --live diagnostic that compares each series against the previous one shifted by the seconds that actually elapsed (a late tick shifts by two, and assuming one reports jitter as drift): on a synthetic root whose ramp puts the left edge inside a *rise* — the only place attack-only smoothing carries state — the worst redraw drift falls from 29.44 tok/s to 0.55 on a ~1,700 tok/s series, the residual being the 5% a 3τ warm-up leaves. Both fixture spans grew to match (Samples + WindowSeconds + SmoothWarmUp), so the published image is warm at the left edge in both filters.
    T117 · 4cd9b6f
  282. Bars become two line charts (LiveStrip.cs → LiveChart.cs). Asked for after watching the real thing: *"the time is spaced out between one burst and the next — with lines the idle period would fall progressively and rise again when new throughput arrives, like Task Manager"*. Right, and the reason it was spikes-and-holes is the write time: a turn's whole usage lands in the second it finished, so the per-second buckets are events, not samples — joining them with a line would have drawn a rate that never existed. The charts therefore draw T114's rolling rate instead: continuous by construction, decaying through a pause because the work really is ageing out of the 60s window and rising when new work lands, with the newest point equal to the number printed above (same kernel, same attack smoothing). Unstacked 1.8px lines, one per series, colour by sticky slot so nothing changes hands mid-read. Two charts, always both — per project above, per token type below, Task-Manager style — which fixes the second thing reported the same evening: the single chart *switched* from the by-type view to the by-project view as soon as a second repo started generating, so its colours changed meaning while it was being read. Neither view is now a mode, so there is nothing to switch. T112's rule survives in the form a line can carry: the ceiling still falls back to the 95th percentile when the peak runs away, and the runs above it are drawn dashed along the ceiling instead of as broken-bar caps — though the kernel compresses a 240k-token second some 30-fold on its own, so it fires far less often. Consequences handled: the --stats live fixture generates 240 seconds (the chart's 180 plus the kernel's 60) so the left edge is warm rather than ramping in from zero, and its deliberate cache write was resized from 240k to 47k — through the kernel the big one drew a hump 25× the ordinary rate and flattened the four projects the image exists to show. Five strings rewritten across all five lang/*.json (a "strip" of columns is now "two charts of the same rolling rate"), plus two captions; the method note says the decay is the work ageing out of the window, not smoothing. Verified against the fixture and the real tail: three repos crossing, two of them decaying to 0.00/s while still on the chart, and both charts' ceilings labelled independently.
    T116 · c28201f
  283. A project's series stops blinking. Reported from watching the real chart: *"if a session stops for a moment its whole graph disappears, and when it comes back its old history comes back too — so a session blinks over time instead of staying until it ages off"*. Exactly right, and the cause was one word in Projects(): membership was tested with win > 0 — tokens in the trailing 60s rate window — while the chart draws 180 seconds. A repo that paused for longer than the rate window was therefore dropped from the result entirely, taking its still-live history with it, and the next turn brought the whole thing back. DrawnSum replaces that test: a project is in the result while it has anything the chart could still draw (the drawn span plus the kernel's reach behind its oldest point), so it now decays toward zero and ages off the left edge like the data it is. Ranking still uses the rate — who is busiest *now* is what decides who gets a slot — so the fix does not disturb T114's stickiness; it is what makes it hold through a pause, which is when a fixed colour is worth having. Callers pass the span they draw (Projects(LiveStrip.Seconds)). Proved with a synthetic root rather than by waiting: a project whose only turn is 100s old — outside the rate window, inside the drawn span — now comes back listed in slot 0 with its rise-and-decay intact and 0 tok/s at the right edge, where before it was absent from the result. No new strings; the fix is that existing ones stop flickering.
    T115 · 6ad2379
  284. LiveRate reports the headline *as a series*, plus sticky slots. The rate the row prints was only ever available for one instant — "now" — so the picture under it had to be built from the raw per-second buckets, which are events: a turn's whole usage lands in the second it finished (the transcript timestamp is the write time), which is why the strip read as spikes separated by holes rather than as a flow. RateHistory(n), TypeRates(n) and Projects(n) now evaluate the *same* age-weighted 60s kernel at every one of the last *n* seconds, so each point means "tokens/s as it stood then" — a continuous quantity that decays through a pause because the work really is ageing out of the window, and rises when new work lands. That is what makes a line honest here, where joining the raw buckets would have drawn a rate that never existed. The 300s ring turns out to be exactly the right size: every point needs the 60s behind it, so MaxRateHistory is 240 — 60s of headroom over the 180 the chart shows. The headline's attack-only lag is applied along the series too, so the right-hand end equals the number printed above it (verified live: 305 tok/s at the edge against 305 in the row) — of two numbers that disagree on screen, both lose. The cost is stated where it lives: smoothing the parts is not quite smoothing the whole, so per-project series stop summing to the total *during* an attack, which is why they will be drawn as separate lines rather than stacked. Second half: `Slot`, a project's fixed colour/legend position, claimed on first appearance and released only when it has nothing left in the window, with Projects() returning slices in slot order instead of by rate. Rate order is right for a snapshot and wrong for a chart — it would swap two lines' colours mid-flight exactly when the busier repo changed. Who fills the four slots is still decided by rate, so a newcomer arriving with all four taken folds into the residual until one frees. --live prints each project's slot and, at the end, every series as a sparkline with its last value, so both claims (stable slots, edge equals headline) are checkable without a window; LiveRate.RateFrom applies the same kernel to a plain array, which is the seam the --stats live fixture will draw through. No user-visible strings: the chart that consumes this is T115.
    T114 · 9668424
  285. The method note becomes an ⓘ. Six lines of 11px prose sat pinned under the panes on every tab, permanently, to explain where the numbers come from — a lot of screen for something you read once, and (reported from the field the moment the Throughput tab made it conspicuous) it read as a wall. It is now a 20px round info button beside "Updated …" in the footer, opening a popup with the same text under a "How these numbers are measured" heading: quiet by default, accent-filled while its popup is up, so the toggle state needs no second control. Click-away closes it (StaysOpen="False" with a two-way IsChecked binding, so the button un-checks when the popup dismisses itself), and it is hidden — and force-closed — whenever the panes are, since a method note has nothing to explain over a status message. A popup is its own top-level window, so --capture-stats (a RenderTargetBitmap over the window's content) structurally cannot see it; --stats method opens it for the on-screen capture script instead, and that is how it was verified. One new key across all five lang/*.json; the note's own text is unchanged, so there is nothing new to say in the README or on the site. The ~110px it gave back went straight into the live strip (180 → 280px), which is the whole reason the block exists: a rate this spiky is a pixel problem before it is a design one. Published docs/statistics-*.png regenerated without the footer wall.
    T113 · b4f9ef5
  286. One second may no longer flatten the minute. Even with T110's honest axis and T111's 220px of height, real traffic has a dynamic range a linear scale cannot hold: measured here, a single turn writing a large cache block lands ~240,000 tokens in one second beside ordinary seconds of ~2,000, so scaling to the peak put a whole minute of actual work on the 1px floor — a chart that was technically faithful and practically unreadable. The ceiling is now the 95th percentile of the busy seconds on screen, and the columns that run past it are drawn broken: fill stops 4px short, then a 2px cap in the primary ink (not a softer grey — it would be mistaken for the grey "others" series it can sit directly above) says *this continues*. Truncation is from the top of the stack, so every segment keeps its true scale; squashing the column to fit would have misreported all of them. Three guards keep the cut from costing more than it buys: the quantile is taken over busy seconds only (including idle ones drags it to zero and clips everything), clipping needs 12 busy seconds (on a sparse strip the few bars are the whole story), and it needs the peak to be a genuine outlier — 2× the quantile — so ordinary traffic keeps a fully faithful peak-based axis and nothing is cut to buy contrast that isn't needed. Clipping data is a thing a monitor may only do out loud, so the hover changes when it happens: how many columns were capped, and what the busiest second actually carried (singular and plural keys, ×5 languages), and it is rebuilt only when that *text* changes, since the peak moves every second but "240k/s" does not. The method note was reworded in all five languages for the same reason. The --stats live fixture gained the outlier it was missing — without one it published a strip the app rarely draws. Verified against the fixture and the real tail in English and pt-BR: with the spike present the axis reads 3k/s and three columns carry caps; on ordinary traffic (peak 7k, quantile 5k) nothing is clipped at all and the axis is the rounded peak, exactly as before.
    T112 · bbaeeb7
  287. The live strip gets its own tab. Asked in the field as *"the 5h chart is the same as the week one, right? could you make a Throughput tab?"* — and the premise was exactly right about the part that mattered: the charts differ, but the live row (number, strip, per-project legend) has no window scope, so "now" was rendered twice, identically, under two different charts. It is now one strip on a third tab, Throughput, at 220px instead of 56 — which is what a 1000:1 dynamic range actually needs (one 240k-token cache write next to seconds of ~2k). The pace tabs keep both *numbers* — the window average with its type legend, and the one-line "Now ≈ N tok/s" — because the pacing report is where you would miss the live rate, and lose only the duplicated picture; each recovers ~90px in a pane that scrolls. The new tab pairs the strip with a stated time axis (3 min ago … now, which 56px had no room for) and, under it, both window averages captioned by window: same tokens, slower clock, so the fast number has something to be read against. Consequences handled rather than discovered: two LiveStrip instances collapse to one (_stripS/_stripW → _strip), the geometry is skipped entirely while another tab is selected (the *number* still ticks on every tab — it is text), a tab arrival repaints without waiting for the next second, and the swatch-suppression rule moves from a global to a per-call-site besideStrip flag: the Throughput tab's averages drop their swatches while the strip above them is stacked per project, and the pace tabs — which no longer sit next to anything coloured — keep theirs. SaveAllTabs gained a third snapshot (-throughput.png), so --capture-stats … live publishes the new tab. Five keys across all five lang/*.json, plus the method note reworded in each: it says "the live strip on the Throughput tab", since it was describing something no longer on the tab you were reading it from. Verified in pt-BR and English on all three tabs; published docs/statistics-*.png regenerated and the site/README gained the third shot. Fixed on sight right after (same day, from a screenshot): the tab shipped taller than its viewport, so the pane scrolled and cut its last line in half — the two window averages became one line each with the window named inline (the per-type split of a *window* average belongs on that window's own tab, not repeated here), the strip settled at 180px, and the content gained a bottom margin so nothing sits flush against the clip edge. Its heading lost the phrase "the slow clock", which read as a riddle to the first person who saw it, for "Window averages — the same tokens measured over a far longer period". With the strip off the pace tabs, the swatch-suppression rule and the _lastByProject flip that drove it were deleted rather than kept: the coloured legend they were protecting is a tab away now.
    T111 · 84c9e3a
  288. The strip's height means something. Two defects behind one symptom, reported from the field as *"the bars are tiny and sometimes there are numbers"*: the row walked its series from index 0 of a feed that keeps 300 seconds while the strip is 180 columns wide (LiveRate.HistorySeconds > LiveStrip.Seconds), so it drew the *oldest* three minutes and clipped the newest two minutes off the right edge — and it then scaled what was left to the tallest bucket in the whole feed, which could be a burst that had already scrolled away, flattening everything on screen onto the 1px floor. It now walks backwards from the newest bucket (one extra column laid out past the left edge, so the slide still has something to bring in) and takes its peak from exactly what it draws. With that fixed the scale is also *stated*: full height is the visible peak rounded up to a readable step (1 / 1.5 / 2 / 2.5 / 3 / 4 / 5 / 6 / 8 per decade, so rounding up costs at most a third of the bar height), ruled across the plot at the ceiling and at its half and labelled in tok/s in a 40px right-hand gutter — Task Manager's treatment, and inside the strip so it costs no layout row (§V.9's one constraint). The labels sit on the plate rather than floated over the plot, so they never cover a column, and the rules live outside the sliding layer, because a number attached to something that moves reads as data rather than as scale. The gutter is dropped below 220px of width, the half-mark is dropped when its label would crowd the ceiling's, and the whole axis disappears while the strip is flat — a scale with nothing drawn against it claims a precision that isn't there. Hover says what full height means; the strip grew 44→56px so two 10px labels have room. Resource *references* rather than resolved brushes for the rules and labels, since the strip is built once and outlives a light/dark switch. Two keys across all five lang/*.json. Verified against the deterministic --stats live fixture and against the real tail — which is where the scale fix earns itself: one 240k-token second (a large cache write) now reads as 250k/s instead of as an unexplained wall of 1px bars. Published docs/statistics-*.png regenerated.
    T110 · 155330e
  289. SafeWalk.cs: one unreadable directory can no longer blank the app. Reported from the field as *"couldn't build the report: the path cannot be traversed because it contains an untrusted mount point"* — a memory junction inside ~/.claude/projects that Windows' Redirection Guard refuses to traverse, which aborted the framework's AllDirectories enumeration and with it the entire Statistics window: no charts, no numbers, just the error. The same single point of failure sat under every local scan (UsageReport.ScanTokens, UsageInsights, ActivityProfile, ContextUsage, TranscriptTail and the recursive ContextScanner helpers), so a denied ACL, a session folder deleted mid-sweep or a path past MAX_PATH anywhere under projects/ cost the user everything downstream of it. The walk is now per directory, so a failure is scoped to the subtree that caused it. Three things the naive version gets wrong and this one doesn't: entries are materialized (GetFiles, not EnumerateFiles) — the try/catch wrappers the codebase already had around lazy enumerables caught *nothing*, because the IO happens later inside the caller's foreach; a reparse point is replaced by the path it names before anything opens it, which both sidesteps the blocked traversal (opening the real directory is allowed — only going *through* the link is refused) and walks a shared target once instead of once per junction (this machine has three pointing at the same _turing-shared\memory); and children come back as DirectoryInfo, whose attributes the enumeration already filled in, so detecting those reparse points costs no extra syscall — ~46ms vs ~34ms over 600 transcripts, and a fresh context scan 95ms vs 73ms. Verified against a hostile fixture (a junction plus a directory with read denied by ACL) where the framework walker throws and the walk returns every readable file exactly once, and against the real tree for equivalence: the --context report is identical line for line, 969 files across 35 projects, and the Statistics window renders in pt-BR. Depth cap plus a visited set for the junction-pointing-at-its-own-ancestor case. No user-visible strings — the fix is that the existing ones stop appearing.
    T109 · b5156ce
  290. Which project is burning it. The strip stacks per project and the live row says how many sessions are active, which across five repos is the question actually being asked — not *how fast* but *where*. Attribution is free: the transcript path already names the project. Deliberately adaptive — with two or more projects generating, the stack is per project; with one there is nothing to attribute and the by-type mix stays, because folding a single-project strip to one flat colour would lose the input/output/cache-create split for nothing. A session counts as active on a turn in the last 2 minutes, not on file presence, or every terminal left open all week would inflate the count. Palette run through the data-viz validator rather than eyeballed: four categorical hues in fixed order (blue/orange/aqua/yellow) plus an achromatic residual, validated as a stack against both themes' surfaces — worst adjacent CVD ΔE 9.1 light / 8.4 dark, worst normal-vision ΔE 20.8 / 16.9, all ≥3:1 contrast in dark. Past four projects the rest fold into one grey "+N others"; a fifth invented hue is never the answer. Every series is direct-labelled with its own rate, and while the strip is by-project the window-average legend drops its swatches for a plain "window average:" caption — otherwise the same three hues would mean token types in one legend and projects in the other. Two bugs found by looking at the output rather than at the code: naming a project by GetFileName(cwd) is wrong because cwd is the working directory *of that turn*, so one cd docs/_preview inside a session renamed the project to "_preview" — and the slug can't be split back into a name either (it encodes separators and literal dashes identically, turning …-shio-2026-3 into "3"), so the name is now recovered by walking the cwd up to the ancestor whose encoding equals the slug. The per-project rates sum to the headline, because the age-weighted kernel is linear — verified live at 141+132=273 tok/s. Nine new keys across all five lang/*.json; --live now prints the split and the active-session count.
    T100 · 2744bd5
  291. The throughput row moves (LiveStrip.cs). The static stacked mix bar under each chart becomes a strip of the last three minutes — one column per second, entering right and leaving left, keeping the input/output/cache-create split — with *"Now ≈ 870 tok/s · 52.0k tokens in the last 3 minutes"* above it. The window average, the charts, the verdict chip and the legend are untouched: this is a row, not a re-timing of the pace report, and the two numbers are deliberately adjacent because they answer different questions. Motion at 1 Hz without per-frame work: the geometry is rebuilt once a second (three StreamGeometry layers, one per token type) and a single TranslateTransform is animated from one column-width to zero over that second, so the rebuild's one-column jump and the slide cancel exactly and the strip scrolls continuously. Every consequence of "the motion is the data" is enforced rather than assumed — nothing running ⇒ nothing moves (an empty strip is cleared once and then left alone; no spinner, no breathing pulse, which would imply activity during idleness), hidden ⇒ stopped (both IsVisibleChanged and StateChanged, since a minimized WPF window still reports IsVisible), and the tail is owned by the window so a closed Statistics window watches nothing. A non-zero second is floored to one pixel — a single small turn is information and must not round to "idle". The method note gains a sentence: an empty strip means no turn landed *on this machine*, never that nothing is running. Five new keys across all five lang/*.json; the window grew 60px for the taller row. Verified by screenshot in English and pt-BR, against the real tail and against --stats live / --capture-stats … live, which feed a deterministic synthetic three minutes so the published image is reproducible; the published docs/statistics-*.png were regenerated.
    T99 · bfb846f
  292. LiveRate.cs: the rolling rate that sits beside the window average, never instead of it. Non-cache-read tokens/second over a trailing 60s, fed by the tail and recomputed on a caller-driven tick rather than on an API poll — no timer inside, which is what lets T99's strip stop dead when the window is hidden and keeps the decay honest (a rate recomputed only on arrival holds its last value forever through a pause). The window is age-weighted, and that is the whole design: a plain rectangular window was built first and measured wrong in exactly the way this metric must not be — one 60k-token turn pinned the display at 1,000 tok/s for a full minute after the work stopped, then dropped to zero in a single step. A triangular kernel (full weight at the current second, zero at the far edge, weights summing to W/2 so sustained work still reports its true rate) makes a pause decay from the moment it starts. Smoothing is attack-only — a large turn arriving in one second is eased in with an 8s first-order lag, but downwards the rate follows the window exactly, so it reaches a true zero 60s after the last turn instead of being snapped there from whatever the lag left behind. Verified against a controlled synthetic burst: eased attack to a 1,415 tok/s peak, then 1200→1000→800→600→400→200→0, linear and exact. Cache reads are excluded from the headline for the same reason WindowPace excludes them, and exposed separately rather than silently — on real traffic that is ~30,000 tok/s of cache read against ~150 tok/s of real work, a 200× ratio that would have drowned the signal. Quiet deliberately means "no local turn landed", not "the user is idle": usage from another machine or claude.ai leaves no transcript here. Internal: no user-facing surface yet; --live [seconds] [--raw] [--root <dir>] prints the rate once a second with the trailing-window split, labelled *throughput, NOT quota*.
    T98 · d4ce3e0
  293. TranscriptTail.cs: a byte-level tail over ~/.claude/projects/**/*.jsonl, so a turn that lands is noticed within ~250ms for the cost of the bytes it appended. A per-file cursor that only ever advances past a newline, a FileSystemWatcher as the fast path and a 3s sweep as the floor — the watcher is a hint, not a guarantee (its buffer overflows, and it can error out), and a missed event must cost latency, never freeze the feed. The two named failure modes are handled rather than discovered: a shrunk file resets its cursor to zero instead of seeking past the end, and a partial last line is held back until its newline arrives instead of being parsed as garbage and lost — both verified against a synthetic root, along with per-project attribution. First sight of a file reads back a bounded 256 KB tail so a consumer starting mid-burst has a rate immediately; anything older than the 300s freshness floor is discarded. Parsing is UsageReport.TryParseSample, now shared rather than duplicated, so §I.1 holds by construction. Measured on the dev machine: 602 transcripts per sweep in 17ms (metadata only — a file untouched since the floor is never opened) and 21 KB read across 40s of two live sessions. Found and fixed on the way: Claude Code writes one line per content block of a single API response, each repeating that response's usage verbatim, so counting lines counts a thinking-plus-tool-use turn twice — the tail dedupes on requestId (falling back to message.id), pruned to the freshness window. Internal: no user-facing surface; --tail [seconds] [--root <dir>] prints turns as they land plus what the sweep cost.
    T97 · f33ec56
  294. The projection stops merely reporting a problem and says what to do about it: *"Stop now and pick it back up around Jul 30, 13:00, and you'd close the week at about 97%."* ActivityShape.Advise walks forward from the next whole hour and returns the earliest hour work could resume at while still closing the week under 98% at the same measured pace per active hour — leaving 2pp of headroom, because advice that lands on 100.0% is advice to get blocked. Only hours the user normally works are offered as an answer (p ≥ IdleThreshold), so it lands on the start of a working stretch rather than on an arbitrary minute or at 03:00, and skipping a night costs nothing in the model, which is why the close often comes in comfortably under the target. When no resume hour works — even the last working stretch overshoots — no sentence is produced at all: the warning stands alone rather than inventing advice. Sequenced last in the block on purpose, since advice built on a shaky projection is worse than no advice; it only ever appears when T87's shape is confident enough to be drawn. Four new keys across all five lang/*.json ("used" and "remaining" framings). The tray notification threshold is deliberately untouched — it stays the wall-clock verdict.
    T90 · 27bc57d
  295. Last week's burn-up, drawn faintly behind this week's on the same axes (HourlyUsage.PreviousWeek). "Is this week worse than the last?" is a question the chart could not answer at all, and the ghost answers it without a second chart, a second tab, or a single new number on screen — the same metric, the same transform, the same accent colour at 32% opacity and 1.5px, drawn first so the live curve always wins where they cross. It reads the folded hourly aggregate, which is the whole reason T88 exists: the raw log is pruned at 8 days and structurally cannot reach back two weeks. Two guards against a ghost that lies: below 50% hour coverage or under 2% total spend it isn't drawn at all, because a line recording *the app having been closed* would read as a quiet week — the one thing it must not do. The hover on its endpoint gives both numbers that matter (where last week finished, and where it stood at this same point in the week), keeping §IV.4's "no new number" constraint. A "last week" legend item appears only when the ghost does, and --stats shape ghost / --capture-stats <base> ghost draw a synthetic previous week so the line could be verified today rather than after two weeks of folded history (same reasoning as T78's --demo-history).
    T89 · 5489d82
  296. HourlyUsage.cs: a permanent per-hour summary of quota actually spent, folded out of usage-history.jsonl before its raw lines are discarded. The raw log is pruned at 8 days, so until now everything older than a week was simply thrown away — which is why the activity shape had to *infer* idle from transcript timestamps. Each expiring day now becomes 24 spends plus 24 coverage counts first (156 bytes/day measured, against ~230 KB/week of raw log), and coverage is stored separately from spend precisely so "we took 20 readings and nothing moved" (measured idle) stops being indistinguishable from "the app wasn't running" (unknown). Spend is the sum of *positive* weekly-utilization deltas between consecutive readings, attributed to the later reading's hour; a drop in utilization is a window reset, not negative work, so it contributes nothing. Folding runs inside UsageHistory.PruneIfStale — nothing is discarded before it is counted — takes only *complete* local days, and skips days already in the store, so re-running it over the same lines cannot double-count (verified: a second fold of 1,811 readings added +0 days). MeasuredProfile() reassembles the same 168 buckets as ActivityProfile from this store, giving the inferred grid an independent second opinion; --activity --measured prints both heatmaps and their agreement, and --activity --fold forces the aggregation so it can be inspected today rather than in a week. Internal: no user-facing surface — it is the storage T89's ghost curve needs.
    T88 · 4fffa54
  297. The weekly projection now follows that shape instead of a uniform slope (ActivityShape.cs). Calibrated per *active* hour — rate = util / active_hours_measured_in_this_window, with the measured hours coming free from the transcript samples UsageReport already scans (WindowPace.MeasuredActiveHours) — then spent forward as util + rate × Σ p over the remaining wall-clock hours. It degrades correctly by construction: where the remainder's active/idle mix matches the elapsed one it reproduces the old straight line exactly, and it only diverges where the mix genuinely differs. On the dev machine's real profile the landing moved from a straight-line time to Thu 13:45, a working afternoon. Faint amber bands mark the projected-idle stretches (red stays reserved for OutageBandBrush = "no reading was logged"; past idle gets no band, the curve is already flat there), with a "usually idle" legend item that appears only when there are bands, and the bands clip at the landing point rather than decorating a stretch the projection has given up on. Deliberate scope: weekly window only, the grey even-pace line / verdict chip / tray icon stay wall-clock, and below 3 weeks of coverage (or under 2 measured active hours, or already at the limit) Build returns null and the straight line is drawn instead. Copy hedges — "around 13:45", never a to-the-minute claim — and the method note states the limitation that usage from another machine or claude.ai leaves no local transcript, so locally-detected idle isn't necessarily real idle. Ten new keys across all five lang/*.json. The profile now refreshes off-thread: a stale cache is returned immediately and recomputed in the background, so only the first-ever call pays the 15s scan. Also fixed a clipped pane: the projection sentence under the throughput bar was cut off at the bottom of both tabs (worse with the longer method note), so each pane scrolls when it must and the window is 76px taller. Previews: --stats shape, --capture-stats <base> shape.
    T87 · 42b2d3a
  298. ActivityProfile.cs: a weekly activity *shape* — 168 buckets (day-of-week × local hour) holding p(active), the recency-weighted share of observed weeks in which that hour saw at least one request. Built from ~/.claude/projects timestamps because usage-history.jsonl is pruned at 8 days and structurally cannot see other weeks; only timestamps are read, not even token counts. Weeks are the rolling windows [now−7(w+1)d, now−7w d), so every week contains each bucket exactly once and no partial-week correction is needed. Two anti-overfitting guards: 0.8/week exponential decay (half-life ≈3 weeks) and a blend toward the user's own flat active-rate prior worth 1.5 weeks of evidence, so a single holiday week cannot claim nobody works on Wednesdays — on the dev machine's 6 weeks that puts a 7% floor under the sleeping hours rather than a zero, which is the honest reading given usage from another machine leaves no local transcript. Presence per bucket, not request counts: one frantic afternoon must not outvote five ordinary ones. Cached at %LocalAppData%\ClaudeTray\activity-profile.json (~1.5 KB, 168 rounded floats) and recomputed after 20h; a cold scan of 93,856 requests across 6 weeks takes ~15s, which is why it is a daily cache and not a per-open scan, and it uses substring tests rather than JsonDocument.Parse per line. ExpectedActiveHours(from, to) — Σ p over the hours a span covers, pro-rating both ends — is the accessor T87 will spend quota against. Headless --activity prints the week as a heatmap with per-day active hours, the busiest bucket, and the expected active hours ahead (--numbers for raw percentages, --refresh to force a rescan, --root for a fixture tree). Internal: no user-facing surface until T87 puts the shape on the chart.
    T86 · 61c079e
  299. Docs, and Block I closed out: a README section (what it is, eager vs lazy, evidence, findings, what-if, grade/drift, the CLI surface, and the privacy line), a two-shot docs/index.html block with a nav entry, an llms.txt feature paragraph, and Context… added to the menu list. Both screenshots come from the --sample fixture, and the fixture was moved to %PUBLIC% first so the published images carry neither the developer's username nor any client project name. The site's privacy panel now states the context promise explicitly: sizes, names, timestamps and frontmatter only, never the contents of your memories, instructions or skills — and it never edits them.
    T84 · 9f3543f
  300. ContextFixture.cs + --sample: builds a throwaway ~/.claude lookalike in temp — healthy, bloated, a byte-identical worktree twin, a dead project dir, an empty dead dir, a zero-memory project, a not-a-path shared dir, plus fake transcripts so measured session zero and the evidence column have something to show. All 16 rules fire on it (18 findings), which is what a fixture is for: no more hoping the dev machine happens to have one of each today. Works for the CLI and the window (--context --window --sample), never touches the real cache or the real drift history, and is rebuilt from scratch each run. It also unblocks a publishable screenshot: this machine's project names are client names, so README images have to come from the fixture. The window grew a ScanRoot so --root/--sample work there too (the watcher follows it). Found a name collision on the way: ContextSample was already the drift-sample record from T78, hence ContextFixture.
    T83 · ed869dd
  301. Live refresh: a FileSystemWatcher over ~/.claude, debounced 900ms, re-scans while Claude Code writes memories in another window — and the footer says "· updated automatically" so numbers that move on their own are explained rather than unsettling. A warm scan is ~40-100ms, so it is a plain re-scan rather than an incremental update: far less to get wrong. Two deliberate limits, both in the code doc: the filter is *.md, which keeps the continuously-appended transcripts out (watching them would mean re-scanning forever), and project instruction files living next to the code are outside ~/.claude, so editing one still needs the Rescan button. An errored watcher (buffer overflow, directory gone) is disposed rather than left silently dead. Verified against a real file event: the footer showed the automatic re-scan at 42ms and the selection survived it.
    T82 · 3662c72
  302. --context-report <file.md>: the whole picture as one markdown document — summary, a project table with grade / estimated / measured / findings / state, every finding with its fix and file, the 90-day evidence table, and a "how these numbers were produced" section so a reader can argue with them instead of having to trust them. The archival companion to T77's prompt: 19 KB and 42 findings on the dev machine. Privacy verified rather than asserted — the report names a memory file 10 times while containing neither its body prose nor its frontmatter description. The grade spread across the full table (3 F, 1 D, 4 C, 5 B, 3 A) also confirms T78's bands separate real projects.
    T81 · 3a7a040
  303. Localization closed out and verified. Every task in this block added its keys to all five lang/*.json as it shipped, so this was the audit: a cross-check of every L.T(...) and {local:Loc} against en.json (298 keys used, 0 missing), a sweep for hardcoded Text=/Content= literals in the new XAML and code (none), and the context.* surface promoted to one banner section per file. Added `--lang <code>`, a process-only display-language override (both tokens are stripped before anything else parses the arguments, or a stray "es" would be read as a project name) — verifying that a window still fits in Spanish shouldn't mean editing the user's settings and restarting. Rendered and inspected in es, fr and pt-BR: no English leaking, no clipping, and locale formatting correct throughout (es-ES's "27 %" spacing, French "2,7 Mo", comma decimals).
    T80 · 683b4ab
  304. Optional context-growth nudge, off by default: one toast when a project's startup context crosses a user-set threshold (default ≈15k eager tokens, above the middle of the measured 4k-22k spread), rate-limited by ContextNudges.cs to at most once per project per week and remembered across restarts. Reuses ToastWindow with a fifth, ochre theme and no confetti - every toast until now has been good news, and this one isn't. Settings -> Notifications gains a "Context load" section with the toggle and a token threshold slider. The tray samples context on launch and every 6h rather than per poll (a warm scan is ~100ms of stats, and weekly drift doesn't need per-minute sampling), which also keeps T78's drift history accumulating without the window ever being opened. Previewable via --simulate-reset context and --capture-toast context.
    T79 · 1fd5187
  305. Context debt grade + drift. An A-F grade per project from its scannable eager context plus a penalty per open finding (high 1500 / medium 600 / low 200 tokens-equivalent; info costs nothing), with bands chosen against the measured 4k-22k spread in §III so they actually separate real projects - on the dev machine the heavy worktrees land on F, shio on D, several on C. The letter always travels with its inputs in the tooltip, because a bare grade is an authority claim and this one is a heuristic. Claude Code's own base overhead is excluded: every project pays it equally, so including it would grade the harness. ContextHistory.cs records each project's eager total to context-history.jsonl (same shape and best-effort discipline as UsageHistory) at most once per project per day and only when the number moved, and the detail pane draws a sparkline plus "+≈620 tokens (+2.4 KB) in the last 7 days". Without a week of history the row stays hidden rather than drawing a flat line that would read as "stable"; --demo-history renders a synthetic series so the sparkline could be verified today.
    T78 · 56bf95a
  306. Safe actions, and a decision. ContextPrompt.cs generates a ready-made cleanup prompt for Claude Code — the findings for this project (or the machine-wide ones) grouped by severity, each with its fix and file, plus whatever is ticked in the what-if simulator, and a closing instruction to show the plan before deleting anything. Copied from the window (with a live "N things worth fixing here" count in both views) and inspectable via --context --prompt [project]. The destructive archive-with-undo was deliberately not built: §II.8 always made it conditional, and the app staying read-only against ~/.claude is worth more than a delete button — measuring is a read, rewriting someone's memory directory on a size heuristic is not. §I.4 and the roadmap's non-goals now record that decision instead of leaving it "under design". Reveal-in-Explorer and open shipped earlier with T72.
    T77 · fbfb78b
  307. What-if simulator: tick any row that actually carries eager cost and the gauge redraws without it, with a panel stating what would be freed per session, the ≈cost per session start, and the before/after ("was ≈39k, would be ≈35k"). No checkbox is offered where removal would free nothing — a tick that can't move the gauge would promise a saving that isn't there. Nothing is written: the selection is read-only state in the window, and the panel says so, because the sanctioned actions belong to T77. Previewable with --context --window <slug> --simulate.
    T76 · 2b24bc0
  308. ContextUsage.cs: evidence of use, mined from the transcripts — which skills and agents were actually invoked over the last 30/90 days, surfaced as a "Used 90d" column in the window, a --context --usage report, and a never-invoked rule. On the dev machine: 38 skills/agents never invoked in 90 days, costing ≈4.4k of eager index in every session — the number that turns "trim your skills" into a decision. Reads 736 MB of transcripts in 0.6s via an ordinal-substring prefilter before any JSON parse, then ~18 MB on later runs thanks to a per-file (size+mtime) cache; runs after the window is already useful, never inside Scan. Privacy: exactly two identifier fields are read from a tool call (input.skill, input.subagent_type) and nothing else. Memory files carry no usage annotation on purpose — a recall leaves no structured trace, so the only evidence would be message content, which the app never reads; parked as T85. A first cut suppressed every claim by requiring the file to be older than the window (mtime as a proxy for age, which a plugin-marketplace refresh invalidates) — replaced by wording that states the fact about the window instead of inferring "never".
    T75 · 6d24444
  309. ContextRules.cs: the advisor. 11 grounded rules (oversized eager index / instruction file, dead index pointers, unindexed memories, oversized memory files, missing or invalid frontmatter, byte-identical memory directories, dead project dirs, stale memories, two full instruction files in one root, missing or 500+ char skill descriptions, bloated settings.json), each carrying severity + one plain sentence + the concrete fix — no finding without a fix. Headless as --context --check, grouped by severity: 41 findings on the dev machine, spot-checked against the filesystem (the byte-identical cluster, the 302 KB dead worktree dir, the unindexed dumont memory). Needed two scanner additions — index link targets and a per-memory-file content hash, with a cache-schema version so an older cache can't serve them empty. Found and fixed a real parser bug it exposed: a bare description: followed by an indented multi-line scalar (no >/|) was read as empty, which produced the one false positive and understated every such skill's eager index cost (shared skill index ≈3.1k → ≈3.2k tokens).
    T74 · 2c4a553
  310. Cross-project overview: an "All projects" row leading the master list, with the machine's total footprint, the shared eager context counted once (not once per project), the heaviest session zero, the ten heaviest eager loads as proportional bars that click through to the project, and the two findings that only exist *between* projects — duplicated memory directories and dead project dirs, each with its fix. Detection stays in ContextRules (shared with --context --check) while the sentences are localized here, so the two surfaces can't disagree about what a duplicate is. The per-session column is deliberately blank on that row: there is no per-session cost for 33 projects at once. Previewable with --context --window all.
    T73 · 8551638
  311. Source breakdown made readable and actionable: a tinted load chip per row (eager / lazy / lazy + index / not loaded) in the same hues the gauge uses, where a skill's chip says both of its costs at once; a health dot for the two signals the scan already carries (unreadable, or untouched for 90+ days); reveal-in-Explorer / open on the row's context menu and on double-click — never an edit, per §I.4; and a sort picker (most eager tokens / largest file / oldest first) applied within each kind group. Preview affordance --context --window <slug> --scroll opens on the table, since the detail pane is taller than the screen.
    T72 · 7742ca6
  312. The session-zero gauge, the window's hero: one stacked bar of what a session loads against the whole context window, split into Claude Code's own ≈32k base overhead (fitted from this machine's transcripts when there are enough, else the measured fallback) / instructions / memory index / skills & agents index, with the transcript-measured median drawn over it as a tick — the bar is an estimate, the tick is a measurement, and the gap between them is information. Window size read off the observed model, so a 1M-context session isn't charted against 200k. Verified on a bloated project and on this repo's zero-memory state.
    T71 · d8d6d2f
  313. ContextWindow.xaml(.cs): the master/detail window on the scanner — projects on the left (name, sources, what a session there costs), and on the right the estimated vs. transcript-measured session zero plus every source grouped by kind with its eager/lazy/index word. Settles the naming decision (`Context…` in the tray menu, "Context Load — memories, skills & instructions" as the title) and the first context.* keys in all five languages. Previewable standalone via --context --window [slug]; the bare --context stays the headless report.
    T70 · 9143582
  314. Bounded, cached scanning: 92ms cold / 76ms warm over 33 projects and 940 files, cache keyed on a path+size+mtime fingerprint (directory mtimes would miss an in-place MEMORY.md edit), file/directory caps reported rather than silent, and a parallel IO-bound walk. Caught a real double-count bug where a one-line CLAUDE.md containing @agents.md billed a 43 KB file twice. (159d714)
    T69 · 3c63388
  315. Session-zero calibration from real transcripts: observed startup context for 27 of 33 projects, a robust base-overhead fit (median residual + Theil–Sen slope) putting the unscannable system-prompt/tools/MCP overhead at ≈32k tokens, and a corrected estimate within ±15% of measurement for 23/27 projects (median error 6.2%). Found that cache_read_input_tokens must be counted too — see the ROADMAP note. (159d714)
    T68 · 3c63388
  316. TokenEstimate.cs: chars→tokens for markdown, classified per line (prose / code fence / table), always rendered as an estimate ("≈4.9k"). (159d714)
    T67 · 3c63388
  317. ContextScanner.cs: discovers every context source per project — the user + project instruction chain with @imports, the memory index and its files, skill/agent frontmatter, settings sizes — splits eager (paid on every request) from lazy and not-loaded, and resolves each ~/.claude/projects/<slug> back to a real path by filesystem probing with the transcript cwd as the authoritative fallback. Verified byte-for-byte against find/stat on two projects. (159d714)
    T66 · 3c63388
  318. Opt-in near-limit flash: the icon blinks every 500 ms above 90% (the point the API reports allowed_warning). Off by default — the warning color tracks the limit regardless. (52cba9b)
    T65 · 3c63388
  319. Softer session-expired tray message, framed around usage rather than an error. (54b0e03)
    T64 · 3c63388
  320. Remaining-mode projection wording — "runs out" rather than a "0% left" that reads like a present value. (c675cff)
    T63 · 3c63388
  321. Remaining-mode wording in the reset toasts ("Had 21% left" instead of "Was 79% used"). (3bd540d)
    T62 · 3c63388
  322. "Show remaining instead of used": the icon counts down from 100% and the tooltip lines read "… left", while the projection, warning color and flash still track the limit. (6c1d7cf)
    T61 · 3c63388
  323. Language selection in Settings (auto = follow the OS) with a restart prompt, since localized strings resolve when a window is parsed. (304b9ca)
    T60 · 3c63388
  324. Embedded JSON language resources for five languages: en, pt-BR, pt-PT, fr, es. (efd5acf)
    T59 · 3c63388
  325. Localization.cs (L.T(...) in code, {local:Loc key} in XAML) and every user-visible string extracted to keys. (214093f)
    T58 · 3c63388
  326. Open the weekly tab by default when the session is idle (a flat 5h chart has nothing to show); projection landing time on the chart. (839f913, a0f2bd0)
    T57 · 3c63388
  327. Day boundaries on multi-day spans. (6e3c40f)
    T56 · 3c63388
  328. API-outage handling: charts keep drawing from the last known local data, with an error banner and the outage marked as red "unavailable" spans along the usage line; --stats error / gapdemo / history previews. (4c1993f, 8a4b4c7)
    T55 · 3c63388
  329. Remaining-quota framing carried into the statistics display. (08be503)
    T54 · 3c63388
  330. Auto-refresh: fresh readings pushed into an open window on the same cadence as the icon. (7672898)
    T53 · 3c63388
  331. Throughput metrics. (de9e9ce)
    T52 · 3c63388
  332. Interactive chart elements. (9b31607)
    T51 · 3c63388
  333. Tab contrast/layout pass + screenshots on the site. (0fec258)
    T50 · 3c63388
  334. --capture-stats: off-screen RenderTargetBitmap capture per tab, since a screen-copy capture can't see a covered window. (c41c7ff)
    T49 · 3c63388
  335. UsageHistory: append-only JSONL log of every live reading under %LocalAppData%\ClaudeTray, with age-based pruning, so the charts draw *measured* utilization instead of inferring the shape from token counts. (c41c7ff)
    T48 · 3c63388
  336. Tray left-click opens Statistics (right-click keeps the context menu). (02b1d5b)
    T47 · 3c63388
  337. StatisticsWindow: the consumption-pace report — 5h session and 7d week usage against the clock, with a verdict. (2913ed8)
    T46 · 3c63388
  338. The 5h session reset as its own opt-in toggle, separate from the weekly ones. (a57d6b1)
    T45 · 3c63388
  339. Timestamped reset-event log at %LocalAppData%\ClaudeTray\reset-events.log, so an anomaly can be reported later with real before/after numbers. (9050f4e, de12b7a)
    T44 · 3c63388
  340. --simulate-reset and --capture-toast dev previews, sharing the live notifier's display strings so the wording can't drift. (de12b7a)
    T43 · 3c63388
  341. Minimum-usage floors, so a reset from a barely-touched window isn't worth a ping. (dbd1128)
    T42 · 3c63388
  342. All reset notifications enabled by default. (a57d6b1)
    T41 · 3c63388
  343. ToastWindow: a custom WPF toast with an animated quota bar and four themes (Surprise / Bonus / Weekly / Session), with a system-balloon fallback so an event is never silently dropped. (de12b7a)
    T40 · 3c63388
  344. A distinct color per reset kind. (d4aaf1a)
    T39 · 3c63388
  345. Detect a partial mid-window credit (e.g. 91% → 50%), not only full resets. (15bbb91)
    T38 · 3c63388
  346. Detect an *unexpected* weekly reset (the counter dropping to 0% before its deadline) and notify. (9050f4e)
    T37 · 3c63388
  347. Poll-cadence idling: once a window is maxed out, consumption is frozen until it resets, so the loop sleeps to just past the known reset and double-checks instead of re-reading 100% every 5 minutes. (3e193c3)
    T36 · 3c63388
  348. Header-less API responses treated as transient, which removed a phantom weekly reset. (b44a058)
    T35 · 3c63388
  349. Signed-out and API-error states show the logo (with a red dot for a live error) rather than a misleading 0%; ToS advisory in the auth messaging. (dda4641, 877b273)
    T34 · 3c63388
  350. Transient-failure tolerance: a single timeout or network blip keeps the last good reading instead of flipping the icon to a scary error. (2713f82)
    T33 · 3c63388
  351. Session-refresh vs. full-login distinction: the /login hint is printed only when no refresh token is on disk, because no CLI flag forces a re-login. (ec79f54)
    T32 · 3c63388
  352. Auto-open Claude Code on a 401 (opt-in, once per signed-out spell) + a faster auth-retry poll cadence while signed out. (2235d41)
    T31 · 3c63388
  353. A distinct "needs auth" icon state. (e6dddf3)
    T30 · 3c63388
  354. "Open Claude Code" menu item (cmd /k claude) — launching the CLI is what refreshes the OAuth token. (fff746b)
    T29 · 3c63388
  355. Token-refresh handling and error surfacing in ApiClient. (8073217)
    T28 · 3c63388
  356. SettingsRow control; Display and Claude Code split into their own pages; wider default window so descriptions aren't cramped. (bd85eb4, 538c38a, 374478b)
    T27 · 3c63388
  357. Notifications settings page. (1d236e2)
    T26 · 3c63388
  358. Start-with-Windows via the per-user HKCU Run key (StartupManager) — no admin, no scheduled task. (e6dddf3)
    T25 · 3c63388
  359. --settings standalone preview flag + the preview-ui skill: build → launch → screenshot → judge, so no UI change is reported done unseen. (cb2c3fc)
    T24 · 3c63388
  360. Non-modal settings dialog, reused instead of stacked on repeat opens. (d892992)
    T23 · 3c63388
  361. "Show usage percentage on the icon" toggle (otherwise just the fill bar). (94bde45)
    T22 · 3c63388
  362. Refresh-interval setting with a live cost estimate for the chosen cadence. (4501499, 24a005f)
    T21 · 3c63388
  363. Settings model persisted as JSON under %LocalAppData%\ClaudeTray, clamping every out-of-range or hand-edited value back to its default. (4501499)
    T20 · 3c63388
  364. WPF + the built-in .NET Fluent theme (ThemeMode="System") hosted on the WinForms STA thread: one System.Windows.Application created lazily and never Run(), so both UI stacks share the same message pump with zero extra dependencies. (4501499)
    T19 · 3c63388
  365. Dependabot action bumps (checkout 5→7, setup-dotnet 4→5, upload-artifact 5→7). (76eee87, d5cbe34, 4bcbb64)
    T18 · 3c63388
  366. CI extension: winget manifests updated automatically on release. (28641af)
    T17 · 3c63388
  367. GitHub Actions build/release workflow. (f8f8836, 1d032f3)
    T16 · 3c63388
  368. Version-parsing hardening so both v1.2.3 and the stray v.1.2.3 tag shape resolve. (4ded0bf)
    T15 · 3c63388
  369. winget manifests (alegauss.ClaudeCodeTray) incl. a pt-BR locale, advertised in the README. Removed once (6b6f1e5) and deliberately restored. (bea59a3, ba088cd, 93521a8)
    T14 · 3c63388
  370. Release scripting (update-release.cmd / .ps1) and version propagation across manifests. (20ddd0b, 0dd0171, 3556095)
    T13 · 3c63388
  371. Updater: checks GitHub Releases, downloads and hands off to the installer for in-app self-update; surfaced as a bold menu item + balloon tip. (ecfa21c)
    T12 · 3c63388
  372. Apache-2.0 license + generated social-preview card. (bea59a3)
    T11 · 3c63388
  373. Inno Setup installer + build-installer.cmd — a per-user install with no admin, from a single self-contained win-x64 publish. <Version> in ClaudeTray.csproj is the one place a version lives. (d16f00b, 980e9a7)
    T10 · 3c63388
  374. Last-refresh timestamp in the tooltip; and red (not green) once a window sits at 100%. (a611198, a0b6176)
    T9 · 3c63388
  375. Icon legibility pass: text fitting, glyph centering and scaling verified at 16 / 20 / 32 px. (849a576, ad2ec09, 8f282b7)
    T8 · 3c63388
  376. Multi-resolution app .ico generated from the same GDI+ logo renderer (--makeicon), plus the --render / --social dev helpers. (9577a0c, bea59a3)
    T7 · 3c63388
  377. Single-instance enforcement via a named mutex; a second launch exits silently instead of adding a second tray icon. (9577a0c)
    T6 · 3c63388
  378. UsageInsights: last 24h of ~/.claude/projects/**/*.jsonl aggregated into a cost-weighted breakdown (subagent share, >150k-context share, per-model split) behind the "Usage insights" submenu and --insights. (9577a0c)
    T5 · 3c63388
  379. Projection-driven icon color: the fill bar turns red when the window is on track to run out before it resets. (9577a0c, 30a9e79)
    T4 · 3c63388
  380. BurnTracker: utilization history → least-squares slope → exhaustion projection (Projection.Ok/Danger/Unknown). (9577a0c)
    T3 · 3c63388
  381. ApiClient: OAuth token read from ~/.claude/.credentials.json, anthropic-ratelimit-unified-* headers parsed into the 5h / 7d / extra windows. (47dae10)
    T2 · 3c63388
  382. WinForms tray shell: NotifyIcon + ApplicationContext, with the usage number drawn as a GDI+ vector (GraphicsPath + outline) at the real SM_CXSMICON size under PerMonitorV2 DPI awareness — never a downscaled bitmap. (47dae10)
    T1 · 3c63388

← Claude Code Tray's product page