* Hide the keyboard layout widget on a single-layout install
There is nothing to read or switch when only one layout is configured, so the
label is noise on the bar most people have. Hide it until the keyboard reports
more than one, and keep showing it on a Hyprland that doesn't report the list
at all rather than hiding the widget everywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Put the keyboard layout widget on the bar by default
The widget hides itself unless the active keyboard has more than one layout,
so shipping it costs a single-layout machine nothing and saves everyone else
from finding it in the plugin list. Sit it just right of the clock, and add it
to existing bars the way the agents widget was added, leaving a curated bar
and a disabled widget alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Cycle the layout with the hyprctl command that exists
switchxkblayout is a hyprctl command, not a dispatcher, so sending it over the
dispatch socket only produced a Lua syntax error and clicking the widget did
nothing. Run it instead, against the keyboard the label was read from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add an idempotent bar add command
Nothing put a widget on the bar without going through the running shell:
plugin enable and bar move both forward to it over IPC, which a migration
cannot rely on. Add writes the config file the way position and transparent
already do, and leaves a widget that is already on the bar where the user put
it, so callers can ask for it repeatedly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Put the keyboard layout widget on bars through the bar CLI
The hand-written jq was a normalizer, a presence check and a splice for what
is now one command that carries all three.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep bar add from writing a bar the shell was not reading
The shell takes a user shell.json only when it parses, says version 1, and
carries a bar layout, and does not deep-merge; anything else leaves the
shipped defaults on screen. Reading and writing the user file regardless
turned a config holding nothing but an idle timeout into a bar holding
nothing but the new widget, and made an unparsable one abort the migration
chain on every update. Work against whichever layout is actually in effect,
seeding the defaults before placing a widget they do not already carry.
A malformed hand-installed manifest fails the whole plugin catalog, which was
enough to refuse a first-party widget, so treat an unreadable catalog as no
answer rather than a no. Leave a widget listed in disabledPlugins off the bar
instead of writing a layout entry the registry refuses to load, and re-check
presence inside the mutation so two adds cannot both miss it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read a widget's default bar section in one place
cmd_defaults spelled out the same "defaultSection, or center when it is
missing or not a section" rule that the add path already asks for by name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Rename bar add to bar put
'omarchy plugin add' installs a plugin and 'omarchy bar add' placed one that
was already installed, which is too much meaning for one verb.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Place a newly added bar widget with bar put
plugin add reached the bar through plugin enable, which forwards to the
running shell, so it first had to poll until the shell noticed the clone and
then failed outright when no shell was there to ask. Putting a widget on the
bar is a config edit, so do that directly and leave plugin enable to the
plugins that need registering rather than placing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Put bar widgets through the shell instead of the config file
Placing a widget existed twice: once in PluginRegistry, which the shell uses
and owns the config it holds in memory, and once as jq against shell.json.
The second was there so migrations could run without a shell, which they do
not need to: the Quattro upgrade hands over the shipped shell.json before it
runs any, and every other path runs inside a session with a shell up. Ask the
shell, and say so and carry on when there is none to ask.
putBarWidget enables only what is not already on the bar, which is what a
caller that cannot know whether it ran before needs, and is the one thing the
existing enable path would not do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Clean up the terminal and reconnect when SSH connections drop
A remote tmux, herdr, or editor arms terminal modes over the SSH pipe
(mouse tracking, focus reporting, the alternate screen) that only it can
disarm. When the connection dies instead of exiting cleanly, those modes
stay armed on the local terminal, and every mouse move floods the prompt
with escape-sequence junk.
Wrap ssh in a shell function that disarms those modes after every exit,
and automatically reconnects when an established interactive session
drops. Remote commands, configured RemoteCommands, and redirected stdin
never reconnect, so their side effects cannot replay, and the retry loop
runs in a subshell so Ctrl-C cancels both the in-flight attempt and the
loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Detect dead SSH connections within a minute
Without keepalives, ssh does not notice a dead peer until TCP gives up,
which can take hours of sitting on a hung terminal with remote-armed
terminal modes stuck on. Ship a client keepalive default so drops are
detected in about 45 seconds, letting the shell's ssh wrapper clean up
and reconnect. ~/.ssh/config is read first and wins, so per-host
overrides still apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fail closed when ssh -G cannot resolve the effective config
An unresolvable configuration could hide a RemoteCommand, so treat it
as non-interactive rather than reconnectable. Also strengthen the
tests from Copilot review: assert the complete disarm sequence, and
verify on a real interactive pty that Ctrl-C during a retry attempt
kills the reconnect loop itself, not just the in-flight attempt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Tolerate the explicit RemoteCommand none when probing ssh -G
The literal "none" is how ssh_config cancels a configured
RemoteCommand, and some OpenSSH versions emit it even when unset, which
would have silently disabled reconnecting entirely. Treat it as no
remote command while still failing closed on real ones and unresolvable
configs, and make the fake ssh -G emit the "none" form so the behavior
tests cover it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix hyprctl output parsing in KeyboardLayout plugin
* Never fall back to a non-active keyboard for the layout label
find(k => k.main) returning nothing fell through to keyboards[0], which is
the case the fix is for: on hardware whose first device is a permanently
English (US) radio-control keyboard, the label was wrong and the 10s poll
kept it wrong. The seat can also hold no active keyboard while a device is
re-added, and older Hyprland has no main field at all. Keep the last known
value instead, and skip entries without an active_keymap, since assigning
undefined to the string property throws before the label is ever set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Run hyprctl directly from the keyboard layout widget
The shell wrapper only existed for a pipeline that is gone, so spawn the
command directly, as Style.qml already does for its own hyprctl query.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read the layout from the keyboard the user types on
Every Omarchy install runs fcitx5 for ~/.XCompose, and it binds a virtual
keyboard that takes the seat's main flag whenever it injects. That keyboard
keeps the us layout the input method gave it, so on a machine configured for
another layout the widget flipped to ENG and the poll kept it there until the
next physical keypress. Skip virtual keyboards and hold the last known layout
instead, which the next poll corrects once a real keyboard is active again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep tracking the keyboard the layout was last read from
Holding a frozen label while fcitx5 owns the main flag went stale as soon as
the layout changed underneath it, and cycling still dispatched against
"current", which is that same virtual keyboard. Remember the keyboard the
label came from, re-read its layout on every poll, and cycle it by name so
the widget shows and switches one device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The class is matched in full, so the bare "foot" alternative never covered
a window carrying org.codeberg.dnkl.foot. That left universal copy sending
CTRL + C into a terminal, the same way it did for the Omarchy TUIs.
default/hypr/apps/system.lua already treats that app-id as a foot window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every keystroke in the search box scanned, lowercased, and split the
full text of every history entry, and the preview pane laid out the
entire selection with WrapAnywhere. A single 1.6MB paste (or a large
file selection) turned that into hundreds of megabytes of work on the
shell thread and stalled the render thread — freezing the whole
desktop.
Cap each entry once as it enters the display, so searching, previewing,
and rendering all work on a bounded prefix. Pasting reads the full entry
back from history by index, so nothing is actually lost. The cut lands
on a line break, keeping a file:// URI from truncating into a bogus path.
Co-authored-by: markbusking <marcosbustos.dev@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Ship herdr with a config that mirrors our tmux setup
Installs herdr through the mise shim, ships the matching config as an
Omarchy default, and adds the usual refresh/restart pair. The keybindings
map tmux sessions to workspaces, windows to tabs, and keep both the prefix
and direct bindings from config/tmux/tmux.conf.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add herdr versions of the tmux dev layout functions
hdl, hds, hdlm, and hsl drive herdr through its socket API instead of
tmux. hsl tiles into a real grid since herdr has no select-layout tiled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Namespace the herdr layout helpers so they stay out of the shell
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Create hdlm's tabs in its own workspace instead of the focused one
herdr tab create follows the focused workspace without --workspace, so
switching workspaces while hdlm loops scatters the new tabs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Lay hsl's grid out in visual order
Splitting the first column repeatedly inserted each new column between it
and the previous one, so uneven counts put the spare row in a middle
column instead of the last.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Report herdr config reload failures instead of swallowing them
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Hide herdr's pane scrollbars to match tmux
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Escape queued herdr layout commands
* Install herdr from the omarchy-herdr package instead of mise
* Use native herdr resize keybindings for tmux-style pane resizing
* Rename the omarchy-herdr package to herdr
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems made a missing Wayland compositor look like broken tests.
The cleanup traps ended on a bare conditional, so when a test skipped
before creating its TMPDIR the trap's last command returned 1 and, under
set -e, that overrode the explicit exit 0.
Six tests that launch quickshell had no compositor guard at all, so they
ran anyway and failed on the Qt platform plugin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
afa2839a5a (Rename bg/fg palette keys to background/foreground)
aliased `color0` to plain background in theme color resolution, but
the Helix template still uses `color0` as the subtle surface shade
behind `ui.cursorline.primary`, `ui.virtual.ruler`, `ui.highlight`, and
`ui.text.focus`. All four now render at exactly the background color
and vanish: no cursorline, no ruler, and the picker's focused row is
only distinguishable by its bold text. This is the same failure that
d80c98f025 ("Make color0 distinct from background/foreground and fix
helix theme", #5538) fixed back when themes defined color0 directly.
Point those scopes at `lighter_background`, the semantic key for a
surface one step off the background. Every first-party theme defines it
distinct from background except Last Horizon and Solitude, which
set the two equal on purpose; those, and legacy themes whose
`lighter_background` falls back to background, render exactly as they
do today.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Luke Hsiao <luke@hsiao.dev>
Pi/omp sessions kept falling into the Claude record when the api
field merely started with 'anthropic'. Kimi and other providers that
speak the anthropic-messages protocol (kimi-coding) were therefore
charged against Claude Code, showing k3 buckets under a Claude tab
even for a user without a Claude login.
Match the codex collector's provider-only attribution: only sessions
whose provider is exactly 'anthropic' count toward Claude usage.
Add a test proving a kimi-coding session sharing the
anthropic-messages api does not land in the Claude record.
Co-authored-by: Luca <luca@itwasarch>
99293aa gave the Tailscale widget a right defaultSection, but the test
still asserted it landed in center after the weather widget, and that
dropbox was the first widget after the tray.
Anchor both assertions on the tray so they track the placement contract
in omarchy-bar rather than hardcoded indexes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two Claude collectors running at once both wrote the cache through a temp
path derived from the target, so the second replace found the file already
moved away and crashed the update with a FileNotFoundError.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Find the Omarchy UKI as root when setting up direct boot
/boot is mounted with dmask=0077 on encrypted installs, so the
unprivileged find returned nothing and direct boot always reported that
no UKI was present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Check for the legacy UKI as root when refreshing limine
The unprivileged file tests were always false on encrypted installs, so
the stale <machine-id>_linux.efi was never cleaned up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Persist the Omarchy shell log across sessions
Quickshell only logs to its instance runtime dir on tmpfs, so when the
shell dies the idle/lock event trail is gone after a reboot (#6628).
Launch the shell through omarchy-launch-shell, which pipes stdout/stderr
into the journal under the omarchy-shell tag — bounded, timestamped, and
persistent — and surface that log in omarchy-debug-idle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Recover a locked session whose lock client died
When the shell dies while the session is locked, Hyprland's failsafe
keeps the session locked with no lock client left, and
omarchy-restart-shell refused to run in exactly that state, leaving
reboot as the only way back in (#6628). Gate the refusal on the lock
service actually holding (or acquiring) the lock rather than on the
session's LOCK state — a dead shell and a crash-handler relaunch that
holds no lock both fail that check — then restart the shell, re-acquire
the session lock, and wait for it to report secure, the same
secure-poll omarchy-system-sleep-lock uses, so the user can
authenticate out of the failsafe. Enable Hyprland's
allow_session_lock_restore so the compositor accepts the replacement
lock client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Keep clicking a notification working after a shell restart
Notification actions lived only in the sending process: `-a` appended
`-A default=default`, so notify-send blocked on a D-Bus ActionInvoked signal and
the caller ran the command when it arrived. Nothing about that reached disk, so a
restored popup had no action to run and its sender stayed blocked forever.
Replace `-a` with `--exec <command>`, carried as an `omarchy-exec` hint into the
snapshot's `exec` role. It travels through the popup files and history, and the
shell runs it on click, so restored toasts behave exactly like live ones and the
sender exits immediately.
That drops the scaffolding whose only job was keeping a blocked sender alive: the
first-run invitations lose their `--show` re-entry and two transient units each,
omarchy-migrate-notify loses its transient service, and the screenshot,
recording, download, and taildrop toasts lose their wrapper subshells.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep a failed toast from failing the work it announces
Moving these sends out of their backgrounded subshells put a fallible command
on the foreground path, where the `&` used to swallow its exit status. A
notification outage — including the shell restart this branch targets — now
propagates:
- taildrop's receiver dies under `set -e` mid-delivery
- omarchy-capture-screenshot reports failure for a screenshot it already saved
- a completed download exits before scheduling its thumbnail cleanup, leaking
the mktemp file
Announcing is best-effort in all three: the work is already done by the time
the toast goes out.
Also drop the first-run sleep that spaced out the welcome and Wi-Fi toasts.
It compensated for the background notify-send processes this branch removes;
each send now returns only once the server has taken the toast, so sending in
order is enough to stack them newest-on-top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Stop tying the preview cleanup to the toast's expiry
The shell loads a notification thumbnail into memory when the toast appears and
never re-reads the file, so the preview only has to outlive that load. Deriving
the cleanup delay from the expiry was false precision, and it turned -t into a
variable for no reason: -t is already the helper's expiry setting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without defaultSection, both 'omarchy bar defaults' and 'omarchy plugin
add' fall back to center, so upgrades landed Tailscale next to the clock
instead of alongside the other status widgets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give SSH commands the user-level tool paths
ssh host cmd runs neither a login nor an interactive shell, so on Arch it
gets the bare sshd PATH and can't find mise-managed tools like the agent
CLIs herdr scans for. Set PATH in the PAM environment (per-user via
@{HOME}), append the user-level dirs in env-bootstrap so login shells and
the uwsm session get them too, and source env-bootstrap before bashrc's
interactive guard for bash variants that read it non-interactively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Don't let an empty PATH turn into a cwd entry
Appending with a bare "$PATH:" prefix leaves a leading colon when PATH
is unset, which shells treat as the current directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The wrappers install and upgrade their tool on first run, so the cooldown
held new versions back for days. Refresh the stubs on existing installs
too, since the generator change alone only reaches fresh installs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mise withholds releases younger than its cooldown, so omarchy update left
tools behind for days. The mup alias already bypassed it by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fit the About window to its rendered content
The About fastfetch layout needs more columns than the shared 875x600
float provides, and the terminal's grid often only reaches its final
size after fastfetch has printed, leaving the output wrapped over the
logo or clipped at the initial 80-column grid (#6465).
Give About a dedicated org.omarchy.about app id so launch-or-focus can
actually match it, render through a loop that repaints on window size
or branding changes, and measure the rendered content to fit the window
around it with even padding, whatever the terminal font or About logo.
Fixes#6465
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Spacing
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin
* Add migration to append WhatsApp Slim to Brave Origin for existing installs
* Don't corrupt flags files that lack a trailing newline
Appending --load-extension= with echo assumed the file ended in a newline.
A hand-edited flags file without one got the flag concatenated onto the
previous line, losing both that flag and the extension.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: akashgagda <akashgagda@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The script is fetched from the branch but calls into the installed
/usr/share/omarchy tree, which can lag it. A packaged build without
bin/omarchy-done aborted apply_user_transition under set -e two thirds of
the way through: NetworkManager was already enabled, iwd was not yet
disabled, and nothing was printed, so the run read as finished.
The completion markers are now written directly instead of through
omarchy-done, and the two remaining unguarded packaged commands warn
rather than abort. Retiring iwd moves up next to the NetworkManager
enable it depends on, so no failure in between can leave both enabled.
An aborted run now says so instead of returning to the prompt on a green
progress line.
Fixes#6575
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
test/shell and test/all inherit `set -euo pipefail`, so the first failing
test file aborts the whole run. One failure then hides every file behind it:
you fix it, rerun, discover the next one, and repeat a file at a time. On a
140-file suite a single unrelated failure can keep most of the suite from ever
reporting.
Keep going after a failing file, then list the files that failed and exit
non-zero. Individual files still stop at their own first failed assertion, so
per-file isolation is unchanged, and a clean run still exits 0.
The files are already independent of each other -- the set of failures is the
same whether the run continues or stops at the first one -- so nothing was
relying on the early abort.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Match display backlight candidates against real globs
[[ ]] does not do pathname expansion, so amdgpu_bl* and acpi_video* only
ever tested for files with a literal asterisk in the name. Every machine
without intel_backlight silently fell through to the alphabetical first
entry, which picks acpi_video0 over amdgpu_bl0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Use the gmux backlight instead of the Touch Bar on T2 Macs
/sys/class/backlight on a T2 Mac holds appletb_backlight and
gmux_backlight. Neither was a candidate, so the alphabetical fallback
picked the Touch Bar and brightness keys dimmed it instead of the
display. Add gmux_backlight and never fall back to the Touch Bar, which
is not a display panel on any Mac.
gmux ranks above the GPU backlights because apple-gmux only registers
its device when the kernel has already selected it for the machine, and
on dual-GPU Macs the GPU's own PWM stops driving the panel as soon as
that GPU suspends.
Fixes#6558
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exporting BROWSER=omarchy-launch-browser into the whole uwsm session made
xdg-settings refuse "set default-web-browser", which broke every browser's
own "Set as default" button. The export only exists for terminal programs
(like gh) to open URLs detached from the terminal process tree, so move it
to default/bash/envs where interactive shells still pick it up.
Fixes#6590
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The upgrade repoints the mirrorlist and the [omarchy] server, then ran
pacman -Sy. A plain -Sy keeps the legacy database whenever the new server's
copy isn't newer, so the checksums stay stale and every re-download of a
rebuilt package aborts as corrupted.
Fixes#6576
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add a Fireworks balance collector and teach the agents panel prepaid ledgers
The omarchy-agent-usage-fireworks collector reads serverless token usage
from the Fireworks billing API, grouped by day and model for the last 30
days, and reshapes it into the shared record contract. Fireworks does not
expose its prepaid ledger through the documented API, so the record carries
an estimated balance instead of rate limits: credits configured in
~/.config/omarchy/agents/fireworks.json minus rated account costs since the
funding date. Credentials come from FIREWORKS_API_KEY/FIREWORKS_ACCOUNT_ID,
the auth.ini that firectl set-api-key writes, or — last, so an explicit
login wins — the key opencode stores for its fireworks-ai provider.
The panel gains two generic capabilities any agent record can use: a
balance object draws a BALANCE section — remaining credit, a fuel-gauge
meter that drains toward empty and lights the bar alarm below 10%, and
funded-versus-spent detail — and hasPromptStats: false keeps prompt and
session counts out of today's tooltip for agents whose billing API only
ever reports tokens, on this machine and through synced snapshots.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Feed Claude and Codex usage from pi, omp, and opencode sessions
A subscription burned entirely through another coding agent leaves no
native Claude Code transcripts and no Codex session files, so the panel
showed nothing for it. pi and omp write compatible JSONL sessions, and
opencode records per-message provider, model, and token usage in its
message database; the claude and codex collectors now scan all three —
filtered to Anthropic and OpenAI providers respectively — and merge those
numbers into their local stats. Fireworks stays out on purpose: its billing
API already sees that traffic server-side, and a local scan would count the
same tokens twice.
The collector tests pin XDG_DATA_HOME so a developer's real opencode
history cannot leak into fixture runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Stop pipefail from turning grep -q SIGPIPE exits into false negatives
grep -q exits at the first match, and when the producer is still writing
it dies with SIGPIPE. Under pipefail that 141 becomes the pipeline's
status, so hardware checks like lspci | grep -q read as "not found" on
exactly the machines they target. The T2 defaults migration hit this and
silently skipped real T2 Macs (#6608).
Redirect grep to /dev/null instead of -q wherever a pipeline feeds grep
in a pipefail context, so grep reads all input and the producer never
gets killed. The install-time T2 checks aren't run under pipefail today
but are switched too, since they're the same detection line the issue
calls out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Re-run the T2 defaults migration its broken hardware check skipped
The SIGPIPE bug marked 1785944594 as applied without doing anything on
affected T2 Macs. The original migration is idempotent, so a fresh
migration can just source it now that the guard is fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address Copilot review: fix OCR grep pipeline and prove the T2 repair
screen_contains piped tesseract into grep -Fqi under the acceptance
suite's pipefail, the same SIGPIPE false negative the rest of the branch
fixes. The T2 test's lspci stub now keeps writing past the pipe buffer
after the match so every scenario exercises the SIGPIPE case, and a new
case runs the rerun migration against fixtures a bitten install would
have.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A bar surface is built per monitor, so panel routing had several live copies
of the same widget to choose from and took whichever registered its slot
first. Pick the one on the monitor Hyprland has focused instead, preferring
an already-open copy so hide and toggle still reach the visible panel.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Extract the speed test gauge cluster into a shared SpeedTestOverlay
The dial cluster -- scrim, ignition sweep, self-ranging dials, run-again
button -- moves from the network speed test panel into qs.Ui with the
labels, unit, title, scale stops, and layer namespace as parameters, so
other measurements can wear the same cluster. The network panel keeps
its process handling and becomes a thin dressing of the overlay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add a disk speed test and move speed tests under Trigger > Tests
omarchy-disk-speedtest streams live write and read MB/s once a second by
sampling the backing block device's kernel I/O counters while dd workers
generate the traffic, the same way the network test samples the
interface counters. The stress data is an incompressible urandom chunk
staged in RAM, written with fdatasync per pass and fadvise drop-behind:
O_DIRECT silently falls back to the page cache on btrfs, and zeros never
reach a compressed filesystem at all. Scratch files are created
exclusively per invocation and removed even when a dismissal interrupts
the run mid-phase.
The omarchy.disk-speedtest panel dresses the shared SpeedTestOverlay
with write and read dials in MB/s, titled with the hardware model of
the disk under test.
The menu grows a Trigger > Tests submenu holding the new Disk Speed
Test and the Network Speed Test, which moves there from Setup > Network.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make the disk speed test reproducible, direct, and read-first
Successive runs could swing 40% because the settled figure was just the
last one-second sample of a single buffered dd stream, taken while btrfs
copy-on-write churned the extent allocator on every rewrite pass and the
fadvise cache-eviction dance stayed advisory.
The test files are now marked NOCOW, which is what makes O_DIRECT truly
direct on btrfs -- with checksums on it silently falls back to the page
cache -- and lets every rewrite land in place. Four parallel workers per
phase give the device a queue depth it can stretch out on, and the
figure the dial settles on is the steady-state average over the whole
phase with the first warm-up second excluded, not whatever rate the
final second happened to catch. Together this tightens successive runs
from +/-40% to a few percent of each other, at the device's actual
spec throughput.
The read phase now runs first, staged against freshly written files,
with the read dial on the left. Workers also only loop while the main
script lives, so a dismissal that loses the kill race can no longer
leave an orphan hammering the disk forever, and any worker dying before
the deadline fails the run instead of passing off partial figures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the menu aliases from the speed test entries
Aliases are reserved for established alternate names users already
type, kept for compatibility -- not something new entries pick up by
default. Note that in the menu definition header and AGENTS.md so the
next entry doesn't repeat it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Group dial readouts with thousands separators
A gen5 disk reads five digits; 11,450 scans, 11450 doesn't. Uses the
locale's grouping separator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Trim redundant overlay props and unused imports from the speed test panels
The network panel restated the overlay's default unit and scale stops,
and both panels carried imports and an omarchyPath property nothing
uses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Publish the specific speed test error whichever handler fires last
Process exit and stderr stream-finished have no guaranteed order, so a
failure that beat the collector showed the generic message forever even
when the command emitted an actionable one; the collector now replaces
it once the text lands. Also stop clearing the error on every stdout
line: only a new run should do that, or buffered output delivered after
a failed exit erases the failure message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Arm the disk speed test cleanup before any scratch file exists
A preflight failure -- tmpfs target, missing device statistics, not
enough free space -- exited between mktemp and the trap, leaking the
scratch files. Cleanup also now unlinks before stopping the workers and
sweeps once more after, so even a cleanup cut short by an impatient
SIGKILL leaves no names behind and a final worker pass cannot recreate
one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The pacman configuration says nothing about debug symbol packages. Someone who
wants `glibc-debug` so that gdb or Valgrind has symbols to work with has to
discover on their own that Arch publishes `-debug` repositories, find a mirror
that serves them, and work out that the versions only line up while the system
tracks Arch current.
Appending the three repository sections to `pacman-edge.conf`, commented out,
will put them within reach without turning them on. The comment records that the
packages match the edge channel only, and that debuginfod already covers most
debugging on edge, so enabling them is often unnecessary. Only the edge template
gets them, because on stable and rc the upstream debug packages are newer than
the pinned packages they would need to match.
ref: https://github.com/basecamp/omarchy/discussions/6185
* Fail the snapshot when Snapper is installed but has no configs
omarchy-snapshot create loops over the configs snapper reports. With none,
the loop body never runs, so it prints "Create system snapshot" and exits 0
without capturing anything. Every update then reports a snapshot it never
took, and the absence only surfaces when a rollback is needed and the
snapshot list turns out to be empty.
* Say so when the update proceeds without a snapshot
The update ignores exit 127 so a system without snapper updates quietly.
Any other snapshot failure was being swallowed by the same expression,
which let the update continue with no indication that it was now
unprotected. Keep continuing, but say it out loud.
* Point the snapshot repair hint at how the installer runs it
Also hold the green header until a snapshot will actually be attempted,
so the no-config failure doesn't open with a success banner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Continue the quattro upgrade when the pre-upgrade snapshot fails
The upgrade runs under set -e, so the new non-zero exit from an
unconfigured Snapper would have aborted a re-run at the snapshot step
instead of proceeding like omarchy-update does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Keep root= in the kernel cmdline when upgrading to Quattro
The packaged drop-in /etc/limine-entry-tool.d/omarchy-defaults.conf sets
KERNEL_CMDLINE[default] with the += operator. limine-entry-tool.conf documents
what that costs: "+= appends parameters to an existing cmdline ... Ignores
/etc/kernel/cmdline and /proc/cmdline". As soon as the drop-in lands, the tool
stops auto-detecting the cmdline.
Fresh installs are unaffected, because the ISO writes /etc/default/limine from
default/limine/default.conf with @@CMDLINE@@ substituted. The upgrade path never
created that file. A pre-quattro install that relied on the auto-detected root=
therefore ends up with a cmdline that has no root= at all, in both limine.conf
entries and in the cmdline embedded in the UKIs. The next boot fails with
"ERROR: Failed to mount '' on real root" and drops to an emergency shell, where
the error gives no hint that the cmdline is the cause.
Capture the boot-critical parameters from /proc/cmdline before the reboot, while
the still-correct cmdline of the running kernel is readable, and write them to
/etc/default/limine, which is loaded last so += keeps the drop-in parameters
instead of replacing them. Copy root=, rootflags, rootfstype, resume,
resume_offset, the cryptdevice and rd.luks keys and rw/ro verbatim rather than
reconstructing them, so LUKS and hibernation setups survive too.
Re-running the upgrade on an already-broken system has no root= left to copy, so
fall back to deriving it from the mounted root, including the subvolume on
btrfs. Any config layer that already pins root= is treated as authoritative and
left untouched.
* Anchor the cmdline guard and harden the repair path
The early-return guard searched for the bare string root=, which matches the
commented example limine-entry-tool.conf ships at line 53:
#KERNEL_CMDLINE[default]+=rw root=UUID=...
That file is present on every stock machine, so the guard always fired and the
function never wrote anything. Match assignments instead, and check
/etc/kernel/cmdline separately since it holds bare parameters rather than shell
assignments.
Three fixes on the repair path:
Assigning boot_params discarded every parameter the collection loop had just
captured, so cryptdevice, cryptkey, resume and ro were dropped, and rw was
forced over a captured ro. Prepend the derived root= instead, and only add rw
when the booted cmdline stated no mount mode.
On an encrypted root, findmnt reports the unlocked mapper device, whose UUID
says nothing about which container to unlock. Emitting it produced a cmdline
that still could not boot while satisfying the final check, so the user rebooted
into the same emergency shell believing it was repaired. Warn and write nothing
in that case.
The allowlist gained rd.luks.key, rd.luks.crypttab, rd.md.uuid, rd.dm.uuid,
rootwait, rootdelay and dm-mod.create.
Verification now also reads the .cmdline section of each UKI. With
omarchy-uki.conf among the drop-ins that embedded copy is what actually boots,
so a green limine.conf alone did not prove the machine would come up.
* Filter guard paths and narrow the dm-crypt and UKI checks
The guard passed /etc/default/limine to grep unconditionally, and that file is
absent on exactly the machines this targets. A missing operand makes grep exit 2
without -q, so a drop-in pinning a real root= went undetected and the function
appended a second one, overriding the explicit setup it promises to leave alone.
Rather than relying on -q returning 0 despite the error, which is a GNU grep
special case and not true of every implementation, filter the paths first and
only grep the ones that exist. The exit status is then unambiguous.
The dm-crypt check gated on the /dev/mapper/* prefix, which also matches plain
LVM, dm-raid and multipath. Those roots need no unlock parameters and were
repairable before, so the prefix test denied them a working root=UUID= and told
them they were encrypted. Gate on the device mapper target type instead.
root_filesystem_encrypted() is not reused here on purpose: it treats every
/dev/mapper/* path and any non-empty /etc/crypttab as an encrypted root, which
suits its own call site but would reintroduce the same false positive.
UKI verification now runs through as_root, since a restrictive ESP fmask would
otherwise make find return nothing and the check pass in silence, and is scoped
to the omarchy_linux*.efi images limine-entry-tool generates so a shared ESP or a
stub without a .cmdline section cannot raise a false "do not reboot" warning.
The allowlist gained rd.lvm.lv and rd.lvm.vg.
* Strip the subvolume before resolving the root device type
findmnt appends the subvolume for btrfs mounts, so the source read back for an
encrypted btrfs root is /dev/mapper/cryptroot[/@]. lsblk cannot resolve that
path, the device type came back empty, and the crypt gate never fired. The
function then wrote root=UUID= with no unlock parameters, limine.conf ended up
carrying a root= so the final check stayed quiet, and the machine still booted to
an emergency shell. That is the layout Omarchy installs when encryption is
picked, so the gate missed exactly the roots it exists for.
The previous /dev/mapper/* prefix test matched the bracketed form by accident.
Moving to the device mapper target type is still the right call, it just needs
the unbracketed source, which findmnt --nofsroot provides.
Also give root_type an explicit empty default. It is assigned inside a branch and
read outside it, and set -u treats a declared-but-unassigned local as unbound, so
a findmnt that cannot answer would abort the upgrade with the quattro packages
already installed and everything from configure_snapper_policy onward skipped.
* Look for the crypt layer across the whole device stack
lsblk -no TYPE reports only the target's own type. On the standard full-disk
encryption layout, LUKS container -> LVM PV -> root LV, that type is lvm and the
crypt layer sits in the parents, so the gate never fired: the function wrote
root=UUID= with no unlock parameters, the final check found a root= and stayed
quiet, and the machine still booted to an emergency shell.
Walk the parents with lsblk -s and look for a crypt layer anywhere in the chain.
That keeps LVM, dm-raid and multipath roots on the repair path, since they carry
no crypt layer and root=UUID= is enough once mkinitcpio assembles them.
root_type is replaced by root_stacks_crypt, which says what is actually being
tested and drops the LVM-versus-crypt caveat the old target-type check needed.
Also drop a vacuous test assertion: piping a bracketed literal through grep -qv
'\[' selects nothing, so the branch was unreachable and the case passed whatever
the script did. The --nofsroot assertion above it is what holds that fix.
* Keep the crypt gate off a pipeline exit status
Capture the device stack and match it from a here-string rather than piping lsblk
into grep -q. Under pipefail a short-circuiting grep can leave the producer with
SIGPIPE and turn the pipeline into 141, which reads as "no crypt layer" and
disarms the gate silently. lsblk writes its whole table in one go, so this is out
of reach in practice, but nothing about the gate should depend on how much output
a helper happens to buffer.
Also correct a stale test comment that described the target-type check the
previous revision used, four lines above the comment explaining why that check
was insufficient.
* Harden the kernel cmdline preservation against false root= pins
The /etc/kernel/cmdline early return trusted a file limine-entry-tool
ignores once a += drop-in sets KERNEL_CMDLINE[default], leaving exactly
the targeted machines unbootable. The pin guard now reads only the
*.conf layers the tool loads, only the default key, and tokenizes the
assignment value so quoted decoys and volatile-root= cannot pin.
/proc/cmdline is tokenized quote-aware so dm-mod.create="..." survives
verbatim, the root= verification is token-anchored, and an unverified
cmdline now blocks the reboot instead of only warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Ask limine-entry-tool for the effective cmdline instead of parsing its configs
--get-cmdline default answers whether root= survives the tool's own
config merge, replacing the glob, grep and quote-aware tokenizer walk
over the config layers, and the quote-aware /proc/cmdline parsing
reverts to plain word splitting. The verification and the reboot gate
stay: they are what catches anything the simpler paths miss.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(network): drop the redundant rescan on the bar click
Opening from the bar ran open() and then a bare refresh(). open() already
triggers onOpenedChanged -> refresh(true), which defers the PHY scan by
disabling the scanner and re-enabling it from scanRestart. The bare
refresh() that followed defaults scanWifi to false, so it took the other
branch and set wifiDevice.scannerEnabled synchronously on the click frame,
undoing the deferral and stalling the open on NetworkManager's access-point
flood. It also double-started the DNS and band probes.
Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(network): keep wifi rows QObject-free to prevent a delegate crash
wifiRow() embedded the WifiNetwork QObject in the row it returns, and those
rows are list-model data, so every delegate held a live QObject wrapper in a
var property. When NetworkManager churns the list -- a scan's access-point
flood, an AP disappearing -- the object can be destroyed while a delegate is
still incubating, and quickshell segfaults in QObjectWrapper::wrap_slowPath
on the dangling wrapper.
Project primitives only and resolve the backend object at action time via
the existing networkForSsid(). Both failNetworkAction() and
checkActionCompletion() already no-op on a null network, so a row whose
network has since vanished is handled the same way it was before.
Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(bluetooth): keep device rows QObject-free to prevent a delegate crash
Same crash class as the wifi rows: scrollRows embedded the BlueZ Device
QObject in list-model data, so every delegate held a live wrapper in a var
property. Discovery churn -- a scan timeout dropping a device, an unpair --
can destroy the object while a delegate is still incubating, and quickshell
segfaults on the dangling wrapper.
Project primitives for both the scroll rows and the connected rows, and
resolve the backend object by address in deviceFor() for the click actions.
The keyboard flow already went through deviceAt(), which reads the live
device arrays directly rather than model data, so it is untouched.
Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(network): guard row disconnects against a vanished network
Row activation resolved the WifiNetwork with networkForSsid() and passed the
result straight to disconnect(), which falls back to connectedWifiNetwork
when handed null. A row is a primitive snapshot, so scan churn can remove its
backing object while the row is still on screen -- activating it then tore
down whatever happened to be connected at that moment rather than doing
nothing.
Route both row paths through disconnectRow(), which resolves first and only
acts when the row still maps to a live network. disconnect() keeps its
fallback for callers that mean "drop the current connection".
Also covers the bar-click open path, which had no regression: the suite
already asserts against Panel.qml source, so assert the closed branch calls
open() alone and never a second refresh().
Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: shrijit <shrijitsrivastav@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: reopen wifi passphrase prompt after a wrong saved password
A failed first connection attempt leaves the network profile saved, so the
network shows up as known. Clicking it again reconnects with the stored
wrong PSK and fails with WifiAuthTimeout, but the inline passphrase prompt
only reopened on NoSecrets, leaving no way to re-enter the password short
of forgetting the network.
Treat an auth timeout on a protected network as a wrong saved passphrase
and reopen the prompt; connectWithPsk overwrites the stored PSK on submit.
Fixes#6582
* Scope the wifi passphrase reprompt to panel-initiated connects
Background auto-connect retries also fire connectionFailed; without a
gate they would pop the passphrase prompt open unbidden, stealing focus
and wiping a passphrase mid-entry when another network fails.
For the gate to see the failure, the action safety-net timer must
outlast NetworkManager's 25s supplicant timeout -- at 15s it cleared the
action state before WifiAuthTimeout arrived, so a wrong saved password
showed "Timed out connecting" instead of "Wrong password". Bump it to
30s.
Also share the one ConnectionFailReason map between the Model.js
helpers instead of building a second partial copy inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Faking HOME alone was never enough: the shell QML and the agent usage
updater read XDG_STATE_HOME and XDG_CACHE_HOME directly, so a test
quickshell inherited the session's real paths. The bar widget contract
test instantiated the agents widget, whose refresh ran the real
collectors against the empty fake HOME and wrote hollow "Waiting for
auth" records into the developer's real usage data files — hiding the
agents widget from their bar — while littering the real cache with
per-tmpdir scan files.
Point XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_STATE_HOME under the fake
home in every test that boots quickshell with one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The panel resolves marks by walking assets/<id>-light.svg then
assets/<id>.svg, advancing on Image.Error. But the candidates binding is
re-evaluated whenever the provider objects are rebuilt, and a fresh array
identity with identical content reset the walk to the -light candidate.
Re-pointing source at a URL whose load already failed emits no
statusChanged, so the walker never advanced again and agents that ship a
single mark — Claude — fell back to the generic bar glyph on light
surfaces.
Key the reset on the candidate URLs instead of the array identity, and
defer the error advance one tick so stepping source from inside its own
status change doesn't trip the binding-loop detector.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add agent usage collectors that write display-ready data files
One omarchy-agent-usage-scan-<agent> collector per AI coding agent prints a
complete display-ready usage record — identity, tier, status, rate limits,
and today/week/all-time stats. omarchy-agent-usage-update runs every
collector it finds and writes the records atomically to
~/.local/state/omarchy/agents/usage/, so anything that displays usage only
ever reads JSON from there.
The Claude collector absorbs what the shell previously did in-process:
transcript scanning, the stats-cache/history fallback, credentials parsing,
and the OAuth limits probe, now with a probe throttle and last-good limits
kept across network failures. The Codex collector is the existing scanner
reshaped to the shared record contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Redo the model-usage plugin as omarchy.agents watching usage data files
The panel is now strictly a display. It discovers the JSON records that
omarchy-agent-usage-update maintains under
~/.local/state/omarchy/agents/usage/, watches them for changes, and draws
whatever appears — so adding an agent means shipping a collector, never
touching the panel. Marks resolve by convention (assets/<id>.svg with an
optional -light twin), the limits meters read a generic limits array, and
the per-provider QML adapters and in-plugin scanner scripts are gone.
Cross-device sync aggregation stays in the shell and keeps the snapshot
field names older versions wrote, so mixed-version fleets still merge in
both directions.
With the provider fan-out gone, the widget takes its real name: the plugin
id becomes omarchy.agents. A migration renames it wherever a user's config
mentions it — layout entries keep their settings and position, a disabled
widget stays disabled — then primes the data files once and drops the old
scanner cache. The migration test also drops a stale assertion that expected
migrations to restart the shell themselves, which c992cdff moved to
omarchy update.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address Codex review: synced-only tabs, limits retry, history fallback
Three data-availability gaps from review. An agent whose records only exist
in synced snapshots — a collector installed on just one machine — now gets
its tab by unioning the synced aggregate into the provider list, with rate
limits blank since those never travel. A Claude limits probe that reaches no
server at all writes retryAdvised into its record, and the shell honors it
with one 30-second retry instead of waiting out the full refresh interval,
restoring the old boot-before-DHCP behavior. And a machine with only
history.jsonl — no transcripts, no stats-cache — still reports today's
prompt and session counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address second Codex pass: history-only visibility, targeted retries
Today's prompt and session counts now count toward an agent's presence in
the bar, so a machine whose only Claude source is history.jsonl shows up
without waiting for limits. And the 30-second limits retry passes the
advising agent ids to the updater, so an outage at one provider no longer
puts every other collector on a retry treadmill.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop omarchy-cmd-present jq guards from the agents migrations
jq ships in the default package set, which makes it a runtime invariant per
AGENTS.md — call it directly. The migration tests lose their now-unused
omarchy-cmd-present stubs with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the scan infix from the collector command names
Collectors are omarchy-agent-usage-<agent>; the updater skips its own name
when globbing them, and the update test proves it with a decoy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep the credential store out of the printed usage record
The Claude collector now reads .credentials.json once into three scalars —
the access token, its expiry, and the plan label — instead of passing the
parsed store around. The token reaches nothing but the Authorization header
of the limits probe, and only the plan label may travel into the record,
which is what CodeQL's clear-text-logging alert on the record print was
unable to see when the whole dict flowed through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>