Changelog
1.22.52
agents artifacts share --visibility me|org— identity-gated share pages (PHNX-3260). Extends the P1public|unlistedtiers (RUSH-3135) with two Phoenix-gated GET visibilities.meis visible only to the signed-in owner (the viewer's PhoenixuserIdmust equal the stampedowner);orgis visible to any signed-in Phoenix user whose verified email domain matches the owner's company domain (org_domain, stamped from the owner's verified email on PUT). A mismatch on either returns 404 — the same body as a missing object — so a wrong viewer cannot even learn the page exists. Both are hidden from the public gallery andshare list, likeunlisted. Publishingorgfrom a public inbox (gmail/googlemail/outlook/hotmail/live/icloud/me) is refused with 400, andme/orgPUT without a Phoenix identity is a loud 400 (a BYOWRITE_TOKENalone cannot publish them). Browser GET of an unauthenticatedme/orgURL 302s to${PHOENIX_ID_BASE}/login?return=<url>(the same Google OAuth as/device, no new IdP); the returned one-timephoenix_ticketis redeemed once viaPOST /api/v1/auth/ticketand exchanged for an HttpOnly, HMAC-signed__Host-phoenix_sharecookie on the share host. An existing CLIAuthorization: Bearerstill works with no redirect.me/orgresponses sendCache-Control: private, no-storeandX-Robots-Tag: noindex;public/unlistedGET is unchanged (anonymous 200). Because the Worker template changed, already-provisioned BYO endpoints needagents artifacts share updateto serve the new gate. Source:cli/src/lib/share/worker-template.ts,cli/src/lib/share/{backend,publish}.ts,cli/src/commands/share.ts.Harden the browser daemon against the socket-timeout wedge (PHNX-3289). Four fixes to the browser/daemon reliability surface.
agents daemon statusno longer errors on macOS withps: etimes: keyword not found— the process-uptime probe now shells the portableps -o etime=and parses it with the same helper the keychain reaper uses.waitForSocketraises its ceiling from a flat 6s to 15s and re-probes across an IPC-server restart (requiring two stable accepts), so a browser start/navigate that lands in a restart window no longer intermittently throwsTimeout waiting for browser daemon socket. Newagents browser stop --daemonstops the browser daemon and clears a stale/wedgedbrowser.sock, failing loud if a live server still holds it, so the nextstartcomes up clean. Andagents browser start --device <host>now resolves the browser/profile on the TARGET device instead of the local one — a bare start from a browserless box forwards to--deviceinstead of failing with a misleadingNo supported browser found. Source:cli/src/commands/daemon.ts,cli/src/lib/browser/ipc.ts,cli/src/commands/browser.ts.Managed share: a BYO-published page no longer locks the rightful Phoenix owner out of their own handle (PHNX-3291). The share Worker's handle-ownership check (
assertHandleOwner) scanned every page under a namespace and 409'dhandle takenwhen any page's stampedownerdiffered from the writer's PhoenixuserId. But a BYOWRITE_TOKENpublish stampsowner = SHARE_NAMESPACE(the namespace string, e.g.octocat), not a userId — so once a namespace held any BYO page, its legitimate Phoenix handle owner got a 409 on every subsequent publish, and could neither publish nor change a page's--visibility(which blocked the me/org tiers from PHNX-3260 for that user entirely). The__handles/<handle>claim object is now authoritative when it exists: the recorded userId may write and anyone else is refused, regardless of a stray page's owner stamp. The page-owner scan remains as the pre-claim fallback but ignores BYO namespace stamps (owner === handle), so a first Phoenix publish can still claim a handle previously used only by BYO pages. Source:cli/src/lib/share/worker-template.ts. Requiresagents artifacts share updateon already-provisioned endpoints (the Worker template hash changed).sessions resume/sessions attach/sessions focusno longer SSH-probe the fleet before attaching a live local tmux pane (PHNX-3292). First unique match now wins: a selector naming a live pane on THIS box — the fullag-<agent>-<8hex>aliasagents tmux lsprints, or its bare 8-hex suffix when it names exactly one live local pane — attaches with zero SSH, for baresessions resume <alias>and deprecatedsessions attach <alias>too, not just--attach-only. PreviouslycollectSessionCandidatesran first — two fleet sweeps (transcript pool + live roster) that stall on offline devices (~2 minutes measured on yosemite-s0, two copies ofunreachable or no agents CLI — skipped) — and bareresumeof a live alias didn't even reach the tmux-attach path at all. A miss still races the reachable fleet in parallel:isDefinitiveMatch/selectorAllowsEarlyExit(RUSH-2203) now cover a tmux alias and an exact 8-hex short id, not just a full UUID, so the first peer to answer with that pane aborts the rest of the sweep instead of waiting out every offline box's timeout. Two LIVE panes (local or fleet-answered) sharing the same 8-hex suffix fail closed with both names printed, rather than guessing; a dead/retained pane is never attached.--deviceskips the local gate entirely and scopes the race to the named box(es). Source:cli/src/lib/session/local-tmux-attach.ts(new,attachLocalLiveSelector),cli/src/commands/{sessions,sessions-resume,attach,focus}.ts.agents tmux attachtears the session down when the agent exits (PHNX-3293). Attaching to a wrapped pane and exiting the agent (/exit, Ctrl-c) used to print[detached]and leave the tmux session intmux ls— the v6 pane-died hook onlydetach-clients when a client is attached, and the attach verbs never killed theremain-on-exithusk.tmux attach,sessions focus/resume --attach-only, andgo's local-tmux attach now destroy the session when every pane is dead and still keep it on Ctrl-b d. A piped (no-TTY) localagents run --interactiveno longer wraps in tmux, so session-tracker tests cannot leak live "trust this folder" panes.agents tmux killwith no name opens a picker whose preview is the pane's last screen (so a leaked first-run dialog is distinguishable from real work);tmux lsprints that snippet on each row. Source:cli/src/lib/tmux/session.ts,cli/src/commands/tmux.ts,cli/src/commands/focus.ts,cli/src/lib/exec.ts.agents browser startno longer silently mints a logged-outauto-chrome(PHNX-3296). With no configured default and no existing launchable profile, start now errors and points atagents setup/agents browser use <name>/ thebrowser.devicefleet hub instead of popping a signed-out Chrome. A pre-existingauto-chromeor legacydefaultstill resolves.agents setup's browser pick replaces the misleading "auto-detect on first use" opt-out with an explicit "None — this box uses the fleet hub" choice. Source:cli/src/lib/browser/profiles.ts,cli/src/commands/setup-preferences.ts,cli/src/commands/browser.ts.sessions detach/sessions stopof a unique live local session no longer wait for fleet SSH timeouts (PHNX-3298). A live pane already visible locally is the hit: a full UUID or a unique 8-hex prefix skipsgatherRemoteActiveentirely, so a sleeping peer cannot printunreachable or no agents CLIor stall SIGTERM. Two local matches fail closed without waiting for unanswered boxes. A genuine miss still races the fleet and aborts remaining SSH on the first unique reachable live row; browse /focuswith no id stays all-settle. Source:cli/src/commands/go.ts,cli/src/commands/detach.ts,cli/src/commands/sessions-stop.ts.agents monitors addrefuses immediately when a reachable peer already has the same watcher (PHNX-3299). The fleet duplicate guard used to collect every peer before reporting a clash, so a single slow box forced the full 12-second timeout even when another peer had already answered with the same fingerprint. The fan-out now aborts remaining SSH captures the moment a peer reports a matching monitor, while the no-match path still waits for the whole fleet so uniqueness can be proved. Source:cli/src/commands/monitors.ts,cli/src/lib/monitors/remote.ts.agents repo sync userself-heals a non-git or partial~/.agentsby adopting it in place — no re-clone, no data loss (PHNX-3301). When the user-layer checkout lost (or never had) its.git, sync hard-failed withNot a git repo: ~/.agentsand the only fix was a destructive re-clone that wiped runtime state (.cache/.history/scratch/ device config). It now git-backs the existing directory in place with plumbing only (init/remote/fetch/update-ref/read-tree/checkout-index/restore, so it never trips the fleet git-guard): materializes only the MISSING tracked files, restores a stale-stub top-levelagents.yamlfrom origin (backing up the previous copy to.history/agents.yaml.pre-adopt.bakfirst), preserves gitignored runtime state, and surfaces (never clobbers) real local edits. The remote URL is resolved from an existingorigin,AGENTS_USER_REPO_URL, or a device-local record left by a prior healthy sync — never hardcoded.agents sync statusnow flags a non-git~/.agentsas a distinct drift state instead of burying it as "N missing". Source:cli/src/lib/git.ts,cli/src/commands/sync.ts,cli/src/commands/repo.ts,cli/src/lib/sync-status.ts,cli/src/commands/status.ts.Owner notifications forward to a capable fleet peer when this box can't deliver (PHNX-3303). The owner's delivery provider for the rush-backed channels (imessage / telegram / slack / discord via the
rushCLI) is macOS-only, soagents feed post --level important/--blocked,agents notify, and monitornotifyactions run from a headless Linux worker used to record the post but fail to reach the owner withowner failed: … rush CLI not found on PATH. They now hand the delivery over SSH to a reachable macOS peer that DOES have the provider — the same rerouteagents messagealready uses — instead of stranding the important post. It is best-effort: it never throws or blocks the post, only the macOS-only rush family triggers a forward (a Linux-capable transport like openclaw-telegram stays local), and when no capable peer is reachable the existing clean local error stands. A box that received a forward never forwards onward (AGENTS_OWNER_NO_FORWARDguard). Source:cli/src/lib/channels/owner-forward.ts,cli/src/lib/notify.ts,cli/src/lib/feed-broadcast.ts.agents artifacts share <file>now derives a stable slug automatically (PHNX-3310). The default comes from the HTML<title>or Markdown frontmattertitle:, then the filename, so repeat publishes of a long-running artifact update the same URL;--slugremains an optional explicit override. Source:cli/src/lib/share/publish.ts,cli/src/commands/share.ts.The release attestation now shards its test suite across the fleet instead of pinning one box.
release-attestation-produce.sh— the full-suite run that mints a release's exact-tree attestation — ran the ~13k-test suite on a single auto-picked box at--maxWorkers=2(~880s measured, ~14 min per attestation). It now fans the suite across the fleet viatest.sh --shard Nby default, resolving N from the eligible workersagents devices pickreports (capped) and falling back to a single box only when fewer than two are eligible. The suite is throughput-bound, so this is ~1/N the wall time (~269s on one box → ~31s on nine). Each shard still runs vitest at--maxWorkers=2 --retry=2, so the RUSH-3015 per-box flake mitigation is unchanged — sharding adds machines, not per-box concurrency. New--test-shard <n>/--test-devices a,b,coverrides mirrortest.sh. Source:cli/scripts/release-attestation-produce.sh.Shared pages now show who shared them and — the point — a visibility cue (PHNX-3260 follow-up). The share Worker injects a slim attribution bar at the top of every served HTML page:
Shared by <handle>,Made with <agent>, the date, and a colour-coded visibility chip — 🔒Only you(me), 🏢Anyone at <org>(org), 🌐Public, 🔗Unlisted— so a viewer can tell at a glance who can see the page, the thing the raw served HTML never surfaced. All values come from metadata the Worker already stamps (visibility,owner/handle,org_domain,agent,date); no new publish-time metadata and no CLI change, so it ships withagents artifacts share update. Non-HTML assets (images, JSON, the OG cover) are served byte-for-byte; metadata is HTML-escaped; the rewritten body drops the now-stale R2 etag. Source:cli/src/lib/share/worker-template.ts.
1.22.51
publish-computer-helper-mac.shcuts the helper's own tag (PHNX-3228). It publishedComputerHelper.app.zipto the CLI'sv<version>release, but since the client was repointed at per-helper tags the resolver asks forcomputer-mac/v<x.y.z>(helperTagincli/src/lib/helper-versions.ts). The asset therefore landed at an address nothing requests, and there was no way to cut a new computer-mac release at all — the last publisher still coupled to the CLI's version line. It now cutscomputer-mac/v<x.y.z>, requires the helper's version explicitly instead of defaulting tocli/package.json(that default was the coupling), and refuses an existing tag, since the upload uses--clobberand would otherwise replace a binary an installed CLI already pins. Symmetric withpublish-computer-win.sh. Source:cli/scripts/publish-computer-helper-mac.sh. It also cuts the tag itself now.gh release create --verify-tagrefuses to invent a tag absent from the remote, and nothing else pushedcomputer-mac/v<x.y.z>—release.shdelegates helper tagging to "where the helper is released", which is this script. So it ran green through build + notarize and then failed at the release step, meaning no new helper version could be cut. The tag is pushed AFTER a successful build, deliberately: pushing first would leave a published, immutable address with nothing behind it whenever notarization failed. Origin, not the local ref, is what makes a helper release immutable. Adding tag creation introduced a wedge: a run that tagged and then failed to push left a local tag, and the guard read that as "published" and told the operator to cut the next patch — burning a version over a transient network error, and (since the guard precedes the build) making that version uncuttable from the checkout without a manualgit tag -d. A local-only tag now resumes the interrupted publish instead. Immutability keys on the published RELEASE, not the tag. A tag with no release is an interrupted run — whether the push failed or the release creation did — and both now resume rather than burning the version. The asset is what an installed CLI downloads, so the release is the thing that must never be replaced. The published-release check fails CLOSED. A non-zeroghis not evidence of absence — unauthenticated, offline, and rate-limited all exit non-zero — so reading that as "not published" would have carried on intogh release upload --clobberover a live binary, the exact outcome the guard exists to prevent. Only an explicit not-found means not-found; anything else stops the release and prints whatghactually said.agents traces syncretains and reports failed sessions instead of stranding them (PHNX-3267). The sync watermark advanced to the max mtime of successful uploads, so a later success moved it past an earlier failed session and the next run'sfile_mtime_ms > watermarkfilter skipped that session forever — the "failures are retried" comment was false. A live 1.22.49 sync on a real device reported 11,073 uploaded and 6,050 errors with no way to tell a transcript cleaned off disk from a genuine upload failure. Sync now records each failed session's identity and typed, redacted evidence in the ledger and unions those retry-worthy ids back into the row query regardless of the watermark, so a stranded session is re-attempted until it succeeds. Failures are classifiedtranscript-unavailable(the file is gone — expected history, not re-read, aged out after 14 days),parse-failed, orupload-failed(both retried);agents traces syncprints the breakdown (… errors (N transcripts no longer on disk · M parse/upload failures — will retry)) andagents traces statuslists the outstanding retry set with example detail. Source:cli/src/lib/traces/sync.ts,cli/src/commands/traces.ts.Helper binaries no longer download through a repository-rename redirect.
HELPER_RELEASE_REPOstill namedphnx-labs/agents-cliafter the repository was renamed tophnx-labs/agi-cli. Nothing was broken — GitHub redirects a renamed repo, and both slugs returned HTTP 200 — but every signed helper asset (MenubarHelper.app.zip,Agents_CLI.app.zip,ComputerHelper.app.zip,computer-helper-win.exe) was resolving through that redirect, which is one re-created repository away from pointing elsewhere. What actually protects the download is the sha256 +codesign+ designated-requirement + Team ID verification inhelper-download.ts; this removes the reliance on the redirect. Theagents.yaml$schemaURL, the CHANGELOG link, the star nudge, and thepackage.jsonrepository/issues metadata moved with it. The npm package name is deliberately unchanged — it is still@phnx-labs/agents-cli, and renaming it would orphan every installed CLI. That distinction is now pinned by a test. Source:cli/src/lib/helper-download.ts.publish-computer-helper-mac.shcarried the old slug too, and that one is a write path:REPO_SLUGreachesgh release view(the immutability guard),gh release create, andgh release upload --clobber. Publishing signed helper binaries was going through the rename redirect. Found by sweepingscripts/and.github/after the source tree was already clean — the source sweep alone would have missed it. Three further live paths carried the old slug and were missed by the first sweep:ssh-tunnel.ts:170WIN_HELPER_RELEASE_REPO(a separately hardcoded constant — the download URL forcomputer-helper-win.exe, the fourth signed asset, which the first version of this change claimed to cover and did not);commands/feedback.ts:14, which opens real GitHub issues; andfactory/snapshot.ts:30, which polls this repo's PRs.installations/migrate.tsalso wrote the old URL into theagents.yamlheader it generates. Separately,.github/workflows/tests-windows-host-e2e.yml:54gated ongithub.repository == 'phnx-labs/agents-cli', which is permanently false after a rename — that job had silently stopped running on every push to main and on its daily cron. The JSON Schema's own$idmoved with it —state.tsandmigrate.tswrite that URL as the$schemahint into every user'sagents.yaml, so a mismatched$idwould make editors reject their own config.The menu-bar helper's staleness check compares the HELPER's version, not the CLI's (RUSH-3230).
installAndStartServicestampedgetCliVersion()as the installed helper's version, andmenubarSetupStale()compared againstgetCliVersion()too. Once helpers gained their own version line that was wrong in both directions: every CLI release made an unchanged helper look stale and reinstalled it — recopying the bundle under the running helper, whichKeepAlivethen restarts (the #2109 storm: a new pid every 5-15s, 578 launches in one log) — while a genuinely newer helper at the same CLI version never looked stale at all, so it could never install. The stamp is now JSON recording what the helper actually IS:releasewith its helper version, orlocalwith the source path + mtime for a dev build (menubar's build.sh hardcodesCFBundleShortVersionString, so a local build has no version to compare). Staleness compares like with like, treats a kind change (local <-> release) as stale, and never downgrades when the installed helper is ahead of the floor. A pre-JSON stamp is stale exactly once and is re-stamped in the new format, so the migration cannot loop.agents menubar status/doctornow print three labelled lines — helper installed, helper available, CLI version — instead of conflating two axes into one, and the permanent false "(mismatch —agents menubar setupupdates it)" hint is gone. Source:cli/src/lib/menubar/install-menubar.ts.The Windows helper's "asset missing" error names a tag that exists (RUSH-3230).
downloadWinHelperExebuilt its error message from`v${version}`— the CLI's tag shape — while the URL it had just tried came fromhelperTag('computer-win', version). So a genuine 404 sent the reader looking forv1.0.0, which does not exist, instead ofcomputer-win/v1.0.0, which does. The mac path was corrected when helpers moved to their own tags; the Windows path was left behind. Also drops agetCliVersionimport that had no call site — the last trace of the old CLI-version coupling in that file. Source:cli/src/lib/computer/ssh-tunnel.ts.Claude status-line delegate no longer fork-bombs the machine. The delegate self-reference guard compared the saved command against the exact literal
agents __claude-statusline, so a delegate seeded with the same private subcommand under a different binary name (e.g.agents-dev __claude-statuslinefrom a dev install, or an absolute path) was not recognized as us. Every status-line render thenspawnSynced that command, which read the same delegate and spawned another — unbounded recursion. Observed on a real box: ~4,900 livenode __claude-statuslineprocesses accumulated over a 2-day uptime, exhausting 96 GB of swap and driving load past 300, so keystrokes lagged. The guard now matches the__claude-statuslinesubcommand under any binary name or path (isStatusLineSelfReference),installClaudeStatusLinerefuses to persist such a command as a delegate (and deletes an already-poisoned delegate on re-install), aAGENTS_CLAUDE_STATUSLINE_DELEGATEDenv marker hard-caps delegation at one hop, and the delegatespawnSyncnow carries a 5 s timeout. Source:cli/src/lib/claude-statusline.ts.
1.22.50
Fixed
Teams now persist observed teammate failure evidence and keep independent DAG branches moving. Placement, local/remote launch, cloud dispatch, dependency, and process-exit failures carry a stable code, sanitized message, exit code, retryability, and observation time in
teams statustext/JSON. A runnable node with a durable placement failure fails independently; a node blocked only by pool capacity or load stays pending with retryable evidence so a later wave can launch it. Descendants name failed or missing--afterblockers instead of spinning until--max-waves.teams start(no--watch) reports teammates that failed during the wave in aFailed this wavesection (JSON:failed[]with evidence) and exits non-zero when a wave produced only failures. Because a failed launch now keeps its record (evidence) instead of deleting it, re-running the identicalteams add --name <name>after a failure requiresteams remove <team> <name>first. Successfully resuming a failed teammate clears the prior attempt's evidence as the replacement entersrunning; a replacement that fails to launch restores the original terminal state and evidence intact.Register the Phoenix session with Prix after sync (PHNX-3257). A successful (non-dry-run)
agents traces syncnow fire-and-forget POSTs the Phoenix bearer toapi.prix.dev/api/v1/traces/link, so the Prix web console can mint a token and serve live trajectories instead of fixtures. Scoped to the managed Phoenix backend only — a BYO/self-hostedAGENTS_TRACES_WRITE_TOKENis never sent to Prix. Source:cli/src/lib/traces/sync.ts.Ship
traces-daily-syncroutine (PHNX-3258). Dailyagents traces syncat 02:00 UTC to accumulatebucketHistoryfor drift signals; requires a device pin (agents routines devices traces-daily-sync --set <name>) before it fires. Source:cli/routines/traces-daily-sync.yml.--strategy balancednow spreads across your provider accounts, not just native logins (RUSH-3182). A setup-token or API-key account added withagents accounts addis now a first-class balancing candidate for every harness its provider can authenticate — claude, codex, grok, cursor, opencode (a harness with only a native login and no provider adapter, like kimi, keeps balancing its native logins). Before, balanced only rotated across accounts that sat in a version home, so a worker's shared setup-tokens never participated and--accountcouldn't select them. The run path folds those provider accounts into the candidate list and injects the picked one through the existing--accountpath (a setup-token still authenticates viaCLAUDE_CODE_OAUTH_TOKEN); the other candidate consumers — the watchdog, session recovery, teams placement — keep the native-only list, so nothing else changes. On a box dominated by verified native logins a usage-less provider account is deprioritized until the daemon fetches its usage; on a worker (setup-token majority) it participates immediately. Source:cli/src/lib/accounting/account-pool.ts,cli/src/lib/accounting/account-pool-collect.ts,cli/src/lib/accounting/rotate.ts,cli/src/commands/exec.ts.The attestation producer skips the helper manifest by default too (RUSH-3216).
release.shgained--with-helpersso an ordinary release does no helper work, butrelease-attestation-produce.shkept the same unconditionalrelease-manifest.shverification — so the coupling survived one step upstream. Found live: a one-line comment fix innative/computer-mac/scripts/build.sh(anapps/cli/→cli/path in prose) changed that helper's input digest and aborted an otherwise-clean 1.22.49 attestation, for a helper the tarball no longer ships and the CLI resolves from its own tag. The producer now takes the same--with-helpersflag, default off. Source:cli/scripts/release-attestation-produce.sh.scripts/publish-computer-win.sh— a publish path for the Windows helper (RUSH-3228). Its release now triggers oncomputer-win/v<x.y.z>rather than the CLI'sv*tag, but nothing in the repo cut such a tag, so the trigger would have been dead — trading a wasteful 165 MB rebuild on every CLI release for no rebuild at all. The tag is the publish action (release-exebuilds, smokes on a real windows-latest runner, and uploads the exe + sha256), so a mis-shaped tag is a silent no-op: the script refuses av-prefixed or non-semver version, refuses an existing tag because the upload uses--clobberand an installed CLI may already pin it, and is dry-run by default. Symmetric withpublish-computer-helper-mac.sh. Source:cli/scripts/publish-computer-win.sh.scripts/test.sh --shard <n>— fan the suite across n fleet workers (RUSH-3230). Uses vitest's own--shard=i/n, drawing workers from the same auto pool a single--device autorun uses, sorole=worker/role=personalmarks govern the fan-out too. This is the change that moves release time, and the reason is arithmetic: a measured full run is 3,079s of CPU at 11.5× parallelism on one box, so wall equals CPU/workers (269s) — the suite is throughput-bound, not bound by any single slow file. Adding boxes divides the CPU: 3 ≈ 93s, 6 ≈ 47s, 9 ≈ 31s. Shards run concurrently and every one is waited on before reporting, so a failure in one does not hide the others. Requiresagents≥ 1.22.49 fordevices pick --json; an older CLI fails naming the version and the fix rather than passing through a commander error. Source:cli/scripts/test.sh.scripts/test.sh --devices a,b,c— name the shard workers explicitly (RUSH-3230). Pins the fan-out to known-idle boxes instead of auto-picking, and skips thedevices pick --jsondependency, so sharding also works from a machine whose installed CLI predates 1.22.49. Source:cli/scripts/test.sh.--shardfails loud on a count below 2 and on a conflicting target flag (RUSH-3230).--shard 0previously passed the numeric check, ran zero shards, and still printedAll 0 shards passed.with exit 0 — a false green that reported success having run no tests. It now requires at least 2 (a single worker is--device auto). Separately,MODEwas last-write-wins with no cross-flag validation, so--shard 6 --device boxsilently dropped one of the two purely on argument order; conflicting target flags now die naming both. Source:cli/scripts/test.sh.--devicesobeys the same 2-worker floor, and is validated before prerequisites (RUSH-3230).--devices oneboxderived the shard count from the list length and skipped the floor entirely, running a one-shard fan-out. The list is now resolved and checked immediately after argument parsing rather than inside the dispatch branch, so a bad invocation reports its own problem instead of dying on a missingrsyncfirst. Source:cli/scripts/test.sh.Split
ssh.device-config.test.ts, the second-slowest file (RUSH-3230). 18 subprocess tests in one file at ~44s locally (151s on a loaded worker) — 8.4s per test. Now three files along its existingdescribeboundaries (per-device config / fleet-wide defaults + role + describe / retired-subcommand tombstones) over a shareddevice-config-test-harness.ts: 45s → 19s wall, all 18 tests still passing, none changed, skipped, or dropped. Source:cli/src/commands/device-config-test-harness.ts.Split
daemon.test.ts, the suite's floor (RUSH-3230). It was 35 subprocess tests in one file at ~53s locally (159s on a loaded worker) — the slowest file in the repo, and therefore the whole suite's floor, because vitest parallelises across files and runs one file's tests sequentially in a single worker. Now three files by theme (command surface / services + webhooks / doctor + logs) over a shareddaemon-test-harness.ts, so they run concurrently: 53.5s → 26s wall, all 35 tests still passing. No test was changed, skipped, or dropped. Source:cli/src/commands/daemon-test-harness.ts.
1.22.49
- Check CLI benchmark numbers into
docs/benchmarks.md(RUSH-2385). Commander-bootstrap and audit-hook means from the yosemite-s1 vitest bench (PR #2349) plus pointers to OPT-01/OPT-02 live in git. Linear is not the ledger. Source:cli/docs/benchmarks.md.
fix(view): keep Claude session/week quota slots aligned, render omitted windows as unavailable, and show only the unlabeled last-active timestamp
No native binary ships in the npm tarball (RUSH-3100).
Agents CLI.app(2.6 MB) andMenubarHelper.app(3.3 MB) were copied intodist/bybuild, listed infiles, and hard-gated by twoprepackchecks — so every Linux and Windows user downloaded 5.9 MB of signed macOS bundles, and the tarball could only be packed on a Mac holding them. All three are gone; helpers are fetched on demand from their own release tags and verified exactly as before (sha256 + Developer ID team + notarization + designated-requirement pin). Unpacked tarball: 21,337,724 → 15,629,419 bytes.buildstill clears any stale bundle fromdist/so an upgrade cannot leave one behind. This is what lets an ordinary release be produced without a signing Mac: the prepack gates required the signed.apps to be present at pack time, which is why the attestation was macOS-only. Both gate scripts are retained — they remain the right check for cutting a helper release. Source:cli/package.json.The attestation producer no longer seeds signed helper apps into its worktree (RUSH-3100). That seeding existed only because
prepackrefused to pack without them, so it was the workaround for the coupling above; its comments claimed "the prepack gates still decide … fails the pack exactly as before", which stopped being true the moment those gates were removed. Source:cli/scripts/release-attestation-produce.sh.Trace storage can now be deployed from the CLI (RUSH-3140).
agents traces setupprovisions the isolatedagents-tracesR2 bucket and Worker, uploads the canonical private trace Worker, binds its write and Phoenix identity secrets, enables its workers.dev route, and mapstraces.agents-cli.sh.agents traces syncemits the rich Phoenix Evals index shard (RUSH-3142 M3). The per-deviceindex.jsonnow carries duration/error stats, ranked sessions needing attention, derived topic counts, and structured tool-failure counts split into real failures, repository guards, and auto-mode permission denials recorded on failed tool calls. Topic classification uses only session metadata plus tool mix and is lazily cached in a self-healingsession_topicstable keyed by transcript mtime + size; per-session trajectory JSON and the incremental upload ledger are unchanged. Source:apps/cli/src/lib/traces/{classify,sync}.ts,apps/cli/src/lib/session/db.ts.The offloaded suite no longer unpacks inside your DotAgents repo.
scripts/test.sh --deviceshipped the tree to~/.agents/test-runs/agents-cli— but~/.agentsis itself a git repo, so anything in the suite callinggit rev-parse --show-toplevelresolved to~/.agentsinstead of the shipped tree (release-manifest.test.tswent looking for~/.agents/native/computer-mac/Sourcesand four tests failed with a confusing "helper input missing"), and the tree showed up as?? test-runs/in the operator's own repo status on every worker. The tree now lands in~/.cache/agents-cli/test-runs/tree, which has no git ancestor — the same reasonsandbox.sh's~/workspaceschoice never hit this. The shipped tree is still given its own blank git repo, because parts of the suite resolve paths from a repo root and would otherwise hard-fail. Source:apps/cli/scripts/test.sh,apps/cli/scripts/bound-repo-root.sh.scripts/test.sh— the suite is offloaded by default and can no longer land on your machine by accident. The full vitest suite pins a box for several minutes, and the only offloaded path used to be thetest:remotepackage.json alias. Offloading was opt-in at each call site becausescripts/sandbox.shtook a hand-composed command string rather than a verb, so every call site opted out:scripts/build.shandscripts/release-attestation-produce.shboth ranbun run teston whatever machine invoked them. Newscripts/test.shis the one entry point — it offloads to a crabbox by default, takes--device <box>to run on any fleet Linux box, and runs locally only under an explicit, loudly-warned--here. When the offload target is unavailable it fails naming the exact--devicecommand instead of silently falling back.build.shand the attestation producer now both call it (the producer gains--test-device/--test-here), so a macOS signing box no longer doubles as the test runner just because a native helper needs notarizing.sandbox.shgains the sibling projects' verb vocabulary (sandbox.sh test), which also fixes its bare default runningbun install && bun run testat the monorepo root, where no test script exists. Source:apps/cli/scripts/test.sh,apps/cli/scripts/sandbox.sh,apps/cli/scripts/build.sh,apps/cli/scripts/release-attestation-produce.sh.A helper-only GitHub release no longer blocks the CLI release train.
release-attestation-produce.shseeded its helper manifest fromgh release list --limit 1— unconditionally the newest release. But not every release is a CLI release: the Windows computer-helper workflow publishes helper-only releases (assetscomputer-helper-win.exe+.sha256) into the samev<version>tag namespace. One of those shadows the last real CLI release, the manifest seed silently misses, every helper then reads as "changed", and the producer hard-fails oncomputer-mac— a helper it never rebuilds — withhelper computer-mac input changed but this producer never rebuilds it. Observed live:v1.22.48(helper-only, published 09:54Z) shadowedv1.22.47and blocked the release. The seed now walks back through recent releases to the newest one that actually carries arelease-manifest.json. Source:apps/cli/scripts/release-attestation-produce.sh.Watchdog, device-probe, self-heal, keychain-reap, and state-dir-check are now supervised daemon services (RUSH-3193 #3). These five bare
setIntervaltimers inrunDaemon()are nowPeriodicServices registered onServiceSupervisor, alongside secrets-broker, browser-ipc, account-state, session-index, and monitors — each gets the same per-tick deadline, error boundary, and park/backoff circuit breaker, and now reports measured health throughagents daemon servicesinstead of an inferredrunning (unsupervised)label.schedulerandwebhook-receiverremain outside the supervisor (neither fits thePeriodicServiceshape — seecli/src/lib/daemon/AGENTS.md). Source:cli/src/lib/daemon/watchdog-service.ts,device-probe-service.ts,self-heal-service.ts,keychain-reap-service.ts,state-dir-check-service.ts,cli/src/lib/daemon/daemon.ts.agents daemon servicesshows every service's live health, with live enable/disable/restart (RUSH-3193 #4). The daemonservicesview now reports state / last-run / consecutive-failures / last-error for every supervised service (secrets-broker, browser-ipc, monitor-engine, account-state, session-index), not just the two socket services;--jsongains the full service list while keeping the existingsecretsBroker/browserIpcfields.agents daemon services enable/disable/restart <id>now take effect live through the supervisor instead of requiring a daemon restart. Source:cli/src/commands/daemon.ts,cli/src/lib/daemon-services.ts,cli/src/lib/daemon/supervisor.ts.Self-heal's staggered boot-time tick is restored (RUSH-3193 #17). The
ServiceSupervisormigration (RUSH-3193 #3) fired every periodic service's first tick immediately at daemon start, including self-heal — dropping the ~30s post-boot stagger the old inline timer used so shims/PATH could settle before self-heal's first sweep, without making daemon launch itself busy.PeriodicServicegains an optionalstartupDelayMs(default 0, no change for other services);SelfHealServicesets it to 30s. Also fixes a stale comment onwatchdog-service.tsclaiming an SSH fan-out thatrunWatchdogPassdoes not do (it's host-local). Source:cli/src/lib/daemon/service.ts,cli/src/lib/daemon/supervisor.ts,cli/src/lib/daemon/self-heal-service.ts,cli/src/lib/daemon/watchdog-service.ts.Usage refresh is per-device — the
usage.primary-hostbroadcast is gone (RUSH-3193 #15). Every host now reads its own usage directly from the provider APIs, so the old model where one primary device fetched fleet usage and SSH-broadcast a token-free derived envelope for subscribers to import is removed. Theusage.primary-hostconfig key (and itsinteractive.hostfallback for usage) is deleted, along with the publisher/subscriber role split; each daemon runs the local usage refresh on every host. Source:cli/src/lib/daemon-ticks.ts,cli/src/lib/usage-fleet.ts,cli/src/lib/usage-refresh.ts,cli/src/lib/device-config.ts,cli/src/lib/config-keys.ts.Daemon service supervisor: per-service contract, error boundaries, deadlines, health (RUSH-3193 P1). The daemon's background services used to be bare
setIntervalclosures sharing one event loop — a throw escaping a tick's local try/catch killed the whole daemon, and a hung tick (an unbounded SSH/keychain await) latched its overlap guard forever, silently freezing that one service for the daemon's life (observed ~51h). A newDaemonService/PeriodicServicecontract plus aServiceSupervisorgive every registered service its own timer, a per-service error boundary that can never propagate toprocess.exit, and a hard per-tick deadline that always releases the overlap guard even when the tick itself hangs; repeated failures park the service and retry it with exponential backoff while every sibling service keeps ticking, andsupervisor.health()reports state/last-run/consecutive-failures for every registered service. The session-index warm service is migrated onto it as the first proof of concept —agents daemon services listnow shows it assession-index. Other background services are unchanged; they migrate in follow-up PRs. Source:cli/src/lib/daemon/{service,supervisor,session-index-service}.ts,cli/src/lib/daemon/daemon.ts,cli/src/lib/daemon-services.ts.Managed Cursor accounts now use the account they display (RUSH-3196). Cursor runs and direct version aliases force Cursor's HOME-relative file credential store (
~/.cursor/auth.json) inside the selected version home instead of silently falling through to one macOS Keychain login. Stalecli-config.jsonmetadata no longer counts as signed in; existing Keychain credentials are left untouched and an unseeded managed version asks for Cursor's normal login flow.Grok billing events now reach
agents view(RUSH-3197). The daemon publishes freshly collected local-event snapshots instead of rejecting everylast_seensource as a failed refresh. Current Grok weekly meters cache normally; an expired or missing event tells the operator whichgrok@versionto run once rather than leaving a silent plan-only gap.Rebuilt the internal documentation as a compact architecture and decisions corpus, with command syntax remaining generated from the CLI.
Added
agents devices pick— prints the device automatic placement would choose for offloaded machine work: the least-loaded reachable POSIX box from the same auto poolagents run --device autodraws from, sorole=worker/role=personalmarks govern it. The name alone goes to stdout so scripts can consume it (box="$(agents devices pick)"); candidates and load go to stderr.--jsonadds every candidate and each exclusion reason. Fails loud with the excluded devices named when no worker is eligible — it never answers "run it locally".
Changed
scripts/test.shnow auto-picks a fleet worker by default instead of requiring crabbox.--device autosays the same thing explicitly,--crabboxselects the previous disposable-crabbox path, and--device <box>/--hereare unchanged. The old default needed the crabbox binary plus provider credentials, so the no-argument invocation failed on any box without them. An auto-pick that lands on the local machine runs in place without the--herewarning when that machine is itself a pool worker.scripts/release-attestation-produce.shgains--test-crabbox, so all three oftest.sh's lanes are reachable from a release. It previously exposed only--test-deviceand--test-here, leaving a release on a box with no fleet worker in reach unable to ask for the disposable crabbox it can still use.
Changed
- Native helper downloads are decoupled from the CLI release.
helper-download.tsused to buildreleases/download/v${cliVersion}/<asset>, keying every helper to the CLI's own tag. That was coupled in both directions: a CLI release had to re-stage every helper asset onto its new tag or the download 404'd, and a helper fix could not reach anyone without cutting a CLI release. Helpers now publish to their own tags (menubar/v1.0.0,keychain/v1.0.0,computer-mac/v…) and the CLI records a per-helper floor inhelper-versions.ts— the build it was tested against, and the immutable tag it falls back to offline.
Fixed
The keychain helper download could never have worked. GitHub rewrites a space in a release-asset name to a dot on upload, so an asset staged as
Agents CLI.app.zipis served asAgents.CLI.app.zipwhile the CLI requested the spaced name — a permanent- The asset is now published as
Agents_CLI.app.zip: a name GitHub preserves verbatim, chosen deliberately rather than mirroring GitHub's rewrite (matchingAgents.CLI.app.zipwould work only for as long as that normalization rule holds).helperAssetUrlsadditionally refuses any spec whose asset name contains a space, rather than minting a URL that cannot resolve. The extracted bundle directory keeps its space (Agents CLI.app) — that is the on-disk bundle name macOS and the TCC grant key on, and it is unchanged.
- The asset is now published as
The Windows helper exe is decoupled too.
ssh-tunnel.tsis a separate implementation that never routed throughhelper-download.ts, and it still keyedcomputer-helper-win.exetov${cliVersion}— the identical coupling, meaning every CLI release had to re-stage a ~165 MB binary or the download 404'd. It now resolvescomputer-win/v<x.y.z>from the same floor table.
Removed
release.shno longer stages helper fallback assets onto the CLI's own tag, and no longer gates a release on one. With helpers resolving from their own tags, those assets were work nothing reads — and the keychain one was republished on every release underAgents CLI.app.zip, a name GitHub rewrites to a dot and no client can fetch. The harddieon a missingComputerHelper.app.zipis gone with them: it would have failed an otherwise-good release over an asset the runtime no longer consults.
Fixed
The menu-bar helper build could emit a Developer-ID-signed bundle that was never notarized, and say nothing. Notarization was gated on
[ "$MODE" = "release" ] && [ "$SIGN_ID" != "-" ], so any invocation without thereleaseargument on a machine holding a Developer ID produced a real signed bundle, passed every self-test, and exited 0 — whilespctlreportedrejected / source=Unnotarized Developer IDwith no stapled ticket. Gatekeeper on macOS 26+ rejects such a bundle as "damaged" and crashes AppKit at launch. The three credential guards meant to fail loud on missing Apple creds also lived inside that branch, so they were unreachable in exactly the case that needed them. The gate is now the signature ([ "$SIGN_ID" != "-" ]): a Developer-ID signature always implies notarization, whatever the mode. Ad-hoc builds are unchanged — they cannot be notarized by construction andprepackalready stops them shipping.A credential-less debug build on a machine that holds a Developer ID now signs ad-hoc (with the
.devbundle id) instead of failing:SIGN_IDis auto-detected from the keychain, so demanding notary creds unconditionally would have broken the local build loop on every signing-capable Mac. The downgrade is resolved beforeInfo.plistis written — a late one would emit an ad-hoc bundle carrying the production bundle id and poison the user's Accessibility grant.MENUBAR_HELPER_SIGN_ID=-remains the explicit opt-out and is now documented in the script.
Fixed
The full test suite was red on
main, and pull-request CI could not see it. A top-levelawait import('bun:sqlite')insrc/lib/sqlite.tsmade that module un-lowerable to CJS, so every test that spawns a subprocess throughtsxdied at transform time withTop-level await is currently not supported with the "cjs" output format— 49 failures across 7 files, none of which touch SQLite. Both runtime arms now load throughcreateRequire, so there is no top-level await to lower. The specifier is held in a variable rather than written inline, keeping the off-Bun collector from trying to resolve a module that only exists under Bun (what the oldas stringcast did for the dynamic import).This mattered beyond the suite:
release-attestation-produce.shruns the full suite fail-closed, while PR CI runs only the tests selected for a diff. A failure outside every recent diff's selection is therefore invisible until release time, where it blocks the attestation and with it any publish.
Fixed
Restored operator-facing master-key custody guidance to
docs/secrets.md. The docs reset (2d96d05d6) rewrote the page from a manual into an architecture summary and dropped the section that names the machine-local key path (~/.agents/.secrets-key/passphrase), states that resolution is non-interactive, points headless sync atAGENTS_SYNC_PASSPHRASErather than the master key, and forbids exporting the master key from a shell rc file — the exact advice that caused RUSH-1968 on seven machines. Fourdocs-hygieneguards had been failing ever since, which is the alarm working as designed rather than test staleness.Restored the
**Severity rubric**section todocs/observability.md, removed by the same reset.doctor-findings.test.tspins this prose copy againstFINDING_SEVERITY, so its absence meant the docs-cannot-drift guard was simply not running.add-targets-user-repo.test.tsno longer enumerates thestate.jssurface. Its partial mock hand-listed every export, so addinggetCliVersionCachePathtostate.tstook the whole file down withNo "getCliVersionCachePath" export is defined on the mock. It now spreadsimportActualand overrides only the directory resolvers it redirects, so a new export cannot break it again.release.sh --with-helpers— an ordinary release publishes the CLI and nothing else (RUSH-3216). Two couplings survived helpers moving to their own tags, and both are now behind this opt-in (default off): stagingComputerHelper.app.ziponto the CLI'sv<version>tag, which publishes an asset no client requests; andrelease-manifest.sh require, which re-derives every helper's input digest and aborts when one moved without a rebuild — failing a good CLI release over Swift the CLI does not ship. Source:cli/scripts/release.sh.A new
release.shflag is inert until it is merged (RUSH-3216).release.shexecsrelease-worktree.sh, which checks outorigin/<default>and re-runs the script from there, so the second parse is the merged copy and an unmerged flag dies asunknown flageven though the local parser handles it. Pass--orchestration-phaseto skip the re-exec when exercising a new flag pre-merge. Source:cli/scripts/release.sh.Managed share URLs use your Phoenix handle, not a UUID (RUSH-3224).
agents artifacts sharenow publishes toshare.agents-cli.sh/<handle>/<slug>where<handle>is the local-part of the signed-in email ([email protected]→muqsitnawaz), and the default slug is<readable>-<16hex>(a short view-id). Owner metadata is still the Phoenix userId; the Worker 403s a PUT to someone else's handle and 409s if two accounts collide on the same local-part. HTML publishes rewrite Chrome-savedfile://…#sectionTOC links to in-page hashes and inline local sibling images as data URIs, so a self-contained plan (including a 1 MB page with embedded screenshots) stays viewable after upload. Source:cli/src/lib/share/{backend,worker-template,html,publish}.ts.A remote interactive
--devicerun now prints its session id as it connects (RUSH-3227). When Claude (or a resume) already has a real id before the TTY is taken, stderr showsSession <uuid> on <host>andagents sessions resume <uuid>so the id exists while the connection is live, not only after OpenSSH closes. A launch id is not printed as a session id. Source:cli/src/lib/hosts/reconnect.ts,cli/src/commands/exec.ts.A remote interactive session that closes now prints its id (RUSH-3227). After
agents run --device(including--raw),sessions focus/resumeover SSH, or a remote tmux attach ends, the CLI writes the full session id andagents sessions resume <id>under OpenSSH'sShared connection … closed.line, so a dropped tab is not a bare shell. Auto-reconnect still swallows the 255 it will retry; this fires only when the user is actually back locally. Source:cli/src/lib/hosts/reconnect.ts,cli/src/commands/{exec,go,focus,resume,attach,sessions}.ts,cli/src/lib/session/remote/remote-list.ts.Re-pin the keychain helper to the binary that actually shipped in 1.22.47.
scripts/verify-keychain-helper.shcomparedbin/Agents CLI.apptobeb02d…, but@phnx-labs/[email protected]andkeychain/v1.0.0both containa49080…. Linuxnpm packtherefore died at prepack and blocked every release attestation. Same class of fix as #835 / #912. Source:cli/scripts/Agents CLI.app.sha256.A session with no observer no longer reports as healthy (RUSH-3125).
classifyHostLinkdecides a session's host link from two local signals — the owning IDE window's heartbeat and tmux's attached-client count — and when it had neither it fell through toconnected. That is the wrong default for a detector: the rows it silently blessed were exactly the ones with no observer (a bare terminal, a team spawn, a cloud task, or any--devicesession whose pane lives on another machine, since every signal it reads is local). There is now anunknownlink, which says the question is open rather than answering it;connectedis returned only on positive evidence — a counted client or a republishing window.unknownis not a loss signal: it never promotes a status, and it no longer clears a derivedattachedpresence, so a plain terminal session you are sitting in keeps its marker. Everything else is unchanged and deliberately so — a deliberateagents sessions detachis still excluded first,tmuxClients === 0is still authoritative, an absent client count still never reads as zero, and per SES-18a arunningsession still keeps its status. Source:cli/src/lib/session/host-link.ts,cli/src/lib/session/active.ts, spec SES-18a.A tmux status bar can show which agent session a pane is running — and never a fabricated one.
agents runwraps an interactive agent in a tmux session namedag-<agent>-<8 chars>, built from(options.sessionId ?? randomUUID()).slice(0, 8)(cli/src/lib/exec.ts). Reading those 8 characters as a session id is quietly wrong: the launcher pre-assigns an id only for Claude, so on a box running two harnessesag-claude-c8c4a2c8carries a real, resumable short id whileag-codex-364cd550carries a throwaway that resolves to nothing — and the two are indistinguishable on sight.createSessionnow stamps two session-scoped tmux user options,@ag_session_idand@ag_agent, so a format like#{?#{@ag_session_id},#{@ag_session_id},#{@ag_agent}}renders a real handle where one exists and falls back to the harness name where none does, instead of printing somethingagents sessions <id>would reject. The id is published only when the harness actually received it — a newisHarnessKnownSessionIdmirrors the two branches inbuildExecCommandthat put an id on the command line (a native resume on any harness with aresumespec, and Claude's create-with---session-id), soagents run codex --session-id Xwithout--resume, where X never reaches codex, correctly publishes nothing. This is the in-tmux counterpart to the existing name-parsing recovery tier (shortIdFromName+resolveNamesToSessionIdsincli/src/lib/session/active.ts), which validates a short id against the session DB; a tmux format cannot do a DB lookup, so the option carries the answer instead. Best-effort throughout: a failed stamp never fails a launch, and an unset option renders empty. Source:cli/src/lib/tmux/session.ts,cli/src/lib/exec.ts.
traces: drift signal for topic buckets
agents traces sync now computes a drift signal for each topic bucket by comparing today's error and stall rates against a 14-day rolling history stored inside the shard. Buckets that cross a 0.20 absolute-delta threshold are marked degrading or improving; the rest are stable. Buckets with fewer than 3 historical days are skipped to avoid noise on fresh deployments.
The shard now carries two new fields:
bucketHistory— rolling 14-day array of per-bucketBucketStats(errorRate, stallRate)driftSignals—DriftSignal[]for the current sync, sorted by errorDelta descending
--dry-run --out <dir> seeds history from the previously written index.json in the output directory, so successive local runs accumulate signal without hitting the network.
agents traces sync --dry-run --out <dir>computes the derived trace shards from your localsessions.dband writes them to a directory — no Phoenix sign-in, no worker, no upload — so you can verify your real trajectories before the hosted path is wired.- The per-session drill-down (
sessions/<id>.json) now emits aSessionDetail(ametasummary — spanMs/turns/tools/errorCount/tokens/cost/outcome/repo — plus a plain-languagewhereItWentWrong) that the Phoenix Evals console consumes directly, instead of the raw internal trajectory shape.
1.22.48
On your own machine, every Claude run uses your normal login — not the worker setup-token (RUSH-2395). The credential now follows device role, not run mode. A device marked
config.role: personal(your interactive box — set it withagents devices role <name> personal) authenticates every Claude run from its per-version login, whether it opens a TUI or is a headless one-shot likeagents run claude "fix the bug". Before, the choice keyed only on whether a prompt was present ("this opens a TUI", not "a human is present"), so a headless run on your laptop grabbed the reservedauth-bundle setup-token and took the session off your login — surfacing as/statusreportingAuth token: CLAUDE_CODE_OAUTH_TOKEN. The setup-token stays the credential for headless runs on worker devices (and unmarked boxes), which have no keychain login to defer to. Routines follow the same role gate. Source:apps/cli/src/lib/harness/adapters/claude.ts,apps/cli/src/lib/exec.ts,apps/cli/src/lib/daemon/runner.ts,apps/cli/src/lib/device-config.ts.A hand-cut release no longer dead-ends on a fresh attestation store or a lost
RELEASE_ATTESTATION_DIR(RUSH-2970). Three traps that cost roughly eight failed release attempts across 1.22.43-1.22.46 are fixed at the source. (1)release-attestation-produce.shnow seedsrelease-manifest.jsonfrom the last published GitHub release before falling back to an empty one — a fresh store had no recordedcomputer-macinputDigest, so the helper loop read "input changed" and told the operator to runpublish-computer-helper-mac.sh, which does not write a manifest, so the instruction looped forever on a byte-identical helper. (2)release-worktree.shnow exportsRELEASE_ATTESTATION_DIRto the caller's store when it is unset: the throwaway release worktree resolved its own empty.release-attestations, sorequirereportedmissing exact attestation keywith?for every key component — which reads like a key mismatch rather than a wrong directory. An explicit export still wins. (3)publish-computer-helper-mac.shnow sourcesheadless-sign-context.shitself: its own documented invocation (agents secrets exec apple.com -- ...) injected the notary creds but left the Developer ID signing keychain locked, so a headless publish died incodesignwitherrSecInternalComponentunless the operator knew to source that context first — which nothing said. On a Mac without the release-box pass files it is a no-op. Source:apps/cli/scripts/{release-attestation-produce,release-worktree,publish-computer-helper-mac}.sh.Session rows show what the agent DID, and dead crash-orphans stop piling up (RUSH-3011). The
agents sessions watch --jsonrow (which the AGI EXT Fleet reads) now carries a recap so a row reads as the agent's work, not its stale first prompt:titlefollows a best-source-wins ladder — a/rename/harnesslabel(which also holds an agent-generated title) → the last agent line → the first-prompt topic — andrecapSource('label'|'last'|'prompt') names which rung won, so a session that produced work shows an agent-derived line. The first user turn is cleaned intouserPromptClean/userPromptKind(withlastAgentLineexposed) so a screenshot path folds to[image], a pasted$ cmdto the command, and a/skillinstall path to/<name>— path noise never shows on the "You" line — and the recap card'sPrompt:line uses the same cleaning. Separately, a crash-leaked--devicetunnel session that is genuinely dead and days-stale (abandoned+ dead pid) is folded OUT of the reconnectable set (resumable: false,recovery: null), so the "Needs reconnecting" list stops ballooning; a live pid (idle-but-unfinished) or a recently-closed session is never reaped. Source:apps/cli/src/lib/session/{prompt,active,render}.ts,apps/cli/src/lib/session/remote/watch.ts,apps/cli/docs/sessions.md.Keep generated session names in
label, separate from the first-prompttopic(RUSH-3011). Claudeai-titleand/renameevents now populate the canonical name field consumed by fleet clients, and an empty live metadata record can no longer erase an existing generated title or launch handle. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/session/db.ts.fix(teams): observe AgentManager's fire-and-forget init rejection so a lost init race (dir removed mid-mkdir) fails the awaiting caller instead of crashing the process as an unhandled rejection — the flake that failed two fully-green release attestation runs
Per-account 429 backoff + auth verdicts derived from fresh usage fetches — the last four 'usage unavailable' accounts recover (RUSH-3036). After the 20-min probe throttle shipped, four of eight Claude accounts still showed
usage unavailable: the 429 backoff was per-PROVIDER, so the first account to hit its quota in a refresh pass parkedclaudeentirely and every account after it in the loop's fixed iteration order was never fetched — the same accounts starved every pass. The backoff is now scoped per account (usage-backoff.tsrecordsclaude@<usageKey>.<deadline>; a provider-wide penalty still parks everyone, and callers with no account identity keep the old behavior). Second, the auth-health probe no longer spends a second request on the same endpoint: a fresh (≤20 min) successful usage snapshot is the same authenticated request with the same shared setup-token, soprobeAuthHealthderives aliveverdict from it and only fires a network probe when no fresh evidence exists — roughly halving fleet endpoint load.agents devices ping --strictpassesforceLive, which skips the derivation entirely — its contract stays a genuinely live request that surfacesrevokedimmediately — and derivation additionally requires a local credential (signedIn), so a fleet-imported snapshot on an unsigned home never reads aslive. The auth probes themselves are account-scoped too, so a probe 429 parks only its account. Source:apps/cli/src/lib/usage-backoff.ts,apps/cli/src/lib/accounting/usage.ts(UsageOptions.usageScope),apps/cli/src/lib/auth-health.ts(verdictFromFreshUsage),apps/cli/src/lib/usage-refresh.ts,apps/cli/src/lib/daemon-ticks.ts.agents menubar doctorno longer claims a healthy just-upgraded helper is "running the OLD binary" (RUSH-3038). The stale-process check compared a pid start time parsed fromps -o lstart, which prints whole seconds, against the installed bundle's sub-second mtime.restartMenubarHelperAfterSwaprestarts the helper within the same second as the swap that triggered it, so a correct upgrade always tripped the strict<: on zion at 1.22.46 the pid started at…353000and the bundle was written at…353700— 700 ms apart, one second — and doctor reported the helper stale and told the user their Accessibility grant "may not be trusted", after an upgrade that had already restarted it onto the new binary. The bundle mtime is now truncated to the same whole second before comparing, so a pid that started in the swap's own second reads fresh while a pid a full second or more older is still caught. Source:apps/cli/src/lib/menubar/install-menubar.ts.agents run codex --mode autostops prompting for approval. Codex declared onlyplan/edit/skip, so--mode autosilently degraded toedit— andeditisapproval_policy="on-request", which asks before every command the sandbox denies. Every unattended caller that passes--mode auto(AGI EXT's agent launches, teams teammates, routines) therefore sat on an approval dialog nobody was there to answer. Codex now has a realauto: theagents-autopermission profile, identical sandbox toagents-edit(workspace,~/.agents, the regenerable toolchain cache roots, network on), withapproval_policy="never"— a sandbox-denied command surfaces to the model as a plain command failure it can work around instead of stopping the run. Autonomy is the approval axis only:autodoes not widen the sandbox, and--mode skipremains the only mode that removes it. Interactive shim launches (a barecodexat your own terminal) still pinedit, where an approval prompt is the useful outcome. Source:apps/cli/src/lib/codex-policy.ts,apps/cli/src/lib/harness/adapters/codex.ts,apps/cli/src/lib/agent-spec/agents.ts,apps/cli/src/lib/exec.ts.Hooks can be scoped to a permission mode: new
matches.permission_modepredicate (RUSH-3050). Ahooks.yaml/ subrulehooks.yamlentry may declarematches: { permission_mode: plan }(string or array) so the hook fires only when the harness reports that mode in the hook's stdin JSON (permission_mode, or Grok-stylepermissionMode). The predicate is deliberately fail-open on absence: a harness that never reports a mode (Codex, and most others — Claude Code is the one that does) keeps firing the hook, so declaring it never silently disables a hook fleet-wide. Enforced in both the TS reference evaluator (shouldFire()) and the generated shim gate, pinned together by the existing conformance suite. Motivating case: the plan-presentation Stop hook fired on every stop in every session and false-positived on ordinary answers that mentioned plans; it can now declarepermission_mode: plan. Source:apps/cli/src/lib/types.ts(HookMatches),apps/cli/src/lib/hooks/match.ts,apps/cli/src/lib/hooks/cache.ts,apps/cli/docs/hooks.md.New device config key
description— a one-line, fleet-synced answer to "what is this box FOR".agents devices config <name> description "gpu box — cuda 12.4"stores a single-line summary in the device's tracked~/.agents/devices/<name>/agents.yamlunderconfig.description, so it syncs to every machine viaagents repo push/pullexactly asroledoes, and any box may set it for any device (sharedvisibility). Because it will be shown by the device-list renderer it is validated: a newline is rejected outright and the value is capped at 80 characters — over-long input fails with a readable error naming the cap, never a silent truncation.notesis unchanged: it stays the appended list of long-form operator scratch, and both key descriptions now state the distinction. Source:apps/cli/src/lib/device-config.ts.Dismissing a discovered device is now a fleet-wide fact (RUSH-3062). The ignore-list lived at
~/.agents/.history/devices/ignored.json— a gitignored, per-machine path — so a node dismissed on one box kept re-surfacing as an auto-discovery suggestion on every other box. It now lives in the tracked, synced central~/.agents/agents.yamlunderfleet.ignored, a list of{ name, ignoredAt, ignoredOn }entries (who dismissed it, when, on which box — the read side for a futureagents devices ignored). An ignored node is deliberately not a device, so it has no per-device doc; central agents.yaml is the established home (it already carriesfleet.defaults). Writes go through the existingupdateMetapath (withMetaLock+ atomic write), and a malformedfleet.ignoredblock is still a hard error rather than a silently-emptied set. A one-shot migration folds any existingignored.jsonintofleet.ignored(union by name, legacyupdatedAtkept asignoredAt) and removes the legacy file only after the central write lands; running it twice is a no-op. TheloadIgnored/isIgnored/addIgnored/removeIgnoredsignatures are unchanged, so no call site churn. Source:apps/cli/src/lib/devices/registry.ts,apps/cli/src/lib/devices/config-migration.ts,apps/cli/src/lib/state.ts.
type: feat
Collect root-disk capacity in the existing device health probe and cache static hardware specifications for seven days.
type: feat
Put real capacity in the default agents devices list — a spec cell (cores / total RAM / total disk), a disk used column beside load and mem, and the per-device description as the tail column (truncated first on narrow terminals, then role; the numbers never truncate). Add agents devices describe <name> <text> (task-shaped sugar over the description config key) and agents devices ignored (dismissed nodes — when, and which machine dismissed them). devices list --json gains description and disk totals inside health (additive only).
- Interactive
agents runnow spawns directly by default (RUSH-3066).tmux.enabledis an explicit per-device opt-in for addressable panes instead of the default wrapper: the wrap's benefit (a unique%panesoagents sessions --activecan tell co-located agents apart, andagents focusre-attaching without forking) accrues to the control plane, while the cost — mouse, scrollback and clipboard behavior — is paid by whoever is at the terminal. Turn it back on per device withagents config set devices.<name>.tmux on.--no-tmuxand--disable-tmuxremain compatible no-ops while wrapping is off.
type: fix
Make opt-in tmux-wrapped runs default to mouse interaction, OSC 52 system clipboard integration, and 20,000 lines of scrollback while preserving the user's tmux configuration.
agents sessions trace --no-redactreads honestly in compare and lineage too (RUSH-3077). The single-trajectory footer already saidUnredacted (local only)under--no-redact, but the compare (--compare) and lineage (--tree) renderers hardcoded aSecret-redactedfooter regardless of the flag — claiming a redaction that never happened. Both now share the single-trajectory labelling logic (redactionLabel): compare derives it from the two compared trajectories' ownredactedstate (a mixed pair still reads redacted, never a false safe-to-share claim), and lineage takes the trace's redaction flag. Redacted-by-default output is unchanged. Source:apps/cli/src/lib/session/trajectory-html.ts,apps/cli/src/commands/sessions-trace.ts.BREAKING: the top-level
agents usagecommand is removed (RUSH-3079). It was a second, worse surface for the same dataagents viewalready renders — per-account quota/rate-limit usage with the account, version, and auth state beside it — and in practice most harnesses printedstaleor "does not publish usage data" there. Useagents view(optionallyagents view <agent>,--refreshfor an explicit collection,--jsonfor scripts). The name is retired:agents usagefails as an unknown command and never auto-corrects into a live command. The shared usage library behindagents viewand rotation/backoff is untouched. Source:apps/cli/src/commands/usage.ts(deleted),apps/cli/src/cli/command-registry.ts,apps/cli/src/lib/startup/command-registry.ts.agents browserbinds--deviceat start (RUSH-3086, RUSH-3087).--devicestays onagents browser startand is rejected on later verbs; the starting machine records a localtask → deviceindex sotype/click/screenshot --task postresolve the device without repeating the flag. A verb naming an unknown or killed task exits non-zero and lists the open tasks (real URL + device), and never opens a second browser.screenshot -o /path/x.pngnow writes exactly that path — the daemon still sandboxes its own cache, and the CLI copies out. Source:apps/cli/src/lib/browser/task-index.ts,apps/cli/src/commands/browser.ts.
type: fix
An offline device keeps its spec cell (RUSH-3096).
agents devices listrendered a down box as a bareci-runner-fsn1 linux offline, because a failed probe wrote a row with no hardware facts over the cached one. Cores, total RAM, and root-disk capacity now survive an unreachable probe and render beside the offline marker —ci-runner-fsn1 linux 8c 16G 500G offline. Load, memory, and disk-used stay blank (there is no current reading for a box that did not answer), and the Fleet capacity footer still counts only reachable devices. Source:apps/cli/src/lib/devices/stats-cache.ts,apps/cli/src/commands/ssh.ts.The macOS keychain broker (
Agents CLI.app) now has a download-on-demand fallback (RUSH-3100). The signed + notarized keychain helper still ships inside the npm tarball, but a release now also publishes anAgents CLI.app.zipGitHub release asset per tagged version. A machine whose tarball lacks the bundle fetches that asset for its exact CLI version on the explicitagents setup secretspath (and the upgrade self-heal) — verified by sha256 + codesign + Developer ID Team + notarization before install — reusing the shared helper-download machinery already behind the computer and menu-bar helpers. The secrets hot path (getKeychainHelperPath) stays synchronous and network-free: when no bundled helper exists it now fails loud pointing atagents setup secretsrather than a bare "reinstall". Unlike the menu-bar helper it pins no designated requirement, since keychain items are gated by the access-group entitlement + biometry, not a DR-keyed grant. Source:apps/cli/src/lib/secrets/download-keychain.ts,apps/cli/src/lib/secrets/install-helper.ts,apps/cli/scripts/release.sh.The menu-bar helper can be fetched on demand, mirroring the computer helper (RUSH-3100 Stage N).
MenubarHelper.appis now ALSO published as a signed + notarizedMenubarHelper.app.zipGitHub release asset, so a machine whose npm tarball lacks the bundle can fetch + verify it from thev<version>release.agents menubar enable/agents menubar setupdownload it when no bundled or local.appis present; a downloaded bundle is verified (sha256 +codesign --verify --deep --strict+ Developer ID Team2HTP252L87+ a designated-requirement pin on bundle idcom.phnx-labs.agents-menubar+spctlnotarization) before it is ever installed — the DR pin is what keeps the Accessibility grant alive across upgrades, so a substituted bundle is refused loud, never silently accepted. The.appSTILL ships inside the npm tarball this release (bundled-first, download-fallback); the download runs only on the explicit enable/setup path, never on the every-invocation startup self-heal, which stays synchronous and no-ops when no local source exists. The download and verify machinery is shared with the computer helper in a newsrc/lib/helper-download.ts(no duplicated code path). Source:apps/cli/src/lib/helper-download.ts,apps/cli/src/lib/menubar/download-menubar.ts,apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/src/lib/computer/download.ts,apps/cli/scripts/release.sh.Fix: the menu-bar helper's Accessibility row now reads "AGI Menu", not "MenubarHelper" (RUSH-3101). Since the RUSH-3076 grant-persistence fix, the compiled executable was still named
MenubarHelper, and launchd execs that Mach-O directly — bypassing LaunchServices name resolution — so macOS fell back toCFBundleExecutablefor both the Accessibility list row (blank icon) and the "would like to control this computer" prompt. The executable inside the bundle is now namedAGI Menu; the bundle folder (MenubarHelper.app), bundle id (com.phnx-labs.agents-menubar), and designated requirement are all unchanged, so the existing Accessibility grant is unaffected by the upgrade. Every basename-matching check moved with the rename:classifyMenubarProcesses/installedExecutablePath(install-menubar.ts), theorphan-reap.tsprotected-service regex, and the Swift side'sSingleInstance.swift/ChildProcessSelfTest.swift, now reading a sharedHelperIdentity.executableNameconstant. Source:apps/cli/menubar/Package.swift,apps/cli/menubar/scripts/build.sh,apps/cli/menubar/Sources/MenubarHelper/HelperIdentity.swift,apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/src/lib/tmux/orphan-reap.ts.Fixed a module-initialization cycle that crashed any entry point reaching
helper-downloadfirst.helper-download.tsimported two pure sha256 helpers (parseSha256Asset,sha256File) fromcomputer/ssh-tunnel.ts, whose import graph runsbrowser/drivers/ssh→browser/chrome→secrets/*→secrets/download-keychain, which imports back intohelper-downloadwhile it is still evaluating — beforeEXPECTED_TEAM_IDis bound. The result was a hardReferenceError: Cannot access 'EXPECTED_TEAM_ID' before initializationthrown fromsecrets/download-keychain.ts:45, which took down thedrift-syncapply path and theself-healisolated-home sweep (both run real subprocesses) and blocked release attestation, since the producer runs the full suite fail-closed. The two helpers now live in a dependency-free leaf module,lib/sha256-asset.ts, so nothing in the download path pulls the ssh/browser/secrets graph in at module-init time. Source:apps/cli/src/lib/sha256-asset.ts,apps/cli/src/lib/helper-download.ts,apps/cli/src/lib/computer/ssh-tunnel.ts.agents accounts labelreads like the task: label by<harness>@<version>, or pick from a list (RUSH-3126). Labeling the account behind a specific install used to require knowing its email —agents accounts label codex work --account [email protected]— even though the user is looking atagents view codex, which is organized by version. The first argument now also accepts<harness>@<version>:agents accounts label [email protected] worklabels whatever account is signed in there, and the label stays bound to the account identity, socodex#workkeeps selecting it after that account moves to a newer install. A bare-harness call with several signed-in logins opens an interactive picker (identity · default marker · versions) instead of erroring; the same account signed into multiple versions counts as ONE login, so it never demands a selector. Non-interactive callers keep the loud error, now naming both script-safe forms, and--account <email|id>still works. Passing both@<version>and--accountis refused as contradictory. Source:apps/cli/src/commands/accounts.ts(groupLabelIdentities).Managed artifact sharing: sign in and publish with zero Cloudflare setup (RUSH-3135, Phase 1). Signed-in users (
agents auth login) publish to the already-live managed endpoint atshare.agents-cli.sh— the Phoenix access_token fromphoenix-session.jsonis the bearer, so there is no R2 bucket, Worker, or write token to provision.share status,share list,share revisions, andunshare/share deleteresolve through the sameresolveShareBackendseam, so a managed-only user can see and take down what they just published instead of being told to runagents artifacts setup.share analyticsis a scoped P1 exclusion (Cloudflare Web Analytics is BYO): a managed user gets a message naming that, not the generic "Not configured" error. The Worker verifiesAuthorization: Beareragainst Phoenix ID (GET ${PHOENIX_ID_BASE}/api/v1/auth/me), stampscustomMetadata.ownerwith the verifieduserId, and namespaces the R2 key by that userId so one user cannot write another's prefix.--visibility public|unlisted(defaultpublic) replaces the--unlisted/--privatebooleans; those flags stay as hidden aliases mapping to--visibility unlisted. Unlisted GET now sendsX-Robots-Tag: noindex(it was already hidden from the gallery/list).orgvisibility is 400 — that is Phase 2.--meta owner=/visibility=/expires-at=is rejected client-side (those keys are Worker-stamped). BYO Cloudflare is unchanged: a staticWRITE_TOKENis still honored when the bearer matches it (checked first, so the platform endpoint may set both principals);AGENTS_SHARE_BACKEND=byoor a caller-supplied write token forces the BYO path while signed in. Already-provisioned BYO endpoints needagents artifacts share updateto pick up the new Worker (template hash changed). Source:apps/cli/src/lib/share/{backend,worker-template,publish,delete,config}.ts,apps/cli/src/commands/share.ts.Managed share deploy wires
PHOENIX_ID_BASEso Phoenix-bearer auth survivesagents artifacts share update(RUSH-3138). Cloudflare's script-upload API clears Worker bindings/secrets wholesale; the managed Worker readsenv.PHOENIX_ID_BASEto verify bearers at${PHOENIX_ID_BASE}/api/v1/auth/me, so every update wiped it and Phoenix PUTs 401'd while BYOWRITE_TOKENkept working. The deploy path now re-appliesPHOENIX_ID_BASEas asecret_textafter every script upload (same Secrets API asWRITE_TOKEN), sourced from the CLI'sPHOENIX_ID_BASE(env, else the deployed Phoenix ID service). Pure-BYO deploys do not set it. Source:apps/cli/src/lib/share/{provision,backend}.ts,apps/cli/src/commands/share.ts.Security: the remote browser-control consent gate now covers the whole launch path.
agents browser remote-control off(the default) was only enforced on thebrowser startcommand, butnavigate,click,screenshot,evaluateand the other page verbs open a browser implicitly when the caller has no live task. Abrowser navigate --device <box>therefore opened a browser on a machine whose owner never opted in, whilebrowser start --device <box>was correctly refused. The gate now sits in the daemon at the two points that can launch a browser (BrowserService.startand the create branch ofresolveOrCreateTask), so every implicit-LAUNCH verb is covered. It does not cover attaching: a request naming an existing task (--task, or the single-match-by-caller path) returns before the gate and can drive that task's tabs. That is pre-existing and tracked separately —remote-control offmeans "no new browser", not "no access". The consent marker rides the IPC request rather than the daemon's environment: a daemon auto-started by a fleet-remote CLI inheritsAGENTS_FLEET_REMOTE=1permanently, and reading that would have refused every later local drive. Source:src/lib/browser/service.ts,src/lib/browser/remote-control.ts,src/lib/browser/ipc.ts,src/lib/browser/types.ts.agents browser profiles edit. An existing profile's description, endpoints, secrets, viewport, and binary could not be changed without deleting and recreating it —-d/--descriptionexisted only oncreate, andupdateProfile()had no CLI caller at all.profiles edit <name>reusescreate's flag spellings (minus-b/--browser, which keys the on-disk profile cache) and validates the merged record, so a binary edit re-resolves the browser path and a--target-filteredit re-checks the--electrongate. Source:src/lib/browser/profiles.ts,src/commands/browser.ts.profiles doctorflags a profile that resolves to the wrong machine's browser. A profile declared by one device but bound tocdp://localhost:PORTis evaluated on the machine running the command, so the name meant a different browser on every box — silently handing an agent a logged-out stranger instead of the credentialed profile it asked for. The check now names both the declaring device and this one.ssh://profiles are unaffected: they address a host, so they mean the same browser from anywhere. Source:src/lib/browser/runtime-state.ts(identityLoopbackMismatch).Fixed: editing a profile's endpoint could collide with itself. The local port scan
createProfileruns was never applied on update, and applying it naively would have failed every edit against the profile's own stored port. Extracted asassertLocalPortFree(profile, { ignore })and now used by both. Source:src/lib/browser/profiles.ts.agents browser profiles rename <from> <to>. A profile could not be renamed at all:profiles editrefuses a name change because the name keys the on-disk runtime dir, every endpoint/fork dir derived from it, and thebrowser.profilepointer. The only route was delete-and-recreate, which silently abandons the browser's--user-data-dir— where a profile's logins live. On a real agent browser that is gigabytes of session state and every account it has ever signed into.renamemoves the config (staying in whichever store it already lives in), moves every cache dir belonging to the old name, and repoints bothbrowser.profileandbrowser.viewerwhen either pointed there — a danglingbrowser.viewersends every artifact back to the OS default handler, which is the exact bug the viewer seam was built to fix. Refuses while the profile is in use, because moving a--user-data-dirout from under a running browser corrupts it; refuses when the name exists in BOTH stores, since rewriting one would leave the other listed under the old name with its data already moved away; and validates every destination BEFORE moving any of them, so a collision on the second endpoint cannot strand the first one's logins under a name with no config entry.osjoinsdefaultas a name a profile may not take — it is the reservedbrowser.viewervalue meaning the OS handler. Source:src/lib/browser/profiles.ts,src/commands/browser.ts.Profile-name validation is shared between
createandrename. The shape rule lived inline inprofiles create, so a second caller would have accepted namescreaterejects. NowassertRegistrableProfileName, which also refusesdefault— the reserved alias meaning "this machine's configured profile" (RUSH-2709), not a name. Source:src/lib/browser/profiles.ts.User-facing pages open in your configured browser profile, not the OS default.
agents browser navigatehonouredbrowser.profileand nothing else did:agents fleet login,agents devices lease,agents feedback,agents sessions trace --open, andagents browser sessions --openeach shelled straight toopen/xdg-open, so every one of them landed in whatever the OS handler happened to be — on a Mac with Arc set as default, all of them opened in Arc while the configured Comet profile sat unused. They now route through one seam (showUrl/showFile), which resolves the viewer once. This matters beyond tidiness: the configured profile is where the fleet's logins accumulate, so a page opened there is one you are already signed in for, and a login it acquires is inherited by every later agent. The seam does not auto-start the browser daemon: showing a page is a side errand, so blocking it on a cold start would be a surprising stall. Daemon already running -> the viewer; not running -> the OS handler. Source:src/lib/open-url.ts.New
browser.viewerconfig key (device scope) — a profile name, orosto keep using the OS default handler. Unset followsbrowser.profile. Deliberately distinct frombrowser.profile: one is the profile agents drive, the other is the browser that shows you a page. Source:src/lib/device-config.ts.New
showIPC action — opens a tab bound to no task, so the abandoned-task reaper never closes a page you are reading. That is the whole reason it is notnavigate. Screenshots, PDFs and recordings still go to the OS app, where Preview and QuickTime are the better viewer. Source:src/lib/browser/service.ts,src/lib/browser/ipc.ts.New
agents browser show <url|file>— the CLI entry point to that seam, so external tools (a renderer's--open, a script) can show a page in the configured profile instead of shelling toopen. Use it instead ofnavigatefor anything a person will read:navigatebinds a task and the reaper closes a task's tabs.--os-browserforces the OS handler;--jsonreports where it landed. Source:src/commands/browser.ts.--device interactiveresolves to the machine the human is at.--device autopicks a box by load; this picks the one box someone is actually looking at, pinned asinteractive.host. It resolves in the shared host matcher rather than per command, sobrowser,run,sessionsandsecretsinherit it;teamsandsshresolve it explicitly because they leave the fleet passthrough before the matcher runs. A few narrower--devicesurfaces do not consult the matcher. None of them mis-routes: most fail loud, anddevices harnessesfilters to an empty result. Wiring them up is a follow-up. It exists because a skill cannot teach a host name: guidance that says "deliver it to" is wrong on every other fleet and stale the moment the pin changes, so agents were left inferring the target or skipping the step. A fixed token is something documentation can state literally and have be correct everywhere. When no host is pinned it refuses and names the command that fixes it, rather than falling back to the local machine — running on a headless worker with nobody watching is the exact failure the sentinel prevents, and it would fail invisibly. Source: src/lib/devices/interactive-host.ts.agents sessions stop <id>ends a live agent outright, and a tmux-wrapped agent no longer leaves an orphaned dead session behind (graceful shutdown, #5a/#5b). Two halves of one gap — a single close should tear down every layer (agent → tmux mux → tab):- New verb
agents sessions stop <id>on the session-lifecycle axis (besidedetach/attach): it stops the interactive process and tears down its tmux/mux session — reusingdetach's exactstopInteractiveteardown (kill-sessionwhen tmux-hosted, else SIGTERM→SIGKILL the pid) and resolution (remote sessions stopped over SSH, cloud/team refused) — but does NOT resume it headless. Usedetachto keep an agent working unattended,stopwhen the work is over. AGI EXT calls it when a user genuinely closes an agent tab so the agent + mux shut down instead of lingering idle. Source:apps/cli/src/commands/sessions-stop.ts. - The tmux-wrap
pane-diedhook is nowsession_attached-aware (AGENT_HOOK_SCHEMAv6): with a client attached itdetach-clients as before (soresolveAfterAttachreads the exit status, EXEC-23b unchanged); with no client attached itkill-sessions outright. A wrapped agent that exits unattended — a closed terminal, or a/exitafter the user detached — previously left a deadremain-on-exithusk on the socket until the daemon's periodic reap, showing as an orphaned idle session. Source:apps/cli/src/lib/tmux/session.ts,apps/cli/src/lib/exec.ts.
- New verb
agents accounts listis grouped, aligned, and label-first (RUSH-3053). The native-logins section was a ragged, ungrouped wall — 15 logins in one flat list, no columns, no labels, opaque ids dumped raw. It now groups logins by harness (printed once per group), aligns label · identity · version into columns, marks the default account per harness with*, and prints therun <harness>#<label>/run <harness> --account <name>selector hints inline. Provider bundles keep their own aligned section.--jsonoutput is unchanged. This completes RUSH-3053's list-redesign track — the<harness>#<label>account selector itself shipped in 1.22.47. The renderer is extracted as the pure, unit-testedrenderAccountList. Source:apps/cli/src/commands/accounts.ts.
type: breaking
Browser profiles now live only in each declaring machine's devices/<machine>/agents.yaml. The fleet registry is the read-time union of those files: a name declared once is identity-bearing, while the same name declared by several devices is fungible. Leftover central browser: entries are not claimed on first read — run agents browser profiles claim on the machine that hosts the browser. Only profiles that machine can actually launch are moved into its device file; the rest stay central until that machine claims them. A configured default that no device declares is now an error on agents browser start, not a silent fallback to a logged-out auto-chrome. profiles prune only considers profiles this device declares (--fleet is gone; deleting a peer's declaration is not possible).
type: feat
agents browser profiles listshows WHERE, not a stored scope. The column is the devices whose owndevices/<machine>/agents.yamldeclares the name — true by construction, no field to drift.--jsonaddsdevicesandkind(identitywhen exactly one device declares the name,fungiblewhen several do).profiles addis an alias ofcreateand printsAdded "<name>" on <device> (port N).Source:src/commands/browser.ts.profiles doctorfailswhereon the original comet-local shape. An identity-bearing name whose endpoint is loopback, viewed from a box that is not the declaring device, exits non-zero and names both machines. Local binary/port/onboarding checks are skipped so they cannot paint a green local chromium over someone else's logins.ssh://endpoints and fungible names are unaffected. Source:src/lib/browser/runtime-state.ts(identityLoopbackMismatch),src/commands/browser.ts.
type: breaking
agents browser no longer asks the caller which machine a profile lives on. The daemon reads the device-declaration registry: a name this machine declares connects locally; a name only other machines declare is tunnelled to a reachable declaring device (and the command output names which one); a name nobody declares fails loudly, listing similar names, and never auto-creates a logged-out local browser. Identity-bearing profiles share one connection (no Electron fork, no second chrome-data). Runtime keys are <profile>@<device> instead of <profile>@endpoint-N; leftover @endpoint-N dirs are renamed onto the new key.
browser remote-control offnow gates attaching to a running browser, not just launching one (RUSH-3064). PR #2932 moved the consent gate toBrowserService.startand the create branch ofresolveOrCreateTask, closing the implicit-launch bypass — but the two early-return attach paths (an explicit--task, or a lone caller-identity match) returned before it, andtabAddreachedcreatePageTargetunconditionally. So on a machine whose browser was already running with live tasks, a fleet-remote caller could act in the owner's authenticated profile with consent off (agents browser tab-add --device <box> --task <name>— and task names are discoverable through the ungatedstatus). The gate now sits at the top ofresolveOrCreateTask, the one chokepoint every page/close verb resolves through, so a fleet-remote attach is refused exactly as a fleet-remote create already was. It stays per-request (IPCRequest.fleetRemote), never the daemon's env, so an auto-started daemon that inheritedAGENTS_FLEET_REMOTE=1never refuses a subsequent local drive. Local drives are unaffected. Source:apps/cli/src/lib/browser/service.ts.
type: fixed
A leftover local comet-local@endpoint-N chrome-data on a worker is no longer renamed onto the declaring device's key and attached over localhost when the daemon should tunnel. Identity-bearing browsers stay on the declaring machine.
type: fix
agents devices capture no longer wipes fleet.ignored — the captured manifest carries device dismissals forward instead of rebuilding the fleet block without them.
type: feat
Deletion verbs normalize on
remove(specific item) andprune(bulk stale), old spellings kept as hidden aliases. The CLI had drifted into seven different words for "delete" across groups —remove,rm,delete,gc,cleanup, plusprune. The canonical pairremove <name>/prune(already the pattern incommands,hooks,skills,versions) is now applied to the stragglers:agents route rm/agents devices rm/agents projects rmmakeremovethe primary spelling (rmstays an alias);agents browser profiles delete→remove(aliasdelete); and the bulk-stale sweepsagents browser gc,agents lease gc,agents mailboxes gc, andagents routines cleanupbecomeprune(each old verb stays a hidden alias). No invocation breaks — the old verbs still resolve; they just no longer appear in--help.agents secrets remove/delete(key vs whole-bundle) andagents artifacts share delete/unshareare deliberately left as-is: those pairs encode a real distinction, not drift. Source:apps/cli/src/commands/{route,ssh,projects, browser,lease,mailboxes,routines}.ts.An interactive
--deviceagent now survives a dropped connection, and one blink no longer takes out every tab (RUSH-3125). A remote interactive agent was a direct child of the sshd session holding its TTY, so a brief network disruption SIGHUPed it and the in-flight turn was lost — while the auto-reconnect layer re-attached on the stated premise that "the agent is still running there." Durability had been gated on the peer'stmux.enabled, which defaults off (and was off across the whole fleet), even though that toggle is an ergonomics preference about tmux's mouse, clipboard, and scrollback at the local operator's keyboard. The two are now separate: the interactive dispatch exportsAGENTS_REMOTE_INTERACTIVE=1and the peer wraps the run in a detached tmux session regardless of the local toggle, so the agent outlives the link that carries it.--raw,--no-tmux, andAGENTS_NO_TMUX=1still win, so the escape hatch keeps working over--device; a remote run on a peer with no tmux installed is now refused with an install hint rather than started as something a blink would kill. Separately, the interactive stream no longer rides the sharedControlMaster—ControlPath=cm-%Chashes only host/port/user, so every agent tab pointed at a peer shared one master and OpenSSH closes all its channels when it dies (six tabs on one socket, observed live). Probes and fan-outs keep multiplexing. Source:apps/cli/src/lib/exec.ts(shouldWrapInTmux→resolveTmuxWrap),apps/cli/src/lib/hosts/dispatch.ts,apps/cli/src/lib/types.ts.
type: feat
Feed attention is now modeled as an explicit lifecycle the CLI reconciles, the foundation for a feed-driven Needs-You surface. OpenBlock carries generation/source/state/sourceCursor (back-compatible — derived for existing blocks), the answer/continue/clear paths write a resolution tombstone before the block clears so a resolved ask can no longer silently resurrect, and a new pure reconcileAttention merges the open-block ledger, session lifecycle, and a CLI-supplied PR signal into one canonical AttentionItem.
Stream and answer the canonical operator feed.
agents feed watch --jsonnow emits one versioned agents, attention, activity, and scope projection for thin clients, including retained peer rows across disconnects.agents feed answer <attention-key>atomically claims the first answer and routes it over the recorded reply rail; concurrent losers returnalready_answeredwithout injecting twice. Pull-request attention is refreshed by the CLI on a bounded TTL. Source:apps/cli/src/lib/feed/{watch,answer,pr-status}.ts.agents authhelp no longer claims plan tiers. The command's description now reads "the account layer behind team spaces" — there are no plan tiers — and its help notes state that signing in is optional: every local feature works with no account, and team spaces are the one thing an account unlocks. READMEs and docs updated to match, and license references now name FSL-1.1-Apache-2.0. Source:apps/cli/src/commands/auth.ts.The
git-outputtests no longer fail on any box that exports a git identity (unblocks the release attestation).git-output.test.ts's fixture authored its commits with-c user.email=…, but git'sGIT_AUTHOR_EMAIL/GIT_COMMITTER_EMAILenvironment variables outrank-cconfig — so on a machine that exports them (developer laptops and the agent fleet do) every fixture commit was silently re-authored to the ambient identity,collectCommitsmatched none of them, and four tests failed with a bareexpected +0 to be 2. CI never caught it because its runners export no such vars, which made this a local-only failure that also wedgedrelease-attestation-produce.sh— that script is fail-closed, so no attestation was written and no release could be cut from an affected box. The fixture now passes identity through the environment it builds for git, and always assigns those four vars so an ambient value can never decide fixture authorship. Verified by causation: the unchanged test is 4-failed with the vars set and 9-passed with them cleared. Source:apps/cli/src/lib/output/__tests__/git-output.test.ts.permission_mode_notwas typed, documented, and silently inert — every gated hook fired anyway (RUSH-3116). The predicate shipped inshouldFire()(src/lib/hooks/match.ts:156), but nothing at runtime calls that function: the generated hook shim decides fire/skip with a hand-mirrored Python copy of the same logic embedded insrc/lib/hooks/cache.ts, and the mirror was never updated. Somatches: {permission_mode_not: plan}fell through toreturn Trueand the hook ran in plan mode regardless — fail-open, so nothing was left unguarded, but the predicate did nothing while still charging a Python gate subprocess per fire. Verified on published 1.22.47:MATCHES_JSON='{"permission_mode_not":"plan"}'against apermission_mode: planpayload printedFIRE, while the positivepermission_modecontrol correctly printedSKIP. The Python gate now implements the negative form with the same fail-open-on-absence and camelCase (permissionMode) handling as the TS reference. Source:apps/cli/src/lib/hooks/cache.ts.The shim/TS conformance suite now pins itself to the
HookMatchessurface, so a predicate cannot ship half-implemented again.cache-matches.test.tsalready cross-checked the shim's real bash+Python decision againstshouldFire()"so the two implementations can't drift" — but only over a hand-written fixture list, andpermission_mode_nothad no fixture, so the suite stayed green through the drift. A new test reads theHookMatchesinterface out oftypes.tsand fails with the offending key name when any declared predicate has no conformance fixture;permission_mode_not,git_dirtyandproject_haswere all uncovered and now have fixtures. Source:apps/cli/src/lib/hooks/cache-matches.test.ts.New hook predicate
matches.permission_mode_not— gate a hook OFF in one mode without enumerating every other one. The existingpermission_modeis an allowlist, so "run everywhere except plan mode" had to be spelled as a list of every other mode — and that list silently stops matching the moment a harness adds or renames one, which for a guard means it quietly stops guarding. The new predicate names the mode to skip instead, so an unknown mode still fires: the failure direction is "ran unnecessarily", never "did not run". Same fail-open-on-absence rule as its positive twin (a harness that reports no mode keeps firing), reads bothpermission_modeand Grok's camelCasepermissionMode, and ANDs with the positive form when both are declared. Motivation, measured from~/.agents/.cache/perf/perf.db: nine guards fire on everyBashtool call for a combined 292 ms before the command runs, across 285,667 recorded fires of which 284 (0.099%) changed the outcome — and four of them (merge-guard,pr-description-reminder,large-file-add-guard,git-require-clean-tree) cannot fire meaningfully during a planning turn. Source:apps/cli/src/lib/hooks/match.ts,apps/cli/src/lib/types.ts,apps/cli/docs/hooks.md.Interactive Claude on a keychain-less Linux worker now authenticates from an attached setup-token, instead of falling back to an empty per-version login and printing "looks logged out". Two gaps combined to break it: (1)
generateVersionedAliasScript— theclaude@<version>alias shimagents runactually invokes for a pinned/balanced version — carried a hand-copied subset of the config env that had dropped the Linux.oauth_tokenfallback block the main shim'sclaudeAdapter.shimConfigEnvBashhas (the alias env now reuses that adapter block verbatim viaVERSION_DIR, so the two can't drift again); and (2) nothing wrote the.oauth_tokenfile the shim reads — it was referenced only by the read in the shim, never created — so the fallback could never fire.agents accounts attach <setup-token-account> claude@<version>now writes the resolved setup-token to<version-home>/.claude/.oauth_token(mode 0600) on Linux, a no-op on macOS where the credential lives in the keychain. Headless dispatch was unaffected (it injects the token viabuildExecEnv); this fixes the interactive path only. Source:apps/cli/src/lib/installations/shims.ts,apps/cli/src/commands/accounts.ts.A regression test now pins the
claude@<version>alias shim to the.oauth_tokenfallback, so the alias env cannot silently drop it again.generateVersionedAliasScript('claude', …)is asserted to contain the block, alongside the existing main-shim assertion. Source:apps/cli/src/lib/installations/shims.test.ts.agents run claude --interactiveon a worker no longer prints a false "⚠ claude looks logged out" banner when it authenticates from a setup-token. The pre-launch check probed only native credentials (empty on a keychain-less worker), so it warned even though the shim's.oauth_tokenfallback authenticates the run. It now also treats a resolvable per-version setup-token as signed in (no-op on macOS, where the credential is in the keychain). Source:apps/cli/src/commands/exec.ts.A leased box that can't finish setup fails loud and is stopped, instead of provisioning a billed box that dies at "agents-cli is not set up".
agents run … --leaseranagents setupon the fresh box with>/dev/null 2>&1 || true— output and exit both discarded — so a setup that never left~/.agents/.systema git repo was invisible, and the agent then ran straight into the run-side gate (ensureInitialized) that refuses with "agents-cli is not set up". The bootstrap now gates on that exact postcondition: it capturesagents setup's output, and if the system repo still isn't a git repo it prints the real cause and aborts (exit 97) rather than running the agent. A box this run provisioned whose bootstrap failed is stopped (even without--fresh/--keep-box) so unusable capacity doesn't bill until the idle-GC window; a reused box is never auto-stopped. A~/.agentsconfig-copy failure is now surfaced instead of silently swallowed. Source:apps/cli/src/lib/crabbox/lease.ts.New versions are licensed FSL-1.1-Apache-2.0. Use, modification, and redistribution remain free for every user. Offering agents-cli as a competing commercial product or service is barred. Each version converts to Apache-2.0 two years after it is made available. Already-shipped Apache-2.0 versions are unchanged. Source:
LICENSE,apps/cli/LICENSE.
type: fix
AGI Menu no longer re-prompts for Accessibility on dev machines, and the clip-paste hotkey (Cmd-Shift-V) no longer depends on the grant at all. A locally-built (ad-hoc) menu-bar helper now signs under a distinct com.phnx-labs.agents-menubar.dev bundle id, so it can never poison the shipped app's Accessibility grant (macOS keys the grant to the bundle id and revokes it when a same-id binary fails the stored Developer-ID code requirement). An ad-hoc build also can no longer overwrite a healthy Developer-ID install when the recorded owner path vanishes. A release now hard-fails if the shipped helper's designated requirement ever drops the pinned bundle id or Developer ID team, since that would silently revoke every user's grant on upgrade. And when the grant is missing, Cmd-Shift-V now silently copies the host:path reference to the clipboard (press Cmd-V) instead of re-showing the system permission modal on every paste — auto-type stays on only where Accessibility is already granted.
type: fix
Cmd-Shift-V once again pastes the clip reference in a single keystroke. 1.22.47 decoupled the paste from Accessibility so it never prompted — but that meant an ungranted machine silently fell back to copying the token to the clipboard, so you had to press Cmd-V yourself (two keystrokes). The helper now prompts for Accessibility once per launch to restore the one-keystroke auto-type, then falls back to the clipboard silently if you decline (no per-paste nagging). Because dev builds now use a distinct .dev bundle id and the release pins the helper's designated requirement, granting it once sticks across upgrades — so a single prompt is all it takes.
Auto-reconnect now works for every harness, not just Claude (RUSH-3125). When an interactive
--devicerun lost its link, only Claude ever reconnected. Claude is handed a--session-idbefore launch, so its id survives the drop; every other harness's real session id is coined on the peer and was read back over SSH after the interactive stream returned — that is, over the link that had just died. The read failed exactly when it was needed, so the run had no id to reconnect with and the process exited straight to a bare shell. A Grok tab therefore showed nothing at all while a Claude tab beside it counted down. Reconnect now falls back to the launcher-mintedAGENT_LAUNCH_ID, which is known before the connection exists and so cannot be lost with it, and the peer maps it to the real session with a purely local lookup. Codex, Grok, Kimi, Droid, Cursor, OpenCode and the rest recover for the first time. Source:apps/cli/src/lib/hosts/reconnect.ts(pickReconnectTarget),apps/cli/src/commands/exec.ts.agents sessions focus --launch-id <id>targets a run by the launch id its dispatcher minted instead of a session id, resolving it from that machine's own hook records. This is what makes the reconnect above work with no network at the moment there is none; it is also usable directly when you know the launch id but not the session. An unknown launch id fails loudly rather than opening the picker, so an automated reattach can never strand itself at an interactive prompt. Source:apps/cli/src/commands/focus.ts.The reconnect give-up notices name a command that still exists (RUSH-3125). When the retry window closed, all three notices told you to run
agents reconnect <id>— a command deprecated and hidden in favour ofagents sessions resume, so the advice printed at the one moment you needed something that worked was stale. They now nameagents sessions resume <id>, or for a launch-id target the peer-side resolver, since no local verb accepts a launch id. Source:apps/cli/src/lib/hosts/reconnect.ts(recoveryHint).Reconnect waits minutes instead of 90 seconds, counts down, and Ctrl-C works (RUSH-3125). The retry budget was 6 attempts over a 2/4/8/16/30/30 backoff — about 90 seconds, shorter than a laptop lid close, a Wi-Fi handoff, a VPN or Tailscale re-auth, or a router reboot. Worse, timers are suspended across sleep, so on wake the whole backoff fired back-to-back before the network was up and the budget was gone in seconds. The bound is now a 15-minute wall-clock window over an unproductive streak, and it still resets the moment a reattach reconnects and holds — so a session that blinks all day keeps reconnecting, exactly as before. The notice counts the window down (
Reconnecting in 30s · 12m14s left · attempt 7 · Ctrl-C to stop) rather than printing an attempt fraction that no longer says when it stops. Ctrl-C during the wait previously hit node's default handler and killed the whole process mid-notice, dropping you at a bare shell with no hint the agent was still alive on the peer; it now exits the loop cleanly (130) and prints where the agent is and how to get back.The terminal is restored after an interactive remote stream dies (RUSH-3125).
ssh -ttleaves the local tty in raw mode, and an agent TUI killed by a dropped link never sends its own exit sequences — so focus reporting and the mode/colour-scheme reports stayed armed and the terminal answered back at a shell that was not expecting it, littering the screen with^[[?997;1n ^[[I ^[[O. Those bytes were also still queued on the tty, so the next reattach handed them to the agent as if they had been typed. Astty -gsnapshot is now taken before the spawn and restored after, the DEC modes a TUI arms are reset (focus, bracketed paste, alt screen, mouse tracking, cursor), and the input buffer is drained. Done insshStreamitself, so every caller that opens an interactive remote stream is covered. Source:apps/cli/src/lib/ssh-exec.ts,apps/cli/src/lib/hosts/reconnect.ts.A release no longer needs
mainto hold still — publish is decoupled from livemain(RUSH-2395 audit).release.shused to squash-merge the release PR intoorigin/mainand then refuse to publish unlessmain's tree byte-matched the attested release tree — so any commit that landed onmainduring the release (or aCHANGELOGmerge conflict) killed it, forcing a ~15-minute quiet window that a busy fleet rarely offers. It now tags and publishes the attested release commit itself — the exact tree CI attested and the tarball was packed from — and merges the version-bump PR asynchronously, after publish, best-effort. The published bytes are the attested tree by construction, somaincan churn freely and the bump-merge can be deferred or hand-resolved without ever wedging the release. The attestation of the release-commit tree remains the sole functional gate; the tag push and publish routing stay lease-gated. The catch-up recovery path (registry behind amainalready at the target version) is unchanged. Source:apps/cli/scripts/release.sh.Fixed:
resolve-target.test.tsfailed on any machine actually namedmac-miniorzion. The fixtures wrote device declarations for two real fleet hostnames and then asserted the resolver would tunnel to them. On a box with thatmachineId()the resolver correctly reports the profile as locally declared, so the tunnel assertions failed — green on Linux CI, red on both Macs, which is where releases run. The suite is now hermetic: fixtures usepeer-alpha/peer-zulu, keeping the sort order the "first reachable declaring device" assertions depend on. Source:src/lib/browser/resolve-target.test.ts.A session launched with a skill is no longer named after the skill's install path. Claude derives its generated
ai-titlefrom the first turn, so a session opened with/continue(or any skill) was namedBase directory for this skill: /home/…/.claude/skills/continue— the scaffolding line the skill injects, not the task. That name lands inSessionMeta.label, which wins on every surface for the session's whole life: the interactiveagents sessionspreview header,--flat/--tree, theagents feed watch/sessions watchstreams, the AGI EXT Fleet row, and the editor tab title. The generated title now goes throughclassifyUserPromptand collapses to/<skill>when — and only when — the classifier reports that injected line. A user's/rename(custom-title) is never rewritten, including one that merely names askills/…path. Fixed at the one point where the label is composed, so every reader inherits it with no reader-side special-casing. Not retroactive: transcript rescans are(mtime, size)-gated with no scan-version invalidation, so a session already indexed under the old derivation keeps its stored label until that file next changes and triggers a rescan — tracked in RUSH-3122. Scoped to Claude'sai-title; a harness that supplies its own title verbatim (e.g. Cursor'schatMeta.title) is unaffected and untouched (RUSH-3123). Source:apps/cli/src/lib/session/discover.ts(finalizeClaudeScan).agents sessions previewshows the fan-out a session left behind, on remote rows too (RUSH-3091, RUSH-3095). The Doing line now carriesN sub-agents · N background shells. Both counts are persisted at scan time (sessions.sub_agent_count/background_shell_count, schema v40) rather than only recomputed per render, which is what makes them visible on a remote or unindexed row — that path renders fromSessionMetaalone throughformatMetaOnlyBody, so it had no events to derive from and silently showed no fan-out at all. A freshly derived count still wins when the caller has parsed events, since the column lags the transcript by one scan. Background-shell detection is a per-harness registry probed against real transcripts, not assumed: claude/kimi flagBashwithrun_in_background, grok flagsrun_terminal_commandwithbackground; codex and droid record no such concept and cursor persists no tool calls locally, so those render nothing rather than0 background shells— a zero would assert "none running" where the truth is "cannot know", andNULL(not scanned) stays distinct from0(scanned, none found) for the same reason. The counts mean "started / left behind", never "still running": a transcript records a start and never a death, the same trapagents devices psdocuments, so live status stays withsessions --active. Source:apps/cli/src/lib/session/highlights.ts,apps/cli/src/lib/session/{db,discover}.ts,apps/cli/src/commands/sessions-picker.ts.agents sessions tracenow reads newer Codex sessions by program, not a wall of "exec". Codexgpt-5.6-sol(codex ~0.145+) runs every shell command inside a JS cell — acustom_tool_callnamedexecwhose code isawait tools.exec_command({cmd:"git status …"}). The trace now unwraps that cell to the real shell command, so a Codex trajectory readsagents 68% · git 15% · scp 13%withgit fetch origin/sed -n …steps and exit codes, exactly like Claude'sBashand Droid'sExecute— instead ofexec 100%with every step labeledexec. Genuine non-shell cells (tools.view_image, raw JS) stay labeled by their code. Source:apps/cli/src/lib/session/parse.ts(extractCodexExecCommand),trajectory.ts(SHELL_TOOLS).agents sessions tracecompares two sessions. Pass exactly two selectors and the same command renders a compare: the two sessions' tool sequences aligned by tool name, the first divergence point (where the runs' tool order stops lining up), the steps each session ran that the other never did, and a per-session summary — in the same three renderings (HTML with stacked lanes on a shared time axis, compact text, and--jsonwithlayout: 'compare'). Three or more selectors, or--tree, still fail loud — lineage (a parent + its team) lands in a follow-up PR. Source:apps/cli/src/lib/session/trajectory-compare.ts,apps/cli/src/commands/sessions-trace.ts.agents sessions trace <id> --treerenders a team's lineage. The third layout of the trace surface, after the single trajectory and the two-session compare: the selected session and every session it spawned, drawn as a delegation graph. The edges are read from the session index, not inferred — a teammate'smeta.jsonparent_session_id(teamOrigin.parentSessionId), with the team's agreed-on spawner (groupSessionsByTeam().spawnerSessionId) filling in for a teammate whose own record names none, bounded to that run's own spawn window so a second run of the same team name never adopts the first run's teammates; the edge carries which record established it. A node is always a real session: an inlineTask/Agentsub-agent is a step inside one transcript and produces no session, so it is never drawn as a node. Each node carries its handle, harness, role, indexed tool count, span, PR number, and a recency class (active/idle/stale) — recency, not a success verdict, because nothing on a session row records whether the work landed. HTML draws a self-contained inline-SVG graph with clickable per-node summaries;--textprints an indented tree;--jsonemitslayout: 'lineage'with alineage: { rootId, nodes, edges, teams, unresolvedParentIds }block plus the root's trajectory. Selecting a child roots the graph at its topmost ancestor, so the whole team is always shown; a referenced parent outside the scanned pool is reported rather than dropped. Source:apps/cli/src/lib/session/trajectory-lineage.ts,apps/cli/src/lib/session/trajectory-html.ts,apps/cli/src/lib/session/trajectory-text.ts,apps/cli/src/commands/sessions-trace.ts.agents sessions tracev2 — a real session debugger, program-aware. The HTML view is rebuilt from a wall-clock waterfall (which crammed all activity into a sliver on long, idle-gappy sessions) into an analysis hero — where the time went, the slowest steps, the command/program mix, and error/idle KPIs — over a readable, execution-ordered step list with expandable output and clean idle-gap dividers. Every shell step is labeled by the effective program it ran (git,gh,agents,bun,sed… via the sharedextractShellProgramsparser;sudo/env/agents sshunwrapped, barecd/exportskipped) across every harness's shell tool — Claude'sBash, Codex'sexec_command,run_shell_command,shell,Execute— so the mix readsgit 94 · agents 81 · gh 75instead of "Bash 98%". Process exit codes show on failures. The "where the time went" share is now keyed by program in both the HTML and--textrenderers off one model field — a Bash-heavy run readsgit 56% · gh 33% · agents 11%, neverBash 100%.--textand the--jsonstep model gain additiveprogram+exitCodefields, and the trajectory'stoolTimeShareis renamedprogramTimeShare(program-keyed) in the--jsonenvelope. Source:apps/cli/src/lib/session/trajectory.ts,trajectory-html.ts,trajectory-text.ts.agents sessions trace(aliasagents trace) — visualize a session's trajectory. A tool-call waterfall over a real time axis (durations, errors, idle stalls, delegations) instead of scrolling the Markdown wall. OnebuildTrajectory()model, rendered three ways and auto-selected by audience: an interactive HTML page on your interactive host for a person, a compact token-bounded text trajectory for an agent (--text,--errors-only), and the versionedsessions-traceJSON envelope (--json) for tools. Single-session in this release; multi-session compare and team lineage follow. Redacted by default, self-contained HTML (no CDN). Source:apps/cli/src/commands/sessions-trace.ts,apps/cli/src/lib/session/trajectory.ts.
type: fix
agents sessions tracesteps read by what the command DID, not theircdprefix. A shell step's label now strips the leading throwaway statements —cd <repo> &&,export X=Y;,set -e,source …, and bareVAR=valassignments — socd /long/path && git fetch originrenders asgit fetch origin, and a multi-line script whose first line iscd <repo>shows its real command instead. Before this, most rows in a coding session rendered an identicalcd [HOME]/…/<repo>and the trajectory was unreadable; now every row is distinct and agrees with its program badge. Pipelines are left intact and a command that is nothing butcdis shown as-is. Fixed at the model (buildTrajectory), so the HTML, text, and--jsonrenderings all benefit. Source:apps/cli/src/lib/session/trajectory.ts.Trace durations roll into hours past 60 minutes. An overnight idle gap now reads
24h01minstead of1441m18s, in both the HTML and the compact text renderings. Source:apps/cli/src/lib/session/trajectory-html.ts,trajectory-text.ts.agents viewstops printing "usage unavailable" for a harness that reports a plan and no meters. Grok's collector writes a subscription tier with no usage windows ({plan: 'SuperGrok Heavy', windows: []}), and the cache deserializer treated "no fresh windows" as "nothing cached" — so--refreshrenderedSuperGrok Heavy, the very next plainagents view grokrenderedusage unavailable, and reading the row also deleted it. Both grok accounts were permanently stuck in the wrong state because the daemon's periodic refresh re-wrote a row that the next read destroyed. A cached row that carries a plan now survives with no windows; a row with no windows, no plan, and no refusal is still dropped, so an all-expired snapshot keeps pruning (RUSH-2858) and a meterless row can never read as a 0% bar or anavailablebadge —deriveUsageStatusFromSnapshotstill returns null for zero windows. Routing keeps the same guarantee from the other side: a windowless snapshot no longer counts as verified usage, so a meterless pool still spreads across its accounts instead of pinning to whichever one ran most recently. Source:apps/cli/src/lib/accounting/usage.ts,apps/cli/src/lib/accounting/rotate.ts(RUSH-3060).
type: feat
The watchdog decider is now an agent, not a heuristic script. Every idle session on the machine (its originating task + transcript tail) is handed to ONE agents run --mode plan call per tick, which judges each: idle-but-unfinished → nudge that drives it to finish; idle-and-done or genuinely-needs-human → skip. The deterministic pre-filter (isLikelyTrulyBlocked, completion/promise regex) and the per-session LLM spawn are gone — one bounded call per tick, only when something is actually idle. A nudge is booked in the cooldown ledger and logged nudge ONLY when delivery is confirmed; tmux/iterm/pty self-confirm, while vscodium's fire-and-forget --open-url is recorded undelivered until the swarm-ext extension acks the verb, ending the phantom-nudge ledger. agents watchdog history gains an undelivered row; the --smart flag is removed (the agent is always the decider). Defaults stay OFF.
- Claude usage now comes from normal Claude Code sessions. Managed Claude
homes install a status line that records Claude's native five-hour and
seven-day rate limits for the active account, so
agents view claudegains fresh bars without reading or copying OAuth credentials. The status line also shows the hostname and active model and preserves any existing status-line command. Compact usage bars now render at one-eighth-cell resolution instead of exaggerating every nonzero value to at least 20%, and the trailing activity and authentication-probe ages identify which event they measure.
1.22.47
agents traces syncpushes derived, redacted trajectories to your Phoenix account (RUSH-3140). A newagents tracescommand group increments oversessions.dbvia thefile_mtime_msgate (only sessions modified since the last sync are uploaded), computes aSessionTrajectoryfor each (steps + gaps + stats, no raw transcript text), appliesredactSecrets()before PUT, and stores the result under<userId>/<device>/sessions/<id>.jsonin an R2 bucket guarded by Phoenix bearer auth — no public GET path exists anywhere. A per-device index shard (index.json) is updated on each run. Three subcommands:agents traces sync(incremental push),agents traces status(show last sync time),agents traces open(open the Phoenix Evals console). The traces Worker usescache-control: private, no-storeon every response. Source:apps/cli/src/lib/traces/{backend,sync,worker-template}.ts,apps/cli/src/commands/traces.ts.
1.22.46
agents authreturns, against Phoenix ID instead of a sibling product's backend (RUSH-2581). 1.22.45 removed the account layer that authenticated against Rush'sapi.prix.dev. It comes back pointed at Phoenix ID (phnx-labs/phoenix-id), agents-cli's own account service:agents auth loginruns a device-code flow whose browser page is Phoenix-branded and Google-only,agents auth whoamireports the signed-in account,agents auth logoutclears this machine and nothing else, and the team surface nests asagents auth space(list/create/members/invite/role/remove). Everything goes through one new seam,lib/identity/— one base URL (PHOENIX_ID_BASE), one session file, one HTTP funnel, one error type — replacing the shape that had the backend URL hardcoded in five files and the session token re-read by seven separate functions. agents-cli reads no other product's credentials: there is no~/.rush/user.yamlfallback. Source:apps/cli/src/lib/identity/{client,index}.ts,apps/cli/src/commands/auth.ts.agents authpoints at the deployed Phoenix ID service (RUSH-2581). ThePHOENIX_ID_BASEdefault shipped namingid.phnx.sh, a domain that was never registered, so everyagents auth loginwould have failed DNS with nothing behind it. It now defaults to the live service (aworkers.devURL until a custom hostname is attached). Override it withPHOENIX_ID_BASEto point at a local backend. Source:apps/cli/src/lib/identity/client.ts.The menu-bar prepack gate hard-fails on an unstapled or thin helper on any OS (RUSH-3031).
verify-menubar-helper.sh's notarization check used to silently no-op whenxcrunwas absent — the exact case a Linux attestation-producer box hits — which let 1.22.44 ship a Dev-ID-signed but un-stapled, thin (single-arch)MenubarHelper.appthat Gatekeeper rejected on every Mac. The gate now hard-fails when the stapled ticket (Contents/CodeResources) is missing andxcrunis unavailable, and hard-fails when the bundled executable is not a universal (fat) Mach-O binary, checked portably withodon any platform. Source:apps/cli/scripts/verify-menubar-helper.sh.The release-attestation producer no longer arms the real-
~/.agentshermeticity guards it needs to get vitest's extended timeout profile (RUSH-3007).CI=trueused to control both the vitest hookTimeout/ignore-pool-error profile ANDtests/setup.ts's leak tripwires against the real developer home — so a producer run on a box with a live daemon (e.g. mac-mini) false-failed 129/129 test files on a fully green, 12,559/12,559-test suite.release-attestation-produce.shnow setsAGENTS_ATTEST_PRODUCER=1(and unsets any ambientCI) to opt into the timeout profile without arming the guards; a genuine CI runner's behavior is unchanged. Source:apps/cli/scripts/release-attestation-produce.sh,apps/cli/tests/hermetic-guards.ts,apps/cli/tests/setup.ts,apps/cli/vitest.config.ts.Rotation remembers a tokens/credits-exhausted account instead of re-picking it every launch (RUSH-3018). A rate limit resets on a clock; running out of credits / hitting a spend cap does not — yet the two were conflated. A billing refusal (
out of usage credits,monthly spend limit) was detected only to trigger one failover and then forgotten, so balanced rotation re-picked the dead account on the next launch, it refused again, failed over again — burning a launch each time. It now persists a clock-lessout_of_creditsmarker per account (noteClaudeOutOfCredits), which the rotation eligibility gate treats as blocking until a later successful run on that account clears it (clearClaudeAccountRefusal) — never a timestamp. Session/rate limits are unchanged (still recover on their reset). Source:apps/cli/src/lib/accounting/usage.ts,apps/cli/src/lib/exec.ts.Harness capability probes can no longer leak grandchild processes (RUSH-3028). The version probe behind
agents view(<cli> --version) and the async manifestcheck:runner executed third-party binaries whose own forked children could outlive the probe — the GitHub Copilot npm wrapper forks a platform-binary downloader into~/Library/Caches/copilot, and under a redirected test HOME that survivor raced teardownrm(the dominant residual ENOTEMPTY suite flake after RUSH-3021). Probes now run in their own process group viaprobeCapture, and the whole group is reaped when the probe settles — on clean exit, on timeout, and on parent death (aprocess.on('exit')hook covers the CLI's hard-exit SIGINT path). On win32, where process groups don't apply, the direct child is killed on settle, matching the oldexecFiletimeout behavior. Source:apps/cli/src/lib/probe.ts,apps/cli/src/lib/agent-spec/agents.ts,apps/cli/src/lib/cli-resources.ts.
1.22.45
BREAKING: the Prix-coupled account layer is removed —
agents auth,agents org, and the plan-tier gates (RUSH-2581). 1.22.42 shippedagents auth login/whoami/logoutandagents orgagainst the Rush product's backend (api.prix.dev), withentitlement.tsreading the Rush billing tier to cap accounts at 3-per-harness on free and to gate theagents insightsfriction sections. agents-cli's identity must not ride a separate product's login and database, so all of it is removed:authandorgare retired top-level names (they fail loudly, no auto-correct), account registration is uncapped again,agents accountsoutput drops thedormantfield/suffix, andagents insightsrenders the full report with noplan/noticeJSON fields. The Rush feature integrations are untouched (cloud/rush.tsdispatch, the opt-in secrets sync driver, the owner-notify transport). The replacement — agents-cli's own account backend with Phoenix branding, built for later Phoenix-ID consolidation — is tracked in RUSH-2581. Source:apps/cli/src/lib/{prix-account,entitlement}.ts(deleted),apps/cli/src/commands/{auth,org}.ts(deleted),apps/cli/src/commands/{accounts,insights}.ts,apps/cli/src/{bootstrap.ts,cli/command-registry.ts,lib/startup/command-registry.ts}.The menubar prepack gate fails closed off-Mac when the bundle has no stapled notarization ticket (RUSH-3026). 1.22.44 was packed on a Linux box where the gate's codesign/xcrun checks silently no-op'd, so an un-stapled dev bundle shipped and Gatekeeper rejected it on every Mac ("not notarized/valid; skipping launch") — the menu bar died until a rollback to 1.22.43.
stapler staplewrites the ticket as a plain file (Contents/CodeResources), so its absence is provable anywhere: the gate now hard-fails the pack when it is missing and no xcrun is available, turning that failure into a pack error instead of a shipped regression. Source:apps/cli/scripts/verify-menubar-helper.sh.A Linux box can now produce the pretested release tarball — the attestation producer seeds the already-signed helper apps (RUSH-3026).
release-attestation-produce.shruns in a fresh worktree whosebin/is empty (the signed.apphelpers are untracked), so on any non-Mac boxnpm packdied at the prepack gates and attestation production — and therefore every release — stayed chained to a Mac. Off a macOS signing box the producer now seedsbin/Agents CLI.appandbin/MenubarHelper.appcopy-if-absent from the caller checkout; the prepack gates still verify them (keychain sha pin, menubar presence), so a wrong or tampered seed fails the pack exactly as before. A Darwin producer's freshly signed apps are never overwritten. Source:apps/cli/scripts/release-attestation-produce.sh.
1.22.44
Browser domain-skill discovery is layer-aware (RUSH-2497).
agents browser start --url <url>auto-loads a site-specific SKILL.md, but the lookup only ever searched the user layer (~/.agents/skills/browser/domain-skills), so a skill shipped in the system layer (~/.agents/.system/skills/browser/domain-skills) — or a project's.agents/— was silently invisible: the browser just opened without the guide.resolveDomainSkillnow searches project > user > system > extra repos, first layer with a match wins, mirroringresolveResourceprecedence;$AGENTS_BROWSER_DOMAIN_SKILLS_DIRstays a single-root override for tests. The project layer resolves from the calling process's cwd — the shared browser daemon does not yet receive the CLI caller's cwd over IPC, so there the project layer follows the daemon's own cwd until RUSH-2996 threads it through. A miss still never breaks browser start, but now logs the roots searched at debug level (AGENTS_DEBUG/DEBUG) — the total silence is what hid this. Source:apps/cli/src/lib/browser/domain-skills.ts.agents artifacts share list/update/delete(and theunsharealias) had a silently-dropped--json/--github-user(RUSH-2687). Commander resolves an option's long name against the WHOLE ancestor chain, not per-command —share <file>(the parent) already declares--json/--github-userfor its own publish-time use, so the same-named flag onlist/update/deletewas swallowed at parse time even when passed alone (--helpstill showed it registered, which made it easy to miss).agents artifacts share list --json | jq …, the exact example indocs/share.md, never emitted JSON. Fixed by renaming the colliding options —--list-json/--for-user(list),--update-json(update),--delete-json/--for-user(delete/unshare) — matching the precedentshare revisionsalready set (--revisions-json/--for-user, RUSH-2683). A CLI-wideenablePositionalOptions()fix was evaluated and reverted: commander copies that setting onto every command created via.command(), so enabling it on the root program brokesessions backfill tools/resources --since … --json …, which relies on a parent command's options staying visible to leaf subcommands viaoptsWithGlobals(). Source:apps/cli/src/commands/share.ts,apps/cli/src/lib/startup/root-command.ts.agents artifacts sharepublishes with a partialshare:config (RUSH-2837).statusand publish both used to treat a missing/emptyaccountIdas "not set up", even whenbaseUrlandWRITE_TOKENwere present — so a fleet rewrite that blankedaccountId(or a partialwriteMetathat droppedshare:) silently killed PR evidence uploads. Publish now requires onlybaseUrl+ the write token;statusreports the endpoint and names an empty account id instead of pretending nothing is configured;writeShareConfigwill not persistaccountId: ""over a stored id;serializeCentralno longer deletesshare:just because a write omitted the key. Source:apps/cli/src/lib/share/config.ts,apps/cli/src/lib/state.ts,apps/cli/src/commands/share.ts.Pick a device-local account after fleet placement (RUSH-2961).
agents run <harness>@now composes with--device <name|auto>: the device resolves first, then the interactive picker lists that peer's installed versions/accounts and pins the selection for this run. Automatic picker placement prefers a ready account, retains signed-out/revoked login targets, and excludes devices whose picker would contain only rate-limited or out-of-credit rows.agents view --jsonexposes each version's cachedauthVerdictso remote and local placement apply the same revoked-token gate. Source:apps/cli/src/commands/{exec,view}.ts,apps/cli/src/lib/{smart-launch,hosts/dispatch,hosts/ready}.ts.BREAKING: nest
agents betaunderagents setup beta; retire top-levelagents apply; dropharness login/logout(RUSH-2981). Enabling a preview feature is setup, not its own noun —agents setup beta list/enable/disablekeep the same~/.agents/agents.yamlbeta.enabledwrite path. Fleet reconcile is alreadyagents fleet apply/agents devices apply; top-levelapplyis gone (unknown command, retired from distance-1 auto-correct). Harness credentials live onagents accounts—harness login/logoutare removed. Does not touch org/auth/trends/audit/unshare (sibling session 01a02234). Source:apps/cli/src/commands/{beta,setup,apply,harness}.ts,apps/cli/src/lib/startup/command-registry.ts.Nest leftover top-level aliases; keep
agents orgas a deprecated spelling (RUSH-2989).agents unsharemoves underagents artifacts unshare(same takedown asartifacts share delete; the top-level name is retired).agents auditmoves underagents events audit(stillevents --include runs, plusverifyfor the legacy hash-chain). Former top-levelagents trendsisagents insights mix(alsoagents insights trends).agents orgstill works and prints a deprecation line; the canonical tree isagents auth space. Prix login stays offagents accounts(that noun is harness keys). Source:apps/cli/src/commands/{auth,org,artifacts,share,events,audit,insights}.ts,apps/cli/src/lib/analytics/mix-commands.ts.agents viewshows Claude usage again — the daemon auth-health probe no longer hammers/oauth/usageinto a permanent 429 (RUSH-2998). The daemon's fleet auth-health probe (probeLocalFleetAuth→probeClaudeStatus) hit the rate-limited/api/oauth/usageendpoint every ~3 minutes on every device. Across the fleet that drove one per-account request quota to a persistent429; the sharedRetry-Afterbackoff then parked usage fleet-wide and the usage cache froze (windows stale-zeroed →agents viewrenderedS: 0% (now) W: 0% (now)for weeks). The periodic tick now re-probes the endpoint at most every 20 minutes (AUTH_PROBE_MAX_AGE_MS) and reuses the last real verdict in between — a ~5x cut in endpoint traffic. Fleet status still publishes every tick (it does not ride that endpoint), and every device keeps a real auth verdict, soagents devices ping --strictand the run auth-preflight keep detecting revocation on every host.agents devices pingforces a genuinely live probe (never the throttled cached verdict). Source:apps/cli/src/lib/daemon-ticks.ts(AUTH_PROBE_MAX_AGE_MS,isCachedFleetAuthProbeFresh,refreshLocalFleetAuthStateforce),apps/cli/src/commands/ssh.ts(runFleetPing).BREAKING: remove
agents serve, its--controlanchor, andagents devices pair-ios(RUSH-3001). The read-only local web companion (agents serve) duplicated whatagents sessions,agents teams status, and the AGI EXT Fleet panel already show, and its--controlmode existed only to anchor the unshipped iOS Fleet Cockpit prototype (apps/ios, untouched since 2026-07-16, no Xcode project) — both are gone, and so is the prototype itself.agents devices pair-ios(the only writer of the device registry'scontrolrole) is removed with it; with no writer left, thecontrol-only dial-exclusion filters (isControlDevice) are removed too, acrossdoctor,apply,fleet status,fleet-capture,smart-launch, the devices host provider, session fan-out, and remote login detection.agents servenow reportsunknown commandand is retired from distance-1 auto-correct;agents devices pair-iosis rejected bydevicesas an unexpected argument. Migration — if you previously ranagents devices pair-ios, that phone or tablet is still in your device registry withrole: control, and the key is now ignored rather than honoured: it becomes an ordinary fleet target.agents fleet status,agents fleet update/run,agents apply,agents doctor --check --devices,agents insights output --all-hosts, and any--device allpassthrough will dial it and wait out a ConnectTimeout;agents hosts listshows it as dispatchable;--device autocan place an agent on it when no device is markedworker; and routine fleet placement can schedule a recurring job onto it, since anunknown-platform device sorts ahead of a real worker alphabetically. Failures are loud, not silent. Remove it withagents devices rm <name>; the stalerolekey is dropped anyway on that device's nextagents devices sync. Session fan-out is unaffected — it still gates on awindows/linux/macosplatform. Source:apps/cli/src/commands/{serve,ssh}.ts,apps/cli/src/lib/devices/{registry,fleet,pool,discovery-policy}.ts,apps/cli/src/lib/{smart-launch,remote-agents-json,device-config}.ts,apps/cli/src/lib/hosts/{registry,providers/devices}.ts,apps/cli/src/lib/session/{watch,remote-list}.ts,apps/cli/src/lib/fleet/remote-login.ts,apps/cli/src/cli/command-registry.ts,apps/cli/src/lib/startup/command-registry.ts.Menu-bar helper restarts after a bundle swap; one-time TCC migration;
agents menubar doctor(RUSH-3019).installMenubarLaunchAgentOnUpgrade()swapped the installed bundle on an upgrade but never restarted the running helper — verified live: a helper pid started 21:44:16 kept running while the bundle under it was replaced at 21:53:46, so it kept requesting Accessibility under the OLD code identity and the grant never stuck. The heal now kickstarts the launchd job (launchctl kickstart -k) after a real content swap (a version bump or the ad-hoc -> Developer ID transition — a plist-only interpreter repoint, RUSH-3005's churn, does not trigger this) and falls back to ending the specific pid(s) still running the old binary whenkickstart -kcan't reach the GUI launchd domain from a non-Aqua shell context, so launchd'sKeepAliverelaunches from the swapped binary. Separately, a machine that transitioned ad-hoc -> Developer ID (6fa36f73a) now gets a one-timetccutil reset Accessibility com.phnx-labs.agents-menubarto clear the dead grant recorded against the old identity, stamped so it never re-runs. The "Paste needs Accessibility" notification now opens System Settings' Accessibility pane on click and names the actual running bundle path. New read-onlyagents menubar doctorreports install path, installed vs CLI version, signing identity stability, and whether a live helper pid predates the on-disk bundle (the stale-process bug this fixes). Source:apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/src/commands/menubar.ts,apps/cli/menubar/Sources/MenubarHelper/Clip.swift.Daemon autostart no longer fires under a redirected (sandbox/test) HOME (RUSH-3021). #2860 gated service-manager registration on
isolatedHomeSuffix()but leftensureDaemonStarted()'s detached spawn ungated, so a test-spawned CLI could fork a daemon into the test's temp HOME; the child outlived the test and raced its recursive teardownrm(theENOTEMPTYflake that cost most full-suite attestation rolls on shared boxes). The launch path now refuses under a redirected HOME via the sameserviceManagerRegistrationAllowed()signal andAGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOMEtest seam; reporting an already-running daemon is untouched, andagents daemon startremains the operator override. Source:apps/cli/src/lib/daemon/daemon.ts(ensureDaemonStarted), regression test inapps/cli/src/lib/daemon/daemon.registry.test.ts.The npm tarball no longer bundles the version-stamped signed CLI binary — releases publish from any OS (RUSH-3026).
dist/bin/agents(the Developer-ID-signed arm64 Mach-O, #315) embedded the release version, so every release forced a fresh Mac build + sign even when nothing native changed — chaining publishing to a single provisioned Mac (mac-mini), whose downtime stranded merged fixes unshippable. The binary is dropped frompackage.jsonfilesand theprepackgates;postinstall's existing run-probe falls back to the JS entrypoint when the binary is absent, so macOS installs keep working (it returns as a per-release GitHub asset in a follow-up).release.sh's home-base phase — already promote-only (download attested tarball, verify, install-smoke,npm publish) — drops its vestigial macOS gate, and a newassert_promote_home_basepreflight (npm token + gh auth, viascripts/promote-home-base-probe.sh) runs BEFORE the release's first mutation, fixing the tagged-but-unpublished failure shape: the old signing preflight was defined but never invoked. The two.apphelpers (keychain, menubar) remain bundled and manifest-reused — unchanged helpers are never rebuilt. Source:apps/cli/package.json,apps/cli/scripts/release.sh,apps/cli/scripts/promote-home-base-probe.sh.agents syncrestores a resource deleted from a version home — no--forceneeded (#2398, #2397). The staleness fast-guard decided sync-ness from source fingerprints alone, so a managed artifact deleted from the version home (a subagent, command, skill, hook, MCP config, or rules file) read as "Already in sync" and only--forcerestored it. The sync manifest now records the artifact paths the last full sync wrote (writtenTargets, reported by each resource writer), andisStaletreats a missing path as stale — oneexistsSyncper path, no content reads, so the fast-guard budget holds. A manifest written by an older agents-cli lacks the field and reads as stale once; that full sync establishes the baseline. Relatedly, four commands (agents commands/skills/hooks/subagents remove) claimed a removed resource "will re-sync on next agent launch" — the shim's launch hook (agents sync --launch) is project-scoped only and deliberately skips version-home reconciliation, so the messages now name the real recovery:agents sync <agent>@<version> --yes.agents doctor --fixwas already immune (it heals from the live-home diff, not the manifest). Source:apps/cli/src/lib/staleness/{index,types}.ts,apps/cli/src/lib/staleness/writers/{types,commands,skills,hooks,mcp,subagents,rules}.ts,apps/cli/src/lib/installations/versions.ts,apps/cli/src/commands/{commands,skills,hooks,subagents,prune}.ts.agents devices listno longer serves fossilized load/mem numbers (#2666). The fleet stats cache (.fleet-stats.json) had no age bound and — since RUSH-2061 removed the daemon's N² fleet warm — no background writer, so the load/mem columns froze at the last manual--refreshand re-rendered as current fleet state indefinitely (observed: a 9-day-old row reporting an idle Mac at 1058% load). Cached rows are now bounded bySTATS_STALE_MS(3 minutes, the same window as the agent-count mirror): a row past the bound is treated like a missing one — re-probed live this call and rewritten — so the defaultdevices list/devices statusread is itself the cache's writer and a stale value can never be presented as current. This cache feeds the fleet scheduler,--device autoaffinity, and the session-start fleet banner. Source:apps/cli/src/lib/devices/stats-cache.ts,apps/cli/src/commands/ssh.ts.
1.22.43
Rush Cloud no longer has a helper that reads the interactive Claude login (RUSH-2359 / incident #1767). Dispatch already sends an email-only account manifest (SING-1b) and never uploaded a token; the leftover
readClaudeCredentialsBlobstill read Keychain /.credentials.json— the exact shape that captured aclaude setup-tokenTTY banner as an Authorization header. That helper is deleted.--leaseSING-1b detection now reads the wrapped rotating blob itself and ignores anything that is not{ claudeAiOauth.accessToken }. Source:apps/cli/src/lib/cloud/rush.ts,apps/cli/src/lib/crabbox/runtimes.ts.fix:
agents sync <agent>self-heals version pointers left aimed at an uninstalled version (RUSH-2471).removeVersionreassigns the default and clears the~/.<agent>config symlink only when it removes the pointed-at version, so a version-home whose launch binary vanished by any other route — grok self-updating its per-version binary out from under the old dir, a manual delete, a half-finished install that seeded the home but never landed the binary — left the global default and/or the symlink resolving to a dead version. Becauseagents use <agent>@<v>sets the default and the symlink together, both dangled in lockstep once<v>'s binary went away:agents sync <agent>would printRepointed <agent> config symlink …and then still fail<agent>@<v> is not installed, because the version to sync is resolved from the raw global default.agents sync <agent>now repoints the global default, the isolated default, and the config symlink off any not-installed version before resolving — the default to the newest non-isolated installed version (never auto-promoting an isolated install), the symlink to the resolved default else the newest non-isolated installed version (the user's real~/.<agent>is never repointed at an isolated install). A pointer already on an installed version (a deliberateagents usechoice), a real config directory, and isolated-only agents are left untouched. Source:apps/cli/src/lib/installations/versions.ts,apps/cli/src/commands/sync.ts.Session preview groups metadata into verb-led rows (RUSH-2757, part 3).
agents sessions preview(and the picker pane) replaces the flat labeled block (Prompt/Todos/Dirs/Repos/Changes/Artifacts/Skills/Plugins/Hooks/Links/Errors/Meta/Tools/Tests) with five scannable verb rows in a fixed gutter —Asked(the originating prompt, quoted),Doing(checklist progress, team lineage, sub-agents),Made(file deltas, reads, artifacts, plan, PR),Health(errors + last test verdict, absent when clean),Cost(msgs, tokens, tool mix) — thenLatest(the full wrapped last message, formerlyLast response:) and one width-cappedDetails ▸fold (session id, skills, plugins, hooks, links, dirs, repos) that shows… +N moreinstead of wrapping and swamping the pane. Msgs/tokens/session-id move out of the header's third line intoCost/Details, so they render once. The metadata-only card (remote/unindexed sessions) uses the same rows. Source:apps/cli/src/commands/sessions-picker.ts(formatCompactPreview,formatMetaOnlyBody,formatHeader,joinWidthCapped— now ANSI-aware).pr-merge-on-greencan actually select a PR (RUSH-2848). The built-in's poll rangh pr list --author @mewith no--repo, soghinferred the repository from cwd; the daemon's cwd is not a git repo and every poll returned empty. The poll now walks registered project slugs (~/.agents/projects/*.yaml), canonicalizes renamed repos (phnx-labs/agents-cli→phnx-labs/agi-cli), and lists with--repo. Verdict matchesmerge-guard.sh: a formal GitHubAPPROVEDreview or an APPROVE comment on THIS PR (carried-from citations do not count). Hiddenagents _internal mergeable-prsprintsowner/repo#n(cwd-independent). Source:apps/cli/src/lib/github/pr-verdict.ts,apps/cli/src/lib/github/pr-mergeable.ts. Companion: phnx-labs/.agents-system#347.BREAKING: nest
agents statusunderagents sync status(RUSH-2864).statuswas the unified sync-drift report sitting as its own top-level command next tosync. It now lives asagents sync status(--jsonUnifiedSyncStatus contract and--yesreconcile unchanged). Top-levelagents statusis gone, not deprecated: it reportsunknown command, andstatusis retired from distance-1 auto-correct.agents sync <agent>still treats the positional as an agent spec;statusis reserved as the subcommand.agents sync status --devicefrom a non-TTY does not inherit umbrella--yes(that flag is reconcile on this command, not "don't prompt"). Source:apps/cli/src/commands/{status,sync}.ts,apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/startup/command-registry.ts.Custom harnesses (deepseek, kimi-chat, glm, …) now work in routines and monitors, and their profiles survive fleet sync (RUSH-2930).
agents routines add --agent deepseekandagents monitors add --run deepseekused to refuse any name outside the native registry; both now accept a custom harness (agents harness list), and the daemon delegates the job toagents run <name>— the same path workflow jobs take — so the profile's host binary, model env, and provider auth resolve exactly as an interactive run. The profile pins its own host version and auth, so@versionpins, balanced rotation, and account-env injection don't apply to these jobs. Separately,agents harness add/fork/editand the legacy-auth migration stored the account's per-device id in the profile, while profiles sync fleet-wide viaagents repo push— every synced custom harness then died on other machines withUnknown account '<uuid>'. The portable account name is stored now, and a profile with a dangling ref reports which harness is broken and the exactagents harness edit <name> --account <name>repair instead of the bare registry error. Source:apps/cli/src/lib/daemon/runner.ts,apps/cli/src/lib/scheduling/routines.ts,apps/cli/src/lib/monitors/config.ts,apps/cli/src/commands/{routines,harness,profiles}.ts,apps/cli/src/lib/profiles.ts.BREAKING: remove the top-level
agents ticketscommand (RUSH-2932). Ticket reads go throughlinear(linear-cli) andgh issue.agents ticketsreportsunknown command, andticketsis retired from distance-1 auto-correct. AGI EXT now calls those CLIs directly. Source:apps/cli/src/cli/command-registry.ts,apps/cli/src/lib/startup/command-registry.ts,apps/ext/src/vscode/tasks.vscode.ts.Expired cached usage windows read as unknown, not 0% (RUSH-2936). A cached Claude usage window whose reset time had passed was zeroed but kept, so a frozen cache (Anthropic has been 429-ing
/api/oauth/usageper account since ~Aug 5) rendered every account asS: 0% (now) W: 0% (now)withusageStatus: "available"— and balanced rotation kept dispatching into genuinely rate-limited accounts (RUSH-2858).deserializeClaudeUsageSnapshotnow drops expired windows (the same rule the Grok collector already applied); an all-expired snapshot deserializes to null, the dead cache entry self-cleans,agents viewrendersusage unavailableplus the recorded throttle reason, and rotation falls back to the coarse cached status instead of trusting a fake 0%. Source:apps/cli/src/lib/accounting/usage.ts.agents doctor <agent>@<version>no longer marks every missing resource critical (RUSH-2947). Target mode'scomputeVerdicthardcodedseverity: 'critical'for anystatus: 'missing'resource of any kind, while fleet mode'sFINDING_SEVERITY— whose own docblock calls it "the single source of truth" — rates the same factwarningfor everything exceptmissing-hook/missing-plugin. A missing command, skill, permission, subagent, rule, or MCP entry now readswarningin target mode too, matching fleet mode; a missing hook or plugin stayscriticalin both.computeVerdictreads severity fromFINDING_SEVERITYvia a newmissingResourceSeverity(kind)helper instead of re-hardcoding it. Source:apps/cli/src/commands/doctor.ts.BREAKING: nest
agents aliasunderagents setup alias(RUSH-2965). Setting up a PATH shorthand is a setup action, not its own noun.agents setup alias add/list/removekeep the same shim behavior (~/.agents/.cache/shims/+~/.agents/aliases.json). Top-levelagents aliasis gone, not deprecated: it reportsunknown command, andaliasis retired from distance-1 auto-correct. Source:apps/cli/src/commands/{alias,setup}.ts,apps/cli/src/lib/startup/command-registry.ts.Block service-manager registration under a redirected HOME on macOS/Linux (RUSH-2968).
launchctlandsystemctl --userare per-user-session and ignore$HOME, so a CLI running under a sandbox/redirected HOME (including every vitest fork) still talked to the user's real service manager.service-manifest.tsalready namespaces the job label, but that only changes the identifier — the registration still lands in reallaunchd/systemd(measured: 89 dead…sandbox-<hash>services accumulated in one user's real launchd from test runs, KeepAlive-retrying torn-down executables). Every registration path now consultsserviceManagerRegistrationAllowed()(false whenisolatedHomeSuffix()is non-null) and skips with a stated reason rather than touching the real service manager: daemonstartDaemon/stopDaemon, menubarrestartMenubarLaunchAgent/disableMenubarService, secretsretireLegacySecretsAgentService, andagents computer start. A test seamAGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1lets the existing shim-based tests continue to exercise the registration code path. Source:apps/cli/src/lib/service-manifest.ts,apps/cli/src/lib/daemon/daemon.ts,apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/src/lib/secrets/agent.ts,apps/cli/src/commands/computer.ts.BREAKING: the top-level
agents inboxcommand is removed — useagents feed(RUSH-2984).inboxwas a Phase-3 observe alias that re-parsed asagents feed(needs-you is already the feed default), the same leftover class astimeline(RUSH-2692). It is gone entirely, not deprecated:agents inboxnow reportsunknown command, andinboxis retired from distance-1 auto-correct.agents feed/feed post/feed post --blockedare unchanged. Source:apps/cli/src/commands/feed.ts,apps/cli/src/lib/startup/command-registry.ts,apps/cli/src/bootstrap.ts.agents authandagents orgnow print a clean one-line error (and a structured{"error"}payload under--json) for signed-out, expired-token, and bad-input states, instead of dumping a raw Node stack trace.Fixed balanced Claude account rotation recording session-limit refusals until their stated reset, skipping those accounts, and surfacing
session-limitedinagents viewinstead of idle usage.Sandboxed monitor
--runand routine children now inherit this host's GitHub CLI auth (RUSH-2860). A monitor fire recordedokwhile the spawned child had no~/.config/gh, sogh auth statusreported "not logged into any GitHub hosts" and the PR it was meant to act on stayed open. The routine sandbox overlay replacesHOMEwith a disposable dir, andghwas never linked into it.prepareJobHomenow links the host'sghconfig dir into the overlay (and only that —~/.agentsis deliberately not linked, since it holds the secrets master key and the encrypted store),buildSpawnEnvpinsGH_CONFIG_DIRand forwardsGH_TOKEN/GITHUB_TOKEN, and the runner refuses to launch when a routine's ownenv:overridesGH_CONFIG_DIRsuch that this host's gh auth would still be hidden — a regression tripwire rather than a runtime guarantee. Not covered on Windows, whereghstores config under%AppData%\GitHub CLI.agents webhooksnow advertises/hooks/slack. The receiver has accepted signed Slack deliveries since the Slack bridge landed, butagents webhooks serveandagents daemon webhooks liststill printed only/hooks/github, /hooks/linear— the one URL you must paste into the Slack app was missing from the banner. The slash-command ack now also says "replying in this channel", matching where the reply actually posts (a slash command carries no thread). Source:apps/cli/src/commands/webhook.ts,apps/cli/src/commands/daemon.ts,apps/cli/src/lib/triggers/webhook.ts.Slack/webhook handlers that run an agent actually dispatch now. A
run.agentorrun.workflowwebhook handler was gated by the routine activation manifest (~/.agents/devices/<machine>/agents.yaml). A handler is not a routine, so its name could never be a member — on any box that had materialized that manifest, every delivery was recordedskippedwith an empty allowlist (can only run on:) while the receiver logged it asfired, andagents routines enable <handler>refuses the name, so there was no way out. Verified live: a real HMAC-signed/agentsslash command reached the public receiver, acked 200, and started no agent. The job now carriesdispatchedBy: 'webhook', the same escape RUSH-2681 gave monitors; a handler'sroutine:delegate still keeps its gate.{{slack.response_url}}is now available to handler prompts. It was parsed off the wire but never reached the{{slack.*}}namespace. Slack accepts a POST there for 30 minutes with no token and no channel membership, so it is the only reply that works before the app has been invited to a channel — the shipped example handler now uses it. Source:apps/cli/src/lib/triggers/handlers.ts,apps/cli/src/lib/scheduling/routines.ts.
1.22.42
Plan-tier gates for
agents accountsandagents insights(RUSH-2424). A newapps/cli/src/lib/entitlement.tsreads the live subscription tier fromGET /api/v1/billing/subscription?agent=agi-cli(the session token in~/.rush/user.yaml), caches it on disk for 15 minutes, and stays offline-tolerant: a stale cache is honored over a failed network call, and no session file at all resolves straight to the free tier.agents accounts add/name/attachnow cap registered accounts at 3 per harness on free, 10 on paid/admin — a 4th add on free refuses before any write (free plan is capped at 3 claude accounts (3/3). agents upgrade — up to 10 per harness.) and the 3rd prints a one-line notice. Downgrading a plan never deletes a credential: over-cap accounts fall out ofaccounts switch/set-default(excluded fromlistSwitchableAccounts) and are listeddormant (upgrade to reactivate)inagents accounts.agents insightskeeps top-line counts, harness mix,insights mix, andagents perffree on every tier; the Friction / Friction-thrash / Dissatisfaction-corrections sections, grouping--by account(the default), and--narrativeare paid — a gated section is replaced by the in-voice noticeFriction and account-split analysis are on the paid plan.in both the text report and--json(a newplan: {tierName, isPaid}field,groups: nullwhen the account breakdown is gated, and the friction/correction facet keys stripped per group otherwise). Source:apps/cli/src/lib/entitlement.ts,apps/cli/src/commands/accounts.ts,apps/cli/src/commands/insights.ts.--profile <name>means the same profile in every browser command, and a profile's name never carries its endpoint (RUSH-2709).BrowserService.startused to overwriteBrowserProfile.namewith the composite<profile>@<endpoint>runtime key, so the key leaked intoprofiles list,status,history, and the feed, and six consumers each invented their own rule for turning one back into the other — which is whystatus --profile defaultcould report "No browser tasks running" while that profile was running. The two concepts are now distinct types:ProfileName(user-facing, always bare),EndpointName, and a brandedConnectionKeybuilt at exactly one site, soconnections.get(someProfileName)— the original miss — no longer compiles. One rule,keyBelongsToProfile, replaces the four ad-hoc reconciliations instatus,stopProfile,findTask, andlistProfileCacheDirs, which also fixes a legacy<name>.<n>fork directory being matched by some of those paths and missed by others.statusrenderscomet-local (endpoint: endpoint-0, port 9222, pid …)instead of the raw key, andstatus --jsoncarriesname(bare),endpoint, andkeyas separate fields. Source:apps/cli/src/lib/browser/{types,service,profiles,runtime-state}.ts,apps/cli/src/lib/browser/drivers/{local,ssh}.ts,apps/cli/src/commands/browser.ts,apps/cli/docs/browser.md.defaultis now purely an alias, and the auto-detected browser profile is namedauto-chrome(RUSH-2709).defaultused to name BOTH a concrete auto-detected profile and the "whatever you configured" alias, and onlystarthonored the alias (its own comment: "narrowly scoped … only here instart") — so--profile defaultreached one profile instartand a different one, or nothing, instop/status/navigate. One resolver,resolveProfileRef, now serves every command. Nothing existing breaks: abrowser.profileconfig value ofdefaultstill resolves (to the auto-detected profile), a profile you literally nameddefaultstill resolves to itself and outranks the alias, and a machine that already carries adefaultprofile keeps using it —ensureDefaultBrowserProfilereuses and regenerates it in place rather than creating a second one, so a running browser and its on-disk runtime dirs are not orphaned. Source:apps/cli/src/lib/browser/profiles.ts,apps/cli/src/commands/{browser,setup,setup-browser,setup-preferences}.ts,apps/cli/src/lib/installations/migrate.ts.agents auth login/whoami/logoutandagents org create/list/view/invite/members/role/remove/leave— the account-layer client over the existingapi.prix.devbackend.agents auth loginruns the already-shipped RFC 8628 device-code flow (POST /api/v1/auth/device/authorization+ poll/token) and writes its own session file, separate fromrush login's~/.rush/user.yaml— soagents auth logoutnever signs you out ofrush, whileagents auth whoami/agents orgfall back to a liverush loginsession automatically if you never ranagents auth login.agents orgmaps to the backend's/api/v1/spacesroutes (not/api/v1/orgs— spaces already enforce the free-tier caps of 1 owned space / 3 members and can exist standalone). Every subcommand takes--json;--space <id-or-slug>is optional and defaults to your sole space. Source:apps/cli/src/lib/prix-account.ts,apps/cli/src/commands/{auth,org}.ts.agents accounts switch <harness> [account]picks the default account for a harness. Interactive mode reuses the run-account picker layout (usage %, headroom, signed-out / rate-limited) and writes the existing per-harness default — the same binding asaccounts set-default, which balanced rotation already honors. Pass an account name to skip the picker;--jsonlists or reports the result. Nativeaccounts name/attachnow refuse an unsupported harness with a named reason (kimi accounts can't be isolated by agents-cli yet (device-scoped login). Supported today: claude, codex, grok.); provideraccounts add --provideris unchanged. Source:apps/cli/src/commands/accounts.ts,apps/cli/src/commands/run-account-picker.ts,apps/cli/src/lib/account-capabilities.ts.agents browser navigatenow reuses a tab in Arc instead of refusing outright (#2786, #2778). Arc crashes onTarget.createTarget, so every navigate was answered with "use a Chromium-family browser" — which meant the document was never shown at all and callers fell back to a rawopen, a new tab per call, i.e. exactly the tab-spam #2779 set out to end. Arc does expose page targets and does honorPage.navigateon them (measured against a live Arc: 33 targets, navigate reused one, tab count unchanged, no crash), so navigate now reuses in place, the same shape as the existing Electron branch. Reuse is narrow and non-destructive: only a tab ALREADY showing the requested URL, or an empty new-tab page, and a target another task owns is left alone. A reused tab is recorded as borrowed — the task drives it but never closes it, so a tab that was open before the task survivesdone(the ruleadoptTabShowingstates for unowned pages, kept here where reuse is unavoidable). When nothing is safe to reuse the actionable refusal stands. TheTarget.createTargetguard from #2778 is unchanged. Source:apps/cli/src/lib/browser/service.ts.
1.22.41
BREAKING: the top-level
agents timelinecommand is removed — useagents feed --filter updates(RUSH-2692).timelinewas a pure alias ofagents feed --filter updates(its own description said so), a second door to one stream against the CLI-surface convention of one owned noun with no duplicated surface. It is gone entirely, not deprecated:agents timelinenow reportsunknown command, andtimelineis retired from distance-1 auto-correct so a stale invocation fails loudly instead of running a neighbouring command.agents feed --filter updatesis unchanged, and the sibling observe aliasesagents inboxandagents rosterare unaffected. Source:apps/cli/src/commands/feed.ts,apps/cli/src/lib/observe-aliases.ts,apps/cli/src/lib/startup/command-registry.ts,apps/cli/src/bootstrap.ts.Session preview leads with the title and wraps the last response to the pane (RUSH-2757).
agents sessions previewnow opens its header with the session's title (session.label— an agent-generated name //rename/ the--namelaunch handle) instead of burying it, and the agent's last response is wrapped to the terminal width rather than running off the right edge as one long line. Wrapping measures visible width (ANSI-aware), so coloured output is not miscounted, and lines that already fit keep their rendered-markdown indentation. Source:apps/cli/src/commands/sessions-picker.ts,apps/cli/src/lib/wrap.ts.release-manifest.shhelper digests are keyed by path relative to--repo-root, andrelease-attestation-produce.shnow writes the helper manifest it needs (RUSH-2766).hash_treeused to bake the ABSOLUTE file path into every helper'sinput-digest, so a digest recorded in one worktree could never match the same recomputation elsewhere —require_helpersstructurally could not pass for any release, and v1.22.40 was finished only by hand-recomputing the manifest inside a fixed-path worktree onmac-miniand re-uploading it to the GitHub release. Paths are now hashed relative to the repo root, so the same digest reproduces across machines and worktrees (demonstrated: identicalsha256:for all three helpers computed from two independent checkouts). Separately,release.shhas always consumedrelease-manifest.json(requireat :231,upload_release_proofat :976) with no producer writing it — the same consumer-without-producer gap RUSH-2749 fixed for the attestation itself.release-attestation-produce.shnow writes/updates it alongsideATTEST.json: an unchanged helper carries its prior record forward,keychain/menubarare re-recorded from the fresh signed assets this script's own Darwin block builds, and a driftedcomputer-macdigest with no prior record fails the producer closed rather than shipping a stale or missing helper record. Source:apps/cli/scripts/release-manifest.sh,apps/cli/scripts/release-attestation-produce.sh,apps/cli/docs/release.md.BREAKING:
agents secretsno longer prints bundle values — the plaintext export and bundle-keygetare removed; run commands underagents secrets execinstead (RUSH-2774). Theeval "$(agents secrets export <bundle> --plaintext)"one-liner put every value of a bundle onto stdout, which inside a coding-agent session means the model's context and the synced session transcript — and agents copied it reflexively from the CLI's own help and scripts.secrets exportis now transfer-only (--device,--to-1password,--to-file; anything else refuses, naming the alternatives);secrets get <bundle> <KEY>refuses, namingagents secrets exec <bundle> -- printenv <KEY>;secrets view --revealrefuses inside an agent session (AGENTS_RUNTIME/AGENT_SESSION_ID/CLAUDECODEmarkers) and its non-TTY--reveal --plaintextescape is gone; the raw-itemsecrets get <item>stays available everywhere — fleet shell hooks running inside sessions capture single ad-hoc tokens into their own variables, the accepted narrower residual. The SSH remote resolve behindsecrets exec --deviceandrun --secrets <b>@<host>still works: it rides a hidden, marker-gated transport (AGENTS_SECRETS_REMOTE_TRANSPORT), so a newer CLI keeps resolving from an older remote during a fleet rollout, while an older CLI against a newer remote fails loud with an upgrade hint. First-party scripts (sandbox.sh, bothrelease.sh) now re-enter themselves underagents secrets exec, so values ride the child environment and never touch stdout. Migration for eval scripts:agents secrets exec <bundle> -- <your command>, orVAR="$(agents secrets exec <bundle> -- printenv KEY)"for a single value. Spec: SEC-9/SEC-9b/SEC-9c, SEC-GAP-10. Source:apps/cli/src/commands/secrets.ts,apps/cli/src/lib/secrets/{headless,remote}.ts,apps/cli/scripts/{sandbox,release}.sh,apps/ext/scripts/release.sh.Fix a publish crash on any non-latin1 label, meta value, or repo name (RUSH-2784).
fetchencodes header values as a ByteString, so a title containing an ellipsis, a curly quote, an emoji, or CJK text threwTypeError: Cannot convert argument to a ByteStringmid-publish — an unhandled stack trace after the body had already been read and the OG cover uploaded. Every free-text header (x-share-label,x-share-repo,x-share-host,x-share-meta, and the provenance set) now goes throughtoHeaderValue(), which transliterates common typographic punctuation to ASCII and drops the rest of the non-latin1 range. Still lossy for a title in a non-latin script, which publishes as(unnamed); full Unicode needs percent-encoding plus a matching Worker decode, tracked as RUSH-2786. Source:apps/cli/src/lib/share/publish.ts.agents sessions share <id>publishes a session as a link (RUSH-2784). Sharing a session took three commands and a detour through the external artifacts-cli, so nobody ran it — every piece already existed (sessions renderproduces a redacted transcript, and the R2-backed share Worker has served artifacts for a month), but nothing wired them together. One verb now renders the session as a self-contained branded HTML page and publishes it:agents sessions share a1b2c3d4printshttps://share.agents-cli.sh/<you>/session-a1b2c3d4. Unlisted by default, unlikeagents artifacts share— a transcript carries file paths, command output, and error text, so it stays out of the public gallery until--publicasks for it (the URL is still world-readable: unlisted is a capability URL, not a secret). The page is escaped rather than passed through, so a session that merely printed a<script>tag does not ship an executable one. Publishing inherits the existing email/credential scan and the 30d default expiry;--reasoning foldkeeps the model's reasoning in collapsible sections,--expire neveropts out of decay. Source:apps/cli/src/commands/sessions-share.ts,apps/cli/src/lib/session/share-html.ts,apps/cli/src/lib/share/publish.ts.agents browser use [name]is the one command for choosing this machine's default browser profile (RUSH-2820). A name writes the existing device-scopedbrowser.profileconfig key; a bare interactive invocation opens a picker over configured profiles and installed browsers, while a headless invocation prints the current default and usage.--unsetandautorestore auto-detect.agents browser profiles useis an alias, and the hiddenprofiles set-defaultcompatibility alias now points users tobrowser use. Source:apps/cli/src/commands/browser.ts.Claude workspace trust now carries across pinned version homes (#2776). Every pinned claude version gets an isolated home, and workspace trust (
projects[<path>].hasTrustDialogAcceptedin.claude.json) never carried into a new one — so the first interactive launch of each newly pinned version re-showed the workspace-trust dialog once per project, complete with the "this folder pre-approves N tool permissions" warning on repos trusted for months (headless runs skip the dialog, so the re-prompt could surface weeks after pinning and read as a trust regression).agents add/agents usenow carry trust via a newclaude-trustmanifest strategy that projects ONLY the accepted trust flags out of the source.claude.json— the login (oauthAccount) and per-session stats stay per-version, same posture as the removed.codex/auth.jsoncarry. A target entry's stamped-defaultfalseis promoted (Claude Code never persists a decline), all its other keys are preserved, the target is backed up before modification, and the write is atomic (a running Claude session rewrites this exact file). Codex was already covered:projects.*.trust_levelrides along inconfig.toml. Source:apps/cli/src/lib/settings-manifest.ts.Arc is a recognized browser type, and
agents browsernow refuses to drive it with a clear error instead of crashing it (#2779, #2778).arcjoins the Chromium-familyBrowserType(detected at/Applications/Arc.app, macOS-only;agents browser profiles seedcreates anarc-localprofile). But Arc answersBrowser.getVersionwhile exposing zero CDP page targets and crashing the moment a new tab is requested (Target.createTarget), so it is not actually drivable. Every tab-creating path now routes through one guard (createPageTarget) that throws an actionable "use Comet/Chrome/Chromium/Brave" error for an Arc profile rather than crashing the user's Arc window. The browser skill documents this and teaches showing a review doc/plan/report viaagents browser navigate --url file://<path>(one reused tab, refreshed in place) instead of a rawopen <file>that spawns a duplicate tab every call. Source:apps/cli/src/lib/browser/service.ts,apps/cli/src/lib/browser/chrome.ts,apps/cli/src/lib/browser/cdp.ts,skills/browser/SKILL.md.Slack can now trigger an agent on your box and reply in-thread. The signed webhook receiver (
agents webhooks serveand the daemonwebhook-receiver) accepts a third source,POST /hooks/slack: it verifies Slack'sv0request signature with a 5-minute replay guard, answers the one-timeurl_verificationhandshake, and parses both slash commands (/agents AGI: …) andapp_mentionevents. A matching~/.agents/webhooks/*.ymlhandler withsource: slack(and optionalcommand/channelfilters) runs an agent scoped to the project named in the message — a handler may now templateproject/cwdfrom the delivery (project: "{{slack.project}}"), with the new{{slack.*}}namespace exposingprompt,project,channel,thread_ts,user,text, andcommand. The agent replies into the same thread through the existingagents send --channel slack --to <channel> --thread <ts>— no new outbound code. The receiver bundle gainsSLACK_SIGNING_SECRET(andSLACK_BOT_TOKENwhen the reply uses the Slack Web API). Setup, a ready-to-paste app manifest, and an example handler are indocs/routines.mdanddocs/examples/slack/. Source:apps/cli/src/lib/triggers/webhook.ts,apps/cli/src/lib/triggers/handlers.ts,apps/cli/src/lib/daemon-webhooks.ts,apps/cli/src/commands/webhook.ts.
1.22.40
type: feat scope: devices
Device approvals and dismissals now follow the user DotAgents repo across the fleet
(RUSH-2377). agents devices register, add, sync, ignore, unignore, and
remove persist a three-state decision under the central agents.yaml
fleet.discovery map: approved, ignored, or absent/pending. After
agents repo pull user, the CLI reconciles those portable decisions into the local
.history/devices registry and ignore-list, resolving approved connection details
live from Tailscale. IPs, SSH users/auth, and reachability remain machine-local and
never enter Git. Source: apps/cli/src/lib/devices/discovery-policy.ts,
apps/cli/src/commands/{ssh,repo}.ts.
Legacy tmux
pane-diedhooks now self-heal at attach and daemon startup instead of relying on a poll that no longer exists (RUSH-2435). The 5-minutetmux-reconciledaemon routine that used to retrofit a stale hook onto a managed tmux session was deleted in an earlier pass (RUSH-2495), leavingreconcileSessionHookswith zero production callers — a session a pre-fix binary left with a stale hook had nothing to repair it.reconcileSessionHooks(full sweep) now runs once at daemon startup and once from the upgrade-time migration (runMigration) as the version-skew one-shot, and the newensureSessionHookRepaired(name, socket)repairs a single session right before it's attached to — wired intoprepareSessionForResume(nativeagents run --resume) and the manualagents focus/agents go/agents tmux attachattach paths. Both share one non-destructiverepairSessionHookIfStaleimplementation so the full sweep and the single-session repair can never drift on what counts as repaired.docs/routines.mdanddocs/specifications.mdno longer describe the deletedbuiltin-routines.ts/tmux-reconcilemachinery as live. Source:apps/cli/src/lib/tmux/session.ts,apps/cli/src/lib/tmux/index.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/migrate.ts,apps/cli/src/commands/focus.ts,apps/cli/src/commands/go.ts,apps/cli/src/commands/tmux.ts.agents sessions resume <id>/focus <id>for a local indexed session no longer runs the write-heavy discovery scan or dials the fleet, so a crash-restart storm of resumes stops crashing ondatabase is locked(RUSH-2477). After a machine crashes and reboots, every previously-open tab relaunchedsessions resume <id>at once. Each resume ran the fulldiscoverSessionspath, whosetryClaimScan/releaseScanbookkeeping areBEGIN IMMEDIATEwriter transactions (lib/sqlite.ts), so even a process that skipped the scan still took the writer lock — dozens at once exhaustedbusy_timeoutand threw an unhandledSQLITE_BUSY("database is locked"), and on a local miss the resume fanned out over SSH to the whole fleet before the tailnet was up, hanging and printing the doubledunreachable … skippedlist. The direct-id path now resolves against the local SQLite index first through a newresolveIndexedSessionById(lib/session/discover.ts) — a plain WAL read (skipExistenceCheck) with the same origin-machine attribution and managed scoping every indexed read gets, no scan claim and no fleet fan-out.focusAction(commands/focus.ts) takes that fast path for a local indexed id: on a single logical match owned by this box it routes straight to recovery (joining a still-live pane via the local live index, or resuming a crashed one in place); a genuine miss, an ambiguous prefix, or a peer-owned row falls through to the existing fleet resolver unchanged.dedupeSessionsByLogicalIdcollapses the self-vs-attribution split of one id, so a full uuid that used to read as "ambiguous (2 sessions)" now resolves to one. Verified: >= 20 concurrentresolveIndexedSessionByIdcalls complete with zeroSQLITE_BUSY, and the resolve issues no SSH fan-out. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/commands/focus.ts.agents monitorsnow correctly reports a failed action dispatch —ok:falseand the specific reason — when the runner declines to start or fails to start the agent process (RUSH-2500). Previously,dispatchActioninlib/monitors/dispatch.tstreated any non-throwing return fromexecuteJobDetachedas success (ok: true). ButexecuteJobDetachedcallsrunWithAttempt, which can return aRunMetawithstatus: 'skipped','blocked', or'failed'without throwing — e.g. when the non-overlapactive_runguard fires because a prior monitor-triggered run is still live, or when the auth preflight fast-path (dead-verdict credit exhaustion) marks the runfailedand returns instead of throwing. The skipped or failed run was recorded in~/.history/runs/with the real status, but the fire record in~/.history/monitors/gotok: true, somonitors runsshowed success whilemonitors logsshowedskipped. The engine's drought escalation also got a false healthy signal and never notified the owner. The fix checksrunMeta.statusafterexecuteJobDetachedand returns{ ok: false, error: runMeta.errorMessage ?? runMeta.status }forskipped,blocked, orfailedresults, making the fire record agree with the runner record. Applies to bothrunandroutineaction types. Source:apps/cli/src/lib/monitors/dispatch.ts.agents sessions --activenow distinguishes "genuinely nothing running" from "discovery failed and was silently swallowed" (RUSH-2507). PreviouslygetActiveSessionsfolded every tmux/teams/terminals source failure into a bare[]— a wedged tmux server, a spawn error, or a nonzerolist-panesexit read identically to an idle machine, and the same collapse happened on the fleet fan-out side, where an unreachable/skipped peer or a failed device-list load produced the exact same "No active agent sessions." as a genuinely idle fleet.listTmuxAgentSessionsnow throwsTmuxDiscoveryDegradedErrorwhen the tmux socket exists but couldn't be read (still returning[]when no socket has ever existed — that case is legitimately empty), and the newdescribeActiveDiscoveryHealth()re-probes it only on an empty result to report which local source degraded.RemoteActiveResultnow carriesskipped/discoveryFailedfromgatherRemoteAgentsJsoninstead of dropping them. The empty-result message inagents sessions --activenames the degraded source or unheard peer instead of a flat "No active agent sessions.". Source:apps/cli/src/lib/session/active.ts,apps/cli/src/lib/session/remote-active.ts,apps/cli/src/lib/session/session-cache.ts,apps/cli/src/commands/sessions.ts.
type: feat scope: projects
agents projects pull <name> fast-forwards every fleet checkout of a named project to
its remote's default branch.
Safety contract: dirty trees, checkouts on a non-default branch, and local commits ahead of upstream are blocked and reported — never overwritten. Fast-forward only; no rebase, no reset.
Every checkout is verified against the project's declared repo slug before it is fast-forwarded, on every fleet device — a bound path that hosts a different repo is blocked. A checkout whose
origincannot be resolved to a slug is blocked too.Missing checkouts are skipped (never cloned) and do not drive a non-zero exit. Blocked or failed checkouts do.
Fleet fan-out reuses the same
gatherRemoteAgentsJsonseam asprojects status, with a 120-second per-device timeout for large repos and slow links.--device/--devicesscope the pull to one or more named fleet boxes.--jsonemits a machine-readable result array (one row per path per device), and reports devices that did not answer (unavailable) or answered unverifiably (unverified) on stderr — matchingprojects status --json.A device whose answer cannot be verified is reported as
unverifiedand drives a non-zero exit, rather than being silently folded in as a device with nothing to report.agents projects pull-local(hidden peer command) is the per-device fast-forward runner invoked by the fleet fan-out. It receives the full{path, expectedSlug}target list, so slug verification runs on remote devices exactly as it does locally, and a malformed or spoofed peer response is rejected loudly rather than partially applied.routines.test.ts: daemon spawned bystartIsolatedDaemonno longer inherits the real productionAGENTS_HISTORY_DIR(RUSH-2545). The test helper spread...process.envinto the daemon's environment without overridingAGENTS_HISTORY_DIR, so the detached daemon process read its~/.agents/.historyrecord — the live production history — and its SIGTERM sweep killed real tmux-wrapped Claude and cgraph-mcp processes on every five-minute tick while the test suite ran. The fix addsAGENTS_HISTORY_DIR: path.join(home, '.agents', '.history')to the daemon spawn env so the sweep is fully confined to the test's tmp directory. A new regression test (daemon env isolation — AGENTS_HISTORY_DIR must not leak) reads/proc/<pid>/environon Linux to assert the daemon's env carries the isolated path, not the parent vitest process's real one. Source:apps/cli/src/commands/routines.test.ts.The daemon hosts signed webhook receivers as a supervised service (RUSH-2548). Public Linear/GitHub → agent webhooks no longer depend on a
nohup'dagents webhooks servetied to an agent session: declare a receiver withagents daemon webhooks add --secrets-bundle <name> [--port n] [--funnel-port 443]and the daemon's newwebhook-receiverservice binds it, restarts it on crash, and brings it back after a reboot.agents daemon webhooks list [--json]andagents daemon webhooks remove <port>manage the declarations. The signing secret resolves headlessly through the secrets broker, so there is noAGENTS_SECRETS_PASSPHRASE; a locked bundle fails that receiver loud inagents daemon logsinstead of binding ingress it cannot verify. Source:apps/cli/src/lib/daemon-webhooks.ts,apps/cli/src/commands/daemon.ts.Webhook deliveries are acked before dispatch, not after (RUSH-2548). The receiver held the HTTP response open for the whole 15-20s agent run, which exceeded Linear's delivery timeout and filled its Delivery failures log with timeouts and retries. A verified delivery is now answered
202 {"ok":true,"accepted":true,"deliveryId":…}immediately and dispatched afterwards. Delivery-id dedup is unchanged, including for a retry that lands mid-dispatch; the response body no longer carriesfired/runs/handlers, and a post-ack failure surfaces as the newwebhook.failedevent plus a line in the receiver's log. Source:apps/cli/src/lib/triggers/webhook.ts.Add named routers --
agents route(RUSH-2556, RUSH-2562, RUSH-2563, RUSH-2564). A router is a reusable, task-typed allowlist of harnesses x models/tiers x linked accounts -- a generalization of a profile (a profile is a router pinned to one harness and one account).routersis a newResourceKind, resolved project > user > system like other resources;agents route create/list/show/allow/link-account/unlink-account/rmmanage it. Harness ids and model/tier tokens are validated oncreate/allowagainst the real agent registry and each harness's resolved tier map/catalog -- an unknown token fails loud, printing the invalid token, and writes nothing. Source:apps/cli/src/lib/routers.ts,apps/cli/src/commands/route.ts.The browser tab reaper now actually runs, and you can trigger it on demand (RUSH-2622).
agents browser done/stopalways closed a task's tabs, but agents routinely never called them, so leftover tabs piled up in the shared profile window. The daemon now closes them itself, on a 5-minute tick: a task whose owning agent session has exited, or one idle pastbrowser.task-idle-minutes(default 30, new device config key —0disables idle reaping only, a dead-session task is still closed).agents browser gc [--dry-run] [--idle-minutes <n>]runs the same pass on demand. Only tabs the daemon opened for that task are ever touched — a tab you opened yourself, or the shared profile window, is never closed. Source:apps/cli/src/lib/browser/hygiene.ts,apps/cli/src/lib/daemon.ts(runBrowserTaskReap),apps/cli/src/lib/device-config.ts(browser.task-idle-minutes),apps/cli/src/commands/browser.ts(gc).The macOS menu-bar helper now recovers an alive-but-frozen daemon after sleep/wake (RUSH-2636). Its separate launchd process reads the daemon heartbeat as well as the PID; after a confirmed stale heartbeat it runs one scoped
agents daemon restartand reports the recovery. Missing, malformed, or PID-mismatched heartbeat state fails closed. Source:apps/cli/menubar/Sources/MenubarHelper/DaemonLiveness.swift,apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.daemon.ts: the launchd Label / systemd unit name is namespaced under a redirected HOME (RUSH-2639, residual). An earlier RUSH-2639 fix baked the caller's HOME into the daemon's plist/unit content so a service-manager-started daemon lands in the right sandbox — butlaunchctl unload/load/listroute by the service identifier alone, and that identifier (com.phnx-labs.agents-daemon/agents-daemon.service) was still one literal, global string. Confirmed directly against reallaunchctl: when a different plist is already loaded under that label, a new instance's ownunload(written to be a no-op for a plist that was never loaded) silently kills the other job instead — exactly what let concurrent hermetic test forks (and a developer's own test suite next to their real always-on daemon) collide. The identifier is now namespaced with a hash of HOME whenever HOME differs fromos.userInfo().homedir(the OS/passwd record, which ignores$HOME) — every hermetic test process, never a real interactive/production invocation. Source:apps/cli/src/lib/daemon.ts.Fixed the macOS test regression introduced by the vitest HOME sandbox, which blocked release 1.22.40. RUSH-2639 sandboxed
$HOMEfor the whole suite so tests can no longer write into the developer's real~/.agents. macOS resolves the login keychain from$HOME, sousage.test.ts'ssetKeychainTokencall had no keychain to write and died withsecurity: SecKeychainItemCreateFromContent (<default>): The authorization was canceled. It surfaced only in the release matrix — PR CI runs Linux, which has no keychain — so it halted a release after the changelog was folded and the PR opened, rather than failing on the PR that introduced it. The test now installs an in-memory backend via the existingsetKeychainBackendForTestseam, which is strictly better than what it did before: writing a token into the real login keychain was the same class of bug RUSH-2639 exists to prevent. Source:apps/cli/src/lib/__tests__/usage.test.ts.
type: fix scope: services
The menu-bar helper's and the agents computer helper's launchd plists now carry HOME, closing the last RUSH-2639 escape. The earlier RUSH-2639 fixes covered exactly one of the three service manifests this CLI writes: the daemon's. generateServicePlist (menu bar) declared only PATH/AGENTS_NODE/AGENTS_ENTRY/AGENTS_BIN, and renderLaunchAgentPlist (computer helper) had no EnvironmentVariables dict at all. launchd applies that dict on top of the login session's environment, never the environment of whoever called launchctl bootstrap, so both helpers resolved the account home whatever home their caller was running under. The menu-bar helper reaches the CLI through its baked AGENTS_NODE/AGENTS_ENTRY, and every agents invocation runs the audit hook → readMeta → ensureAgentsDir, so each call bootstrapped that home's ~/.agents (.system, .history, .cache, routines). Under the hermetic test harness that is a write into the developer's or the CI runner's REAL home — the macOS-only leak that failed the 1.22.40 release matrix. Captured live on a CI runner: pid 10817, parent 1 (launchd), executable <sandbox>/Library/Application Support/agents-cli/MenubarHelper.app/Contents/MacOS/MenubarHelper, registered under the bare label com.phnx-labs.agents-menubar while the real ~/Library/LaunchAgents held no phnx plist. Both manifests now bake HOME + AGENTS_REAL_HOME, and both identifiers get the HOME-namespacing the daemon's already had, so a sandboxed instance can no longer bootout the operator's live helper. The rule lives in one module, apps/cli/src/lib/service-manifest.ts, with a per-generator test so a fourth manifest cannot ship without it. Production identifiers and behavior are unchanged: the namespace suffix is empty whenever HOME matches the passwd home. Source: apps/cli/src/lib/service-manifest.ts, apps/cli/src/lib/menubar/install-menubar.ts, apps/cli/src/commands/computer.ts, apps/cli/src/lib/daemon.ts.
fix: the vitest suite can no longer write into the developer's real
~/.agents(RUSH-2639).tests/setup.tspinned only specific hot spots (AGENTS_DEVICES_DIR,AGENTS_EVENTS_PATH, hook-shim/cache/logs/perf/state dirs) to fork-private temp dirs, leavingHOMEitself untouched — andstate.ts'sHOME(and several sibling modules') is a module-level constant captured once at import time, so any test or subprocess spawn that didn't happen to overrideHOMEbefore its first import resolved against the real home. Two confirmed live offenders:src/lib/routines.test.tswrote fixture routine YAML into the real~/.agents/routines/and~/.agents/.system/routines/, and asrc/lib/daemon.test.tsdescribe block registered/reaped real daemon-instance pid markers under~/.agents/.cache/helpers/daemon/instances/.tests/setup.tsnow redirectsHOME/USERPROFILEto a fork-private sandbox before any test file's imports run, so every HOME-derived path — including ones with no dedicated escape hatch, and subprocesses spawned withenv: {...process.env}— resolves under the sandbox with no per-test effort; new CI-only leak tripwires assert the real~/.agentstop level and~/.claude/settings.jsonare untouched. A newtests/global-setup.tsalso sweeps stale/tmp/agents-vitest-*dirs left behind by killed workers from past runs. Source:apps/cli/tests/setup.ts,apps/cli/tests/global-setup.ts,apps/cli/vitest.config.ts.Routines: a terminal run no longer wedges the scheduler's active-run slot (RUSH-2640). A run that reached any terminal state (failed / timeout / completed) used to keep holding the slot while its recorded pid stayed alive, so every later scheduled fire was refused with
already has an active runwhile the routine sat silently dead. The slot is now released the moment a run reaches a terminal state; a still-runningrecord is also aged out once it passes its own timeout, and the daemon no longer stamps its own never-dying pid on a run's provisional claim (which was what made the slot look permanently occupied and drew the process reaper at the daemon itself). A routine whose run failed now fires again on its next slot with no daemon restart. Three or more consecutive slot-skips for the same routine are surfaced in the daemon log instead of accumulating silently. Source:apps/cli/src/lib/runner.ts.
type: fix scope: menubar, extension
Resolve the standalone Linear CLI from executable GUI-safe paths before ticket creation, and show the underlying launch error when creation fails.
Branding: rebrand the OSS product name from agents-cli to agi-cli in user-facing prose, docs, and repo-path URLs, now that the GitHub repo itself has been renamed
phnx-labs/agi-cli(RUSH-2660). RootREADME.md,CONTRIBUTING.md,DESIGN.md,SECURITY.md,NPM_CONSOLIDATION.md,apps/ext/README.md,assets/videos/README.md,demo/src/scenes.ts,packages/agi-cli/README.md,packages/swarmify-mirror/README.md, and theapps/cli/docs/markdown headers and body prose that use "agents-cli" as the product name now read "agi-cli". Everygithub.com/phnx-labs/agents-clireference in those user-facing files, plus therepository/bugs/homepageURL fields inpackages/agi-cli/package.jsonandpackages/swarmify-mirror/package.jsonand the hardcoded repo defaults inapps/cli/scripts/release.sh, rootscripts/release.sh,scripts/bottle.sh,.github/ISSUE_TEMPLATE/config.yml, andscripts/release.test.sh(plus the companionapps/ext/app/package.jsonrepository.urlits test asserts against), now point atphnx-labs/agi-cli. This is docs/branding only — no runtime behavior change. Not touched: the npm packagenamefields (@phnx-labs/agents-clistays canonical; the package-name flip is a separate ticket), thenpm install -g @phnx-labs/agents-cliline inapps/cli/scripts/install.sh, theagents-clishell-rc marker insrc/lib/shims.ts,~/.agents/~/.agents-system, theagents/agcommand names, the VS Code ext identity (swarmify/swarm-ext), keychain-item naming patterns (agents-cli.<provider>.token,agents-cli.hmackey, etc.), literal runtime message strings quoted in docs (e.g.doctor.ts's"older agents-cli — can't report per-version sign-in"), and repo-path/project-name examples in code blocks. Source:README.md,apps/cli/docs/*.md,packages/agi-cli/package.json,packages/swarmify-mirror/package.json,apps/cli/scripts/release.sh,scripts/release.sh,scripts/bottle.sh,.github/ISSUE_TEMPLATE/config.yml,scripts/release.test.sh.Affected-test Linux CI gate (RUSH-2666). The required
Tests / testcheck is one GitHub-hosted Linux job:scripts/ci-scope.tsplusapps/cli/ci/test-ownership.yamlselect the companion, statically related, and declared-owner tests, fail immediately on an unmapped path, and reuse a proof only for the exact candidate tree. Windows is post-merge best-effort and does not block merge or release. Source:scripts/ci-scope.ts,.github/workflows/tests.yml.Ordinary
release.shpromotes the exact pretested tarball instead of rebuilding and notarizing (RUSH-2666). Functional proof is an immutable attestation bound to the candidate tree, toolchain, lockfile digest, and test-policy version — parent commits and nearby SHAs are rejected, and a missing record fails with that exact key. Helpers are reused from the release manifest or the release stops; sign/notarize is outside this path. The home base install-smokes the attested.tgzand publishes those bytes (npm publish <tgz>, OIDC provenance when GitHub's token exchange is present). Target: ordinary release P99 ≤180 seconds. Source:apps/cli/scripts/release.sh,apps/cli/scripts/release-attestation.sh,apps/cli/scripts/release-manifest.sh,apps/cli/scripts/release-install-smoke.sh.agents permissions listnow answers for every harness whose permissions the CLI writes, not 3 of them (RUSH-2676).applyPermissionsToVersionwrites claude, opencode, codex, cursor, antigravity, grok, kimi, droid, copilot, kiro, openclaw and hermes, but the read side was a hand-written 3-arm switch:readAgentPermissionsreturnednullfor everything except claude/opencode/codex, and the config-file import behindagents permissions add <path>gated on a.claude/.opencode/.codexsubstring, which also excluded kiro/goose/hermes twice over because their configs are YAML. So permissions were installed and then reported as absent — measured against the real write path, 13 written, 3 reported back. A newPERMISSION_TARGETSregistry (apps/cli/src/lib/permissions-registry.ts) declares each harness's config path plus how to read it back into the canonicalPermissionSet, mirroringSUBAGENT_TARGETS; a completeness test pins the key set tocapableAgents('allowlist'), so a newly added allowlist harness cannot be written-but-unreadable. Path detection now matches each harness's own declared filename, longest suffix winning, so.kiro/settings/permissions.yamlis never claimed by a shorter match. The registry also owns the canonical↔native tool vocabularies (GROK_TOOL_BY_CANONICAL,KIRO_CAPABILITY_BY_TOOL, and the rest), whichpermissions.tsimports for the forward serializers, so the two directions cannot drift into disagreeing about whatfs_readmeans. Every reverse projection is lossy in a way the harness's format forces — Kiro collapsesRead/Grep/Globontofs_read, Kimi expands one Bash arg-glob into two picomatch patterns, Codex has no rule list at all — and each target names its own loss rather than pretending the round trip is exact.agents permissions listrenders the canonical allow/deny for the harnesses that had no renderer; claude, opencode and codex keep their native renderings.BREAKING: Goose no longer supports permissions.
goosereadsallowlist: falsein the capability table, and its converter, writer, detector and config path are gone. Goose'spermission.yamlgates whole tools (developer__shell,developer__text_editor), so several distinct canonical rules collapse onto one entry and cannot be read back faithfully — a half-supported capability that reported success while losing information.agents sync gooseno longer writes.config/goose/permission.yaml, andapplyPermissionsToVersion('goose', …)now refuses withAgent 'goose' does not support permissionsrather than silently writing a lossy approximation. An existingpermission.yamlon disk is left untouched; nothing removes it. Goose keeps hooks, MCP, skills, commands, plugins, subagents and workflows.MCP sync no longer writes nothing and calls it success (RUSH-2677).
capabilities.mcpistruefor every harness, but the code behind it was four independentswitch (agentId)chains with four different membership sets — a config-path resolver, a config writer, an installer, and a parser. A harness present in one and absent from another resolved a real path and then fell straight through the writer: no file, no error, andagents sync <that harness>printedAlready in sync. Antigravity was the visible case, and pi, muse and warp had writers the installer never called. They are now one table,MCP_TARGETS(apps/cli/src/lib/mcp-registry.ts), pinned tocapableAgents('mcp')by a completeness test — so a newly added harness must declare a format, or declare with a stated reason that its format is not implemented. A refusal now reaches the user:installMcpServersreports it, the staleness writer forwards it instead of discarding it, andagents syncprints aNot written to <agent>@<version>:block naming the harness and the reason (copilot, amp, kiro and goose are the four with no schema verified against an installed CLI). Three path bugs fell out of the consolidation, each verified against the harness itself: antigravity MCP goes to~/.gemini/config/mcp_config.jsonin the user's REAL home — only~/.gemini/antigravity-cliis symlinked into a version home, so a version-home path lands where agy never reads — and its remote transport is keyedserverUrl, noturl; grok MCP is[mcp_servers.<name>]in~/.grok/config.toml, so the resolver no longer answers.grok/mcp.jsonand the parser no longer reads that TOML as JSON; kimi is read back from the.kimi-code/mcp.jsonthe installer writes rather than.kimi-code/settings.json, so a synced server stops reporting as missing. Claude's user-scope MCP path is~/.claude.json, where Claude readsmcpServers. A malformed existing config is now refused rather than rewritten from scratch — these files hold far more than MCP (hermes' wholeconfig.yaml, openclaw'sopenclaw.json), and the previous parse-or-reset would have destroyed the rest of them. Source:apps/cli/src/lib/mcp-registry.ts,apps/cli/src/lib/mcp.ts,apps/cli/src/lib/agents.ts,apps/cli/src/lib/staleness/writers/mcp.ts,apps/cli/src/commands/sync.ts.A monitor's
--runaction actually runs now — every one of them was silently skipped, fleet-wide (RUSH-2681). Detection worked (the poll ran, the regex matched, the fire was recorded), but the action never executed:agents monitors logs <name>showed the run asskippedwith no output. A monitor synthesizes a one-offJobConfignamed after itself and hands it to the routines dispatch seam (executeJobDetached), which gates it oncheckJobDeviceEligibility→jobRunsOnThisDevice. That function consulted the per-device ROUTINES activation manifest first and short-circuited on its answer — and a monitor's name can never be in that manifest, because nothing undermonitors/ever writes one. Every fire therefore recordedskipReason: "wrong_owner"with the empty-allowlist messageJob '<name>' can only run on:. Measured on one box at 1.22.39: 5 of 5 fires skipped, zero successful action runs ever. The synthesized job now carries an explicitdispatchedBy: 'monitor'marker andjobRunsOnThisDeviceskips the routine activation manifest for it — a monitor already resolves exactly-once ownership through its owndevice:pin (monitorRunsOnThisDevice) before dispatching, so re-gating on the routines manifest was double-gating on the wrong key. The exemption is deliberately narrow: a monitor'sroutineaction fires a real routine, which keeps its activation gate, so a routine that is defined but not activated on this device is still refused withwrong_owner. Monitor names are NOT registered into the device routines manifest — that would conflate two ownership models and polluteagents routines.A second gate sat directly behind it and would have kept every
runaction inert on its own: with the manifest no longer swallowing the job first, the run wasblockedwithexecution_context_missing, becauseresolveJobExecutionContextrefuses an agent job carrying neitherprojectnorcwd(lib/routine-context.ts) andMonitorConfighad no field able to supply one. Monitors now take an optionalcwd(agents monitors add … --cwd <path>, home-relative or~/…) and the synthesized job defaults it to the execution target's home, which stays portable across arunOn:SSH hop. Verified end to end on one box: the same monitor firesskipped/skipReason: "wrong_owner"/ no output on installed 1.22.39, andcompletedwith its agent's captured output on the fixed build. Source:apps/cli/src/lib/monitors/dispatch.ts,apps/cli/src/lib/monitors/config.ts,apps/cli/src/commands/monitors.ts,apps/cli/src/lib/routines.ts.dispatchedByis runtime-only and closed at both ends of the schema boundary:writeJobstrips it andreadJobFileResultrefuses a routine definition that carries it (inert-and-loud, like the existingdevice:/devices:guards), so a hand-authored YAML cannot use the marker to fire a routine on every box regardless of activation — the daemon's load path never callsvalidateJob.agents sessions preview <id>(and the id behindresume/focus) no longer says "No session matching" for a session that is actually running (RUSH-2682). Transcript indexing was lazy — onlydiscoverSessionswrote the index and nothing scheduled it — so a session THIS box just started was listed as running byagents sessions --activewhileagents sessions preview <id>answered "No session matching" until an unrelatedagents sessions*call happened to scan. Measured on zion (agents-cli 1.22.39): a locally-started session took 7.6 minutes to enter the index, while peer sessions arriving via sync landed in ~0s — a box indexed other machines faster than itself. Three fixes: (1) the id resolver now unions the indexed rows with the live-session registry (the same source--activereads) on a cold id miss, so a running session resolves and renders even with no transcript row yet — locally and, because the fan-out peer answers from the same union, cross-device; (2) the daemon incrementally scans this host's transcript dirs into the local index every 20s, so a locally-started session is discoverable within seconds instead of on the next unrelated invocation; (3) the cold-miss repair now waits (bounded) for a concurrent scan to finish instead of returning the pre-scan snapshot as if it were the answer. Source:apps/cli/src/lib/session/live-metadata.ts,apps/cli/src/commands/sessions.ts(computeLocalMetadataMatches,liveMetadataMatches),apps/cli/src/lib/session/discover.ts(waitForScanToSettle),apps/cli/src/lib/session/db.ts(scanInProgressByLivePid),apps/cli/src/lib/daemon-ticks.ts(runSessionIndexWarmTick),apps/cli/src/lib/daemon.ts.agents artifacts sharenow carries provenance and a title, andagents artifacts share listis a real "what have I shared" gallery, not just slugs (RUSH-2683). Every publish auto-capturesagent(AGENTS_AGENT_NAME),session(AGENTS_SESSION_ID/AGENT_SESSION_ID),host(os.hostname()),repo(the current git repo), anddatefrom the exec env/git/clock — never invented, sent only when the environment genuinely carries it.--label <text>(alias--title) sets a human display title shown in the gallery andshare list; omit it and one is derived from the HTML<title>, a Markdown frontmattertitle:, or the filename, with a one-line nudge toward--labelin the human output — never a blocking prompt.--meta key=value(repeatable) attaches structured metadata (recommended keys:kind,project,ticket,status); reserved keys (agent/session/host/repo/date/label/label-source) are rejected client-side and enforced again in the Worker.agents artifacts share listgains--agent/--session/--label-containsfilters (named to avoid a real, pre-existing option-name collision with the parentsharecommand — see below) and returnslabel/agent/session/host/repo/revisionCount/metaper item (every--meta key=valuean agent attached is readable again, in both--jsonand the human table — not write-only); the JSON shape is an additive superset of the prior one. Reserved provenance/label keys are now stripped from stored--metaunconditionally in the Worker (not just overwritten when the matching provenance header happens to be present), so a same-named--metaentry can't smuggle through on a publish that carries no agent/session/host/repo/date at all. The pre-publish email/credential scan now also covers--labeland every--metavalue (previously body-only), since both land in the same publiccustomMetadataas the page itself — gated by the same--force. An explicit--label/derived title with an embedded newline is sanitized to a single line before use, instead of crashing the publish on an invalid header value. Republishing an existing slug now keeps the prior version — R2 has no native object versioning, so the Worker copies the current object to<slug>/rev-<ts>-<rand>before overwriting the canonical key (default keep-all;--no-revisionskips it). The newagents artifacts share revisions <target>command lists a slug's retained history, newest first, via--for-user/--revisions-json(see below for why not--github-user/--json); revisions never appear on the public gallery or inshare listbeyond arevisionCount, and each keeps its own recorded expiry. The Worker template changed (customMetadata capture, revision-on-overwrite, the?revisions=jsonroute, gallery/listing display) — existing endpoints needagents artifacts share updateto adopt it (agents artifacts share statusreports when one is due). Verified against the live productionshare.agents-cli.shendpoint: publish, list with filters, and revisions all round-trip real provenance. Also discovered, and fixed where introduced here: commander resolves a long option name against the whole ancestor chain, not per-command, so a subcommand option sharing a name with the parentsharecommand's own option is silently dropped even when passed alone (verified with an isolated commander repro). This is pre-existing onshare list/update/delete(predates this change) and out of scope to fix CLI-wide here — a real generalized fix needsenablePositionalOptions()audited across 552 commands, tracked as RUSH-2687;list's new filters were named to sidestep it too (--label-containsinstead of--label), though its own--json/--github-userremain affected.share revisionsis new in this change, so it was given non-colliding names from the start instead of shipping a fourth broken instance:--for-user(not--github-user) and--revisions-json(not--json).agents artifacts share delete/agents unsharenow also delete a target's retained revisions by default (--keep-revisionsto leave them) — a republished-then-deleted share previously left its prior world-readable version(s) live until the bucket's 366-day lifecycle sweep; fetching the revisions list to purge is best-effort and never blocks the primary page delete. Known limitation, tracked as RUSH-2701: the revision copy-then-overwrite isn't atomic, so two genuinely concurrent publishes to the same slug can race and silently drop the losing writer's content (no worse than pre-revisions last-write-wins, but it breaks the "keep-all" guarantee for that narrow case). Source:apps/cli/src/lib/share/publish.ts,apps/cli/src/lib/share/worker-template.ts,apps/cli/src/lib/share/delete.ts,apps/cli/src/commands/share.ts.The menu bar no longer groups a session under its harness name or a machine name, and its header now shows the installed CLI version (RUSH-2688). The ACTIVE dropdown grouped a Codex cloud task under a project literally named
codex: a queued Codex Cloud task carries no local cwd and no repo, and the menu's cold-start cloud reader keyed the grouprepo ?? provider ?? "cloud", so a repo-less task fell through to the provider — the harness name — as its "project" (a repo-less task on zion:repo=NULL, provider='codex', prompt='Read README.md…'). The one project-key derivation is now explicit and shared: a working dir → its repo (worktree-aware); a row with no local cwd → its own repo when the provider names one (phnx-labs/agents-cli→agents-cli, grouping cloud work with the matching local repo), else the single explicitcloudbucket — never the harness or a machine name, no fallback chain (activeSessionProjectKeyinsrc/commands/sessions.ts,LocalState.groupKeyin the helper). Separately, the dropdown header hardcodedagents-cli; it now readsagents-cli <version>from the snapshot'scliVersion(getCliVersion(), the valueagents --versionprints), resolved at runtime so a menu-bar helper left running across anagentsupgrade shows the stale version at a glance. Source:apps/cli/src/commands/sessions.ts,apps/cli/src/lib/menubar/snapshot.ts,apps/cli/menubar/Sources/MenubarHelper/{LocalState,StatusItemController,Models,ActiveSessionSelfTest}.swift.agents monitors runs/viewnow show a fire's REAL outcome, reconciled against the run's current status, instead of the frozenokwritten at fire time (RUSH-2690).dispatchAction(lib/monitors/dispatch.ts) only checks a synchronous snapshot:executeJobDetachedwritesstatus: 'running'before spawning and returns immediately (lib/runner.ts'sexecuteJobDetachedClaimed), while the real outcome —completed,failed,timeout, or a process that exits with no captured output — lands later, asynchronously, in that function's ownsettle()on child exit/error. RUSH-2500/RUSH-2681 fixed the SYNCHRONOUS skip/blocked/wrong-owner cases; this closes the async gap. Arunaction's fire record was persisted withok: trueoff that transientrunningstate and nothing ever revisited it, soagents monitors runsshowed a healthy fire history forever whileagents monitors logs(which reads the run record fresh) already showedskipped/failed— the exact divergence the ticket reproduced (fireokat T, runskipped3ms later).resolveFireOutcome(lib/monitors/state.ts) re-reads the run's real, current status byrunIdat render time and correctsokagainst it;agents monitors runsandagents monitors view's Recent fires now call it instead of trusting the storedok, andrunsalso surfaces the corrected status inline (ok (run failed)) when it diverges from the frozen write. The engine additionally stamps a best-effortrunStatusAtFireon the fire record (schema-compatible, additive) so a future daemon-tick reconciliation pass has a cheap signal for exactly which historical fires were frozen mid-flight. Source:apps/cli/src/lib/monitors/state.ts,apps/cli/src/lib/monitors/engine.ts,apps/cli/src/commands/monitors.ts.agents sessions preview <id>can no longer render a DIFFERENT session's transcript (RUSH-2691). For every harness except Claude, the live-row transcript lookup took a session id and threw it away: it answeredWHERE agent = ? AND cwd = ? ORDER BY last_activity DESC LIMIT 1, i.e. the newest transcript in that cwd. With two same-harness agents in one directory — measured on this fleet: two codex sessions in one checkout, ids01a00504-8ac6-…and01a00504-8bd8-…— both rows resolved to a third session's file. Before 1.22.39 that only mis-set a status badge; once the live row began backingpreview/resume/focusit started showing users another session's content, and the picker cached that body against the wrong id. A known id now selects its own transcript, and an id the index has not reached yet renders the honest "not indexed here" card instead of a neighbour's. The id-less fallback (a single session in a cwd) is unchanged. Source:apps/cli/src/lib/session/active.ts.The daemon's session-index tick now reports what it actually indexed, and stops paying for a listing query it never used (RUSH-2691). The tick scanned correctly — that part of the warm index worked — but it called
discoverSessions()with no options, so the number it reported came from that function's trailing listing query, whose cwd filter defaults toprocess.cwd()(the daemon's,$HOME) and caps at 50. The count therefore meant "sessions whose cwd is exactly$HOME, max 50":0on any normal box, every 20 seconds, regardless of how much the scan had indexed, with nothing logged either way. The query was also not free — it ran anarchived_at-writing existence check and a Linear fetch on every tick. The tick now calls a newscanSessionsIncremental(the scan half ofdiscoverSessions, split out so both share one implementation), reports transcripts actually parsed, and skips the listing query. The daemon logs when a tick indexes something or yields the scan claim, so a tick that goes quiet is visible rather than silent.scanOpenCodeIncrementalandscanOpenClawIncrementalgained the progress callback the other eleven scanners already had, so the count covers every harness. Source:apps/cli/src/lib/daemon-ticks.ts,apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/daemon.ts.Deleted eight unreachable resource handlers (RUSH-2695).
apps/cli/src/lib/resources/held tenResourceHandlerimplementations, and eight of them — commands, hooks, mcp, memory, permissions, rules, skills, subagents — had no consumer anywhere outside their own test files. This was not harmless dead weight: RUSH-2677 was filed againstMcpHandler.syncas the site of a silent MCP no-op, pointing the fix at a code path that never runs, and the real defect turned out to be ininstallMcpServers. Eight dead handlers is eight ways to repeat that.resources/workflows.tsstays — it is genuinely live (lib/resources.ts:16,:530) — as doesresources/types.ts, which it imports. Notelib/resources.ts(singular,resolveResource/listResources) is a different, live module; conflating it with thelib/resources/directory is what made this look like a whole-layer deletion at first. Refactor only, no behavior change. Source:apps/cli/src/lib/resources/.agents sync --yesand the--device allfan-out no longer report a refused write as a clean sync (RUSH-2700). RUSH-2677 made a declined resource visible, but only on the interactive and per-agent surfaces:agents sync <agent>@all --jsonand the umbrella payload still emitted a hardcodedok: trueand omitteddeclinedentirely, andrefresh()returnedvoidso the umbrella never saw a decline in the first place. Since the fleet fan-out injects--jsonon each peer, a harness whose MCP config format is unimplemented (copilot, amp, kiro, goose) reported success fleet-wide while writing nothing — the exact silent-success class RUSH-2677 set out to remove, surviving on the surface machines read.refresh()now returns the declines it collected and prints aNot written:block on the human path; the umbrella carries them; both JSON emitters deriveokfrom whether anything was refused; and the fan-out roster rendersN not writtenfor a peer that declined, instead of a flat greenok. Audited everyemitJsoncall site incommands/sync.tsrather than only the two that were reported: three payload shapes can carry a decline and all three now do — the rest are failure payloads,nothing to sync, dry runs,repo-git, orlaunch, none of which run a resource sync (runLaunchSyncreturns aLaunchSyncResultand never callssyncResourcesToVersion). Documented indocs/resource-sync.mdalongside theprunedprecedent. Source:apps/cli/src/lib/refresh.ts,apps/cli/src/lib/sync-umbrella.ts,apps/cli/src/commands/sync.ts,apps/cli/src/lib/hosts/passthrough.ts.agents permissions add ~/.config/opencode/opencode.jsonnow imports instead of printing "No permission sets found" (RUSH-2702). Harness detection built its match suffixes by calling each registry target'shome(''), and OpenCode's entry probes the filesystem to choose between the two spellings OpenCode accepts (opencode.jsonc/opencode.json). With an empty root that probe resolved againstprocess.cwd(), so the same file detected differently depending on where the CLI happened to be run. Detection now reads an explicitaltSuffixeslist covering both spellings OpenCode accepts, so the probe can no longer change which harness a path resolves to (home('')/project('')are still consulted, but every spelling they could return is already an explicit candidate); resolving a real root still probes, which is correct there. Regression-tested for each spelling from a cwd carrying decoy configs and from a bare one — the decoy cwd is what reproduces the bug, which is why single-cwd coverage passed before.The capability matrix in
docs/concepts.mdsaid Goose supports permissions. RUSH-2676 removed that support and updatedAGENTS.mdanddocs/resource-sync.md, but missedconcepts.md— in the very table whose header namessrc/lib/agents.tsas its source of truth. Both the row and the prose list of allowlist-gated agents now match the code.New browser profiles are machine-local by default, and
agents browser profiles prunecleans up the dead ones (RUSH-2716). A profile pins an OS-specificbinary:path and a locally chosen CDP port, so the fleet-synced copy was already wrong on every other box — and because agents mint throwaway profiles freely, every ad-hoc one was landing in the sharedagents.yamland syncing to the whole fleet as junk that nothing removed.agents browser profiles create <name>now writes to this machine's owndevices/<machine>/agents.yaml; pass--fleetfor a profile that really is fleet config (a remotessh://endpoint, or a shape you want on every box). Existing profiles are not migrated — this only decides where a NEW entry is written, so a fleet profile created before this change keeps syncing. The auto-detecteddefaultstays machine-local regardless, as it has since 1.22.38.agents browser profiles pruneremoves local profiles that are dead — their browser is not installed here, or they have never been started — with--dry-runto preview,--fleetto opt into the synced ones (removing one removes it from EVERY machine), and--json. It never removes a profile that is in use (live browser, SSH tunnel, or open task on any of its runtime dirs, composites included), this machine's configured default, or the autodefault. Source:apps/cli/src/lib/browser/profiles.ts,apps/cli/src/lib/browser/runtime-state.ts,apps/cli/src/commands/browser.ts.agents browser profiles listno longer breaks its own columns, anddefaultmeans one thing again (RUSH-2710). The name column was a barepadEnd(20), which returns a longer name unchanged rather than bounding it, so any profile named past 20 characters shifted every later column on its row out of alignment. Columns are now sized to the content, capped at 28 characters, and truncated with an ellipsis — via onepadColumnhelper every column goes through, never a barepadEnd. Separately, the listing used the word "default" for two different things with no way to tell them apart: the profile literally NAMEDdefault(the auto-detected one) and whichever profile this machine resolves a bareagents browser startto (agents config set browser.profile <name>), which are frequently not the same profile. Only the second is a marker now — a*in a leading column with a legend naming it — sodefaultin the name column always means the profile of that name. A newSCOPEcolumn says whether each entry islocal(this machine) orfleet(synced), and--jsoncarriesscopeplusisConfiguredDefault. Source:apps/cli/src/lib/browser/profiles.ts(formatProfilesTable,padColumn),apps/cli/src/commands/browser.ts.
Added
agents routines add/editaccept the same launch-target vocabulary asagents run(RUSH-2719):--agent <agent[@version]>splits into persisted bareagent+ exactversionfields (previously the compound string failed validation withagent must be one of: ...);--strategy pinned|available|balanced(and--balanced) persists a per-routine selection policy that overrides the firing device'srun.<agent>.strategy;--run-on autoplaces the job body on a healthy, signed-in, unloaded fleet device re-picked at each fire via the same picker asagents run --device auto.
Removed
geminiis no longer a routine target: dropped fromROUTINE_AGENT_COMMANDS, soroutines add --helpstops advertising a hard-deprecated harness the add-gate refuses anyway. A legacy on-disk gemini routine still lands a visibleblockedrun record with the deprecation message (the RUSH-2202 gate now keys on deprecation alone, not command-table membership).
Fixed
A hard-deprecated harness pinned with
@versionno longer slips past theroutines adddeprecation gate (the check ranresolveAgentNameon the raw compound string and silently matched nothing).A routine pinned to a version that is not installed locally now saves paused with
pinned <agent>@<version> is not installed on the targetand anagents addrepair, instead of activating and failing at fire time.Branding: fix the remaining agi-cli hero + marketing-domain inconsistencies the first rebrand pass missed. Root
README.mdhero (<h1>/logoalt) now reads "agi-cli" instead of "agents", and the marketing-domain references inREADME.md,DESIGN.md,demo/src/AgentsDemo.tsx, andapps/cli/docs/README.mdnow point atagi-cli.sh(the live marketing domain) instead of the retiredagents-cli.sh. This is docs/branding only — no runtime behavior change. Not touched: theshare.agents-cli.shlinks (that subdomain is not live onagi-cli.sh), the npm package name@phnx-labs/agents-cliand its install lines, and historicalCHANGELOG.md/.changelog/entries. Source:README.md,DESIGN.md,demo/src/AgentsDemo.tsx,apps/cli/docs/README.md.agents browserresolves the task from the caller's identity — no moreNo task specified/export AGENTS_BROWSER_TASK=…in the common path.--taskand$AGENTS_BROWSER_TASKstill win when set; otherwise the CLI stampssessionId/launchIdonce insidesendIPCRequest(from the session-tracker state file at the harness pid, else the process-table anchor viaagentKindFromComm, stamped aslaunchIdso the reaper never treats it as a dead session) and the daemon picks the single live task for that caller. Multiple tasks list label/id/url/age and name--task; page verbs create a task when none resolve; observation verbs anddone/stopnever create. Tasks get a humanlabel(--title, else first navigated host, elseuntitled) while the short machine id remains the address. After a daemon restart, live tasks rehydrate fromtasks.jsonvia the profile endpoint. Version reconciliation restarts the daemon forward only so an older CLI rides a newer daemon.status --profile <bare>matches compositename@endpointkeys. Grammar:navigate [url](aliasgoto),evaluate [expr](aliaseval),logs --task. Errors name a next command. Source:apps/cli/src/commands/browser.ts,apps/cli/src/lib/browser/{service,ipc,types,caller-identity}.ts.CLI surface consolidation — nest or remove overlapping top-level commands.
- Synced vault unlock/lock moves to
agents secrets vault unlock|lock(replaces top-levellogin/logout). Harness-native OAuth sign-out isagents accounts logout <harness>(API-key accounts keepaccounts remove). - Spend caps live under
agents config budget(was top-levelbudget). - Cost and shipped-output rollups nest under
agents insights cost/agents insights output. - White-label manage verbs nest under
agents setup mine(init/list/toggle/remove); top-levelmineis gone. - Fleet poll snapshot moves to
agents devices snapshot(not config). - Matrix runs use
agents run --broadcast(with--list-tasks/--results/--task); top-levelbenchis removed. - Top-level
agents profilesis removed — useagents harness(same~/.agents/profiles/*.yml). - Top-level
agents cpis removed — use plainscp(orscp -3for host-to-host); no fleet broadcast advantage. - Top-level
agents resumefolds intoagents sessions resume(strict id/prompt path + multi-select picker). - Top-level
agents rosteris removed — useagents sessions --active. Source:apps/cli/src/commands/{secrets-vault,accounts,budget,config,cost,output,insights,mine,setup-mine,snapshot,ssh,exec,run-broadcast,harness,profiles,resume,sessions-resume}.ts,apps/cli/src/lib/startup/command-registry.ts.
- Synced vault unlock/lock moves to
Device config moves to per-device files; agent pins leave the tracked tree. The
agents devices config <name>store is now three layers, read in order — built-in default < fleet default < per-device value: per-device settings land in the TRACKED~/.agents/devices/<name>/agents.yamlconfig:block (conflict-free by construction — each machine writes only its own folder), and a new--fleetflag (agents devices config --fleet <key> <value>) writes fleet-wide defaults to centralfleet.defaults.config. This supersedes #2458's centralfleet.devices.<name>.configblock, which a one-time migration folds into the per-device docs (central wins); the same migration extractsagents:/isolatedAgents:pins from the tracked docs into the untracked~/.agents/.history/devices/pins-<host>.json— the root-cause fix for the commit churn that keptdevices/gitignored — and folds the legacyauto-launch.jsonin too. Reads report effective values;--jsongains a per-keysource(device|fleet|default); the TTY menu shows effective values and edits the device layer. Shims resolve default pins from the pins JSON first (device-doc and central fallbacks kept for unmigrated installs);agents fleet capture --from-pinsnow records only this machine's pins (peer pins no longer sync). AGI EXT reads the same per-device docs layered overfleet.defaults.config. Source:apps/cli/src/lib/device-config.ts,apps/cli/src/lib/state.ts,apps/cli/src/lib/devices/config-migration.ts,apps/cli/src/lib/shims.ts,apps/ext/src/core/deviceAutoLaunch.ts.A project renamed in Linear kept its old label in
agents projectsforever —linear.namewas written once and never refreshed.agents projects linkknew the current name (it printed it in the success line) but wrote only the id:def.linear = { ...def.linear, projectId: p.id }spread the prior block, preserving the stalename.projects import --from-lineardid not write the field at all. Nothing in the tree ever set it, so every recorded label dated from some older version. Measured on a real registry after a workspace consolidated six Linear projects into three:agents-cli.yamlreadname: Agents CLIfor a project the board calls AGI,rush-app.yamlreadRush Appfor Rush, and three defs still pointed at project ids Linear had deleted. The label is not inert —apps/ext/src/core/managedProjects.tssurfaces it aslinearProjectNamein the AGI EXT Fleet panel, and agents read it when naming the work, so both reported a project name that no longer existed.linkandimportnow writenamefrom the live Linear row every run,linkreports the replacement (renaming "Agents CLI" → "AGI" (Linear is authoritative)) and drops aurlbelonging to the project it just unlinked, and theprojects statuscard leads with the name instead of the bare uuid (linear AGI 8eb8f5b1-…). Re-runagents projects link <name> --linear "<project>"to repair an existing def. Source:apps/cli/src/commands/projects.ts,apps/cli/src/lib/project-import.ts.One canonical
formatBytes, and byte sizes now read the same everywhere. Five copies had drifted into three different renderings of the same number. Four of the five call sites change output at some boundary:agents pruneshowed1.50 GBwhere every other surface showed1.5 GB; session artifact sizes capped atMB, so a 1.4 GiB transcript rendered as1433.6 MB; andagents shareplus the browser artifact list capped atGB, so a 2.3 TiB entry rendered as2355 GBand now reads2.3 TB. Onlyagents inspectis byte-identical to before — its implementation is the one all five now share. Source:apps/cli/src/lib/format.ts,apps/cli/src/commands/{prune,share,inspect,sessions}.ts,apps/cli/src/lib/browser/sessions-list.ts.Capability version gates now honor OpenClaw's
-Nrebuild suffix.supports()and the command version gate each carried a privatecompareVersionsthat split on.and dropped the trailing-N, so2026.2.19-2compared equal to2026.2.19. Both now use the canonical comparator inlib/agent-spec/primitives.ts, which treats a higher-Nas newer. No capability currently declares a-Nfloor, so nothing changes today — the gate is simply no longer wrong for the version scheme OpenClaw actually ships. Source:apps/cli/src/lib/capabilities.ts,apps/cli/src/lib/commands.ts.SESSION_AGENTSand the sessions command surface documented accurately. The spec said "exactly these 12" while the code has 13 (musewas missing), and both it andSES-IF-2required asessions syncsubcommand that has never existed. The command list also omitted seven real subcommands (render,bookmark,stats,insights,optimize,watch,backfill resources) while still listing verbs that655b22512retired behindresume. Source:apps/cli/docs/specifications.md,apps/cli/docs/architecture.md.The sessions picker now shows a real preview for a peer's session, not just its metadata. Arrow onto a row another device owns in
agents sessions(the fleet browser or the query picker) and the pane fetches that peer's already-computed preview digest over SSH — prompt, checklist, dirs, changes, skills, errors, tests, and last response, the same card a local row gets — repainting in place when it lands (a "fetching preview fromover SSH…" note shows meanwhile, and an unreachable peer degrades to the old metadata card without re-dialing on every keystroke). Peer-supplied digest strings are scrubbed of terminal escapes before they reach the TTY, and a version-skewed peer that can't answer the digest envelope falls back cleanly. Source: apps/cli/src/commands/sessions-picker.ts(sanitizeRemoteDigest),apps/cli/src/lib/session/remote-list.ts(fetchPeerPreviewDigest),apps/cli/src/lib/picker.ts(registerPreviewRepaint).Resource pickers number their rows. The interactive lists behind
agents inspect <repo> --commands(and the other resource views built on the same picker — plugins, skills, hooks) prefix every row with its position in the filtered list (1.…12.), right-aligned so labels stay columnar. Where the caller prints a total —agents inspectshowscommands (12)above the picker — the row numbers read against it. Filtering renumbers from 1 over the matches. Source:apps/cli/src/lib/picker.ts(PickerConfig.numbered),apps/cli/src/commands/resource-view.ts.
Breaking
Fleet routing:
--device/-Donly. The-H/--hostrouting flag is removed from every command that used it for remote dispatch. Use--device <name>(or-D). Scripts using--hostfor fleet routing must switch. Legacy--hostis still stripped from forwarded remote argv for mixed-version fleets, but is no longer registered or documented as a user flag. Unchanged:agents hostsnoun, Docker-H/--host, webhook bind host, harness model--host.Unify native logins and provider credentials under one account model (RUSH-2527).
agents accounts name <agent@version> <name>gives a durable name to a harness's own signed-in login — metadata only inmeta.accounts.native(a stable id + identity key + scope), never the harness's OAuth/session credential, which stays in the harness home. Native and provider accounts now share one name namespace and oneaccounts/accounts viewrenderer (text +--json). New positional grammar:accounts name <source> <name>,accounts attach <account> <target>/detach <account> <target>, andaccounts sync <account> <device>. Only version-scoped nameable harnesses (Claude/Codex/Grok, plus Muse when a live email is present) can be named or attached, always to an exactagent@version. Device-scoped harnesses (Cursor/OpenCode/Antigravity/Kimi/Droid) are unsupported for native naming — their API-key path is a provider account.attachvalidates the live identity before binding and injects no secret or env;resolveAccountSelectionresolves explicit → exact-target binding → device-scoped binding → per-harness default.removerefuses while a binding, a default, or a harness profile still references the account. Bindings are honored end-to-end:agents runand routines select the bound account at spawn — a provider account injects its env, a native account is validated live and pins the installed version that holds it (never forwarded/injected, fails closed for a remote/cloud target or a cross-harness login) — andagents viewplus the fleet/harness inventories render the durable account name. Source:apps/cli/src/lib/account-registry.ts,apps/cli/src/lib/account-capabilities.ts,apps/cli/src/commands/accounts.ts,apps/cli/src/commands/exec.ts,apps/cli/src/lib/runner.ts,apps/cli/src/commands/view.ts,apps/cli/src/lib/devices/{fleet,harness}-inventory.ts,apps/cli/src/lib/types.ts.agents cloud run(Rush) never reads or uploads a native Claude OAuth login (RUSH-2527, SING-1b, breaking). Two changes: (1) the account manifest sent on every non-balanced dispatch no longer includes acred_fphash computed by reading each Claude version's OAuth token — it carries version + account email only; (2) the token-upload retry path is removed entirely — the--upload-account-tokensflag, theAGENTS_RUSH_UPLOAD_TOKENSenv var, the recorded consent file, andbuildAccountTokensPayloadare gone. There is no consented way to copy a rotating harness login to the cloud (it would be invalidated on its next refresh and log the fleet out). When Rush Cloud asks for a token (a new account or a rotation), dispatch now fails loud and steers to a portable provider account:agents accounts add <name> --provider anthropic --auth api-key(orsetup-token), then dispatch under that account. Source:apps/cli/src/lib/cloud/rush.ts,apps/cli/src/commands/cloud.ts.agents run --host --copy-credsandagents run --leaseno longer copy a native OAuth / session login to another device (RUSH-2527, breaking for those flags). Both used to serialize each signed-in runtime's rotating login — the Claude OAuth token and codex/grok/geminiauth.jsonfiles — onto a persistent host (--copy-creds) or an ephemeral leased box (--lease) so it booted logged-in. A rotating harness login copied across machines is invalidated on its next server-side token refresh and logs the rest of the fleet out, and the fleet-auth contract forbids it on every device, ephemeral or not (docs/specifications.mdSING-1b). Both now fail loud when asked to copy a signed-in native runtime and steer to the portable, non-rotating path: create a provider account (agents accounts add) and push it withagents accounts sync <name> --device <host>(a policy-neverbundle, safe to reuse on many devices). A profile-dispatch--leaserun carrying its own portable auth (a BYOK gateway) still works; only the native-login copy is refused. Explicitagents accounts syncandsecrets export --hostare unchanged. Sources:apps/cli/src/lib/hosts/credentials.ts,apps/cli/src/lib/crabbox/runtimes.ts(buildCredentialScript); shared predicateisNativeOAuthRuntime.agents apply/agents fleet applyno longer propagates a harness login between devices (RUSH-2527, SING-1b).login: syncused to copy each agent's portable login file (Claude.credentials.json, codex/grok/opencode/kimi/ antigravityauth.json) from the source box to every target — a rotating token that a single refresh then invalidates fleet-wide.applynow emits nopush-loginaction and captures no credential (snapshotAuthreads nothing); everylogin: syncagent that needs a login is surfaced as needs-login with the honest reason and the portable alternative — log in on the box itself, oragents accounts sync <name> --device <host>a policy-neverprovider account. The internalapply --recv-authreceive path is gone. Sources:apps/cli/src/lib/fleet/auth-sync.ts(isCredentialSafeToPropagate),apps/cli/src/lib/fleet/apply.ts,apps/cli/src/commands/apply.ts.$HOME is never compiled as a "project" ruleset anymore, ending the double injection of the entire ruleset into every session (RUSH-2725).
compileRulesForProjecttreated any cwd with.agents/rulesas a project — and the user layer's own home at~/.agentssatisfies that test — so it wrote a compiled~/AGENTS.mdplus per-agent symlinks (~/CLAUDE.md,~/GEMINI.md, …), and every session whose cwd sat under the home directory then loaded the whole ruleset twice: once as global memory from the version home, once as "project" memory from~/. Measured on one machine: ~145 KB / ~36,000 duplicated tokens per session, on every agent, on every box — and the two copies also diverged, so an agent resolving the stale project copy silently missed the newest rules. The compiler now consults the same reserved-root predicategetProjectAgentsDiralready used (isReservedAgentsDirinstate.ts: the user layer, the system layer, or a git checkout of either canonical DotAgents repo never compile as a project), and a one-shot migration deletes the compiled~/AGENTS.md, its per-agent symlinks, and header-carrying copy fallbacks already written on each machine — hand-authored files without the compiled header are untouched. Source:apps/cli/src/lib/rules/compile.ts,apps/cli/src/lib/state.ts,apps/cli/src/lib/installations/migrate.ts(removeHomeCompiledProjectRules).
1.22.39
The auto-detected
defaultbrowser profile no longer sits in the fleet-sharedagents.yaml, which was wedgingagents repos pull userfleet-wide (RUSH-2161).browseris a central key because named profiles a user creates are real fleet config, but the onedefaultentry inside it is machine-local: itsbinaryis an OS-specific path and its endpoint is a locally chosen free port.createProfile/updateProfilehave routed that entry to the per-device file since 1.22.38 (isMachineLocalProfile), but nothing removed the copy older versions had already written into the shared file, andserializeCentralcould not — it deletes whole device-scoped KEYS, andbrowseris not one. So the stale entry stayed committed and every box rewrote it with its own browser. Measured across three boxes all running 1.22.38: zionchrome+/Applications/Google Chrome.app/..., yosemite-s1brave+/opt/brave.com/brave/brave, mark-1 the same shape, each with an emptydeviceBrowser. Because the pull refuses when an incoming change touches a locally modified path, a permanently dirtyagents.yamlblocked fleet config sync outright — those boxes sat 5, 8 and 79 commits behind, so merged config (anotify.ownerblock, device roles) reached zero machines. A new migration moves the entry intodevices/<machine>/agents.yamland deletes it from central, writing the device file first so a crash cannot lose the profile and keeping an existing device entry when one is already there. Central is edited through a YAMLDocument, so the hand-written comments in the committed file survive rather than being flattened by a re-stringify — which would have re-created the same churn. Named profiles are untouched. Source:apps/cli/src/lib/migrate.ts,apps/cli/src/bootstrap.ts.agents config set devices.<name>.tmux offturns the interactive tmux wrap off for one machine (RUSH-2620). Interactiveagents runwraps the harness in the shared-socket tmux session so every agent gets an addressable%pane— that is what letsagents sessions --activetell co-located agents apart andagents focusre-attach without forking. Until now the only ways out were per-run (--raw/--no-tmux/--disable-tmux) or theAGENTS_NO_TMUX=1env var, so a box whose tmux is broken needed the flag retyped on every launch. The new key is machine-local by design (a broken tmux is a property of one machine): it never enters the fleet-sharedagents.yamland is refused for a peer. Unset still means wrap.agents devices config <name> tmux.enabled offsets the same key. Source:apps/cli/src/lib/device-config.ts(isTmuxEnabled),apps/cli/src/lib/exec.ts(shouldWrapInTmux).agents devices role <name> workermarks which boxes agents run on, and--device autofollows it. Roles are fleet-wide (worker/personal), stored in the sharedfleet.devices.<name>.config.roleblock of~/.agents/agents.yaml, so a mark set on any box travels withagents repo push/pull— the per-device files under~/.agents/devices/are written only by the machine they name, so they could never carry a fleet-wide statement about a different box. Marking ANY deviceworkerturns automatic placement into an allowlist:agents run --device auto,agents teams add --device auto, and the AGI EXT launch commands then pick only from the marked workers. A device markedpersonal(a machine you sit at) is never picked automatically, under any mode, and a paired cockpit stays excluded through its existing registrycontrolrole. When roles leave the pool empty,--device autofails loud naming the fix instead of quietly running on the local machine — throughagents ssh autoand the--host autopassthrough too, not justagents run. Nothing marked = today's behavior, every online device. Widen it back withagents config set auto.pool all.agents devices rolewith no arguments prints who is marked what and exactly which devices--device autowould consider;agents devices listtags marked rows and--jsoncarriesroleplus anautoPoolboolean per device. Source:apps/cli/src/lib/devices/pool.ts,apps/cli/src/lib/device-config.ts,apps/cli/src/lib/smart-launch.ts.
1.22.38
Webhook
stateTotriggers and handlers now fire only on the delivery that actually moved a Linear issue INTO that state, not on every later update while it still sits there. Previously astateTo: Planhandler re-matched on any subsequentIssue/update— a label edit, an assignee change, a description touch — because it checked the issue's current state instead of the transition, which accumulated 11 duplicate plan comments on one issue. It now additionally requires the delivery'supdatedFromto record a state change. (RUSH-2539)A new release home base no longer needs the provisioning profile hand-copied over (RUSH-2541).
apps/cli/bin/embedded.provisionprofilebecame a committed, tracked file in commit2567004b4(RUSH-2535), butrelease.sh's home-base seed step andsigning-home-base-probe.sh's preflight both still checked only the home base's own on-disk working tree -- so a legitimately new home base (or one whose local checkout simply predated that commit) still reported "unprovisioned" or died at the sign step with a misleading "generate at developer.apple.com" message, even thoughgit fetch origin(which both already run) had the file the whole time. Both now recover the blob from the freshly fetchedorigin/<default>ref when it is absent on disk, and the seed step fails loud with the correct recovery guidance (recover from git history; do not regenerate at Apple's portal) instead of limping forward on a warning. Source:apps/cli/scripts/release.sh,apps/cli/scripts/signing-home-base-probe.sh,apps/cli/scripts/build-keychain-helper.sh.release.sh's home-base provisionprofile recovery no longer dies silently underset -eon a freshly bootstrapped checkout (RUSH-2541 follow-up). TheDEFAULT_BRANCH="$(git symbolic-ref ...)"assignment added to recoverembedded.provisionprofilefromorigin/<default>ran underhome_base_wt_snippet's ownset -euo pipefail;git symbolic-refreturns non-zero wheneverrefs/remotes/origin/HEADis unset, which is the normal state of a checkout bootstrapped viainit && remote add && fetchrather thanclone-- plausibly a brand-new fleet home base, exactly the box this feature targets. Underset -ethe bare failing assignment tripped errexit at that line, killing the whole home-base phase with zero output before even the "main" fallback on the next line ran. Guarded with|| true, matching the patternassert_signing_home_basealready established for the identical anti-pattern. Source:apps/cli/scripts/release.sh.Top-level
agents setis removed — useagents models set(RUSH-2579).agents set [email protected] --model opus-5moves toagents models set [email protected] --model opus-5, unchanged otherwise: same selector forms (<agent>@<version>,<agent>:*), same--model/--modeflags, same underlyingrun.defaultsstore (agents config get run.<agent@version>.modelstill reads it).agents setnow reportsunknown command. Source:apps/cli/src/commands/models.ts,apps/cli/src/lib/startup/command-registry.ts.BREAKING:
agents sharemoved under a newagents artifactsgroup (RUSH-2580).artifactsis the noun andsharethe action on it, so the surface now reads noun-then-action like the rest of the CLI. The whole subtree moved down one level —agents artifacts share <file>publishes, andagents artifacts share list|delete|analytics|join|status|updateare unchanged under it. The two provisioning doors collapsed into one:agents artifacts setupreplaces bothagents share setup(flag-driven) andagents setup share(the wizard). It runs the wizard only when no endpoint flag is typed on a TTY; type any of--bundle/--worker/--bucket/--account/--token/--domain/--analytics-token, or run non-interactively, and it provisions directly with what you named. The top-levelagents sharegroup and theagents setup sharesubcommand no longer exist;shareis retired from distance-1 auto-correct, so a stale invocation fails loudly instead of running a neighbouring command.agents unshare <targets...>is unchanged and stays top-level. Source:apps/cli/src/commands/artifacts.ts,apps/cli/src/commands/artifacts-setup.ts,apps/cli/src/commands/share.ts,apps/cli/src/commands/setup.ts,apps/cli/src/lib/startup/command-registry.ts.
Fixed
The daemon orphan reaper no longer terminates live agents after a tmux server restart. A missing tmux session is now treated as unknown; helper processes are reaped only when their pane owner is present and confirmed dead, or a harness-specific rule proves their declared spawner exited.
agents browserstops leaving duplicate and orphan tabs behind (RUSH-2622). Theabout:blankthat a bareagents browser startopens is now registered on the task, sodone/stopactually closes it — it never was, which made every bare start leak one tab permanently.agents browser start --urlnow reclaims a tab that an abandoned task is still holding on that exact URL instead of opening a duplicate; a tab held by a live task, or one you opened yourself, is never taken, and the new--freshskips the reclaim entirely. Tasks also carry alastActionAtstamp intasks.jsonnow. Source:apps/cli/src/lib/browser/service.ts,apps/cli/src/lib/browser/types.ts.
1.22.37
agents.yaml's five writers now emit identical bytes, closing the rest of the sync-blocking drift (RUSH-2505). Unpadding flow sequences fixed one half of this; the other half was that the five in-place writers ofagents.yamldisagreed on collection style.state.tsandmanifest.tspasscollectionStyle: 'block', whilefeed.ts,activity.tsand thenotify.ownermigration inmigrate.tsdid not — so an empty map rendered asmcp:with an indented{}from one group andmcp: {}from the other, and the two rewrote each other forever. Measured against the real committed~/.agents/agents.yaml, the block group round-tripped and the other group did not. All five now serialize through onestringifyDoc()helper that pins the committed shape, so a write produces no diff regardless of which command ran.routines.tskeeps its padding-only form: it writes routine YAML, which has no second writer to disagree with. Source:apps/cli/src/lib/yaml-io.ts.agents routines add <file>no longer rewrites the file you hand it. When the source is already the canonical routine YAML — the normal case for a definition tracked in~/.agents— the definition is left byte-for-byte alone instead of being re-serialized with itsdevices:pin stripped. A source from anywhere else is still copied in.A
devices:pin now says it applies to this box only and namesagents routines devices <name> --set, instead of silently reading asDevices: allon every peer.agents routines edit <name> --cwd <path>and--project-anchor <name>apply and save without opening$EDITOR, so the repair the readiness gate prints is one an agent can actually run.A paused routine now names
agents routines resume <name>as the follow-up.Agent helper processes are reaped when the agent exits (RUSH-2521). An interactive agent runs as the leaf of a detached tmux pane, and tearing that pane down only SIGHUPs its foreground process group — so an MCP server or a harness background daemon that had moved out of that group survived its session and kept its memory indefinitely. Measured on the fleet: one pane holding 2.5 GB of Claude Code background daemons 22 days after its session ended, and 34 orphaned
cgraph-mcp --daemonprocesses on one worker.agents sessions reapand the routines daemon's 5-minute sweep now terminate those helpers as well as the dead panes, andkillSessioncollects a session's helpers as it tears the session down. A helper is attributed to its session (tier 1) through theAGENT_TMUX_SESSION_NAMEthe pane exports and every descendant inherits — the one handle that survives reparenting — and is killed only when that session is gone, or has no attached client AND its agent process has exited; a tmux query that never actually answered (missing/unsupported tmux, a spawn failure, a timed-out server) disables tier 1 for that sweep entirely rather than being treated as "no sessions", andagents sessions reap --json'swarningsarray (plus a daemon log line) says when that happens. Tier 1 needs another process's environment, which is a plain/procread on Linux; on macOS, modernps -Eno longer exposes it at all, so tier 1 safely finds nothing there today (known gap, not a regression — only tier 2 reaps on macOS). A harness that detaches its daemons from the environment (Claude Code'sdaemon run) is matched instead (tier 2) on the spawner pid it declares in its own argv, anchored on the process's real executable rather than a substring match anywhere in its command line, and excludes anything still owned by a live/attached pane — checked two independent ways, tmux's own live pane-pid data (expanded to that pane leaf's current process-tree descendants, so it protects a live agent's own subprocesses too, and works even with no env marker readable anywhere in that tree, covering macOS and anyclaudestarted outside an agents-cli pane) and the env marker when one is present — so agrep/cat/pager, or an agent's own prompt (or a subprocess it spawns) quoting this exact pattern, can never become a kill seed. A process that has genuinely reparented away from every live pane leaf remains reapable. Reaped only once the declared spawner pid is dead.agents sessions reap --dry-runlists what would be collected without killing anything. Source:apps/cli/src/lib/tmux/orphan-reap.ts,apps/cli/src/lib/tmux/session.ts,apps/cli/src/commands/sessions-reap.ts.sessions migrate --hoststops leavingremain-on-exiton server-wide, and lands the migrated session where its reaper can see it (RUSH-2521). The remote launch set the tmux server default toonand never put it back, so every later pane on that box — user splits included — retained a dead corpse when its command finished; it now keeps the option on the migrated agent's own pane (which the liveness probe reads) and restores the server default tooff, matchingcreateSession. Separately, the remote session was created on tmux's bare default OS socket instead of the target's agents socket, so the reaper above never found it and killed the just-migrated, still-live agent astmux-session-goneon its next tick — it now targets the same agents socket the reaper queries. Source:apps/cli/src/commands/sessions-migrate.ts.release.shfails fast when the--devicehome base cannot sign, instead of merging + tagging and then dying at publish. The privileged phase (build + sign + notarize + npm publish) runs only at the very end, after the release PR is merged and the tag pushed. A--device <box>fallback that was never provisioned as a signing home base -- the documented "mac-mini is down, use--device zion" path on a box that has a Developer ID cert only in its login keychain, no headless-unlockablerush-signing.keychain-db, and noapple.com/npmjs.comsecrets bundles -- failed there, after both irreversible acts, leaving a tagged-but-unpublished release (RUSH-2535: npm stuck at 1.22.35 withv1.22.36tagged). A new preflight runs a read-only readiness probe (scripts/signing-home-base-probe.sh) ON the resolved home base, over the sameagents sshhop the publish uses, BEFORE the crabbox/PR/merge/tag phases; an unprovisioned box aborts at the preflight naming the exact missing piece. Provisioning a new signing home base remains RUSH-2541. Source:apps/cli/scripts/release.sh,apps/cli/scripts/signing-home-base-probe.sh,apps/cli/scripts/signing-home-base-probe.test.ts.Collapse routine enablement to one enabled/disabled flag; remove
enable-project(RUSH-2540). A routine now has exactly one state — enabled or disabled — owned by this device (meta.deviceRoutines).agents routines enable <name>materialises a project routine (from the current project or a registered one) and turns it on in one step;agents routines disable <name>turns it off. The separate project opt-in (agents routines enable-project/disable-project/projectsand themeta.routines.projectsallowlist) is gone, andagents routines resume/pauseremain as hidden aliases.agents routines listnow shows discoverable project routines from your registered projects as disabled rows. Safety is unchanged: a cloned repo's.agents/routines/*.ymlcan never auto-fire — enablement lives in user-owned local state and a project YAML's ownenabled:is never trusted for firing, andsynconly refreshes definitions, never enablement. Source:apps/cli/src/lib/routines-project.ts,apps/cli/src/commands/routines.ts,apps/cli/src/lib/types.ts,apps/cli/src/lib/state.ts.Project /
--add-dirgrants reach Claude, Codex, Cursor, Kimi, and Grok — not just Claude and Codex. Multi-repo projects already attached sibling checkouts as grants onagents run --project/agents teams --project; only Claude (--add-dir) and Codex (workspace_roots) consumed them, so a Cursor / Kimi / Grok agent on the same project saw the primary cwd alone. Now: Cursor and Kimi take the native--add-dirflag (same as Claude); Grok always gets a short--rulesnote naming the siblings, and when a non-off OS sandbox is active (GROK_SANDBOX/--sandbox) writes a project-local.grok/sandbox.tomlprofile (agents-project) with those paths asread_writeand selects it. Codex is unchanged. Harnesses with no multi-root surface (OpenCode, Droid, …) still ignore the grants — that is a harness limitation, stated in the capability map (lib/add-dir.ts) rather than papered over. Source:apps/cli/src/lib/add-dir.ts,apps/cli/src/lib/exec.ts,apps/cli/docs/projects.md,apps/cli/docs/teams.md.Writing
agents.yamlno longer pads flow sequences, so~/.agentskeeps syncing fleet-wide. The YAML emitter defaults to padded flow output ([ a, b ]), but the committedagents.yamluses[a, b]. Installing the feed / activity-log hooks (ensureFeedPublishHook,ensureActivityLogHook— run back-to-back onagents feed) re-serialized the whole document, flipping any committed flow node — e.g. a notify hook'scommand: [agents, notify, "{message}"]— to the padded form. That left the git-backed~/.agentsworking tree permanently dirty on that one file, soagents repo pullrefused and seven fleet boxes silently fell 37-52 commits behind, receiving no new project definitions, routines, or rules (RUSH-2505). All the writers that round-trip committedagents.yaml/routine YAML now passflowCollectionPadding: false, matching the committed formatting exactly so a write produces no diff:feed.ts,activity.ts, thenotify.ownermigration inmigrate.ts, and the routine-definition writerserializeJob(same synced repo).The generated HTML command reference is now browsable, not search-only.
docs/command-reference.htmlgains a sticky sidebar tree covering all 104 command groups and every subcommand — collapsed by default, one entry per card, so the surface can be explored without knowing what to type. Search filters the tree alongside the cards and auto-expands the groups holding matches; clicking a group scrolls to it, expands it, and highlights it, and the active entry follows as you scroll. The rootagentscard is anchorable at#agentsinstead of shipping an emptyid="". Still one self-contained file with no external assets. Source:apps/cli/scripts/gen-command-index.ts,apps/cli/scripts/gen-command-index.test.ts.agents daemon servicesexposes every hosted responsibility as an independent, toggleable service. The daemon previously ran all of its hosted subsystems together — secrets broker, browser IPC, scheduler, monitors, watchdog, device probe, self-heal, keychain reap, account-state refresh, and state-dir checks — with only coarsedaemon.enabledandscheduler.enabledkill switches. Now each responsibility is listed underagents daemon services, andagents daemon services enable|disable <id>persists per-service state in~/.agents/daemon/services.yaml. Missing keys default to enabled, so upgrades are no-ops. The running daemon reads the catalog at startup and gates each timer or hosted process by its toggle; a few services (scheduler, monitor engine) also re-evaluate onSIGHUPreload. Source:apps/cli/src/lib/daemon-services.ts,apps/cli/src/commands/daemon.ts,apps/cli/src/lib/daemon.ts.A disabled secrets broker fails loud instead of falling back to per-request Touch ID. When
secrets-brokeris disabled,agents secrets unlock/status/startreport the disabled state up front, and any keychain-backed bundle read throws a clear error naming theagents daemon services enable secrets-brokerrecovery step. This closes the path where disabling the daemon's broker silently turned every secret read into a fresh biometric prompt. Direct keychain reads remain available when explicitly opted in viaAGENTS_SECRETS_NO_AGENT=1. Source:apps/cli/src/lib/secrets/agent.ts,apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/commands/secrets.ts.One event engine:
eventsis the source of truth;auditandlogsare aliases. The unified event stream already covered ops + agent activity; run-dispatch outcomes now land there too asrun.dispatched(written from the same exec chokepoint that used to append the separate hash-chained~/.agents/.history/audit/log.jsonl). New CLI surface:agents events --include/--excludefamilies:ops,activity,commands,runs,security(sessions-style, mutually exclusive).agents events --exclude commandsdrops high-churncommand.start/command.endnoise.agents events --include runslists dispatched-run outcomes (whataudit listused to show).agents events stats/agents events rotateown housekeeping (moved off the logs tree as the canonical home).agents audit≡events --include runs;agents audit listsame;agents audit verifystill walks a legacy hash-chain file if present, else reports clean and points at the events stream.agents logs≡events;logs audit≡events --include ops;logs stats/rotatere-dispatch. A barelogs <id>redirects tosessions/hosts logsfor content.- Coverage: secrets/keychain (
secrets.*), browser, computer, daemon lifecycle (daemon.start/stop/error/infomirrored from the daemon log), plus every module already covered by the CLIcommand.start/command.endchoke point. Source:apps/cli/src/lib/event-families.ts,event-stream.ts,events.ts,audit/log.ts,daemon.ts,commands/events.ts,commands/audit.ts,commands/logs.ts.
Nest disposable leases under devices. Manage crabbox leases with
agents devices lease setup|list|stop|gc; the removed top-levelagents leasecommand now fails as unknown, whileagents run --leaseand--boxkeep their existing behavior. Source:apps/cli/src/commands/lease.ts.Nest Funnel management under the daemon command. Use
agents daemon funnel status|up|downfor webhook ingress; the former top-levelagents funnelcommand has been removed. Source:apps/cli/src/commands/daemon.ts.
Fixed
Menu bar NEW DEVICES no longer lists already-registered or ignored boxes. The daemon's pending-device sentinel writer (
reconcilePendingSentinels) now re-subtracts the registered roster as well as the ignore-list, so a hermetic run that empties the registry view while writing the livedevices-pending/dir cannot surface every fleet box as "new". Soft-fail probe ticks (no tailscale) still prune dismissed sentinels, and the probe fires once on daemon start so leftover pollution clears without waiting for the 3-minute interval.release.shno longer deadlocks when the stuck release is one nothing can finish. Two guards could each name the other as the way out, leaving no version publishable: the stuck-tag guard refused to bump past an unpublishedv<main-version>("finish that release first"), while the catch-up guard refused that same version ("no complete merged release PR ... cut the next patch through the normal release PR flow"). Hit on 2026-08-10 with npm at 1.22.35 andv1.22.36tagged: 1.22.36 could not be finished at all, because its CI-tested tree predates the prepack version-gate fix (1dffc78bc) and so its ownnpm publishrejects a correct binary.scripts/stuck-release.shnow takes the resolved bump kind and main's version, and exempts exactly one case —patch-from-mainstepping over main's own version, which is precisely whatvalidate-bump.shdocuments that bump kind for. Every other stuck version still blocks, and every other bump kind still blocks on this one, so a genuine died-between-tag-and-publish jam is reported exactly as before. Source:apps/cli/scripts/stuck-release.sh,apps/cli/scripts/release.sh,apps/cli/scripts/stuck-release.test.ts.
type: breaking
Remove the top-level agents wallet command and its payment-card storage implementation. Payment-card management will return under agents secrets in RUSH-2532.
Removed the top-level agents whoami command. Use agents login and agents logout to manage synced-secrets access.
type: breaking
Remove the top-level agents worktree command. Team worktree isolation remains available through agents teams --worktree and --enable-worktrees.
Remove the top-level
agents defaultsandagents exportcommands. Configure run defaults and the projects root throughagents config; isolated installs are removed withagents remove <agent>@<version> --isolated.Removed the top-level
agents hostscommand group.agents devicesis now the sole user-facing fleet registry. The registry subcommands map to devices:agents hosts list|add|remove→agents devices list|add|rm. The per-hostagents hosts check <name>probe has no standalone replacement — fleet health isagents devices status(a rollup over every registered device, no per-host argument), and the per-host readiness probe still runs internally viaensureHostReadyat dispatch time. The two dispatched-task commands with no devices equivalent moved under devices:agents hosts ps→agents devices ps(list tasks dispatched withagents run --device <name> --no-follow; reconciles running records;--json), andagents hosts stop <id>→agents devices stop <id>(aliaskill).agents hosts logs <id>is dropped becauseagents logs <id>already views and follows host-dispatch task logs. The dispatch fabric is unchanged:agents run --host/--device, teams remote, and cloud--provider hostall still work. Source:apps/cli/src/commands/ssh.ts,apps/cli/src/lib/startup/command-registry.ts.Removed the top-level
agents lockandagents helpercommands.agents lock(theagents.lockSHA-256 resource manifest, generation +--frozenverification) andagents helper(the macOS Keychain.appinstall/status/where surface) are gone, along withsrc/lib/lock.tsand its tests. The signed Keychain helper still installs and repairs itself automatically — the postinstall installs it, andgetKeychainHelperPath()'s lazy staleness check reinstalls it on the nextagents secretsuse (or when you reinstall agents-cli) — so no user action replacesagents helper install. Verify:agents lockandagents helpernow print an unknown-command error.Removed the top-level
agents publishcommand. Skill registry search and install remain available for existing indexes, but generating and pushing an index is no longer a built-in agents-cli surface.Remove the resource-profile command tree.
agents profileand the resource-profile aliasesagents profiles use/statusno longer register;agents profilesremains the provider/host profile surface for Kimi, DeepSeek, and other custom harnesses. Source:apps/cli/src/commands/profiles.ts,apps/cli/src/lib/startup/command-registry.ts.Remove top-level pull and push commands.
agents pullandagents pushare no longer registered; useagents repo pull <alias>andagents repo push <alias>instead. Source:apps/cli/src/lib/startup/command-registry.ts.agents sessions watch --jsonis the canonical incremental session-state stream for AGI EXT and other long-lived consumers (RUSH-2484). It reads one startup snapshot, then tails the canonical publisher's row-delta journal rather than re-running live-session or fleet gathers. Versioned NDJSONreset,upsert,remove, device-scope availability, and heartbeat envelopes carry opaque row keys and monotonic per-stream sequence numbers. Rows include CLI-owned recovery metadata, and unavailable device scopes retain their last rows.--localdisables fleet aggregation.Fleet readiness now comes from the existing device/account JSON contracts (RUSH-2484).
agents devices list --jsonincludes effective config and resource health;agents devices status --jsonincludes each row's effective profile/config; andagents devices accounts --jsonexposes quota verdict, capture/reset timestamps, and an explicit unavailable reason.agents run auto --interactive --device auto --strategy balanced --mode autoexcludes unreachable, overloaded, signed-out, rate-limited, and out-of-credits choices and fails loud when no eligible placement exists.agents tickets list --jsonis the canonical Linear/GitHub backlog read for UI consumers (RUSH-2484). It returns one stable task shape, cycle metadata, and independent availability/error state for each tracker so one failed source does not erase another source's rows.Skills and commands can declare
aliases:in their frontmatter. Resource resolution now matches a declared alias in addition to the canonical file/dir name, soresolveResource('skills', 'browser')finds a skill that listsbrowseramong itsaliases:. The canonical name always wins a collision — a real resource namedbrowserbeats any resource that merely aliases it, in any layer — and layer precedence (project > user > system) still applies among aliases.listResourcessurfaces each resource'saliases. This is the prerequisite for housing a resource under a plugin namespace (e.g.agi:browser) while a bare name keeps resolving. (RUSH-2504)Credential pushes now ride a hardened SSH posture: pinned host keys + no connection reuse (RUSH-2527). Every
agents secretsoperation that moves credential bytes across the fleet —agents accounts sync <name> --device,agents secrets export <bundle> --host,agents fleet apply --provision-secrets, and a remote bundle resolve (run --secrets b@host/secrets exec --host) — now verifies the destination against the CLI-managed known_hosts store (a changed host key is refused) and never leaves a reusable 60s SSHControlMastersocket behind that an unrelated lateragentsinvocation could silently reuse. This matches the posture the--copy-credsdispatch already used, extended to the explicit provider-account and secrets-export transports. Read-only browse calls (secrets list --host) are unaffected and keep the fast multiplexed baseline. Source:apps/cli/src/lib/secrets/remote.ts(credentialTransportSshOpts),apps/cli/src/lib/secrets/push.ts.Daemon routines and watchdog emit first-class events. Scheduler fires write
routine.start/routine.end(including spawn failures, host-monitor finalization, and terminal cloud dispatch) and each watchdog pass writeswatchdog.actionwith live/stalled/nudged counts onto the unified stream — filterable viaagents events --module routine|watchdogor--event routine.start. Daemon process lifecycle remains the existing log mirror (daemon.start/stop/error/info). Source:apps/cli/src/lib/daemon.ts,runner.ts,events.ts.One verb gets you back into a session. Getting back in used to mean choosing among seven commands, where the right one depended on internal state you could not see —
sessions focus,sessions attach,sessions reconnect,sessions resume,sessions go, top-levelagents resume, and rawagents tmux attach.agents sessions resume <id-or-alias>now detects the state instead: it attaches a live tmux pane, brings a headless session to the foreground, or recovers an ended one on its owning device. It accepts a full id, an id prefix, or anag-<agent>-<shortid>tmux alias, and takes--attach-only(attach one living process or refuse, never fork a copy). Bareagents sessions resumekeeps the multi-select history picker.agents sessions --helpdrops from 21 commands to 16.Retired, hidden but still working for one release — each prints the replacement on stderr:
agents sessions attach <id>andagents reconnect [id]→agents sessions resume;agents sessions go <id>→agents sessions resume <id> --attach-only.agents sessions focusis hidden and becomes the internal lifecycle dispatcher (resumedelegates to it), so it is deliberately not warned. One behaviour is deliberately dropped: bareagents reconnect(no id) auto-attached the most recent session started in the current directory with no prompt; bareagents sessions resumeopens the picker instead, so that zero-typing path goes away whenreconnectis deleted after its deprecation window (tracked on RUSH-2498). Kept:agents sessions detach(the genuine inverse — it stops the interactive process and respawns the agent headless, which nothing else does),fork,migrate, and the rawagents tmux attach <name>escape hatch.A live tmux alias now attaches instead of being keyword-searched.
agents sessions resume ag-kimi-632c1fbcattached nothing before: the alias fell through to a keyword query and returned unrelated text matches while the pane was alive and attachable. The alias's hex is the launch id, not the harness session id, and a harness that writes nostate/sessions/<pid>.jsonrecord cannot be mapped back to a session at all — so such a pane had no working selector and rawtmux -S … attachwas the only way in. The pane name is now a sufficient handle.#{pane_dead}is still queried at attach time, so a dead pane routes to recovery rather than attaching a corpse.A full UUID is one session, not an ambiguity.
agents sessions focus <full-uuid>could answer"…" is ambiguous (2 sessions). Use more of the id.with no longer id to give: a transcript that had synced to a second machine counted twice, and one copy had no transcript file left. Synced copies sharing one full id now count as one logical session (SES-IF-2a). A genuine prefix collision between different ids still reports both.agents sessions reapis removed (it shipped in 1.22.36 alongside the dead-pane reaper). The 5-minute daemon sweep stays — it is the part that matters, and it only kills sessions where all panes are dead. The daemon calls the reaper directly, andagents tmux kill <name>(idempotent) already covers the manual case.
Source: apps/cli/src/commands/sessions-resume.ts, apps/cli/src/commands/focus.ts, apps/cli/src/commands/{attach,reconnect,go}.ts, apps/cli/src/lib/session/types.ts.
A tmux-wrapped run killed mid-work no longer reports success (EXEC-23b).
agents runwraps an interactive run in tmux and reads the agent's exit status back off the pane. On the three paths where tmux could not report one — the pane unreadable because the server or session went away, or dead with no#{pane_dead_status}— it returned0. So an interactive run whose tmux server died under it ([server exited unexpectedly], the agent stranded at an approval prompt) printed a failure banner readingexit 1and handed its caller0: anything scriptingagents runcounted a killed run as a clean finish. Those cases now resolve1and print an explicit "outcome unknown" banner, matching the rule the--hostfollow path already used ("the remote's own exit code, or 1 if unknown"). Exit0is reserved for an outcome tmux actually reported: a confirmed-alive pane (a cleanCtrl-b ddetach) or a dead pane whose status tmux read as0. One helper,tmuxRunExitCode(pane, knownAlive), is now the single decision behind everyrunInTmuxreturn path, so the banner and the returned code cannot disagree. That includes the native-resume attach (agents run <agent> --resume), which previously returned a hardcoded0without ever querying tmux —prepareSessionForResumenow returns the pane it resolved so the resumed run can be asked about too. Known cost, accepted deliberately: the daemon's 5-minute dead-pane reap can remove a cleanly-exited session in the moment between its pane dying and the status read, so such a run reports1; once the pane is gone there is no other evidence, and a false unknown beats a false success. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/tmux/session.ts,apps/cli/docs/specifications.md.agents sessions --browser/--computernow name the agent session that drove the task (RUSH-2549). Every browser row used to readunlinkedwithlast known owner: UNRESOLVED@<host>, because task identity lived only in the browser daemon'stasks.json— a filesaveTaskStaterewrites from the LIVE task map, soagents browser stoperased the link and a daemon restart emptied it. Observed on a real machine: a profile'stasks.jsonwas{}while itssessions/dir held dozens of task dirs full of captures. Identity is now written once at task start to a durablebrowser_sessionsrow and is never deleted, so a finished task still resolves its session. The link also keys onAGENT_SESSION_ID— carried by every agent, measured on 5 of 5 live agent processes — in addition toAGENT_LAUNCH_ID, which was on only 2 of 5 and was previously the sole join key. Computer-use invocations get the same durablecomputer_sessionsrow: they already resolved identity correctly (stampProvenancereads the session env) but recorded it only in the event ledger, which prunes at 7 days / 50 MiB, so a run vanished on day 8; the row keeps it listed with identity, timing and a total action count (rendered asN actions (per-verb detail pruned from the event log)), and never reconstructs the pruned per-verb detail. The table carries its own long retention (TOOL_SESSION_MAX_AGE_DAYS, swept from the listing path) so it cannot grow without bound, and reads are limit-bounded. Both tables are metadata only — screenshots, PDFs and recordings stay on disk under.cache/browser/<profile>/sessions/<task>/, referenced by path; nothing copies bytes into SQLite. Identity is resolved in the calling CLI process, never daemon-side (the shared daemon would attribute every task to itself). Schema v39, additive. Forward-only: captures taken before this cannot be linked retroactively, because their identity was already discarded — those rows readunlinkedhonestly. Known gap:agents browser start --host <device>still records no session, because the SSH dispatch forwardsAGENTS_ACTOR*/AGENT_TERMINAL_IDbut notAGENT_SESSION_ID(lib/hosts/dispatch.tswithActorEnv); tracked on RUSH-2549, not fixed here. Source:apps/cli/src/lib/session/db.ts,apps/cli/src/lib/browser/service.ts,apps/cli/src/lib/browser/sessions-list.ts,apps/cli/src/lib/computer/sessions-list.ts,apps/cli/src/commands/browser.ts,apps/cli/src/commands/computer-actions.ts,apps/cli/docs/browser.md,apps/cli/docs/computer.md.
type: feature
Add usage.primary-host, a user-scope agents config key that pins the device authoritative for fleet usage while falling back to interactive.host when unset.
Fleet usage primary runtime. When
usage.primary-host(orinteractive.host) is set, only that device runs frequent live usage API refreshes; peers pull a token-free snapshot. Standalone mode keeps per-host refresh when no pin is set.VSCodium agent tabs get the right chip on focus/resume (#2478). The
vscodium-agentspawn URI now carriesagent/sessionId/titlefromsessions focusandsessions resume, so remotessh … tmux attachtabs open with the harness icon and status bar instead of a generic shell. The terminal engine'sSurfaceItem/LaunchRequestplumb the optional identity through; other backends ignore it.Rename the top-level webhook receiver command to
agents webhooks. The singular spelling is no longer registered; useagents webhooks serve.
1.22.36
agents cp <src> <dst>— first-class fleet file transfer (RUSH-2297). New top-level command that copies files and directories between fleet hosts (local-to-remote, remote-to-local, and remote-to-remote) using the same SSH/device fabric asagents ssh. Either endpoint ishost:path(remote) or an absolute local path. Remote~and literal$HOMEin a path are resolved on the remote host before transfer — never in the caller's shell — preventing the silent-failure class where a shell variable expands to the local user's home directory instead of the remote one. Unknown devices fail loudly with a human-readable error before any SSH connection is attempted. Recursive directory copies use-r; two-remote transfers route through the local machine (-3) so no direct SSH trust between fleet boxes is required. Source:apps/cli/src/commands/cp.ts.Faster warm CLI bootstrap: skip redundant sync spawn + multi-install PATH scan (RUSH-2324). Ordinary (non-
--help/--version) invocations no longer fork the detached auto-pull worker when a cycle finished in the last five minutes (parent inspects the.last-syncstamp and fresh*.lockmtimes under the fetch cache), and the multi-install install-root scan is memoized beside.update-checkfor the same window. Saves the measured ~7.3ms spawn + ~1ms PATH walk on the warm path without changing sync semantics. Source:apps/cli/src/lib/auto-pull.ts,apps/cli/src/lib/auto-pull-worker.ts,apps/cli/src/lib/self-update.ts,apps/cli/src/bootstrap.ts.Unknown / typo'd top-level commands no longer load every command module (RUSH-2329). A misspelled name used to call
registerAllEagerCommands(and the lazy sessions/teams/cloud tree) solely to build the "did you mean" candidate list — ~250–330ms of dynamic import on cold start. Spellcheck now walks the plain-stringKNOWN_TOP_LEVEL_COMMANDSset (same first-seen order for tie-breaks) and registers only the auto-corrected command before reparse. Distance-1 auto-correct and--hostre-routing after correction are unchanged. Source:apps/cli/src/index.ts,apps/cli/src/lib/startup/spellcheck.ts.CLI bootstrap no longer loads
versions.tsvia the brand edge (RUSH-2331). Everyagentsinvocation statically importsbrand.jsforresolveBrandName/disabledCommandsForActiveBrand. That module used to importagents.jssolely forreservedBrandNames()/validateBrandName()(mine/setup only), andagents.jspulls the fullversions.tsgraph — ~90ms of module evaluation on the--version/ secrets-broker / bare-help path.brand.tsnow reads the zero-depagent-cli-commandsleaf (pinned equal toAGENTS[*].cliCommandby test); the self-update → primitives redirect from the same ticket remains. Source:apps/cli/src/lib/brand.ts,apps/cli/src/lib/agent-cli-commands.ts.Synchronous secrets-broker reads no longer pay ~140ms of CLI bootstrap (RUSH-2335).
dist/index.jsis now a slim shell that statically imports only the leaflib/secrets/sync-commands.jsand dispatches__secrets-get/__secrets-ping/__secrets-lock(plus__vault-age-helper,__shim,__daemon-run,__daemon-tick) before loading the full commander graph. Everything else arrives viaawait import('./bootstrap.js'). Coldnode dist/index.js __secrets-pingwas measured at ~160–180ms against a ~22ms bare-node floor; the leaf-only path evaluates in roughly one bare-node spawn plus the agent handler. Source:apps/cli/src/index.ts,apps/cli/src/bootstrap.ts.agents --help/--versionno longer run the macOS menu-bar self-heal (RUSH-2346). On darwin the startup self-heal (installMenubarLaunchAgentOnUpgrade()) ran on every invocation, including the pure documentation paths, dynamically importinglib/menubar/install-menubar.jsand doing its filesystem checks before deciding to no-op. It now carries the samehelpOrVersionRequestedgate the update check, background sync, andensureInitializedalready use, so help and version skip the self-heal's filesystem work and itsinstallMenubarLaunchAgentOnUpgrade()call. (On the default path thelib/menubar/install-menubar.jsmodule is still pulled into the import graph via the staticmigrate.ts→ routine-readiness → hosts chain; shedding that residual import cost is tracked separately.) Source:apps/cli/src/index.ts.1Password import now captures each item's notes as a description (RUSH-2348).
agents secrets import --from 1password:<vault>previously read only an item's credential value and dropped itsnotesPlainnotes entirely. The importer now extracts a NOTES-purpose field (1Password'snotesPlain, or a field labelled notes) as descriptive metadata on the imported secret, while the CONCEALED credential remains the value — notes are never selected as the secret itself. Source:apps/cli/src/lib/onepassword.ts.buildExecEnvstrips an ambientCLAUDE_CODE_OAUTH_TOKENon non-interactive Claude runs when no per-account setup-token resolves (RUSH-2360). A run dispatched on a provisioned box inherits the launcher's shared, rotatingCLAUDE_CODE_OAUTH_TOKENthroughsanitizeProcessEnv(process.env). When the reservedauthbundle carried this version home's own setup-token it was injected over that ambient value, but when NONE resolved the ambient token was left in place — soagents run claude "<prompt>"could silently authenticate as the shared token, the RUSH-1822 fleet-wide-logout hazard. The non-interactive path now mirrors the routines path (runner.ts:1017-1021) unconditionally: inject the resolved setup-token, else delete the ambient one, so a missing login fails loud (401) against this home's own credential rather than borrowing another's. The strip also runs when no version resolves (version === null), matching the routines guard. Interactive runs are unchanged — they still keep a deliberately-exported token and drop only an inherited copy of their own setup-token by value (#2383). Source:apps/cli/src/lib/exec.ts(buildExecEnv).Add DeepInfra profiles, accounts, and usage (RUSH-2362).
agents accounts add deepinfra --provider deepinfra --auth api-keystores the API key in the existing prompt-free account bundle, andagents profiles add deepinfra --account deepinfraconfigures Codex forhttps://api.deepinfra.com/v1/openaiwithdeepseek-ai/DeepSeek-V3. The daemon-owned BYOK refresh path reads DeepInfra's documented/payment/checklistendpoint soagents viewcan show current usage and prepaid credit without raising a credential prompt. Source:apps/cli/src/lib/profiles-presets.ts,apps/cli/src/lib/account-provider-registry.ts,apps/cli/src/lib/byok-usage.ts.Faster CLI bootstrap: skip
--hostpassthrough module graph when no routing flag (RUSH-2374). Ordinary named invocations (agents view,agents sync, …) no longer dynamically importlib/hosts/passthrough.js(~187ms cold module graph measured on yosemite-s1) just formaybeRunOnHostto return false. Bootstrap now gates that import on a leafhasHostRoutingFlagscan of--host/-H/--device/--hosts/--devices(~0.001ms). Also importmachineIdfrom the leafmachine-id.jsinstead of thesession/sync/config.jsre-export on the routed path. Source:apps/cli/src/bootstrap.ts,apps/cli/src/lib/hosts/routing-flag.ts,apps/cli/src/lib/hosts/passthrough.ts.agents teams statusshowsPR OPENinstead of bareCOMPLETEDwhen a teammate finished with an unmerged PR (RUSH-2380). Process-exit success was reported as COMPLETED even when the PR was still open, so orchestrators composed on top of unlanded work (3/3 edit-mode teammates one day). Delivery is now a separate postcondition (delivery: pr_open | pr_merged | no_pr | …) on status JSON; the human status label isPR OPEN(magenta) when process status is completed and a PR URL is present without a known merge. Process status still drives the DAG (--after); only the display/JSON delivery signal changed. Source:apps/cli/src/lib/teams/delivery.ts,apps/cli/src/lib/teams/api.ts,apps/cli/src/commands/teams.ts.Live agents no longer leave the active set (and become unreachable by
agents message) when the by-pid registry is empty (RUSH-2384). A mid-run headless/teams process can keep working with--session-idon its argv while~/.agents/.cache/terminals/by-pid/has zero files (wrapper pid exited and was pruned, or the launch never recorded one). The headless scan only consulted the registry / SessionStart hook / newest-jsonl heuristic, so foldSubordinateAgents could collapse that process into a parent and drop its id fromagents sessions --active— andagents message <id>resolved purely from that set, so the only recovery was kill-and-redispatch. The scan now reads--session-idfrom the live process argv (Linux/proc/<pid>/cmdline, Darwinps), treats argv identity as "own session" so those children are not folded away, andagents messagefalls back to the same process-table proof before reporting no match. Source:apps/cli/src/lib/session/pid-registry.ts,apps/cli/src/lib/session/active.ts,apps/cli/src/commands/message.ts.agents viewnames the setup-token usage-scope gap instead of painting healthy headless Claude accounts as broken (RUSH-2392).claude setup-tokenmintsuser:inferenceonly; Anthropic's usage endpoint requiresuser:profileand returns HTTP 403. The probe and live usage read previously treated that 403 like any other rejection — auth-health mapped it torevoked, andformatUsageSummarystamped cached barsunverified. The accounts best provisioned for unattended work therefore looked the least healthy. The Claude usage path now detects the known scope-denial body (user:profile/ "scope requirement"), surfacesusage unavailable (headless), and auth-health classifiesreason: usage_scopeasunverified(benign) rather thanrevoked. Minting another setup-token cannot fix bars; interactive login (or device-role strategy, RUSH-2395) is the only path to usage meters. Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/auth-health.ts,apps/cli/src/commands/view.ts,apps/cli/src/commands/versions.ts.A headless run that exits with its branch on an OPEN PR now says so (RUSH-2394). A headless
agents runthat backgroundsgh pr checks --watchand exits strands its own PR: that watcher is a child of the agent process tree and dies with it, so nothing merges on green and the PR sits open with nobody watching. A headless writable run that exits (or crashes) with the branch still on an OPEN pull request now prints a loud stderr warning naming the PR. Advisory only — it never throws and never blocks the run's exit. Source:apps/cli/src/lib/pr-land-detach.ts,apps/cli/src/commands/exec.ts.
Breaking
- Replace version-bound account labels with durable credential accounts. Create API-key, Claude setup-token, or bearer-token accounts with
agents accounts add; native OAuth logins remain harness-managed.
New
Reuse one named provider credential across compatible native and custom harnesses with
agents run --account <name>or a harnessaccount:default, and rotate it without changing the account id.The daemon can no longer crash-loop unbounded (RUSH-2418). Crash-loop prevention existed only as the OS supervisor's retry, uncapped: the launchd plist set
KeepAlivewith noThrottleInterval, so a daemon dying during startup was relaunched on launchd's ~10s default — six times a minute, forever — and the systemd unit'sRestart=alwayshad noStartLimitIntervalSec/StartLimitBurstto ever give up. Three layers now bound it. The plist carries aThrottleInterval(the same fix the menu-bar helper already ships), the unit carriesStartLimitIntervalSec/StartLimitBurstin[Unit](where systemd 229+ actually reads them) withRestartSecpaced so the bursts fit inside the window, andagents __daemon-runinstalls top-leveluncaughtException/unhandledRejectionhandlers — there were none anywhere in the CLI — that route the failure into the daemon's ownlogs.jsonland exit non-zero deterministically instead of dying on Node's default handler with a raw stack. Separately,daemon-health.ts'sconsecutiveFailureswas write-only telemetry; a newdaemon-startrecord now drives an auto-start circuit breaker. Starts are counted up front and cleared only by a daemon that finishes booting, because the crash loop is invisible from the launcher's side — a daemon that spawns and then dies returns a perfectly real pid, so there is no error to observe. After 5 consecutive starts that never report healthy, the implicit auto-start (ensureDaemonStarted, reached fromsecrets unlock,browser start, the watchdog) refuses instead of relaunching a broken daemon on every foreground command, andagents daemon doctorreports the streak and its recorded cause. An already-running daemon is still reported, and the explicitagents daemon startoverride is never gated. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/lib/daemon-health.ts,apps/cli/src/commands/daemon.ts,apps/cli/src/index.ts.agents daemon stopnow proves the daemon's state files were released, not just its sockets (RUSH-2421). The stop postcondition checked the daemon process and the two sockets (browser IPC, secrets broker) but not the three files a graceful shutdown removes: the lifetime marker, the heartbeat, and the daemon's instance-registry entry. Those are cleaned up inhandleShutdown, which never runs on the escalatedkillTreepath or on Windows — so a wedged daemon that had to be killed left all three behind while the stop reportedok: true. That is not cosmetic: a leftover heartbeat is whatresolveLiveDaemonPidconsults to re-adopt a daemon whose pid file is gone, so a dead daemon could read as running, and a leftover registry entry is what the stray-daemon reaper enumerates. Each is now checked and reclaimed the same way the sockets are, and reported inreleased/surviving— with the same ownership rule the broker socket already used: a marker naming a pid that is alive and is not the daemon just stopped belongs to a live successor and is left untouched. Separately,BrowserIPCServer.stop()callednet.Server.close()without awaiting its'close'event, so it resolved while the socket was still bound; since the daemon awaits it before exiting and a successor treats that exit as proof the predecessor's resources are free (SING-11), the proof was false. It now waits for the real release — ending held client connections so the binding is freed immediately, with a 1.5s backstop kept well below the daemon's SIGTERM grace window so a slow close can never be what gets the daemon killed. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/lib/browser/ipc.ts.A graceful
agents daemon stopno longer escalates to a kill when a browser client is attached (RUSH-2421 follow-up). The awaited-close fix shipped with its timeout set to 5s — exactly the daemon's own SIGTERM grace window.net.Server.close()does not complete while any connection is open, and browser clients hold one open on purpose (the socket stays warm between actions), sohandleShutdownwas still insidebrowserIPC.stop()whenstopDaemongave up waiting and rankillTree. Measured: 5512ms for a stop that should take milliseconds.stop()now ends the tracked client connections instead of waiting for them, so the binding is released immediately; the timeout is a backstop only, cut to 1.5s so a slow close can never be what gets the daemon killed.stop()is also idempotent now — a second SIGTERM awaits the same close rather than racing past it because the server handle was already cleared. Separately, the stop's registry-entry check treated its own pid as residue unconditionally, so a daemon that survived the kill lost the recordfindSurvivingStateDirDaemonsreads and the next stop would reportok: truewith it still running; ownership is now decided by the stop's own survivor scan rather thanisAlive, which also fixes the zombie case (a SIGKILLed daemon stays signalable until reaped, sokill(pid, 0)reports it alive). Source:apps/cli/src/lib/browser/ipc.ts,apps/cli/src/lib/daemon.ts.Repeated SIGTERMs during daemon shutdown now run the shutdown once (RUSH-2423).
handleShutdownis reachable from SIGTERM, SIGINT, and the state-dir self-check, and two can arrive together — a service manager SIGTERMing a daemon whose state dir was just removed. It was safe only because every step inside happens to be idempotent, which is a property each newly added step would silently have to re-earn; single-shot is now a property of the function. Alongside it, three internals cleanups with no behaviour change: the daemon's log path is exported asgetDaemonLogPath()and the two commands that rebuilt it from a hardcoded'logs.jsonl'literal (agents daemon logs,agents routines) now call it, so renaming the file cannot silently point them at nothing; the background-tick cadences that were inline literals at theirsetIntervalare named beside the existing tick constants, each with why that cadence and not another; and the comments settle on one term, "state dir", replacing a daemon-dir/state-dir/state-tree mix. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/commands/daemon.ts,apps/cli/src/commands/routines.ts.A torn
meta.jsonwrite no longer permanently disables orphan-worktree cleanup for every worktree name in every team (RUSH-2429).saveMeta()wrote with a barefs.writeFile— no tmp file, no rename — so a process killed mid-write left a truncated, unparseablemeta.json.loadFromDisk()returnednullfor that file from a bare catch, indistinguishable from the file never having existed, soloadExistingAgents()/rescanFromDisk()silently skipped it forever andisWorktreeClaimed()(which readsmeta.jsondirectly, not through the cache) then failed CLOSED on it globally: it scans every record and answers "claimed" the first time it can't read one, so one corrupt record permanently blockedteams add's orphan-worktree teardown for every name in every team.saveMeta()now writes to a sibling tmp file andfs.renames it into place — atomic on POSIX, so a killed write leaves either the previous valid record or the new one, never a torn one.loadFromDisk()now quarantines a record whose content is corrupt — a torn or unparseablemeta.json, the case that actually breaks cleanup — by renaming it tometa.json.corruptwith a warning, so it stops masquerading as "no record" and a subsequentisWorktreeClaimed()scan sees real absence for that entry instead of failing closed on it forever. A transient read error (EACCES/EIO/EMFILE) is NOT corruption: the file is intact and simply could not be read that moment, soloadFromDisk()returnsnullwithout renaming it — renaming a valid record away would itself be the fail-open this fix exists to prevent.isWorktreeClaimed()'s fail-closed behavior for a record that is genuinely present-but-unreadable at decision time is unchanged — a spurious "unclaimed" would rungit worktree remove --forceover a live teammate's checkout, so the guard stays as strict as before; only a corrupt record's permanence is fixed. Source:apps/cli/src/lib/teams/agents.ts.Daemon routines can no longer resolve a stale
agentsbinary from the Node prefix (RUSH-2431). The daemon service manifest now puts the directory containing the runningagentslauncher first onPATH, ahead of the Node bin directory. This stops scheduled routines from shadowing the current binary with an olderagentsinstalled inside the same Node prefix. As a runtime backstop for already-running daemons,commandroutines also inject a shell-function guard (and aPATHprepend fallback) so bareagentsinvocations use the same binary that launched the routine.agents doctornow reports abinary-shadowwarning when anotheragentsbinary onPATHor in a well-known install directory could preempt the running one. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/lib/runner.ts,apps/cli/src/lib/cli-entry.ts,apps/cli/src/lib/binary-shadow.ts,apps/cli/src/lib/devices/doctor-findings.ts,apps/cli/src/commands/doctor.ts.agents computer sessions/agents sessions --computeris task-first, grouped by run (RUSH-2432). The computer counterpart ofagents browser sessions(RUSH-2407): everycomputer.actionevent (already written by everyagents computer <verb>and the embeddedrun --taskloop) is read back from the durable event ledger and grouped by a unique CLI invocation identity — one row per invocation without conflating later OS pid reuse. Each row shows machine, target app/window when known, per-verb action counts, and — when the run'ssessionId/launchIdresolves — the owning agent session's canonical digest. A run with an unindexable identity shows unresolved; one with no agent-session identity at all shows unlinked. On a TTY the picker is interactive with search and an action-list drill-down (no artifact to open, unlike browser);--no-interactive/--jsonprint the flat table. Theagents sessions --computeralias also preserves--limit,--host, and--device. Nothing sensitive is added to the ledger: typed text stays length-only, and a--taskdescription is bounded to 200 characters before it is ever written. Source:apps/cli/src/lib/computer/sessions-list.ts,apps/cli/src/commands/computer-sessions-picker.ts,apps/cli/src/commands/computer.ts.A session no longer vanishes when its transcript file is deleted — the local DB is authoritative for its user turns (RUSH-2436). Session history was a file-anchored index: delete the
.jsonl(viaagents remove's trash, a manualrm, a.historyversion-home rotation, or a box reimage) and the session silently disappeared from everyagents sessionslisting and could not render, even though a DB row and its user-turn text still existed. The user-turn content for every harness is already stored durably in thesession_textFTScontentcolumn, soquerySessions/topSessionsByCostnow KEEP a file-gone session whose content survives — flaggedarchived— instead of dropping it,agents sessions <id> --include userrenders those user turns from the DB (with anarchived — transcript file removedbanner) instead of a bare metadata dump, and the picker preview does the same. A row whose file is gone and whose content is empty (a phantom — a stale/movedfile_path) stays suppressed. Merely listing a file-gone session also no longer PURGES its redacted tool-call evidence: that destructive purge-on-read (purgeToolCallsfrom thequerySessionsmissing-file branch) is removed. A row whose file returns (a recoverable-trash restore) is un-archived on the next listing. A new persistedarchived_atcolumn (schema v38) stamps the first confirmation that a scanned file went missing. Source:apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/tool-store.ts,apps/cli/src/commands/sessions.ts,apps/cli/src/commands/sessions-picker.ts.Optional off-box session backup to Cloudflare R2 (
agents sessions export --to-r2/import --from-r2). A durable, on-demand backup target for session transcripts — protection against losing the whole box, not just a file on it.export --to-r2uploads each selected session as its own encrypted object undersessions/<machine>/<agent>/<session>in ther2.backupsbucket;import --from-r2downloads and restores them through the normal placement (deduped, local-always-wins). Bodies are sealed client-side with AES-256-GCM under the sharedR2_SYNC_ENC_KEY(zero-knowledge — Cloudflare only ever stores ciphertext); credentials come only from ther2.backupssecrets bundle (keychain, never env/disk). It is a pure backup target: on-demand only, no daemon cycle, and not the retired multi-writer sync. A missing/lockedr2.backupsbundle fails loud with an actionable message, never a silent no-op. Source:apps/cli/src/lib/session/sync/r2.ts,apps/cli/src/commands/sessions-export.ts,apps/cli/src/commands/sessions-import.ts.agents syncnow prunes resources removed from source, instead of only ever adding (RUSH-2438). A command or skill deleted from a DotAgent repo used to linger in every installed version home forever — observed: 8 removed commands stayed live in the/menu of all 7 Claude homes, and--forcedid not clear them. The gap was that the repo-scope reconcile forms (agents sync <agent>@all system,agents sync <agent> system --force) pass a selection, which turned off the full-sync orphan sweeps; those forms now reconcile deletions too. The prune is manifest-bounded so it never over-deletes: the removal set is(names the last full sync recorded) − (names still present in source across ALL layers), intersected with what is actually in the home. Three guarantees follow — a file you hand-authored into~/.claude/…was never recorded so is never touched; a system command removed while a same-named user command still exists is kept (still provided by a layer); and with no sync manifest yet the reconcile prints a one-line notice and prunes nothing rather than guessing. Every harness prunes through the same writer that installed the resource — a native command file (Claude/Grok/Cursor), a command-as-skill dir (Codex ≥ 0.117, Kimi), a Goose recipe. Removals are shown under aPruned from <agent>@<version>block and carried in the--jsonpayload aspruned: { commands, skills }. Hooks are out of scope here — pruning a hook must also GC itssettings.json/hooks.jsonregistration, a Windows-portable-path surface; tracked in RUSH-2456. Hook files stay reconciled by the existing in-write orphan sweep. Source:apps/cli/src/lib/staleness/prune.ts,apps/cli/src/lib/staleness/writers/{commands,skills}.ts,apps/cli/src/lib/versions.ts,apps/cli/src/commands/sync.ts.Stop
agents runfrom triggering a broad Keychain scan (RUSH-2440). Agent-only bundle reads now resolve exact items from bundle metadata instead of enumerating the macOS Keychain, preventing unrelated biometry-protected secrets from raising a Touch ID sheet on every launch. Interactive reads retain the existing enumeration-plus-declared-item union. Source:apps/cli/src/lib/secrets/bundles.ts.Restore the stale-ACL'd
hmackeyheal on the hot read path (RUSH-2441). v1.22.7 moved the once-per-machine no-ACL re-store ofagents-cli.hmackeyontoreadHmacKeyRecordso every hashed keychain lookup (including the SessionStartagents devices liststats probe) converged a biometry-ACL'd key to silent. v1.22.10 (bf79dc885/ #1995) put the heal back insidemaybeAutoRekey, which is bypassed for the hmackey and hashed-name lookups themselves (prepareServiceNamereturns early forHMAC_KEY_ITEM), so an already-migrated machine prompted forever again — and the 1.22.7 changelog still claimed the read-path guarantee. The heal is back onreadHmacKeyRecord: first hashed lookup re-stores no-ACL once (guarded byhealedNoAcl), silent forever after. Source:apps/cli/src/lib/secrets/index.ts.scripts/install.shrestarts a running routines daemon after a dev install (RUSH-2442). The dev install strips the npmpostinstallhook (so it doesn't nudge PATH for a side-by-side prefix), which was also the only place that bounced the daemon onto the just-staged code. A long-lived daemon kept hosting a secrets broker built from the previous install; version skew then wiped held bundles and re-armed Touch ID on the next secrets read. After linking bins,install.shnow reloads a running daemon onto the just-linked binary (best-effort, skipped in CI /AGENTS_NO_HEAL=1, never starts a daemon that wasn't already up). Source:apps/cli/scripts/install.sh.agents sharegains--unlisted/--private, a 30-day default expiry, and a pre-publish sensitive-content scan (RUSH-2443). Root cause of the RUSH-2428 incident: every publish was world-readable with no private option, no default expiry, and no content gate. Unflagged publishes now expire in 30 days (--expire neverfor permanent).--unlisted(alias--private) storesvisibility=unlistedso the public gallery andagents share listomit the page while the direct URL still works (capability URL — unlisted, not secret). Before upload the CLI refuses files that contain email addresses or credential-shaped strings (ghp_…,sk-…,AKIA…,Bearer …) unless--force. Requiresagents share updateon already-provisioned endpoints so the Worker filters unlisted objects. Source:apps/cli/src/lib/share/{publish,worker-template}.ts,apps/cli/src/commands/share.ts.agents share listshows what you've published (RUSH-2444). There was no way to ask the CLI which pages are live under your share namespace — during the RUSH-2428 incident the only way to answer "is anything else of mine public?" was to fetch the gallery HTML and grep it. The Worker now exposes a machine-readable listing atGET /<user>?format=json(a?format=jsondiscriminator on the existing single-segment gallery path, gated on the SAME "does<user>/hold objects" check the gallery uses — so the HTML gallery is untouched AND a legacy flat slug with?format=jsonstill serves its real page rather than a fake empty listing), andagents share listreads it: a human table newest-first by default, or the raw listing with--json(each object carriesslug,url,size,contentType,publishedAt, andexpiresAt). It lists the ACTIVE pages only — expired links and the sibling<slug>.pngOG covers are omitted, mirroring the public gallery. An empty namespace 404s at the Worker, whichlistreads as "nothing published" (via the template-hash signal) and reports cleanly. Because this adds a route toworker-template.ts, the feature only reaches an endpoint afteragents share update(RUSH-2449) deploys the current template; an endpoint that predates the route makeslistfail loud with a "runagents share update" hint (reusing theagents share statustemplate-hash signal, and detecting a live 404 on an unknown-template endpoint / a non-JSON gallery body) rather than returning a wrong or empty result. Source:apps/cli/src/lib/share/worker-template.ts,apps/cli/src/commands/share.ts.agents fleet updatenow verifies each box actually runs the new version, instead of trustingexit 0(RUSH-2446). The rollout ranagents upgrade --yesper device and called a boxokon the exit code alone. That exit code only says the npm global moved — it says nothing about which copyagentsresolves to on that box, and on any dev boxscripts/install.shputs a side-by-side build at~/.local/agents-cli-devwith~/.local/bin/agentspointing at it, earlier on PATH than the npm global. So the rollout reported a clean sweep while those boxes kept running old code, and nothing in the fleet surfaces answered "which boxes actually run the shipped version?" —agents fleet statusreports version skew as a separate glance, and the local multi-install banner is advisory stderr on the box itself that never crosses the SSH hop. After each successful upgrade the rollout now asks the box whatagentsresolves to (symlinks followed) and what version that copy reports. A box on the target readsok runs <version>; a box whose resolvedagentsis a dev build readsstale NOT upgraded — agents resolves to a dev build (0.0.0-dev.<sha>) at <path>, shadowing the upgraded <version> global; a box on some other released version readsstalewith both versions and its resolved path. Those boxes are counted in a newN not upgradedtally, excluded from theokcount, and make the command exit non-zero. A box whose probe cannot answer — no POSIX shell, oragentsunresolvable — readsunverifiedand is likewise not counted as a success, because "we could not check" and "it is upgraded" are different answers. Afailedorskippedbox is not re-probed, so one fault still produces one row. With no explicit version argument the target is derived as the highest released version any probed box reports; dev stamps are never elected as the target, so a fleet of dev builds cannot declare itself upgraded. Source:apps/cli/src/lib/devices/rollout-verify.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/lib/startup/dev-build.ts.agents share update— push aworker-template.tschange out to an already-provisioned endpoint (RUSH-2449).agents share setuponly ever wrote the Worker script during first provisioning, so every endpoint was pinned to whatever template it got on day one — there was no way to ship a Worker-side fix or feature (private publishing,share list, access logging) to someone who already ran setup.agents share updatere-deploys the current template to the existing account/worker/bucket with no re-provisioning (no bucket creation, no route/domain changes) and never regeneratesWRITE_TOKEN: Cloudflare's script-upload endpoint replaces a Worker's bindings/secrets wholesale on every upload, so the existing token is re-applied via the Secrets API immediately after the script upload. Idempotent — re-running it when the deployed template already matches is a no-op (--forceto redeploy anyway).ShareConfignow records atemplateHash, andagents share statusreportscurrent/outdated/unknown(a config from before this field existed reads asunknown, never as stale).agents setup share, run against an already-configured endpoint, now offers "update the deployed Worker" alongside "keep" and "reconfigure from scratch". Source:apps/cli/src/lib/share/provision.ts(hashWorkerScript,updateWorker),apps/cli/src/commands/share.ts(runShareUpdate,shareTemplateStatus),apps/cli/src/commands/setup-share.ts.teams disbandis terminal — PENDING teammates cannot resurrect or re-start (RUSH-2450). Disband deleted log dirs and the registry entry but left the in-memoryAgentManagercache intact; a concurrentteams start --watchsupervisor then re-persisted those records viasaveMeta, so a second disband still found N logs to clear andteams startcould re-launch already-merged work. Disband now drops the registry entry first (writing a durable disband tombstone), then purges every teammate record from disk and the manager cache (AgentManager.purgeByTask).saveMetarefuses to re-persist a teammate whose team carries a disband tombstone. A second disband is a clean no-op;teams starton a disbanded name fails loud. Source:apps/cli/src/lib/teams/agents.ts,apps/cli/src/lib/teams/registry.ts,apps/cli/src/commands/teams.ts.Usage and authentication refresh now have one device-level owner (RUSH-2451). The daemon maintains shared per-account usage snapshots and auth-health metadata;
agents run,view,versions, teams, device inventory, and Factory remain cache-only readers.agents usage <agent> --refreshand other explicit refresh paths enter the same cross-process lease, so simultaneous CLI processes reuse one provider request or local-log scan. BYOK budget reads use the same persisted, atomic cache model. OAuth stays harness-managed and per-device; only safe health/account metadata is shared, while named API keys, setup tokens, and bearer tokens continue to use device-local credential storage. Source:apps/cli/src/lib/account-state-service.ts,apps/cli/src/lib/refresh-coordinator.ts,apps/cli/src/lib/usage.ts,apps/cli/src/lib/byok-usage.ts,apps/cli/src/lib/auth-health.ts,apps/cli/src/lib/fleet-status.ts,apps/cli/src/commands/usage.ts.agents share updatenames the partial-failure window when the write token fails to re-apply (RUSH-2453). Cloudflare's script upload clears Worker secrets;updatere-appliesWRITE_TOKENimmediately after. If that second call fails (network blip, expired API token, rate limit), the live endpoint has no write token and every publish/delete 401s. The error now says so and tells you to re-runagents share update. Config is only rewritten after both steps succeed, so a re-run does not short-circuit on a matching hash — a test pins that self-heal property. Source:apps/cli/src/lib/share/provision.ts,apps/cli/src/commands/share.ts.agents --help/--versionno longer load the migration graph (RUSH-2454). Follow-up from RUSH-2346: the menu-bar self-heal was already gated, butfoldLegacySystemRepo()and the v19runMigration()hop still ran on pure documentation paths and dynamically importedlib/migrate.js— whose static imports pull the hosts/routine/teams/daemon/menubar graph (~287 modules). Both hops now share the samehelpOrVersionRequestedgate. The always-on fold itself moved to a leaflib/migrate-fold.ts(fs +createLinkonly), and the fullmigrate.jsimport is deferred until a missing/stale v19 sentinel actually requiresrunMigration(). Source:apps/cli/src/index.ts,apps/cli/src/lib/migrate-fold.ts,apps/cli/src/lib/migrate.ts.Grok's binary resolver could silently launch a different agent's binary (RUSH-2459). When no
grok-*filename in a version-home's.grok/downloadscarried the pinned version string — which happens routinely, since grok self-updates its binary in place — the dispatcher shim, the directgrok@<version>alias, andgetBinaryPathall fell back to whatever a plain directory listing returned first, with no validation that the candidate was actually a grok binary. On one machine a stale, unrelated 99-byte wrapper script (exec cursor-agent "$@") sorted before the real ~127MB self-updated binary and was launched instead, soagents run grokand Factory's "New Grok" silently ran Cursor. A second, compounding bug made the shim's "exact version match" a no-op for the versioned home: it greppedls's full path output, and the version-home's own path always contains the version string, so it matched every candidate unconditionally. Both are fixed: the exact-match check now scopes to each candidate's filename, and when no filename genuinely carries the version, the fallback rejects any candidate under 1MB (real grok binaries are ~100MB+) and prefers the most recently modified survivor — never guessing among untrusted artifacts. Source:apps/cli/src/lib/versions.ts,apps/cli/src/lib/shims.ts.agents add warpinstalls the new Warp Agent CLI (curl installer), not the removedozbrew cask (RUSH-2461).agents add warp@latestfailed withError: Cask 'oz' is unavailable: No Cask with this name exists.— the registry modeled Warp's olderozplatform runner viabrew install --cask oz, a cask that only lives in thewarpdotdev/warptap, not homebrew-core. Thewarpentry now points at Warp's current standalone agent CLI (docs.warp.dev/cli): install via the official cross-platformcurl -fsSL https://app.warp.dev/download/agent-cli | bash, binarywarpat~/.local/bin/warp(self-updating, like droid/muse). It is an interactive TUI — barewarpopens the agent and there is no headless one-shot form (the documented flags are--api-key/--auto-approve/--resume <token>/--version, with no-p/--model/JSON output), soagents run warplaunches the TUI and the exec spec drops the staleoz agent run -p --output-format jsonmapping. The oz→warp binary rename is propagated across version detection (findInPath('warp')), the runtime + generated shims, the reserved-brand list, and the sign-in hint (barewarpopens browser sign-in, or setWARP_API_KEY). Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/exec.ts,apps/cli/src/lib/versions.ts,apps/cli/src/lib/shims.ts,apps/cli/src/lib/agent-cli-commands.ts,apps/cli/src/lib/signin-badge.ts.The 8 daemon-housekeeping routines are now daemon-owned built-ins instead of
.agents-systemconfig-repo files (RUSH-2465).usage-refresh,fleet-cache-warm,session-cache-warm,device-probe,auto-dispatch,watchdog,tmux-reconcile, andlaunch-healthwere shipped asroutines/*.ymlingh:phnx-labs/.agents-system(RUSH-2353, to gain declaration, run history, pause, and device pin over the old hardcodedsetIntervals). A daemon's own housekeeping does not belong in the config repo every install pulls, so the definitions now live in daemon code (lib/builtin-routines.ts) and are injected as the lowest layer oflistJobs()— below project > user > system. The same pid-claimedJobSchedulerschedules and fires them viaagents __daemon-tick <name>(lib/daemon-ticks.ts) exactly as before, so scheduling, run-tracking, pausability (agents routines pause <name>), and device pinning (agents routines devices <name> --set) are unchanged — the singularity guarantees (auto-dispatch's owner-pin, the cache-warms' publish-own/read-union, usage-refresh's per-account cadence/backoff) are preserved because they were never properties of the definition file. Because built-ins are the lowest layer, a same-named~/.agents/routines/file still overrides one, and a still-shipped.agents-systemYAML shadows it during the removal transition (so exactly one definition ever fires).agents routines listtags each with(built-in)and the--jsonoutput carriesbuiltin: true..agents-systemkeeps onlycheck-updates.yml; its removal PR (phnx-labs/.agents-system#272) merges only after this ships, or fleet/session/usage caches, the watchdog, and auto-dispatch would stop fleet-wide. Source:apps/cli/src/lib/builtin-routines.ts,apps/cli/src/lib/routines.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/daemon-ticks.ts,apps/cli/src/commands/routines.ts.Use one secret bundle as an account across compatible agents.
agents accountsnow keeps provider, authentication type, optional URL, stable identity, and API key or long-term setup token in the account's ownagents secretsbundle. Account bundles are permanently prompt-free (policy: never), can be selected explicitly or as a per-harness default, and can be explicitly synced to a worker withagents accounts sync <name> --device <device>. Harness-native auth remains owned by each harness and is never copied. Existing v2accounts.yamlentries migrate transactionally into bundles and retain their stable IDs. Source: RUSH-2470.scripts/release.shtakes--device <name>to pick the release Mac, instead of a hardcodedmac-mini. The build/sign/notarize/npm-publish phase still needs a Mac (codesign/notarytool + the signed binaries in the tarball), but which Mac is no longer welded into the script: it defaults tomac-miniand--device zion(alias--host) routes the privileged phase to any capable Mac at the same tagged commit — so a release is no longer stuck when mac-mini is offline. The git/PR/merge/tag orchestration still runs on whatever box invoked it, and the origin-side lease still guarantees a single releaser.scripts/remote-sign-mac.shgained the same flag. Source:apps/cli/scripts/release.sh,apps/cli/scripts/remote-sign-mac.sh,apps/cli/scripts/release.test.ts.agents sessions --active --device <box>lists only sessions actually running on that box, and an offloaded session previews again (RUSH-2479). A run dispatched withagents run --device <peer>leaves a live shim process on the dispatching machine carrying the remote run's session id, and nothing attributed that row to the peer — so--device zionlisted sessions executing onyosemite-s0, tagged[host/yosemite-s0], and every one of them rendered an empty preview ("Live session — full transcript not indexed here") because the transcript lives on the peer. Three things changed.machineis now the execution host:foldExecutionMachinefolds the machine the dispatch already recorded in the session index back onto the live row before it leaves the box, and the cross-machine fan-out no longer overwrites a peer-reported machine that names a third box. A host scope is then enforced on that field —--host/--devicemeans where the session runs, not which box answered — applied inside the single gather so the interactive browser and--active --jsoncannot disagree. And because the dispatcher's shim and the executing machine's own row now share a dedupe key, the merged fleet view keeps the row that owns the transcript instead of the[host/<peer>]placeholder. A peer's own self-report still outranks this box's index copy, so a genuinely remote row is never re-tagged. New fieldoffloadedFromonActiveSession/--active --jsonmarks the dispatcher's shim row. Contract:docs/specifications.mdSES-23a; narrows SES-GAP-5. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/lib/session/remote-active.ts,apps/cli/src/commands/sessions.ts.Monitors record a per-poll liveness heartbeat, so a watcher that never fires is no longer indistinguishable from one that never ran (RUSH-2485). Change-detection state (
lastValue/lastFiredAt) was written only when a monitor fired or set a baseline, so a--matchmonitor that polled steadily but never matched leftstate.jsonempty — andagents monitors view <name>showedstate: null, exactly what a monitor the engine never touched looks like. A monitor added as the durable lander for a PR read healthy (enabled, daemon active,testevaluating on demand) while doing nothing for ~40 minutes. The engine now writes a liveness record on every poll —lastCheckedAt,checkCount,lastError,consecutiveErrors— kept in its ownliveness.jsonso it never perturbs the baseline logic.agents monitors listandviewsurface it:never polled(yellow),checked Nx · no match yet,STALLED — last poll <ago>(red, when an enabled+owned monitor's last poll falls >3 intervals behind), and per-poll errors.--jsonon both gainslastCheckedAt,checkCount,lastError,consecutiveErrors, andstalled.agents monitors addnow asserts the postcondition — it waits for the engine's first poll and reports whether the monitor was actually picked up, instead of reporting success on config acceptance. And after 5 consecutive failed checks (a source that errors every poll, or an action that fails every fire), the engine notifies the owner once that the monitor is doing nothing. Source:apps/cli/src/lib/monitors/state.ts,apps/cli/src/lib/monitors/engine.ts,apps/cli/src/commands/monitors.ts.agents sessionsattributes offloaded work to the box it runs on, consistently. Resuming a live host-dispatched run by id (agents sessions <id>for anagents run --device <peer>session) no longer fails with "ambiguous (2 sessions)":queryIndexedSessionskept the machine derived from the transcript path for every row, and an empty file path (a remote transcript) fell back to this box — re-attributing the dispatcher's own pool row to itself so it no longer deduped against the executing peer's fan-out row. It now keeps the execution host the dispatch recorded on that row. And a remote teams teammate (agents teams add … --device <peer>) is now attributed to its execution host too:listTeamsActivefolds the teammate'shostNameintomachine/offloadedFromthe same wayagents run --devicerows are folded, soagents sessions --active --device <orchestrator>no longer lists a teammate that is executing on a peer (RUSH-2486, closing SES-GAP-10; the residual of RUSH-2479). Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/session/active.ts.A project is now a set of directories, and every spawn path reaches all of them (RUSH-2489).
ProjectDef.repos[]could already name additional checkouts, but nothing wrote it and nothing that started an agent read it:agents run --project rushlanded indefaultPath ?? rootand every other repo the project bound was invisible to the agent. Work spanning a CLI, its website, and the DotAgents repo it ships into needed three agents or a hand-passed--add-dir.Writing.
agents projects addgains a repeatable--dir <path...>;agents projects setgains repeatable--add-dir <path>/--rm-dir <path>plus--slug <owner/repo>. Each value names a local directory and the slug is read from that directory's ownoriginremote, never inferred from its path — a checkout at~/src/github.com/muqsitnawaz/agents-cliwhose origin isphnx-labs/agents-clirecords what it actually pushes to.--slugcovers a directory with no origin and applies to a single--add-dir. Removals apply before additions, so--rm-dir old --add-dir newre-points a directory in one command. Binding a directory twice, unbinding one that is not bound, and naming a path that does not exist are all refused with the fix named.Spawning.
agents run --projectkeeps returning the primary directory as cwd — unchanged — and merges the project's other directories into--add-dir, deduped against anything passed explicitly, locally and over--host(forwarded~/…, which the receivingagents runexpands against the host's own$HOME).agents teams creategains--project <slug>: it is validated at create time rather than at the firstteams add, the project's primary directory becomes a local teammate's base cwd, and the sibling directories are attached as grants. The teammate record stores the project name, not a frozen list of directories, and the grants resolve per launch — an unpinned teammate on a--devicespool only learns its host from the scheduler at launch time, so add-time resolution would have handed it this box's absolute paths (and already dropped any directory that exists only on the host it landed on). The team cwd precedence is nowworktree → --cwd → project directory → process.cwd(), so an explicit--cwdstill wins; the grants are attached either way, since the siblings are what the project binds rather than where the teammate sits. A--project slug@worktreerun keeps the worktree as cwd and grants the main checkout alongside the siblings. A teammate staged behind--afterresolves its grants when the supervisor finally launches it, so a restart in between changes nothing.Two honest limits, stated rather than papered over. Only Claude and Codex consume
--add-dir(Claude takes the native flag, Codex folds the paths intoworkspace_roots); every other harness ignores the grants and sees the cwd alone. And a bound directory that is absent from the current box is skipped for a local spawn rather than erroring — but is kept for a--hostrun, because the target machine has its own checkouts and this box's filesystem must not decide what exists there.--add-dirvalues are now~-expanded at the consumer (buildExecCommand, for both the Claude flag and Codex'sworkspace_roots). Nothing was doing it: a forwarded grant crosses the SSH boundary single-quoted, so the remote login shell left~/…literal and the harness resolved it as a directory actually named~— a silent no-op. This also fixes the pre-existing case of a user typing--add-dir '~/x'on a--hostrun.The
repos[]walk is now one function shared by the spawn path and the fleet workspace probe (projects status), which previously had its own copy. The probe keeps its own primary (root, notdefaultPath, so a monorepo subproject still probes its checkout) and keeps missing directories so it can still report✗ missing. Also corrects theagents projectshelp note, which still told users to sync definitions with the long-removedagents push/pullinstead ofagents repo push user. Source:apps/cli/src/lib/projects.ts(projectDirsAbs,projectProbeTargets),apps/cli/src/lib/project-root.ts(resolveProjectDirs),apps/cli/src/commands/projects.ts(projectRepoFromDir),apps/cli/src/commands/exec.ts,apps/cli/src/commands/teams.ts,apps/cli/src/lib/teams/{registry,agents,api}.ts,apps/cli/docs/projects.md.agents daemon statusnow reports a daemon running code that has been deleted (RUSH-2493). A daemon keeps running from memory after its entry file is removed — so it answers every probe,systemctl --user is-activereadsactive, and the status command reported healthy, while the process cannot restart and is executing whatever was loaded before the delete. Observed on a dev box: a daemon ran 4h14m from a worktree that had been deleted, silently holding a second routine scheduler (the double-fire class the one-scheduler-one-executor rule exists to prevent), and nothing anywhere named it.statusnow carries aStale codesection listing each such pid with its missing entry, marks theBinary:line(MISSING from disk)when it is this device's own daemon, anddaemon doctorraises it as a problem with the right remediation —agents daemon restartfor your own,kill <pid>for a stray this install owns.--jsongainsbinaryMissingandstaleBinaries, each stale row carryingactionableso a machine caller can tell a ghost it may act on from one that is merely visible.Detection and accusation are scoped separately. Anything actionable — a
doctorproblem, a non-zero exit, akillinstruction — is limited to this device's daemon plus this install's instance registry, the scope RUSH-2368 established after a leaked test fixture was reported as a stray tokill. Merely showing a stale daemon extends to any process running as the same uid, because the incident that motivated this was neither the tracked pid nor registered: it ran from an ephemeral/tmpcwd, whichlib/daemon.tsdocuments as invisible to the registry by design, so registry-gating the display would have left the command silent on the exact case it exists for. A different uid is never named at all — its entry cannot be reliably stat'd and could not be signalled anyway.Three further limits keep it from accusing a healthy daemon: it reports only a provable
ENOENT(statSyncwiththrowIfNoEntry, sinceexistsSyncalso returns false on a permission error — a root-owned daemon under an unreadable parent would otherwise be named); it considers absolute entry paths only, sonode -e '<code>' __daemon-runis never mistaken for deleted code; and that last guard also exempts entry paths containing spaces, whichpsrenders unquoted — a healthy daemon on such a path is never accused, at the cost of not detecting a genuinely deleted one. Source:apps/cli/src/commands/daemon.ts,apps/cli/src/commands/daemon.test.ts.Fixed a Windows-only CI failure in the binary-shadow test.
detectAgentsBinaryShadowswas already comparing files by identity, but its test still compared two path spellings throughfs.realpathSync. On WindowsrealpathSyncdoes not expand an 8.3 short name, so awhere-resolved path and one built fromos.tmpdir()compare unequal even when they name the same file — which is why every PR touchingapps/clisawwindowsfail on a GitHub runner (C:\Users\RUNNER~1\...vsC:\Users\runneradmin\...). The test now identifies the file by basename plus contents, which is spelling-independent and asserts the stronger property.The auto-detected browser
defaultprofile no longer churns the fleet-sharedagents.yaml.ensureDefaultBrowserProfileregenerates that profile on everyagents browser startwithout--profile, writing an absolutebinary:path and a port chosen by probing the local machine. Because it lived in the synced file, a macOS box wrote/Applications/Google Chrome.app/..., a Linux box found that unlaunchable and rewrote/usr/bin/chromium-browser, and the two flipped the tracked file back and forth forever — the single largest source of churn on it. The autodefaultnow lives in that box's own~/.agents/devices/<machine>/agents.yaml, which is gitignored. User-created named profiles stay central and still sync; reads merge both maps, with the machine-local copy winning a name collision (it was written by this box, for this box). Source:apps/cli/src/lib/browser/profiles.ts,apps/cli/src/lib/state.ts.projectRootis machine-local too.ensureProjectRootinfers it from whatever directory the CLI happened to run in, so it is machine state rather than fleet policy; it was being cached into the file every machine syncs. Source:apps/cli/src/lib/state.ts.Browser profile writes no longer clobber a concurrent write.
createProfile/updateProfile/deleteProfiledid an unlockedreadMeta(), mutated, thenwriteMeta(meta)— persisting a snapshot taken before the lock, so a newer write from another process was silently lost. They now go throughupdateMeta, which re-reads under the lock. Source:apps/cli/src/lib/browser/profiles.ts.A config read no longer rewrites the fleet-shared
agents.yaml. The legacy device-config fold hung offgetConfigValue/setConfigValue/unsetConfigValue, so an ordinaryagents config getcould rewrite~/.agents/agents.yaml— a file every machine in the fleet tracks and syncs. With 13 machines each dirtying one shared path on nearly every command, boxes stopped being able to pull at all (yosemite-s0sat 4 commits behind, unable to fast-forward past its own local rewrite). The fold now runs only from a lifecycle entry point (daemon boot,runMigration), never from a read or write. Source:apps/cli/src/lib/device-config.ts.The device-config migration is additive instead of destructive. It used to
fs.rmSyncthe per-devicedevices/<host>/agents.yamlandfs.rmdirSyncits directory after folding. Deleting the source mid-rollout meant a box still on the previous CLI lost the config it was still reading, and a box that re-created the doc got stripped again on its next command. The fold now leaves every legacy store in place; the redundant copy is pruned later by one explicit operator command rather than by each machine independently. Source:apps/cli/src/lib/devices/config-migration.ts.agents devices captureno longer erases a peer's config.captureFleetrebuiltfleet.devicesfrom the captured roster alone, so a device the capturing box had not seen was dropped along with itsconfig:block — observed for real, a capture onyosemite-s0deletedzion's entire config from the shared file. A dropped device now carries itsconfig:forward; the roster fields still reflect live state. Source:apps/cli/src/lib/fleet/capture.ts.Clearing the last model-tier override no longer leaves
model: {tiers: {}}. The emptied container was written back to the sharedagents.yaml, showing up as a spurious local change on whichever box ran the command. It now drops the key, matching how an emptiedhosts:is already handled. Source:apps/cli/src/lib/model-tier-overrides.ts.The agents daemon starts at install/upgrade and on first
agents setup/setup --force, not only afterroutines add.scripts/postinstall.jshealLongRunningProcessescallsstartDaemonon darwin and linux whendaemon.enabledis not false (bounce when already running so upgrades load the new binary; cold start writes the LaunchAgent/systemd unit so KeepAlive/Restart=always apply). If the daemon was running underdaemon.enabled=false, postinstall stops it and leaves it down. First-run /--forceagents setupcallsstartDaemon()after the system repo is ready (hub re-entry without--forcedoes not). The CLI hot path is unchanged — no per-invoke ensure. Companion: phnx-labs/.agents-system#291 (check-updatesdaily +agents repo sync system+agents sync --local -ywhen HEAD moved). Source:apps/cli/scripts/postinstall.js,apps/cli/src/commands/setup.ts.A dev build no longer takes over the installed
agentscommand.scripts/install.shpublished its side-by-side build into~/.local/binunder the production namesagents,ag, andbrowser, so which code ran was decided by PATH order rather than by what you typed — and once the dev prefix was cleaned, those links dangled and the production command failed withno such file or directory. The dev build is now exposed asagents-dev(andag-dev); the script never creates or overwritesagents,ag, orbrowser, and on each run it removes any such shadow link an earlier revision of itself left pointing into the dev prefix — including a dangling one, which[[ -e ]]cannot see.browserleaves the dev link set entirely:agents-dev browser …reaches the same code. The restart of the shared routines daemon (secrets broker, browser IPC, scheduler) is now opt-in behind--bounce-daemoninstead of automatic, because pinning that daemon to a working-tree build changes what the everydayagentstalks to whileagentsitself still looks untouched. The PATH-precedence warning is gone — a distinct name makes ordering irrelevant. RootAGENTS.mdnow states the rule for agents working in this repo: nevernpm i -gfrom the working tree, and never claim the production bin names. This supersedes the dev-build shadow described in the RUSH-2446 entry above — that rollout probe stays as the general backstop for any other install that takes theagentsname. If removing the shadow leavesagentsunresolvable (npm skipped its own~/.local/binlink because the shadow already answered the probe), the install says so and prints the one command that restores it, instead of claimingagentsis untouched. Source:apps/cli/scripts/install.sh,apps/cli/scripts/install.test.ts,AGENTS.md,apps/cli/AGENTS.md.agents devices config <name> [key] [value]is the one settings surface for a device. Bare opens an interactive settings menu on a TTY (and prints the resolved config when piped or given--json);keyreads a value back,key valuesets it with validation (booleans take on/off/true/false),key --unsetrestores the default, andnotes <text>appends an operator note. Device-scope settings — the existingagents.max-concurrent/scheduler.enabled/daemon.enabled/watchdog.enabled/browser.remote-control/browser.profile/notes, plus newssh.user,ssh.auth,ssh.bundle,ssh.bundle-key,ssh.identity-file,platform,auto-launch.enabled, andauto-launch.preferredkeys — now live centrally in~/.agents/agents.yamlunderfleet.devices.<name>.config(synced and backed up with the repo; afleet.devices: alldeclaration upgrades to an explicit roster map on the first write). Thessh.*/platform/ user values overlay the registry's discovery record at dial time, soagents ssh, the ssh_config render, host dispatch, anddevices listall honor them. The retired subcommands (configure,note,set,set-interactive,enable/disable/prefer/unprefer) keep working as hidden tombstones that print a deprecation notice on stderr and forward intodevices config. Existing per-device config (devices/<name>/agents.yamlconfig,defaultBrowserProfile) and.history/devices/auto-launch.jsonfold into the central block via a one-time migration (first config read/write, daemon boot, orrunMigration); device docs keep their agent pins. Source:apps/cli/src/commands/ssh.ts,apps/cli/src/lib/device-config.ts,apps/cli/src/lib/devices/resolve-profile.ts,apps/cli/src/lib/devices/config-migration.ts.agents devices --helpis an intent-based menu instead of a flat 27-command list. The subcommands are grouped by what the operator is trying to do — Discover & register, Inspect, Configure a device, Factory auto-launch, Fleet operations — via the existingregisterCommandGroupsformatter (the same patternbrowser,computer, andsecretsalready use), and the "Typical workflow" block moves from the bottom of the help (.addHelpText('after', …)) into a workflow-firstExamples:section right under the description viasetHelpSections. No commands, flags, or behavior change. Source:apps/cli/src/commands/ssh.ts,apps/cli/src/lib/help.test.ts.The VS Code extension is now AGI EXT; its dashboard is Fleet.
apps/factorymoved toapps/ext— the component is a thin UI wrapper over the CLI, not a factory. The webview tab and navbar read AGI EXT, and the agent-status dashboard formerly called "Factory Floor" is now Fleet. A dashboard tab restored from a pre-rename build is reclaimed rather than left beside the new one. Marketplace identity is unchanged (publisherswarmify, nameswarm-ext), so installs and theswarm-ext://URI keep working. Unrelated systems that share the word keep every identifier, path, and env var they had — Factory.ai/droid (~/.factory,FACTORY_API_KEY, thefactorycloud provider), the beta-gatedagents factorySoftware Factory command (FACTORY_FLOOR_URL,~/.agents/factory.yml), and Rush Cloud's own Factory Floor. Their comments and their user-facing labels still name them —agents teams --task-typeremains a "Factory label" because it configures the Software Factory worker, not this dashboard.agents feed post --notifyraises a local desktop banner on top of any configured broadcast. A feed post already forwards outward throughfeed.broadcastsinks (a Linear comment, an owner-channel iMessage), but reaching the local desktop meant hand-declaring achannel: desktopsink inagents.yaml.--notifyis the per-post, config-free equivalent — the samenotifyDesktopbanneragents run --notifyfires — added on top of whatever delivery is already configured, never replacing it. It routes through the realdesktopchannel provider like every otherchannel:sink, so it appears in the post's broadcast outcomes and the--jsonpayload rather than a side path. Two properties follow: it carries nominLevel, so a routine milestone post can raise a local heads-up without animportant-gated phone sink buzzing the phone; and the banner is local (it reaches whoever is at the machine the post was authored on, a no-op with a stated reason where no notifier exists), so a headless post never mis-fires at the operator's Mac — reaching a phone stays the job of animportant-level owner/broadcast sink. Source:apps/cli/src/lib/feed-broadcast.ts(withDesktopNotify),apps/cli/src/commands/feed.ts.agents subagents listno longer reports every Codex target asmissingwhile the.tomlfiles are present (#2399). Codex custom agents are flat TOML (name/description/developer_instructions) under~/.codex/agents/, but the registry reader used the markdown YAML-frontmatter parser, which returns null on TOML and dropped every entry from the rich listing. Codex still loaded the files at runtime — only the listing surface was wrong. The Codex target now uses a TOML metadata reader (same escape hatch as Kiro JSON / Goose YAML). Source:apps/cli/src/lib/subagents-registry.ts.agents sessions focus <id>reaches a session on another device without--device. Focus resolves an id/identity selector across the reachable fleet (the same resolverresumeandpreviewuse), but it then required the resolved session to ALSO appear in its filtered candidate pool — which is scoped by project, time window, and device — so a peer-owned, older, or other-project session was found and then rejected withSession <id> does not match the selected focus filtersunless you passed--device <host>to pull it into the pool. Focus now uses the fleet-resolved session directly (focusTargetForResolved), hopping to the owning device via the existing recovery path; the display filters scope the browsable picker, not an exact id lookup.resumealready resolved fleet-wide and is unchanged. Source:apps/cli/src/commands/focus.ts.
Grok usage bars ignore expired / missing last-seen billing
agents view grok reads weekly usage from each machine's local
.grok/logs/unified.jsonl (network: false). Two bugs made the bars disagree
across devices and lie after a weekly reset:
- A billing line with no
creditUsagePercentwas coerced to 0%, so a fresh period looked empty instead of unknown. - An expired period (e.g. last week's 100%) was still rendered, so one box
showed
rate-limitedafter the window had already reset.
Expired windows are dropped via the same freshness check used for the Claude usage cache; missing percents no longer invent a bar. Live cross-device parity still requires a Grok session on that box to write a new billing line — there is no network usage probe for Grok yet.
agents inspect --<kind>now uses the same interactive picker + live preview asagents skills list. The drill-down listings were the last ones printing a bespoke two-line-per-entry dump with a[source]tag repeated on every row — 13 of 80 columns spent on one bit of information — and a description truncated mid-Triggers on:boilerplate, which is what 15 of the 20 skills in.systemappend to theirs. Rows are now one line (Name · Size · Description) showing the description's first sentence, and the preview pane below refreshes as the selection moves, carrying the full text plus per-kind metadata: a hook shows what fires it and whether it is wired (previously the drill-down showed only a size, less than the summary view printed); a skill shows its frontmatter triggers, model and tools; a plugin lists its bundled skills and commands one per line. The piped path keeps its plain table plus each plugin's bundled skills and commands, and the picker only appears on a TTY.--jsonkeeps its shape (onedescriptionvalue changes, per the next entry). Also fixes the overview's resource preview cutting its own…(+16)tail at any terminal width,1 files, and the Hooks section leading with*_testscaffolding instead of registered hooks. Source:apps/cli/src/commands/inspect.ts,apps/cli/src/commands/resource-view.ts.agents inspect --hooksno longer shows a line of shell as each hook's description. All 53 hooks in.systemrendered!/usr/bin/env bash— the "first prose line" fallback is a Markdown heuristic, and on a script it returned the shebang with its#stripped. Skipping that line only promoted the next one (set -euo pipefail, a Python docstring), so the agent path now reports a hook as having no description at all — matching what the repo path already did — and the column shows the hook's firing events instead, which is the information that view is for. Descriptions read from the first line of non-Markdown resources (anmcp.yaml, a.toml) are unaffected. Source:apps/cli/src/commands/inspect.ts.agents inspect --hooksnow shows what fires each hook, in the column that was blank. The Description column for a hook was either code or nothing; it now carries the same event summary the overview prints (PreToolUse(Bash),SessionStart), and a hook the manifest does not register shows a blank events cell, with the preview pane spelling outnot registered. A repo target resolves those events from that repo's ownagents.yamlrather than whatever is installed centrally. Source:apps/cli/src/commands/inspect.ts.agents inspect <repo> --routinesshows what a DotAgents repo schedules, and whether it actually fires.inspectcould drill into eight resource kinds but not routines, so the one kind carrying live operational state was the one you could not inspect —agents inspect . --routineserrored withunknown option.--routineslists every routine the repo declares with its schedule, what it runs, the devices whose allowlist enables it, and how it last ran;--routine <name>drills into one; and the plain overview gains aRoutinessection beside Hooks/Plugins/MCP. The listing folds in live state deliberately, because the YAML alone is misleading: a routine's owndevices:pin does not enable it — membership in a device'sroutines:allowlist does (applyDeviceActivation→routineEnabledOnThisDevice) — so a definition-only view reportsgit-review → zionfor a routine that has not fired in days. When routines sit on no device's allowlist the section says so outright and names theagents routines devices <name> --set <device>fix;--jsonreportsdevices(the YAML pin) besideenabledDevices(the live fleet answer), reusingagents routines list --jsonfield names where they overlap so one script consumes both. Rows sort broken-first (inert config, then fires-nowhere, then disabled, then failing) because the overview section shows only the first six — with 24 routines the sort is the section. Three supporting fixes: a config that fails closed (unparseable YAML, a legacydevice:key, a non-listdevices:) is now listed with the reason instead of silently vanishing (readJobFileResult, extracted from the privatereadJobFile); a corrupt peer device file is collected and reported rather than thrown, so one bad file cannot blank the whole listing (routineDeviceIndex, a non-throwing single-pass sibling ofdevicesWithRoutineEnabled); andResourceItem.extranow reaches--jsonfor every kind, not just plugins, so the detail view and its own preview pane cannot disagree. The collector reads*.ymlfiles only —routines/also holds<name>/home/sandbox overlay HOMEs, and a flat readdir counted 49 routines where the repo declares 24. A bareroutines/directory is deliberately not a DotAgents marker: shipping example routines is a normal pattern (this repo's ownapps/cli/routines/), and counting it madeagents inspect apps/cliresolve a source tree as a repo. Source:apps/cli/src/commands/inspect.ts,apps/cli/src/lib/routines.ts,apps/cli/src/lib/routine-activation.ts.Kimi subagents are written in the format kimi-code actually reads, and gated at 0.29.0. The integration emitted a
<name>.yaml+<name>.system.mdpair plus a managed_agents-cli.yamlparent index, targeting theversion: 1/agent:agentspec of the older, separatekimi-cliproduct.@moonshot-ai/kimi-codehas no loader for that schema —system_prompt_pathappears nowhere in its bundle — so every synced Kimi subagent was written to disk and never loaded by any session. Kimi now gets one Claude-shaped<name>.md(frontmattername/description+ body), which kimi-code discovers from its brand home'sagents/dir. Discovery landed in kimi-code 0.29.0; 0.28.x and earlier compile their four agent profiles into the bundle with no filesystem loader, so thesubagentscapability is now>= 0.29.0and older installs skip with a stated reason instead of writing files nothing reads. Verified against a real kimi 0.29.0:--agent no-such-agentreportsAvailable profiles: plan, agent, coder, explore, code-reviewer. Source:apps/cli/src/lib/subagents-registry.ts,apps/cli/src/lib/agents.ts.Homes synced before this fix carry stale
<name>.yaml,<name>.system.md, and_agents-cli.yamlfiles in~/.kimi-code/agents/. A one-shot migration (migrateKimiSubagentsToMarkdown, migration schemav19) removes them from every kimi version home on the first run after upgrading — notagents prune cleanup, which could never reach the two.yamlfiles because they match no subagent enumerator. Without it the leftover<name>.system.mdwould be listed as a phantom subagent named<name>.systemand warned about by kimi-code once per session, since it ends in.mdand carries no frontmatter. A legacy pair is matched by its signature (a<name>.yamlwith a sibling<name>.system.md), so a subagent you named<x>.systemyourself is left alone.Per-machine config no longer lives in the fleet-shared
agents.yaml. Every device-scope config key now declaresvisibility, which asks who READS it.sharedkeys stay in the syncedfleet.devices.<name>.configbecause a peer resolves them — the ssh fields andplatformare needed to dial a box before it is reachable,agents.max-concurrentdrives teams placement,auto-launch.*andnotesfeed fleet views.machinekeys —browser.profile,browser.remote-control,scheduler.enabled,daemon.enabled— move to that box's own~/.agents/devices/<machine>/agents.yaml, which is gitignored. That is what stops 13 machines writing one tracked path. Source:apps/cli/src/lib/device-config.ts,apps/cli/src/lib/state.ts.browser.remote-controlwas a consent leak. It gates whether OTHER machines may drive this box's browser, and its own help text promised "device-local, never synced" — but it was stored in the file the fleet syncs, so one box's opt-in propagated to the rest on pull. It is now machine-local, and setting or reading a machine-local key for a peer is refused outright with theagents ssh <device>form to use instead. Source:apps/cli/src/lib/device-config.ts.A new config key cannot silently pick the wrong home.
ConfigKeySpecis a discriminated union, so omittingvisibilityon a device-scope key is a COMPILE error — the same disciplineMETA_KEY_SCOPEalready applies toMeta. The migration no longer folds machine-local keys at all: they already sit where the new read path looks, and copying a peer's would spread that consent flag fleet-wide. Values an older CLI wrote centrally are still honored until overwritten, so a mixed-version fleet keeps working. Source:apps/cli/src/lib/config-machine-keys.ts,apps/cli/src/lib/devices/config-migration.ts.Monitor
condition: { mode: every }no longer fires on an empty observation (RUSH-2488).everypreviously returnedfire: truefor every tick regardless of the observation, so a poll whose command produced no rows still dispatched the action with an empty{event}. It now stays silent on an empty or whitespace-only observation and fires on every tick that carries real output. This gives a poll-driven monitor the "re-fire while the watched set is non-empty" semantics that a silently-failed action dispatch needs to be retried — an action failure leaves the same non-empty observation next tick, soeveryre-fires (bounded byrateLimit), wherematch/on-changewould dedupe and never retry. No shipped monitor usedevery, so there is no behavior change to an existing monitor. Source:apps/cli/src/lib/monitors/engine.ts,apps/cli/src/lib/monitors/engine.test.ts.agents monitors addrefuses a monitor that duplicates one you already have. A monitor's NAME was not its identity:writeMonitoroverwrites by name and nothing compared arguments, so two watchers polling the same source on the same interval and firing the same action were one trigger fired twice — under different names, with no warning. One real box accumulatedopen-pr-watch,pr-ci-fail, three stalepr2222-*watchers and an agent-added lander all polling the same PR queue.addnow refuses a same-name collision (which would silently overwrite) and a same-behavior collision — checked across the fleet, not just this box, because the case that actually bites is two agents on two different machines creating a watcher for the same work item with the same arguments, and neither can see the other's monitors dir. The check reuses the cross-machine fan-outsessions --activealready runs, names the device and monitor it clashed with, and when a peer is unreachable says so explicitly rather than treating "could not ask" as "no duplicate";--forceproceeds anyway. Identity is a fingerprint over source + condition + action, deliberately excluding name, description,enabled, and placement (device/devices/runOn) — placement is who executes, not what runs, and hashing it would let the same watcher be re-added by varying only the owner. Different arguments still coexist — a watcher for PR #2517 and one for #2600 are two monitors, not a clash; that is the common case. Also documents the split the subsystem already had but never stated: a durable monitor is config, an agent's per-work-item watcher is running state, and runtime (last-seen value, fires, rate-limit counters) lives under~/.agents/.history/monitors/. Source:apps/cli/src/lib/monitors/fingerprint.ts,apps/cli/src/lib/monitors/remote.ts,apps/cli/src/commands/monitors.ts,apps/cli/docs/10-monitors.md.agents sessions focus/attach/resume/preview, the bareagents sessions <id>,run --resume, andsessions --resolveno longer hard-fail when a fleet device is offline. Resolving a session id used to abort the moment any registered device was unreachable — even when the session lived on a box that WAS reachable — printingCould not resolve session while these devices were unavailable. Now an unreachable peer is a one-line warning: the session resolves against the reachable fleet and attaches, and the command fails only when the id is found on no reachable device (worded so the offline, unchecked peers are named, not blamed). The exit code for that offline-peer resolution failure changes from2to1(an ordinary not-found failure, no longer a distinct "could not decide" code) across every resolver consumer —focus.ts,attach.ts,resume.ts,exec.ts, and all threeresolveSessionMetadataValuecall sites insessions.ts. (RUSH-2492)Dead tmux sessions are now reaped automatically every 5 minutes. Sessions stay open after their process exits because
remain-on-exit onis set so the harness can inspect the exit status. Previously they accumulated indefinitely — 127 tmux sessions with 48 dead (~38%) observed on a production fleet machine. The daemon now cleans them up on a 5-minute timer (same cadence as the keychain reaper) and immediately on startup to clear the backlog. Safety invariant: only sessions where all panes are dead (pane_dead=1) are killed; any session with a live pane is never touched. On-demand cleanup is also available:agents sessions reap [--json] [--socket <path>]. Source:apps/cli/src/lib/tmux/session.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/commands/sessions-reap.ts.agents sessions --activenow attributes cursor/grok/kimi/droid sessions. The ps-scan identity path fell back toloadHookSessionIndex(), which scansterminals/sessions/— a directory populated only by the@agents/session-trackerpackage, which is not deployed on most fleet machines. The SessionStart hook that IS deployed writesstate/sessions/<pid>.jsoninstead. The ps-scan path now also triesreadStateSessionRecord(pid)after the index lookup finds nothing, so non-Claude harnesses that carry no--session-idargv can be attributed from the deployed hook's state file. Source:apps/cli/src/lib/session/active.ts.agents routinesis now an interactive browser. On a terminal, the bare command opens a filterable, grouped picker (reusing the same picker primitive behindagents sessions) instead of printing nothing but help. The project / device group headers show as inline dividers, and selecting a routine drills into four blocks — Definition, Next fire, Recent runs, Stats.agents routines --jsonand any non-interactive shell keep the exactagents routines listoutput, so pipes and the menu bar are unaffected. (RUSH-2503)agents secrets export --hostraises Touch ID at a terminal instead of demanding a prioragents secrets unlock. Pushing a locked keychain-backed bundle to another machine failed with "Secrets bundle '' is not unlocked in the secrets agent" even for a human typing the command at a TTY —resolveBundleForPushpassedagentOnly: trueunconditionally, while its sibling reads (view --reveal,exec) already decided this per-invocation withisHeadlessSecretsContext() || !isInteractiveTerminal(). The push is strictly less exposed than theview --revealthat already prompts: it prints a key count, never a value, and nothing captures its stdout, so the prior-unlock requirement was an inconsistency rather than a boundary. An agent launch, a piped/CI run, andpushBundleToHost(fleet apply) are unchanged — they stay broker-only and still fail fast with the unlock hint, which is what keeps the RUSH-2440 keychain-enumeration prompt storm off the agent path.export --plaintext/--to-file/--to-1passwordare also unchanged: they emit values into a pipeline or onto the screen, so they remain unconditional automation primitives. Spec updated in lockstep (SEC-13b and the §4.2 materialization table). Source:apps/cli/src/lib/secrets/push.ts,apps/cli/src/commands/secrets.ts,apps/cli/docs/secrets.md,apps/cli/docs/specifications.md.A short session id resolves even when a fleet peer is offline.
agents sessions preview <shortid>,agents resume <shortid>,agents sessions attach, andsessions --resolve --jsonpreviously refused to resolve whenever any registered device failed to answer the fan-out — printingPartial session resolution: <peers> did not answerand rendering nothing, even though exactly one session on the reachable fleet matched — so on a fleet carrying any permanently offline device, every short-id lookup was voided. Once the sweep is over, a selector that is a complete id or at least 8 hex characters wide (the printedshortIdwidth) now resolves from a single reachable match. Deliberately unchanged: a keyword-shaped query still fails closed even when it is all hex characters (facade,decade— searches, not identifiers); a label still fails closed at any length, because labels are free-form and collide by design; a prefix collision between peers that did answer still never resolves — it fails closed while a peer is missing, and surfaces as an ambiguity listing both machines once every peer has answered; and RUSH-2203's mid-sweep early-exit stays full-UUID-only. This amends the normative SES-9a requirement, which previously mandated fail-closed for every short prefix; the accepted collision risk is stated there, including that a time-ordered UUIDv7/ULID prefix collides far more readily than a random UUIDv4 one and the residual exposure is a collision hiding on the peer that did not answer. Source:apps/cli/src/commands/sessions.ts(isUniqueEnoughSelector,metadataResolveOutcome,renderSessionPreview),apps/cli/docs/specifications.md§SES-9a / §SES-IF-2a,apps/cli/docs/sessions.md.agents teams addwarns and blocks when the base checkout is behindorigin/main, requiring--confirmto proceed. Pointing a team at a stale repo (local cwd, or the repo provisioned on a--devicehost) meant teammates reasoned and built against code that had already moved on — the real incident was a 71-commit-stale checkout on another box that nobody had fetched.teams addnow fetches origin, counts how far behindorigin/<default>the base is, and refuses with a sync command (git … merge --ff-only origin/main) unless you pass--confirm; with--confirmit prints a one-line advisory and continues. An offline/unreachable/non-git base can't be assessed and never blocks. Cloud teammates clone fresh in the provider and are skipped. Source:apps/cli/src/commands/teams.ts,apps/cli/src/lib/teams/worktree.ts,apps/cli/src/lib/teams/remoteWorktree.ts.Webhook handlers (
~/.agents/webhooks/*.yml) can now anchor a dispatched agent in a project. A handler'srun.agent/run.workflowaction previously always ran at the target box's$HOMEwith no repo checkout and a hard-codedautomode, so an inbound GitHub/Linear event could not fire an agent that actually edits a repo without routing through a routine. Handlers gain three top-level fields mirroring a routine's job config:project(namedagents projectsexecution anchor — the run lands in that project's base directory),cwd(portable execution directory, resolved underprojector$HOME), andmode(plan/edit/auto/skip/full, defaultauto). They thread into the dispatchedJobConfigfor therun.agent/run.workflowpath and override theroutine:delegate;run.commandis unaffected (use acdin the command). Source:apps/cli/src/lib/triggers/handlers.ts.
1.22.35
agents doctor --fixandagents upgradepurge stale multi-install agents-cli copies instead of only warning (RUSH-2415). Pre-1.22.30 installs (and "unsafe legacy helper installer" / npx-cache trees) re-introduce the Touch ID storm + usage-API revocation class fixed in v1.22.30. Detection already existed (findAgentsCliInstalls/ multi-install warning) but left remediation to the user. Bareagents doctor --fixand a successfulagents upgradenow delete npx-cache installs, non-atomic helper installs, and pre-1.22.30 package roots when a fixed peer already exists on the box — never the running copy, never a lone pre-fixed install that would strand the machine. The multi-install warning points atagents doctor --fix. Source:apps/cli/src/lib/self-update.ts,apps/cli/src/commands/doctor.ts,apps/cli/src/index.ts.agents share delete(aliasagents unshare) takes down a published page (RUSH-2428).agents sharepublishes to a public URL with no way to take one down — the Cloudflare Worker already implements an authedDELETE, the CLI just never exposed it. The new command accepts a full share URL,<user>/<slug>, or a bare<slug>(resolved against your own namespace exactly as publish does), takes several targets at once, and by default also deletes the sibling<slug>.pngOG cover (--keep-coveropts out) — without it, republishing over a slug replaced the page but left the old cover screenshot publicly readable, which is what made a real takedown slow.{"ok":true}from the Worker is not treated as proof: a follow-up check must resolve 404 before the command reports success, and it errors loudly (non-zero) instead of if it can't verify the object is actually gone. An already-missing target is an error by default;--if-existstreats it as a no-op success. Source:apps/cli/src/lib/share/delete.ts,apps/cli/src/commands/share.ts.Codex
editruns can write under the repo's.agents/again. Codex'sworkspace-writesandbox hardcodes any.agents/(and.codex/) directory as read-only, but agents-cli keeps every git worktree at<repo>/.agents/worktrees/<slug>— so a Codex session whose cwd was the repo root hitEROFS: read-only file systemon any write into a worktree (a build'sdist/, generated files) and then had to prompt for per-command approval to escalate. Every interactive, headless, and direct-launch Codex path now makes the run's<repo-root>/.agentswritable:agents run codex(and the Windows shim delegate) add it to theagents-editprofile'sworkspace_roots, and the adopted POSIXcodexshim resolves the repo's.agentsfrom$PWDat run time (worktree-aware) and passes it via Codex's own--add-dir. (Routines use a separate overlay-HOME sandbox with their ownallow.dirsand are unchanged.) Naming the.agentsdirectory itself is the only override Codex honors — a nested sub-path makes bwrap refuse the mount. Only an existing.agentsis added; out-of-workspace and~/.configwrites stay gated, network unchanged. Source:apps/cli/src/lib/codex-policy.ts,apps/cli/src/lib/project-key.ts,apps/cli/src/lib/exec.ts,apps/cli/src/lib/shims.ts.
1.22.34
A failed
agents teams addno longer strands anagents/<name>branch that breaks every retry (RUSH-2356). The worktree was created before the teammate record was persisted and nothing removed it when the add failed, so the nextteams addwith the same--worktreename died onfatal: a branch named 'agents/<name>' already exists— observed 2026-08-07, forcing three renames before a teammate could be created. Two guarantees now hold. Name uniqueness and the--afterdependency graph are validated beforecreateWorktreeruns (AgentManager.validateAddPreconditions), so a duplicate name, unknown dependency, or cycle never creates a branch at all. A failure after the worktree exists — a missing harness CLI, a launch error, a cloud dispatch failure — removes the worktree and its branch before the command exits non-zero, and prints the exactgit worktree remove/git branch -Dpair if that teardown itself fails. Teardown is scoped twice over, because deleting a live teammate's worktree would destroy real work: only a worktree that same add created is a candidate, and only when no live teammate claims it (AgentManager.isWorktreeClaimed, a raw meta.json scan across every team, counting only non-terminal records). That second check matters because the add can fail after the record is durably saved —spawn()saves a staged teammate and only then runs the retention pass, which refreshes every sibling and can throw on a distributed one — so a teammate already recorded and waiting on an--afterdependency keeps its worktree. If we can't prove a worktree is an orphan, it is left in place with the manual removal printed — the claim check fails closed, so an unreadable or half-written record answers "claimed" rather than "free": only a genuinely absent record (ENOENT) proves nothing claims the worktree, because the alternative error deletes a running teammate's uncommitted work. The pr-watch fixer path (reactWithTeammate) and a partially-failedcreateWorktree(branch ref created, checkout not) run the same guarded teardown — the fixer path matters because it stages its teammate with--afterwhen it follows a source teammate, so it can reach the failure branch with a live, durably-recorded, merely-pending teammate already owning the worktree.One user-visible surface change: a duplicate name, unknown
--afterdependency, or dependency cycle is now rejected before the worktree step, so it surfaces as the raw validation message under the friction idteams/add-precondition-failedrather than wrapped inCould not add <agent> to <team>:underteams/add-failed. Source:apps/cli/src/commands/teams.ts,apps/cli/src/lib/teams/agents.ts.teams addno longer silently discards pending teammates past the retention cap (RUSH-2356).listCompleted()classified every non-RUNNING status — includingpending— as "completed", socleanupOldAgents()reaped staged--afterteammates once the team's history crossed the 50-record cap:teams add --afterprinted a full success block for a teammate whose record then vanished. Retention now filters on a real terminal-status set (completed/failed/stopped); a pending or running teammate is never a reap candidate.spawn()also asserts its record is durably on disk right after writing it, so a failed write can no longer report success for a teammate that doesn't exist. Source:apps/cli/src/lib/teams/agents.ts.A session recorded on Windows no longer has its
cwdcorrupted when read on macOS or Linux (RUSH-2358).normalizeCwd()is the shared cwd-normalisation path for every harness's session filtering and display, and its fallback usedpath.isAbsolute(cwd)— which is platform-relative. A Windows-rooted path (C:\Users\dev\repo\...) is not absolute on POSIX, so it fell through topath.resolve(), which silently prefixed the reading process's own working directory onto it. Transcripts sync across the fleet, so any session captured on a Windows box and indexed on a POSIX box has carried a corruptedcwdsince the fallback was introduced — visible as wrong directory attribution inagents sessionsfiltering and display, and as a lostworktree_slug. The normalisation now recognises a Windows-rooted path (drive letter or UNC) on POSIX and treats it as already absolute, mirroring the existing branch that recognises a POSIX-rooted path on Windows. Source:apps/cli/src/lib/session/discover.ts.A dead
--deviceteammate no longer reports RUNNING forever, andresumerelaunches it (RUSH-2366). The remote liveness probe only distinguished ".exitsentinel present" from "absent" — a process killed before it could write its exit code (SIGKILL, OOM, box lost) fell into the same "absent" bucket as a still-running teammate and stayed RUNNING indefinitely, soagents teams resumekept routing it to steer/mailbox instead of relaunching. The probe now resolves three states (ALIVE / EXITED / GONE) in one round-trip; GONE (process confirmed dead, no sentinel) latches FAILED. A separate stale-manager race is also fixed: a long-livedteams start --watchsupervisor's cached teammate could re-persist a stale in-memory RUNNING over a terminal status a different CLI invocation (agents teams stop) had just written to disk —updateStatusFromProcess()andrescanFromDisk()now adopt a newer on-disk terminal status instead of overwriting it. Source:apps/cli/src/lib/teams/agents.ts.agents messagecan now reach a detachedagents run --device <host> --no-followdispatch (RUSH-2366 follow-up).getActiveSessions()has no visibility into a detached dispatch's live remote process — only~/.agents/.cache/hosts/<id>.json(the recordagents hosts psreads) does — soagents message <name>reported "No running agent... matches" for a dispatchagents hosts psshowed running with a live pid, and the only recovery was killing and re-dispatching it (losing all context).agents messagenow resolves the same host-task records and reroutes the message over the existing--hostpassthrough to the box that actually owns it, or fails with an actionable "already<status>" message when the dispatch has already finished. It heals the dispatch record withreconcileRunningTasksbefore deciding, the same stepagents hosts stop/psalready take — a detached record never self-updates, so a finished run stays stampedrunningon disk and would otherwise be routed through an SSH reroute that could only fail instead of being reported finished locally. Source:apps/cli/src/commands/message.ts,apps/cli/src/lib/mailbox-target.ts,apps/cli/src/lib/hosts/tasks.ts.teams add --worktreeno longer nests a new worktree inside another teammate's worktree (RUSH-2366 follow-up). When the caller's ambient cwd was already inside a linked worktree (e.g. an orchestrator agent dispatchingteams addfrom within its own worktree),createWorktreeresolved the placement root viagit rev-parse --show-toplevel, which returns that worktree's OWN root rather than the main checkout's — landing the new worktree at.../worktrees/A/.agents/worktrees/B. Cleaning up A then destroyed B along with it, even though B's commit history survived on its branch ref.createWorktree/removeWorktreenow resolve viagetMainRepoRoot()(the existinggit-common-dir-based helper), which always points at the main checkout regardless of which worktree the caller is standing in. Source:apps/cli/src/lib/teams/worktree.ts.An interactive Claude run authenticates from your own login again, not the headless setup-token (RUSH-2395).
buildExecEnvinjected theauthbundle's per-account setup-token intoCLAUDE_CODE_OAUTH_TOKENfor every resolved Claude launch, so a hand-driven session on a personal machine reportedAuth token: CLAUDE_CODE_OAUTH_TOKENin/statusand ran on the probe credential instead of the login the user had established. That token exists for runs with no human present (usage probes, routines, dispatched runs), and it isuser:inference-scoped, so it also cannot read usage (RUSH-2392). Injection is now gated on the run resolving headless; an interactive run is left on its per-version login, and an inherited copy of that same token is dropped so a nested interactive launch from inside a headless agent's shell does not keep authenticating as it. A token the caller exported themselves is matched by value and left alone. Source:apps/cli/src/lib/exec.ts.Multiple Cursor accounts are now real — each run authenticates as the account you pick (RUSH-2400). Previously
agents view cursorshowed several Cursor accounts and balanced rotation picked one, but every run silently authenticated as the single live~/.cursorlogin:buildExecEnvgave Cursor no per-account isolation, so the version/account you selected was discarded at exec. Cursor has no config-dir env var, but its OAuth token — the login gate — lives at$XDG_CONFIG_HOME/cursor/auth.json(verified empirically;~/.cursor/cli-config.jsonis only metadata).buildExecEnvand the versioned-alias shim now pinXDG_CONFIG_HOMEat the version home for Cursor (the same XDG mechanism muse uses), soagents run cursor@<account>, the account picker, and balanced rotation each launchcursor-agentauthenticated as that account's own token — isolated per child process, so two accounts run concurrently without clobbering one another.agents view cursornow verifies each account's signed-in state against its own token (CREDENTIAL_FILE_SEGMENTS.cursor), and a migration seeds the current global login into the active account's home so no one is logged out on upgrade. The routine overlay path is unchanged (seeds from the active login by design). Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/shims.ts,apps/cli/src/lib/agents.ts,apps/cli/src/lib/migrate.ts.agents browser sessions/agents sessions --browseris task-first, with agent outcome previews (RUSH-2407). On a TTY, the picker now groups every screenshot, PDF, and recording by browser task instead of printing one row per file, newest task first. A task whoselaunchIdstill resolves to an indexed agent session shows the canonical session digest (prompt, changes, tests, last response) before its captures; a task whose owning run already stopped is labeled unlinked rather than silently dropped. Downloads get their own group. Search matches task name, profile, the linked session's agent/topic, or an artifact filename;enteropens the highlighted capture (or drills into a capture list first for a multi-capture task). New--no-interactiveflag prints the previous flat table;--jsonoutput is unchanged. Source:apps/cli/src/lib/browser/sessions-list.ts,apps/cli/src/commands/browser-sessions-picker.ts.agents sessions --routineis outcome-aware: run history, then linked sessions (RUSH-2409). Selecting a routine now renders its canonical run history first — every run's id, trigger, status (completed/failed/blocked/skipped/missed), start/duration, exit/error, execution type (agent/command/workflow), placement (local/host/cloud), and log/report paths — and links each run to the indexed agent session(s) it produced (agent/version/account/model/token/cost/duration/tool metadata). A command-only routine (e.g.auto-dispatch, which produces no transcript) no longer dead-ends into the generic "No sessions found" copy: it shows its runs and states plainly that no agent session is produced, and command/blocked/skipped/missed attempts appear with no fabricated session row. Counts distinguish run records from linked sessions, and routine selection stays global across every working directory. The drilldown is the default routine view; an explicit--flat/--treeor a session id/query keeps the scoped session listing/picker. The canonical source is the run history under~/.agents/.history/runs/<routine>/(listRuns), not the session index — no divergent state added. Source:apps/cli/src/commands/sessions.ts.Inspect Watchdog decisions and actions from the CLI (RUSH-2410).
agents watchdog history [sessionId]renders every session inspection plus persisted decisions, nudges, rotates, and errors newest-first, with duration/limit filters, optional heartbeat rows, and safe JSON that excludes transcript content. The audit retains 5,000 events, and persisted transcript context is capped at 4,096 characters per event.agents watchdog statuspoints to the history command instead of leaving the audit log undiscoverable. Source:apps/cli/src/commands/watchdog.ts,apps/cli/src/lib/watchdog/{runner,log,history}.ts.agents ssh <device>mirrors your project directory on interactive login (RUSH-2412). An interactiveagents ssh <device>with no command now starts the remote login shell in the same home-relative directory you launched from —agents ssh yosemite-s0from~/src/applands in~/src/appon the target when it exists, else the remote home — matchingagents run --host. It reuses the one canonical portable-cwd resolver (deriveMirroredCwd/remoteCdPrefix), is best-effort (a missing checkout never fails the login), injection-safe for paths with spaces or metacharacters, and covers POSIX and Windows PowerShell. An explicitagents ssh <device> <cmd…>is unchanged. Source:apps/cli/src/lib/devices/connect.ts,apps/cli/src/lib/project-root.ts,apps/cli/src/commands/ssh.ts.Make live Watchdog ticks explain each session (RUSH-2416).
agents watchdognow prints the tick date/time and attention-worthy sessions with their stable id, label/topic, agent, host app, machine, project, activity, start/activity ages, cwd, latest preview, and exact decision reason. Healthy/non-actionable inspections collapse into one count;--verboserestores every row and--jsonremains the complete machine-readable result. Source:apps/cli/src/commands/watchdog.ts,apps/cli/src/lib/watchdog/runner.ts.agents daemon startno longer defeats its own launchd/systemd launch (RUSH-2417).startDaemon()and the daemon child'sclaimDaemonInstance()resolve the same<daemonDir>/daemon.lock, and the launchd/systemd branches busy-waited onwaitForPid(3000)while still holding it — the release only happened instartDaemon's outerfinally. The freshly-launched daemon therefore hitEEXISTagainst a holder that was alive (the parent CLI process) and exited with the false "another daemon is mid-takeover" warning, so the service-manager fast-start path failed deterministically on every fresh install and fell back to a detached spawn. The start lock is now released the moment the launch has been issued — afterlaunchctl load/systemctl start/ the detached spawn — instead of after the pid wait. The concurrent-start guarantee is unchanged: twostartDaemon()calls still cannot both launch, because launchd (one plist label) and systemd (one unit) no-op a second start and the detached path is covered byclaimDaemonInstance's last-wins takeover (SING-11). Source:apps/cli/src/lib/daemon.ts.Reap orphaned keychain
watch-lockhelpers after ungraceful daemon death (RUSH-2419); await secrets broker socket release on close (RUSH-2421). The auto-lock-on-sleep watcher is still never killed while its owning daemon is alive, but a separate reaper path now cleans it up once the parent is provably gone (OOM, SIGKILL, killTree) — closing the leak that onlyclose()used to handle. Hosted and standalone brokerclose()now wait fornet.Server'scloseevent with a 2s bound so a successor cannot race a half-released socket. Source:apps/cli/src/lib/secrets/reaper.ts,apps/cli/src/lib/secrets/agent.ts.
- Searchable command API reference.
apps/cli/docs/command-reference.htmlis generated from the complete registered Commander tree and covers every visible command and nested subcommand, argument, flag form, choice, default, alias, example, and note.npm run verify:indexnow fails when the HTML or canonical JSON drifts from the CLI. Source:apps/cli/scripts/gen-command-index.ts.
agents inspectdrill-downs render readable previews and return the resources that actually exist. The detail view wrapped a description at a hardcoded 100 characters regardless of terminal width, so a long plugin or skill description lost its sentence at 80 columns and wasted the space at 200; it now wraps to the real width, and detail rows are width-aware too.--ruleslisted the composedAGENTS.mdoutput and its symlinks while hiding every fragment underrules/subrules/, soagents inspect . --rule foundationsexited 1 — rules now resolve fromsubrules/.--hooksdid a flatreaddirand reported the event directories plus doc files, disagreeing with the count the summary view printed for the same repo (12 vs 39); both now read throughlistHookEntriesFromDir. Plugin detail gains the bundle's real recursive size instead of(bundle), plus itsauthor,dependencies, and which execution surfaces it ships (hooks/,bin/,.mcp.json, …) —authorwas present in everyplugin.jsonon disk but missing from thePluginManifesttype, so nothing read it. Source:apps/cli/src/commands/inspect.ts,apps/cli/src/lib/types.ts.agents inspectno longer crashes when aplugin.jsonoragents.yamlfield's JSON/YAML type contradicts the interface it is cast to.loadPluginManifestand theagents.yamlhook reader both cast parsed input straight to a typed shape and validate almost nothing, so any field can be a string where an array was declared, or a number where a string was. Four crashes came from that:"dependencies": "some-plugin"reached.join()(truthy.length, no.join) while building the plugin list, so one bad manifest took downagents inspect .,--plugins,--json, and even a query for a different, valid plugin; a numeric"version"and a non-string"description"threw later at render, breaking the detail view and (fordescription) the plugin list; and inagents.yaml, a scalarevents: PreToolUseor a numericmatches.prompt_containsthrew insummarizeHook, breaking bareagents inspect <repo>and — through the user-level manifest —agents inspect <agent>on every machine. Every uncontrolled field now routes through one of two coercion helpers, so a scalar renders where a list was expected and an object drops its row instead of printing[object Object]. Source:apps/cli/src/commands/inspect.ts.One malformed
mcp/*.yamlno longer takes downagents inspect <repo>with an unhandled stack trace.validateMcpYamlConfigreturnsnullfor some bad shapes but throws for others —argsthat is not a string array, a non-stringcommand, a non-mapenv— and the three directory scans that call it (discoverMcpConfigsFromRepo,listMcpServerConfigs, and the layered project/user MCP resource scan inversions.ts) let that throw escape, so a single bad file in a cloned repo crashed the whole command and hid the valid configs beside it. Scans now skip the offending file and name it on stderr, exactly as they already skip anull; explicit single-file operations still throw loudly. Source:apps/cli/src/lib/mcp.ts,apps/cli/src/lib/versions.ts.
1.22.33
- Release tags are annotated with the folded changelog notes.
scripts/release.shnow createsv<version>as an annotated tag whose message isRelease <version>plus the body of.changelog/<version>.md(the same notes that already become the release PR body). Agents keep writing one fragment under.changelog/next/; there is no separate tag-description channel. The already-published missing-tag recovery path uses the same helper with--force. Source:apps/cli/scripts/release.sh.
agents sessions --routines once again opens the routine picker across every working directory instead of an empty current-repository session browser.
1.22.32
Routines validate execution context before activation and fire once per schedule slot (RUSH-2290). A routine selects one execution
projectplus a portablecwd; add/edit save proven blockers paused, durable slot and active-run claims prevent duplicate or overlapping launches, and every blocked/skipped/pre-spawn attempt remains visible without requiring a session transcript. Source:apps/cli/src/lib/routine-context.ts,apps/cli/src/lib/routine-readiness.ts,apps/cli/src/lib/runner.ts.Daemon: self-terminates if its own state dir disappears; the routines test suite reaps leaked daemons instead of letting them run for days (RUSH-2367). Three real daemon processes were found alive on a fleet box for up to 3.5 days, each spawned by a vitest fixture under its own
/tmpHOMEand invisible to everyagents daemonguard, since a differentHOMEresolves a different state dir and instance registry. The daemon now polls (AGENTS_DAEMON_STATE_DIR_CHECK_MS, default 60s) for a per-lifetime marker and exits gracefully if it disappears — unlike the pid and heartbeat files, status repair cannot recreate that marker after deleting the state tree. This is the only defense that survives the whole test runner being killed externally before any in-test cleanup can run. The routines test suite also gained a leak detector that fails the run and force-kills anything it spawned (or, on CI, any daemon it finds under its own fixture prefix from a previous interrupted run) that survived past its own test. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/commands/routines.test.ts.Daemon:
status/doctor/servicesno longer misreport test fixtures as duplicate daemons to kill, and no longer renderhealthyfor an unreachable service (RUSH-2368). Duplicate detection used to scan every__daemon-runprocess on the box, so a fixture daemon under its ownHOME(and therefore its owngetDaemonDir()/instance registry) — a separate install, or a leaked test process — showed up as a stray for the reader tokill, contradicting the reaper (reapStrayDaemons) andstop's postcondition, which already scope by the instance registry. Duplicates are now read from that same registry (findSurvivingStateDirDaemons), never a rawpsmatch. Separately, the secrets-broker and browser-IPC health lines derived theirhealthy/downverdict from the daemon's persisted last-ok record, which is only updated at the daemon's own startup — a broker that went unreachable hours into a still-running daemon renderedhealthy (unreachable)on one line. The verdict now comes from the live probe; the persisted record supplies only supporting last-ok/last-error context. Source:apps/cli/src/commands/daemon.ts,apps/cli/src/lib/daemon.ts.Agent installations are frozen, and
agents update <agent>@<installed-version>moves the release inside one (RUSH-2372). An installation used to be identified only by its version-dir name, so the vendor release was the identity: moving to a new release meant a new directory, which dangled every default, project pin, routineversion:, and profile that named the old one — and two installations of the same release could not coexist at all. Each install now carries aninstallation.jsonwith a stable opaque id plus the release currently on disk; the name is frozen for life and only the release moves, so every reference keeps resolving. Pre-existing version dirs migrate on first read.agents updatetakes--to <release>(latestby default),--account <label>to disambiguate when several installations match,--json,agents update list <agent>, and routes through--hostto update a peer's installation. The target release is fetched into a sibling directory and launched there before it replaces the working one, so a release that cannot start is discarded rather than installed; a post-swap failure restores the previous one. Update strategies are chosen from the agent registry's declared capabilities, so every harnessagents addmanages is covered (npm package, shared self-updating binary, install script) — and a harness whose binary lives outside the directory being swapped is refused with the reason instead of being recorded as updated. Not to be confused withagents upgrade, which updates agents-cli itself. Source:apps/cli/src/lib/installations/*,apps/cli/src/commands/update.ts,apps/cli/src/lib/versions.ts.agents doctordetects and self-heals a hook whose generated shim wrapper is missing or broken, and the menu bar shows it (RUSH-2382). A native hook command could read as wired when its generated~/.agents/.cache/shims/hooks/<name>.shtarget was absent, a dangling symlink, empty, or non-executable — the harness silently never ran the hook.agents doctornow emits ahook-runtime-brokencritical finding for every hooks-capable harness (not just the settings formats the wiring inspector understands), and the daemon's safe self-heal regenerates one broken shim per unique path per pass with post-repair verification, never retrying or recursing into resource sync within the same pass. The macOS menu bar System row now readsN critical · M warnings(orall set) fromagents doctor --json'sfindingson the existing 15-minute poll, and the submenu lists up to 5 actionable findings with remediation (any kind, not just this one), with a+N more — run agents doctorrow past the cap. Source:apps/cli/src/lib/hooks.ts,apps/cli/src/lib/self-heal/checks/hook-runtime.ts,apps/cli/src/lib/devices/doctor-findings.ts,apps/cli/menubar/Sources/MenubarHelper/{Models,StatusItemController,AgentsCLI}.swift.Menu bar: the ROUTINES section is now a collapsible project-group accordion. Routines render the same way ACTIVE sessions do — one collapsible header per project group (a project name, or the
Operations/All projects/Cross-projectspecials the CLI derives from each routine'sprojects:field), collapsed by default, click▶to fold every routine in that group inline. Each header carries per-state glyph counts (◔upcoming,✕failing,⃠missed,⏸not-ready) and a paused tail, so a collapsed group still shows what is inside it; the header row also names the group count (ROUTINES · … · N groups). Expanding a group orders it attention-first, then by next run, then paused last. This replaces the flat group labels plus the single "All routines…" flyout for a CLI that emitsprojectGroup; an older CLI falls back to the previous view. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.
1.22.31
agents secrets unlock --keysfolds in the scoped-hold surface; thesecrets lease/leases/revokecommands are deleted (RUSH-2350).unlocknow takes--keys K1,K2to hold ONLY that subset of a bundle behind its own expiry instead of the whole bundle — the one capabilityleasehad thatunlockdid not, now on the command agents already reach for. Without--keys,unlockbehaves exactly as before (whole bundle). An unknown or empty key subset fails closed (Unknown secret lease key(s): …/requires at least one key),--keysscopes exactly one local bundle (rejected with--all/--host), andsecrets statusnow names the held keys of a scoped hold;secrets lock <name>releases it. The duplicatesecrets lease/secrets leases/secrets revoketrio is gone — it had zero consumers, and its jobs (list holdings, release one) are alreadysecrets statusandsecrets lock. No alias or deprecation shim (per the repo's no-unasked-shims rule). The underlying lease model (src/lib/secrets/lease.ts) is unchanged —unlock --keysreuses it. Source:apps/cli/src/commands/secrets.ts(scopeHeldEnv, theunlock/statusactions),apps/cli/src/lib/secrets/{agent,session-store}.ts,apps/cli/src/commands/secrets.scope.test.ts,apps/cli/src/commands/secrets.flags.test.ts.Session previews use bookmark and focus as separate actions (RUSH-2373). The shared interactive browser for ordinary, team, and routine sessions now uses
*to bookmark the highlighted row,b/--bookmarksto filter bookmarks, andfto focus the highlighted session through the same attach-or-recover flow asagents sessions focus. Enter still resumes. The non-TTY surface is nowagents sessions bookmark; its flag, JSON keys, code symbols, and durable store are bookmark-only, with a one-time migration from~/.agents/.history/favorites.jsontobookmarks.json. The retired favorite command and flag are not aliases. Source:apps/cli/src/commands/sessions-browser.ts,sessions-bookmark.ts,focus.ts,apps/cli/src/lib/session/bookmarks.ts,apps/cli/src/lib/migrate.ts.
1.22.30
agents secrets import --forcenow repairs a bundle whose metadata record is undecryptable (#2305). A file store whose key was lost or rotated out from under it leaves bundles present but unreadable — exactly the state provisioning exists to fix.agents secrets export <bundle> --host <box> --remote-backend file --forcedrives the remote's ownimport, which died on the unreadable record (Bundle 'x': failed to decrypt) and wrote nothing, so the only route left was deleting the record by hand on an already-degraded store. With--force, an undecryptable record is now treated as absent and recreated.Still refused without
--force: recreating unconditionally would destroy a healthy bundle for someone who merely forgot to setAGENTS_SECRETS_PASSPHRASE. OnlyBundleUndecryptableErrorqualifies — a locked keychain or logged-out vault still throws, so a recoverable state is never mistaken for a lost key. Source:apps/cli/src/commands/secrets.ts(resolveImportBundle).Documented the routine reliability contract (RUSH-2290).
docs/specifications.mdnow specifies routines normatively: a new §Routine execution & readiness section (RT-1..RT-11) pins thatprojects(plural) is grouping metadata only while a singularproject/--project-anchoris the execution anchor, that a routine's working directory resolves on the execution target (with a canonical cwd-resolution table — rootless project or bare relative cwd anchors at the target$HOME), that a proven readiness blocker saves the routine paused with a stable code (project_not_found,cwd_missing,codex_workspace_untrusted,agent_auth_failed,execution_context_missing, …), that run history owns attempts while sessions/logs/reports are optional children, and thatrepois an external Git/cloud/webhook identity, not a local cwd. §Scheduling & execution singularity gains SING-11..SING-13 (one scheduled fire launches at most once; the slot claim and the active-run claim are separate; a routine never overlaps itself), and run statuses gainblocked/skipped. Most of the contract is marked[Intended](RT-GAP-1, SING-GAP-3) — the target the reliability work implements — while the landed guarantees (daemon singleton + catch-up consolidation, run-first history, definition/activation split, menu bar read-only) are marked Current.docs/03-routines.mddocuments the same model for users. Docs-only; no runtime change. Source:apps/cli/docs/specifications.md,apps/cli/docs/03-routines.md.Daemon: one instance per state dir via last-wins takeover (RUSH-2352). A second
agents __daemon-runfor the same state dir — from ANY install path (homebrew,.localdev, nvm, npx) sharing one~/.agents— now evicts the incumbent and takes over, instead of exiting and leaving it running. This inverts the old first-wins refusal, an owner product decision: a restart always replaces the previous daemon.claimDaemonInstanceSIGTERMs the live pid-file owner of its own state dir, waits for its graceful shutdown to release the secrets broker socket and browser IPC binding (hard timeout →killTree→ bind), and never binds before that release — closing the two-brokers-on-one-socket orphan. In-flight detached routine children survive and are adopted by the new daemon, never killed. A daemon serving a DIFFERENT state dir (a separateHOME, a test fixture) is never a takeover target. Source:apps/cli/src/lib/daemon.ts.Daemon housekeeping ticks are now routines, not a second scheduling concept (RUSH-2353).
watchdog,device-probe,tmux-reconcile,launch-health,fleet-cache-warm,session-cache-warm,usage-refresh, andauto-dispatchwere hardcodedsetIntervaltimers insiderunDaemon()— undeclared, no run history, no pause, no device pin. They are now shipped system routines (gh:phnx-labs/.agents-systemroutines/*.yml) that invoke the same tick body one-shot viaagents __daemon-tick <name>(apps/cli/src/lib/daemon-ticks.ts), fired by the same pid-claimedJobScheduleras every other routine. Same schedules, same effects; each tick now shows up inagents routines list, accumulates history inagents routines runs/stats, and can be individually paused.auto-dispatch— polling Linear for delegated tickets — is the one that mattered most: it previously fired unpinned on every daemon in a fleet with no coordination, and can now be pinned to a single device withagents routines devices auto-dispatch --set <device>. Requires the pairedphnx-labs/.agents-systemrelease that ships the new routine YAML. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/lib/daemon-ticks.ts,apps/cli/src/index.ts.Existing device activation lists automatically retain all eight formerly always-on daemon ticks during the routine migration.
Restored
agents daemon— runtime, hosted-service, and failure visibility for the always-on daemon (RUSH-2354).agents daemonhad been deliberately removed while every mechanism it needs (getDaemonStatus,startDaemon,stopDaemon,readDaemonLog,signalDaemonReload) stayed implemented and unreachable. It's back asstatus(identity — state/pid/uptime/heartbeat/owning install — plus every duplicate__daemon-runprocess on the box, plus per-service health),start/stop/restart,enable/disable(a new persisteddaemon.enableddevice kill switch —disablestopsroutines add/routines start/routines catchup/webhook auto-start;agents daemon startstill starts it explicitly),reload(SIGHUP),services,logs(-n/-f/--level/--since/--json), anddoctor. A new persisted per-subsystem health record (apps/cli/src/lib/daemon-health.ts) tracks the secrets broker and browser IPC server's consecutive-failure streaks, so a subsystem failure no longer just scrolls out of the daemon log. There is still noagents daemon jobs— scheduled work staysagents routines;status/doctorpoint failures atagents routines stats. Source:apps/cli/src/commands/daemon.ts,apps/cli/src/lib/daemon-health.ts,apps/cli/src/lib/device-config.ts.Daemon:
stopasserts its postcondition instead of assuming it (RUSH-2355).stopDaemonused to fire SIGTERM and report success without checking anything, so a stop that silently left a resource bound still read as "stopped". It now waits for shutdown, escalates viakillTreeon a wedged daemon, then verifies each resource released — the secrets broker socket, the browser IPC binding, and no surviving__daemon-runfor this state dir — reclaiming any stale socket the ungraceful exit left behind.agents daemon stopprints what released vs what survived, exits non-zero when a resource could not be released, and carries a structured result under--json. In-flight detached routine children survive a stop deliberately and are reported, never killed. It never reports success on an unverified stop. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/commands/daemon.ts.OpenCode sessions are no longer pruned from every listing (RUSH-2357). OpenCode keeps all sessions in one SQLite file, so the index stores a composite
file_path(opencode.db#ses_<id>). The staleness gate split that string with dirname/basename and looked the composite basename up as a directory entry — it never matched, so every OpenCode row was classified as a deleted file and dropped fromagents sessions. A second gate then hid it as an "unmanaged" install because its single shared DB is never under a version home. Both now key off the composite FORM: existence is tested against the CONTAINER file, and a single-DB session is treated as managed (there is one canonical store, not a per-install dotfile). Any future single-shared-DB harness inherits the fix. Source:apps/cli/src/lib/session/parse.ts,apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/discover.ts.OpenCode session reads are bounded again, and one bad row no longer hides every OpenCode session (RUSH-2358).
parseOpenCodenow projects a tool part to just the fields it reads, droppingstate.attachmentsand cappingstate.output/ an oversizedstate.input: on a realopencode.dbthe largest tool part is 1.3 MB of base64 image attachment that the previous output-only truncation left whole, inflating one session's loaded payload 6.4×. Everyjson_extractin the OpenCode scan and parse queries is now guarded byjson_valid— SQLite aborts the entire query on a malformed value, so a single unparseablepartormessagerow previously dropped all OpenCode sessions fromagents sessions, with no message in a non-TTY run. Thetodoread is gated by asqlite_masterrow count instead of a blanket catch, so a locked or corrupt database no longer reads as "no todos" — and the count avoids theundefined-vs-nullsplit betweennode:sqliteandbun:sqlite, which a sentinel check would have inverted on the shipped Bun binary. Source:apps/cli/src/lib/session/parse.ts,apps/cli/src/lib/session/discover.ts.agents sessions' OpenCodeaccountfield now resolves fromauth.json, the same sourceagents view/agents doctoralready use (RUSH-2358). The prior resolver queriedopencode.db'saccount/account_state/control_accounttables, which are permanently empty on a real, actively-used install (verified: yosemite-s1, OpenCode 1.16.0, 35 applied migrations, zero rows in all three) — soaccountalways read null regardless of login state.resolveOpenCodeAccountId(new export,apps/cli/src/lib/agents.ts) is now the single source of truth for both surfaces: the sorted,+-joined provider ids holding a validauth.jsoncredential. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/session/discover.ts.OpenCode sessions now carry the same usage/burn fields as every other harness (RUSH-2358).
agents sessionsreads what OpenCode's own SQLite DB already records:input_tokens,cache_read_tokens,cache_write_tokens(siblings of the existingoutput_tokens),cost_usdandmodelfrom the session row,duration_msfrom its timestamps,tool_call_countfrom its tool parts,todosfrom itstodotable,recent_directories_touchedfrom the files it edited, andworktree_slugfrom its cwd.parseOpenCodenow truncates only a tool part's output (not the whole part), so largeeditparts are no longer dropped. Newercost/modelcolumns and thetodotable are probed, so an olderopencode.dbstill scans.accountstays null when no signed-in account is recorded, andaccount_key/account_org/git_branch/cost_usd_nocacheremain not-applicable for OpenCode. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/session/parse.ts.agents sessions preview <uuid-or-8-char-id>resolves rich previews across the fleet (RUSH-2370). ID-shaped lookups use the SQLite ID index rather than scanning recent history; full UUIDs can stop at the first exact owner, while short prefixes wait for every selected peer and surface ambiguity or unreachable peers. The owning device renders remote cards. A boundedlru-cacheL1 handles process-local reuse, normalized transcript-derived preview facts are cached durably by actual file mtime + size, and live status stays outside that cache behind a 15-second TTL. Text output now includes plugin provenance alongside skills, hooks, errors, tests, messages, tokens, timing, and artifacts;--jsonemits a versioned envelope. The existing detailedagents sessions <id>renderer and--previewspelling remain unchanged. Source:apps/cli/src/{commands/{sessions,sessions-picker}.ts,lib/{memory-cache.ts,session/{db,session-cache}.ts}}.Full command index doc —
docs/command-index.md+docs/command-index.json. Everyagentscommand and subcommand with its argument names and one-line description, on one scannable page, plus a machine-readable tree. Generated deterministically from the CLI's own command tree (npm run gen:index), regenerated on release, and gated in CI (npm run verify:index, in the cli-preflight and cli-docs jobs) so it can never drift from the shipped surface.agents secretsdrops the vague--forflag: duration is--ttl/--until, harness narrowing is--agent.--forwas doing two unrelated jobs on sibling commands —secrets unlock --for <agent>meant a HARNESS whilesecrets lease --for <duration>meant a DURATION — so an agent that learned the flag from one command passed it wrongly to the other, andunlock --for 8hsilently read8has a harness name.unlockkeeps its existing--ttl/--untiland now narrows with--agent <agent>;leasenow takes--ttl <duration>(matchingunlock) alongside its existing--agent.--forno longer exists anywhere undersecrets, and a test asserts Commander's real option tree so it cannot come back. Breaking for anyone scriptingsecrets unlock --for <agent>— no consumer existed in this repo or the fleet's own tooling, so no alias was added (per the repo's no-unasked-shims rule). Source:apps/cli/src/commands/secrets.ts,apps/cli/src/commands/secrets.flags.test.ts.Cursor launches no longer hang in a blank terminal after agents-cli adopts its launcher (RUSH-2345). Native launcher discovery now follows symlinks and rejects any candidate that resolves back into the agents shim, using the durable adopted-original record instead. Imports store the immutable native target, startup migration repairs existing two-hop loops, and generated shims refuse to recursively execute themselves. Cursor is also restored to prompt-less interactive selection because current builds open a native TUI with no arguments. The same centralized repair fixes
agents run cursorand Factory Cursor tabs.agents viewandagents runkeep custom harnesses independent from their native host. The unfiltered view still lists each fork as its own custom harness block, whileagents view claude(and its filtered JSON form) shows only Claude versions. Exact custom names are resolved before native harness names, so a fork remains viewable and runnable through its configured host even when its name matches a native or hard-deprecated id. Source:apps/cli/src/commands/view.ts,apps/cli/src/commands/exec.ts.Fixed a Touch ID storm from version-skewed clients evicting the daemon's secrets broker. When the always-on daemon hosts the secrets broker on one installed version and a differently-versioned client invokes (e.g. a
~/.localdev build alongside the Homebrew/npm install), the client tore the daemon's broker down at zero held bundles. Because teardown only recognizes the standalone broker's pid claim, it unlinked the daemon's socket without stopping the daemon — which then refused to re-host and was orphaned until restart, leaving every secrets read to cold-start a one-off broker and re-prompt Touch ID.ensureAgentRunningnow defers to a live daemon (shouldClientEvictSkewedBroker) and never evicts a daemon-hosted broker; daemon version upgrades are handled by the postinstall restart, and the wire protocol is already version-checked, so a code-skewed daemon broker stays wire-compatible. Source:apps/cli/src/lib/secrets/agent.ts.The daemon is now one process per device, whatever launch entry started it — stops a duplicate-daemon pile-up that double-fired every routine. One install exposes two launch entries (the compiled
dist/bin/agentsbinary and thenode <shim>JS entry) with differentprocess.argv[1], and the stray-daemon reaper keyed on that path — so the two never reaped each other, duplicates accumulated (78 live__daemon-runobserved on one box), and each scheduled routine fired from many daemons at once (measured: a once-per-hour routine spawning 10+ times per tick, once-per-day Claude routines spawning ~8 concurrent runs — burning account tokens and colliding intofailed). The reaper now enumerates the device singleton from an on-disk instance registry under<daemonDir>/instances/(keyed by the daemon dir, which is per device —AGENTS_DAEMON_DIR??<HOME>/.agents/.cache/helpers/daemon), so every daemon of one device registers in the same place and the survivor reaps the rest regardless of how each was launched; a genuinely separate install/home or test fixture resolves elsewhere and is left alone. Identity rides the shared registry rather than reading another process's environment, which hardened macOS hides fromps. Source:apps/cli/src/lib/daemon.ts(registerDaemonInstance,reapStrayDaemons).agents run <agent>now says the harness is not installed instead of exiting 127 (RUSH-2339). On a machine without that harness the launch used to exec the bare CLI name and die withsh: 1: exec: cursor-agent: not found, behind a⚠ <agent> looks logged outbanner that was also wrong.agents runnow probes the executable it is about to spawn and exits1withagents: <agent> is not installed on this machine.plus theagents add <agent>fix, before any spawn. The probe is existence-based, so a harness you installed yourself (Homebrew, a vendorcurl | sh, a distro package) with no agents-cli version home still launches, and a machine with managed versions but no pinned default still gets the shim's ownagents use <agent> <version>guidance rather than a wrong "not installed".The Claude usage/auth-health probe no longer reads your interactive Claude Code login — the fix for repeated Anthropic logouts. The daemon's usage (~60s) and auth-health (~3min) warms authenticated with a file-based setup-token when one was provisioned, but otherwise fell through to reading Claude Code's interactive OAuth token from the keychain /
.credentials.jsonand firing it atapi.anthropic.com/api/oauth/usage. Anthropic sees an interactive login used programmatically from a background loop and revokes it (the fleet-wide-logout class, RUSH-1822).loadClaudeOauth's read-onlyaccessTokenCachepath now returns nothing when no setup-token is provisioned — it never reads the interactive login, in the keychain or in a file — matching the "interactive/rotating login is untouchable" invariant in docs/credential-management.md. The now-obsolete no-ACL access-token cache is removed with it. An account without a minted setup-token shows "usage pending" inagents view(seed one via the mint-auth path) but still runs normally; account rotation is unaffected. Source:apps/cli/src/lib/usage.ts.Sort
agents view <agent>accounts by email after the default. Managed installs previously followed semantic-version order after the default, forcing users with several Claude, Codex, or other harness accounts to scan a shuffled email column. Rows without an email remain deterministic in version-descending order, and--jsonkeeps its existing default-then-version order for automation compatibility. Source:apps/cli/src/commands/view.ts.agents sessionsaccepts version filters in the same forms as other harness commands (RUSH-2363). An installed positional selector such asagents sessions [email protected]now routes to the structured agent/version filter instead of searching for that literal text and returning no matches. The equivalent split form,agents sessions --agent claude --version 2.1.181, is also supported; unknown or uninstalled positional pairs remain ordinary free-text queries. Source:apps/cli/src/commands/sessions.ts.
1.22.29
Name provider accounts once and find their installed versions automatically (#2300). After completing a harness's normal login,
agents accountsdiscovers distinct signed-in accounts across every installed version andagents accounts name worknames one through a picker (--from [email protected]is the non-interactive form). The synced registry stores only the harness id and a SHA-256 identity fingerprint—no OAuth material, email, provider id, per-device binding, or cross-harness account group.agents run <agent> --account <label>live-scans candidates and chooses a healthy matching version; it fails instead of falling back to another identity.Sessions: one "favorite" vocabulary, no more "star". The interactive browser (
agents sessions) footer readf favorites · * star, presenting two names for one mechanism —*toggles a favorite,ffilters to favorited, and both write the single store inlib/session/favorites.ts. The*action, the--favoritesflag help, theagents sessions favoritecommand help, and the docs now all say "favorite"; the★/☆glyphs and thefavorites/favoriteJSON keys are unchanged. Source:apps/cli/src/commands/sessions-browser.ts,sessions-favorite.ts,sessions.ts.Menu bar: collapsible DEVICES roster, a Focus action on sessions, and richer routine submenus. The dropdown now has a DEVICES section near the bottom — the full registered fleet as one accordion, folded by default so the long list never walls the menu, each row
<name> · <platform>with live load% merged from the warm fleet cache (never a faked online/offline) and a Copyagents ssh <name>action; NEW DEVICES moved to sit just above it. Each live-session›submenu leads with ▶ Focus session (attaches locally or SSHes to the owning box viaagents sessions focus). Each routine submenu now leads with a last-run line —● running now(server-verified),✓ completed · ran 45s · 2h ago, or✕ failed exit 1— plus the next fire. The device roster rides the existing 3-minutemenubar snapshot --jsonpoll (a cheap local registry read,MenubarSnapshot.devices); everything else renders from fields the snapshot already carried, so no new timer and no per-open shell-out. Source:apps/cli/src/lib/menubar/snapshot.ts,apps/cli/menubar/Sources/MenubarHelper/{StatusItemController,Models,LocalState}.swift.Fix
agents viewandagents usagelabeling Codex's weekly or monthly quota as session usage when the native CLI publishes the long-duration limit in itsprimaryrate-limit window. Codex windows are now labeled from their reported duration (S,W, orM) instead of their primary/secondary position.
1.22.28
agents run/agents teams--device/--host: fail loud when a pinned harness version is not installed on the target (RUSH-2313). A concrete pin like[email protected]is checked against the remoteagents view --jsonlisting duringensureHostReadybefore the run is marked dispatched. Missing pins exit non-zero naming the box, the pin, what is installed there, andagents ssh <box> -- agents add <agent>@<ver>— so detached fleet drains no longer printDispatchedand then die only in the remote log. Aliases (@latest/ …) still resolve on the remote; a bare agent name still only warns. Source:apps/cli/src/lib/hosts/ready.ts,dispatch.ts,teams/agents.ts.fix: Windows-host e2e suites can resolve
win-miniagain under the hermetic device registry.tests/setup.ts(RUSH-2042 / #1572) redirectsAGENTS_DEVICES_DIRto an empty fork-private directory so unit fixtures never leak into the real fleet registry. The livessh-tunnel.e2e/browser/drivers/ssh.e2esuites still need a realDeviceProfileforAGENTS_TEST_WIN_HOST, so after #1572 everytests-windows-host-e2e.ymlrun failed immediately withUnknown device 'win-mini'— even on a tailnet-joined runner that could ssh to the box. The setup file now seeds the private registry from the real fleet entry (or synthesizes one fromssh -G <host>) whenAGENTS_TEST_WIN_HOSTis set; the real registry is never written. Source:apps/cli/tests/seed-e2e-win-host.ts,apps/cli/tests/setup.ts.OpenCode timeout-sample spool dir is Windows-safe (#1869). The generated
agents-cli-hooks.tsplugin usedPERF_SPOOL.slice(0, PERF_SPOOL.lastIndexOf("/"))to mkdir the perf spool parent. On WindowsgetPerfDir()is backslash-separated, solastIndexOf("/")returned -1 and the slice dropped one character — the sample write was fail-silent and never landed. The plugin now importsnode:pathand usespath.dirname(PERF_SPOOL). Source:apps/cli/src/lib/hooks.ts.Bash-command classifier: single-source tool registry + broader unwrap (#1889). The embedded Python activity-log hook (
ACTIVITY_LOG_HOOK_SCRIPTinactivity.ts) hand-duplicatedbash-command.ts'sTOOL_REGISTRYandVALUE_FLAGSwith a "keep them in sync" comment — the two had already drifted (Python was missingrmdir, andagents/lineartwo-level tools). Both tables are now generated from the TypeScript source (pythonToolRegistryLiteral/pythonValueFlagsLiteral) at module load. Separately,unwrapCommand(and the matching Python_unwrap_command) now peelsexport VAR=…,set -euo pipefail,for/untilloop bodies,if/thenbranches, and(command)subshell prefixes that previously classified asother. Source:apps/cli/src/lib/session/bash-command.ts,apps/cli/src/lib/activity.ts.Active-session polls skip re-parsing quiet transcripts (#2047).
computeLiveSignals(the per-session tail/parse behindagents sessions --activeand the menu-bar badge tick) now memoizes by transcript path + mtime +pidAliveinside the process, so a 30s poll no longer re-tails every live session whose file has not changed. Positive Claude transcript-path resolutions are also memoized while the file still exists, avoiding a full walk of every Claude version-homeprojects/tree per pid per tick. Source:apps/cli/src/lib/session/active.ts.perf(sessions): throttle headless
ps/lsofscan + memoize process table across an active-session poll (#2047)agents routines devices --set/--clearno longer abort when a fleet peer is offline (#2118). Pinning a routine used to fan out pause/resume to every registered device and throw on the first unreachable one — often after the pin had already succeeded on the target — so a single asleep laptop made fleet pins unusable and the error looked like the pin failed. Offline peers are now skipped with a warning; the command exits non-zero only when a selected target device cannot be reached. Source:apps/cli/src/commands/routines.ts.agents models claudeno longer lists a per-cloud bare-minor id as a plain catalog entry (#2233). The id-scan fallback (scanClaudeCatalogIds, used when the structured alias/perCloud maps yield fewer than two models) used to surface short forms likeclaude-opus-4-1that only appear as afoundry:/ cloud-scoped field next to a real firstParty id (claude-opus-4-1-20250805).dropBareLegacyIdsnow drops any id that is a dash-boundary prefix of a more-specific sibling also present in the scan — covering both the bare-major.includes("claude-opus-4")artifacts (#1892) and these bare-minor cloud-metadata values — while keeping genuine bare currents with no sibling (claude-sonnet-5) and not collapsingclaude-opus-4-1intoclaude-opus-4-10. Source:apps/cli/src/lib/models.ts.Menu-bar dropdown density toggle removed. The
Density: Auto/Rich/Compactfooter item and the whole rich/compact rendering fork are gone; the dropdown now always renders the rich rows (session/work titles, expanded Routines and Recent sections). Removes themenubarDensityUserDefaults key andMENUBAR_DENSITYenv override, and updates the menu-bar docs to match. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift,apps/cli/docs/menubar.md.The macOS secrets broker can now hold an explicit key subset under its own lease id and expiry (RUSH-2255).
agents secrets lease <bundle> --keys K1,K2 --for 8hauthenticates once, stores only those resolved values in broker memory and the restart session, and reports lease metadata without exposing values. Unknown, missing, and expired keys fail closed. Source:apps/cli/src/{commands/secrets.ts,lib/secrets/{agent,lease,session-store}.ts}.Scoped leases can now be listed and revoked by id (RUSH-2256).
agents secrets leasesshows bundle, keys, and remaining lifetime;agents secrets revoke <lease-id>wipes exactly that broker grant and its restart session. Source:apps/cli/src/{commands/secrets.ts,lib/secrets/{agent,session-store}.ts}.Fleet devices accept an explicit SSH private-key path (RUSH-2265).
agents devices set <name> --auth key --identity-file <path>stores the path on that device and every shared SSH invocation passes it to OpenSSH with-i, so devices no longer depend on whichever key the ambient ssh agent happens to offer. Switching a device from password to key auth also removes stale password-bundle metadata from the registry.Detached agent dispatch now runs on Windows OpenSSH hosts (RUSH-2267). Headless
agents run … --host <windows>launches through a hidden PowerShell process, preserves actor/session/env context, and uses durable Windows-native log, follow, reconcile, stop, and cleanup operations.agents repo pullfast-forwards a clean behind-only checkout instead of failing with "Cannot rebase onto multiple branches" (RUSH-2282). After fetch, a 1-behind tree integrates viamerge --ff-onlyagainst the tracking ref; only genuinely diverged histories enter rebase. Avoids re-runninggit pull --rebaseon a multi-entryFETCH_HEAD(common after bare fetch when the remote has several branches, or under concurrent fleet fetch). Source:apps/cli/src/lib/git.tspullRepo.agents perfsplits intentional deny (exit 2) from crashes (exit 1) — hook health is readable again (RUSH-2294). Deny-by-design guards (ask-user-question-guard,plan-html-reminder,git-guard, …) exit 2 on purpose; the warehouse previously counted any nonzero exit aserrorCount, so a working guard read as a "92%-error" hook. Exit 0 = allow, exit 2 =blockCount/blockRate, exit 1 / other =errorCount/errorRate. The hooks table column is nowERR/BLOCK/TO(err:…% block:…% to:…%). JSON rows gainblockCount/blockRate. Source:apps/cli/src/lib/perf/db.ts,apps/cli/src/lib/hooks/profile.ts,apps/cli/src/commands/perf.ts.Claude launches reuse an unchanged file-backed setup-token instead of decrypting it on every exec (RUSH-2317). The process-lifetime cache is isolated by version home and invalidates from the encrypted credential file's identity, ctime, mtime, and size; token rotation is picked up on the next launch, missing per-account tokens are negatively cached, and plaintext tokens remain memory-only. Source:
apps/cli/src/lib/claude-account-token.ts,apps/cli/src/lib/secrets/filestore.ts.Repeated full session listings reuse stable transcript membership (RUSH-2318). The process briefly caches settled transcript directories by mtime+size, rereads recently changed directories, and expires every entry within the filesystem timestamp-precision window, avoiding burst-time repeat reads without hiding filesystem-only creates or deletes. Source:
apps/cli/src/lib/session/db.ts.agents sync --yesdrops ~1s/agent of dead work and reuses still-fresh fingerprints (RUSH-2320). Measured on a real install:getActuallySyncedResourceswas ~1055 ms/agent on the unattended path that never reads it;buildManifestre-hashed every file (~716 ms) after a no-op force sync; the guard-hit path spent ~12 ms building inventories it then discarded. Unattended sync now skips the interactive inventory, the skills detector is stat-first on size before any content read,buildManifestcarries still-fresh fingerprints from the previous manifest, andsyncResourcesToVersionruns its no-change guard before pattern expansion (accepting a caller-suppliedavailableinventory so multi-version fan-out does not re-scan). Source:apps/cli/src/lib/refresh.ts,apps/cli/src/lib/versions.ts,apps/cli/src/lib/staleness/.fix:
agents run <agent>no longer dead-ends when every account is logged out — it launches so you can sign in. A harness with one installed, signed-out version had no reachable login path at all:agents run cursorexited withno healthy cursor account under strategy 'balanced' — excluded: 2026.07.23 (signed_out), theagents run cursor@account picker marked the logged-out rowdisabledand offered onlyNo usable accounts — cancel, andagents use cursoronly set a default. The zero-healthy guard (RUSH-2132) treated a missing login like an exhausted account, but they are opposites — a throttled account must not be launched, while a signed-out one is fixed precisely BY launching, since the harness's own TUI is the login surface. On a human-facing terminal run (a real TTY and no--json), a single sign-in-recoverable account now launches directly (naming the version and the login command) and several open the account picker with auth-blocked rows selectable and labelledlaunch to sign in;rate_limited/out_of_creditsstill fail loud, and off a TTY — or under--json— both classes keep the exact watchdog-parsed error, now with the harness's login command alongside--strategy pinned. Source:apps/cli/src/lib/rotate.ts(isSignInRecoverable,signInRecoverableCandidates),apps/cli/src/commands/run-account-picker.ts(pickSignInLaunchVersion),apps/cli/src/commands/exec.ts. (RUSH-2334)sessions --activeno longer shows retained dead/queued rows, and every process row now carries its PID (RUSH-2336). Bare--active(CLI table/JSON, the interactive browser,focus, and the menu bar) previously kept a row alive as long as its pid wasn't known dead — so a queued-but-not-started row, or a process of genuinely unverified liveness, could still show up. The canonical selector (isRunningLiveSession) now excludesqueued/closed/crashedoutright (still reachable via--queued/--closed/--crashed) and requires a real process row to positively verify its machine, a positive pid, andpidAlive === true; a cloud row stays active on its provider + task id alone. Every process-backed--active --jsonrow now guaranteesmachine/pid/pidAlive: true, and the human CLI row and the menu bar's session detail both show the matchingmachine:pid(orprovider · taskIdfor cloud) locator. Source:apps/cli/src/commands/sessions.ts,apps/cli/src/lib/menubar/snapshot.ts,apps/cli/menubar/Sources/MenubarHelper/.agents projects statuscard scans faster: grouped warnings, a fleet health summary, and a truthfuldeadlabel (RUSH-2337). On a busy fleet the card printed each host's git drift twice — once in the inlinefleettable, then again as one 2-line block per host in the warnings footer (18 lines for 3 facts) — and readdead 41 finished or lost (41 crashed), which contradicts itself when every dead session is a crash. Now: (1) the warnings footer groups by root cause — all behind hosts collapse to one warning listing each with its count (4 hosts behind origin/main — mac-mini ↓172, yosemite-m2 ↓217, …) under one shared remediation, dirty/missing the same, a lone host keeps its full sentence, grouped per probed path so two repos never merge (mirrors doctor'semitGroup); (2) a one-line fleet health summary (6/13 clean · 4 behind · 4 dirty · 1 missing) sits above the per-host table, which keeps its branch/drift detail; (3) thedeadrow names the status directly when singular (dead 41 crashed); (4) the fleet-wide rollup carries afleet snapshot · as of HH:MMstamp; (5)linearshows a completion percent (468/547 done (86%)). Source: apps/cli/src/lib/project-probe.ts, apps/cli/src/lib/project-status.ts, apps/cli/src/commands/projects.ts, apps/cli/docs/11-projects.md.
1.22.27
agents sessions --teamsgroups sessions by team (RUSH-1997). Instead of one flat list with a[team/handle]tag,--teamsnow prints a report grouped by team: each team names its spawner (the orchestrator session that created it) and spawn time, and every teammate row shows its mode + handle. Team-flagged spawns that carry no teammatemeta.json— headlessagents runsessions, or teammates whose team record aged out — sink into a trailing(no team)bucket, so a realagents teamsteammate and a bare SDK spawn are never shown as the same thing.--teams --flat/--treekeep the plain inline table, and a search query keeps the interactive picker.TeamOrigin(also on--teams --json) now carries the teammate'sstartedAtspawn time and asource(meta= teammate,entrypoint= bare spawn). Source:apps/cli/src/lib/session/team-filter.ts,apps/cli/src/commands/sessions.ts.agents run --device autoandagents teams add --device autonow choose from live fleet health instead of 14-day launch affinity (RUSH-2001). The picker probes candidates in parallel, excludes unreachable, overloaded, and missing-agent machines, prefers signed-in candidates with the lowest normalized load, and keeps execution local when no remote is better. Remote readiness now usesagents view --json, preserving the installed/sign-in split instead of treating every remote sign-in state as unknown. Source:apps/cli/src/lib/smart-launch.ts,apps/cli/src/lib/teams/placement-probe.ts,apps/cli/src/lib/hosts/ready.ts,apps/cli/src/commands/exec.ts,apps/cli/src/commands/teams.ts.agents devices harnesses/agents devices accounts— per-device harness + account readiness (RUSH-2003). Two new fleet lenses:harnesseslists every installedagent@versionacross the fleet with its account, signed-in state, quota (highest usage-window utilization;*= cached snapshot), and a singlereadyverdict (signed in AND not rate-limited);accountscollapses that to one row per account, naming which harnesses share it. Both SSH-probe each online device (bounded likefleet ping, so one unreachable box can't stall the glance) and read the daemon-warmed usage cache —--refresh(--live) forces a live quota read. Scope with--agents <csv>/--device <csv>;--jsonemits per-host rows (harnesses) or account groups (accounts). Source:apps/cli/src/lib/devices/harness-inventory.ts,apps/cli/src/commands/ssh.ts.A session that ran on another device now resumes ON that device, and a typo'd command with
--hostsaysunknown command(RUSH-2022). Two bugs found while recovering ~15 sessions after a machine crash, both of which sent recovery down the wrong path.(1)
agents resume <id>restarted a remote session locally. The harness keeps its conversation state on the machine that produced the session, but nothing checked which machine that was — so a peer-owned session started the agent here, against state this box had never seen (sessions-resume.tseven swapped inprocess.cwd()when the recorded directory did not exist locally).agents resumenow re-runs itself on the owning device over SSH;--hereoverrides. The bareagents sessionspicker routes the same way, andagents sessions attachhops as an attach (its detach record and the headless process it stops both live on the owner). The multi-selectagents sessions resumeinherits it: each tab it opens runs the canonicalagents resume <id>, which now routes itself, and its no-tab-backend path routes explicitly. Root cause of the population that made this common: a run dispatched withagents run --device <box>was indexed with no origin machine at all, so the index claimed the dispatching box; it now records<box>, which also means such a run finally shows up underagents sessions --host <box>. Source:apps/cli/src/lib/session/resume-owner.ts,apps/cli/src/lib/hosts/session-index.ts,apps/cli/src/commands/resume.ts.The hop carries its "don't route again" pin as an exported env var, not a flag, so it works against a peer still on an older CLI. Sessions indexed before this release keep their old machine tag — re-dispatch or a fresh scan corrects them.
(2) The
--host/--devicerouter answered for commands that do not exist. It runs before commander parses, soagents session resume --host <box>(one letter offsessions, which does accept--host) reported`agents session` does not support --host/--device— a true statement about a command nobody typed and the opposite of the truth for the one they meant. Unknown names now fall through tounknown command '<name>'with a did-you-mean, and the spellcheck can suggest the lazily-registered groups (sessions/teams/cloud/…) it previously could not see. A real command with no remote semantics still gets the flag-support error. Source:apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/startup/command-registry.ts.agents publishis in the lazy command table.commands/packages.tsregisters it at top level but the registry did not list it, so it only resolved through the unknown-command fallback that loads the whole command tree. Found by the new test that pins the command-name set against the real tree.A mistyped command keeps its
--host. The distance-1 auto-correct now runs before the router instead of after commander gave up, soagents docto --host <box>corrects todoctorand runs on<box>— previously the corrected command re-parsed locally with a--hostit did not accept. Four routing-table entries naming commands that do not exist (cli,packages,versions,daemon) were removed; a test now keeps both routing tables to real command names.Cross-machine
agents … --jsonfan-out caps each peer's stdout at 16 MiB instead of buffering it unbounded (RUSH-2065). The shared fan-out (gatherRemoteAgentsJson, behindagents sessions --active,agents feed, and every other fleet-wide JSON sweep) streamed each peer's output into memory with no ceiling, under onePromise.all— so a single peer returning a corrupt or pathologically large payload could retain ~170 MB and OOM the whole sweep. Each peer's capture now stops and SIGKILLs the connection once it would exceed the ceiling, treating that box as unreachable (reported inskipped) so the rest of the fleet still renders. The bound and the UTF-8-safe accumulator now live once inapps/cli/src/lib/ssh-exec.ts, shared with theagents sessionsbrowse fan-out that already had the guard. Source:apps/cli/src/lib/remote-agents-json.ts,apps/cli/src/lib/ssh-exec.ts,apps/cli/src/lib/session/remote-list.ts.Auth-health probes once per account, not once per version home (RUSH-2111). The daemon's every-3-minute auth-health refresh fanned
probeLocalFleetAuthover every installed version home at once, so a box with several Claude homes signed into one account fired that many concurrent requests at the same provider OAuth endpoint — racing its rate limit into a429that then parked the whole box's usage reads behind aRetry-Afterpenalty (usage-backoff.tssurvives that penalty; this removes its cause). Installs are now grouped by account and the live probe runs once per (agent, account), fanning the one verdict out to each home's per-version cache row. Homes with no resolvable account are still probed individually. Source:apps/cli/src/lib/auth-health.ts.Fallback-chain agents now receive their own active rules preset before dispatch (RUSH-2129).
runWithFallbackresolves each attempted entry's harness/version home and runs the same skip-fast preset synchronization as the primaryagents runpath, so a rate-limit handoff cannot launch against stale rules. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/rules/run-sync.ts.Gemini hard-deprecation: routines daemon can no longer execute a legacy Gemini routine (RUSH-2202). RUSH-2060 gated
agents add/import/sync/run/routines addagainst a hard-deprecated harness, but the routines daemon's own executor (runner.ts) had no equivalent gate — a Gemini routine written before RUSH-2060, or synced/edited on disk directly, would still fire and try to build agemini …command against a backend Google retired.executeJob/executeJobDetachednow reject a hard-deprecated agent up front, before any version/account resolution or sandbox prep, and record afailedrun with the same deprecation message every other entry point already shows. Also removed the now-unreachable Gemini model-catalog extractor (models.ts), Factory's Gemini model-catalog fetch (agentModels.ts), and the dead Gemini sandbox-config writer (sandbox.ts/gemini-settings.ts— its generic JSON helpers stay, since Antigravity's permission writer reuses them), plus a staleprofiles.mddoc line still listinggeminias a live profile agent. Source:apps/cli/src/lib/runner.ts,apps/cli/src/lib/models.ts,apps/cli/src/lib/sandbox.ts,apps/cli/src/lib/gemini-settings.ts,apps/factory/src/core/agentModels.ts.Routine transcripts now archive as
origin='routine'sessions (RUSH-2271). A Claude (and Codex) routine writes its transcript to the per-versionCLAUDE_CONFIG_DIR/CODEX_HOMEhome, not the sandbox overlay the archiver scanned — so routine runs were indexed as ordinaryorigin='cli'sessions and never linked to their routine or run.archiveRoutineTranscriptsnow reads the same per-version homebuildExecEnvwrites to (re-pointed to each failover attempt's account as the chain advances), scoped by a pre-spawn baseline so it copies only that run's transcript out of the shared home, andagents sessions --routineshows them again. Kimi relocates too but its routine-archive discovery reader is a separate follow-up. Source:apps/cli/src/lib/runner.ts,apps/cli/src/lib/routines.ts.Release lease detects a holder killed from outside (RUSH-2274). An externally killed release (SIGKILL, a severed ssh, a rebooted box) left its lease on
originandscripts/release-lease.sh statusreadheldfor up to the 30-minute TTL with nothing actually releasing. The lease now records the holdinghost,pid, and that pid's start time, andstatusreportsholder-alive=yes|no|unknown. A holder that is provably gone is reclaimed by the nextclaimimmediately instead of waiting out the TTL, and a newrelease-lease.sh cleardrops such a lease without starting a release. A live holder is never taken at any age, an unprobeable one (another box, or a lease from an older release) still falls back to the TTL, and a reused pid or an unreaped zombie counts as dead rather than as a live release. Source:apps/cli/scripts/release-lease.sh,apps/cli/scripts/release.sh.agents outputand the session index no longer under-count Windows hosts (RUSH-2286). A Windows box could report zero token burn / zero sessions even when it was actively used, because two per-harness scanners insession/discover.tsfailed on Windows: the OpenClaw scan gated onwhich openclaw, which is POSIX-only (whichthrows ENOENT on Windows, so the whole OpenClaw scan silently returned before indexing anything), and the Grok scanner recovered a session's version fromsummary.grok_homewith a/-only regex that never matched a backslash-separated Windows path. The OpenClaw presence check now uses the cross-platformhasCommand, itsopenclawinvocations route throughexecFileShellSpecso a Windows.cmd/.ps1shim actually launches, and the Grok version regex normalizes separators first. Separately, JSON relayed from a Windows peer over SSH (agents output --host <win> --json,agents sessions … --json) is now stripped of any PowerShell#< CLIXMLbanner before parsing (stripClixmlinhosts/remote-cmd.ts), so a fleet-wide rollup that folds in a Windows box no longer drops it on aJSON.parsefailure. The banner strip is applied at every remote---jsonboundary a Windows peer's output flows through: theremote-agents-jsonfan-out, the sessionremote-listlist/payload/tool-search parsers, the--hostfleet passthrough (agents view --host all), andagents output's per-device fetch. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/hosts/remote-cmd.ts,apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/remote-agents-json.ts,apps/cli/src/lib/session/remote-list.ts,apps/cli/src/commands/output.ts.agents outputnow reports the burn split and a--pricing no-cachescenario (RUSH-2287). The productivity rollup collapsed token burn into a single counter. It now breaks the burn into uncached input / cache-read / cache-write tokens wherever the harness records a per-message cache split (Claude, Codex, Gemini, Droid) — aburn split:line in the text report and the three counts onburnand everybreakdownrow in--json. New--pricing no-cachereprices cached tokens at the model's full input rate to model "what would this cost with caching off?"; the text report leads with that figure (breakdown columnburn(nc)) while--jsonalways carries bothcostUsdandcostUsdNoCacheso a dashboard can choose. The saving is surfaced in actual mode too (caching: actual $X vs no-cache $Y). Backed by four new session columns (input_tokens,cache_read_tokens,cache_write_tokens,cost_usd_nocache, schema v37) populated at scan time; pre-upgrade sessions show total-only until re-scanned. Source:apps/cli/src/commands/output.ts,apps/cli/src/lib/session/{db,discover}.ts,apps/cli/src/lib/pricing/cost.ts.Routine failures now reach the owner's phone, not just the local desktop (RUSH-2288). A
failed/timeoutroutine finish, or a pre-spawn failure such asauth_failed, now also pings the owner over the same channel stackagents notifyuses (theowner.channelsinhumans.yaml, or the legacynotify.owner), delivered in-process by the daemon — no shelling out tossh mac-mini agents notify. This closes the gap where a failed scheduled routine on a headless fleet box was invisible, and specifically covers theauth_failedcase the per-routineagents notifyprompt can never send (its agent never spawned). If the primary owner channel cannot deliver from the box, the daemon walks the remaining configured channels as fallbacks (Telegram and intrusive/voice channels are excluded). Green routines of any kind stay silent, the existing desktop thresholds are unchanged, and delivery is deduped per job+runId. Source:apps/cli/src/lib/routine-notify-owner.ts,apps/cli/src/lib/daemon.ts.Add
agents bench list,agents bench run, andagents bench results: benchmark cells fan out through the existingagents runpath with isolated fixture copies, bounded concurrency, custom harness names, wall-time/exit/token capture, and durable JSON results under~/.agents/.history/bench/(RUSH-2302, RUSH-2303).New user quickstart: install, harnesses, teams, fleet.
apps/cli/docs/QUICKSTART.mdwalks a fresh install throughagents setup, adding and logging into harnesses, a minimalagents teamssmoke test, and setting up a fleet (agents devices sync,agents devices set-interactive,agents apply) — linked fromdocs/README.mdand the root README's Quickstart. Docs only — no runtime change. Source:apps/cli/docs/QUICKSTART.md,apps/cli/docs/README.md,README.md.agents insightsdetects agent silent stalls (model goes idle until you resume). When the assistant is last to speak and the next user message is ≥5 minutes later, facets count duration-bucketedsilent stall: 5-15m/15-60m/1h+friction signals; resume nudges (continue,keep going, …) after that silence also count asresume after silent stall. Report, actions,--narrative, and/sessions-insightsinstruct models to call these out (not reframe as "user was slow"). Extractor version bumped to 5 so cached facets recompute. Source:apps/cli/src/lib/session/insights.ts,commands/insights.ts,docs/06-observability.md.agents insightssplits silent stalls by harness and model. The By-agent/account table now shows per-group stall and resume-nudge counts (so laziness is visible without--json). Stalls are also attributed to the model that last spoke (silentStallsByModel, "Silent stalls by model" section). Extractor version 6. Source:apps/cli/src/lib/session/insights.ts,commands/insights.ts.Redact Claude OAuth setup-tokens (
sk-ant-oat01-…) from logs and exports, and reject a malformed one before it reaches the auth header (#1767). The log redactor masked API keys (sk-ant-api03-…) but not OAuth setup-tokens — the genericsk-rule can't reach anoat01token because the hyphen afterantbreaks its run — so a captured setup-token could leak verbatim into a run log or session export.resolveClaudeSetupTokenalso now validates the stored value: a corruptauthbundle entry (e.g. a capturedclaude setup-tokenTTY banner+ANSI blob, the exact #1767 shape) is refused instead of being injected asCLAUDE_CODE_OAUTH_TOKEN→ an invalidAuthorization: Bearerheader that crashes the run; the caller falls back to the normal login. Source:apps/cli/src/lib/redact.ts,apps/cli/src/lib/claude-account-token.ts.Stop the interactive host auto-reconnect spinning forever on a flapping link (#1884). A reattach only refills the retry budget now if it reached the host and held the remote pane for at least 10 seconds. Before, the budget refilled on the preflight probe alone, so a link that reconnected and dropped the user straight back out — or an attach that died at TTY negotiation every time — printed
Reconnecting … (attempt 1/6)on every cycle forever andMAX_ATTEMPTSbounded nothing. A link that keeps dropping now spends the budget and gives up with a message that says so ("kept dropping again within 10 seconds of getting back in"), distinct from the unreachable-host "couldn't reconnect". A session that blinks all day and reconnects into a working pane each time is unaffected. Source:apps/cli/src/lib/hosts/reconnect.ts,docs/hosts.md.agents models claudeno longer lists bare legacy ids that 404 (#1892). The native-binary id-scan fallback (scanClaudeCatalogIds, used when the curated maps come up empty) is now word-boundary anchored and matches the id body atomically, so it can't scrape a bare-major prefix (claude-sonnet-4) out of the binary's own dottedclaude-sonnet-4.6"Typo in model ID" troubleshooting string, out of a suffix-glued token (claude-opus-4-1x), or out of a token glued to a preceding identifier char. The existingdropBareLegacyIdssibling-drop still removes the standalone.includes("claude-opus-4")prefix-check artifacts; genuine bare currents (claude-sonnet-5) are kept. Catalog output is unchanged across all shipped Claude binaries. Source:apps/cli/src/lib/models.ts.Make ended-session focus open the correct recovery target (#2108).
agents sessions focus <id>now reaps metadata-less dead tmux panes, proves the indexed transcript belongs to the exact active version home before native resume, launches Claude from the transcript's original project directory, and passes replacement-version/continueas an interactive positional prompt. Source:apps/cli/src/lib/session/recovery.ts,apps/cli/src/lib/tmux/session.ts,apps/cli/src/lib/exec.ts.The multi-install warning now inventories copies outside
PATHand flags legacy installs that can corrupt the shared macOS helper bundle (#2147). Discovery covers NVM, fnm, Volta, Bun, common npm global prefixes, and npm's_npxcache in addition to resolving everyagentsentry onPATH. Dev installs are no longer hidden: a copy without the atomicapp-bundle-installmodule is labelledunsafe legacy helper installer — remove this copy, because invoking it can still replace a live.appwith a partial bundle. Source:apps/cli/src/lib/self-update.ts,apps/cli/src/index.ts.Newest signed agents-cli install owns the menu-bar helper (#2210). On multi-install Macs (e.g. Homebrew + nvm), a newer release now takes over the helper immediately and an older install can no longer reclaim or downgrade it. Equal-version foreign installs keep the existing owner; missing-helper, Developer-ID repair, and unversioned legacy cooldown behavior are unchanged. Source:
apps/cli/src/lib/menubar/install-menubar.ts.Layered resource listing is ~40% faster.
getActiveResourceProfile()readagents.yamltwice per call — once up front, then again insidegetActiveResourceProfileName()— andlistResources()calls it once per resolved resource, so a listing paid two memoizedreadMeta()round-trips (ensureAgentsDir()plus fourstats each) for every entry. Reading it only after the profile name is known drops one of them. Measured onyosemite-s1against the real~/.agents: one pass over all eight resource kinds (135 entries) went 10.52 ms → 6.23 ms, andagents doctor --jsonspends ~243 ms in this path across 95 listings. No behavior change: the read count is never higher on any path and is unchanged whenever a profile name resolves — the one saved read is the up-front one that theif (!name) return null;guard now skips. TheensureAgentsDir()side effect is unchanged becausegetActiveResourceProfileName()always reachesreadMeta(), viabrand.tslistBrands()when a brand is set and viaresource-profiles.tsotherwise. Source:apps/cli/src/lib/resource-profiles.ts.Routine session discovery now supports an interactive picker and fuzzy names (RUSH-1998).
agents sessions --routineopens a routine picker on a TTY with each routine's last run, run count, and latest-run session count; the selected sessions are grouped by run ID and timestamp.--routine <name>accepts exact, substring, or unambiguous typo matches, and--routinesis an alias for the same session filter.Secret leases now have one scoped, time-boxed domain model (RUSH-2254). A lease names one bundle, an explicit validated key subset, an absolute expiry, harness scope, and sleep-persistence posture. Durations use the broker's 1-minute to 30-day safety bounds, duplicate keys normalize once, unknown keys fail closed, and expired leases cannot project values. Source:
apps/cli/src/lib/secrets/lease.ts.Fleet devices accept an explicit SSH private-key path (RUSH-2265).
agents devices set <name> --auth key --identity-file <path>stores the path on that device and every shared SSH invocation passes it to OpenSSH with-i, so devices no longer depend on whichever key the ambient ssh agent happens to offer. Switching a device from password to key auth also removes stale password-bundle metadata from the registry.agents doctordiagnoses Windows OpenSSH public-key enrollment (RUSH-2266). On Windows it reads the effectiveAuthorizedKeysFile, selects the ProgramData administrator file or per-user profile file, verifies a public-key record is present, and checks that the administrator file grantsFullControlto onlySYSTEMandAdministrators. The audit is read-only and never reads or prints private keys or passwords; fleet doctor forwards a locally authored summary and directs operators to run doctor on that box for details.
1.22.26
Make bare
agents setupa re-runnable onboarding hub with live capability status and direct access to browser, computer, secrets, fleet, share, watchdog, and preference wizards.agents apply --provision-secretspushes the manifest's declared secrets bundles to each device, instead of only printing a reminder (RUSH-1968). This gap is a direct cause of the ticket: an operator who needed secrets on a worker box had no supported path —applysaid "recreate manually" and nothing else — so they hand-exported the file store's master key across the fleet. The provisioning primitive now exists, andapplyruns it as a fifth reconcile phase, last, because it is the most sensitive mutationapplyperforms.It is off by default and is a flag, not a manifest field:
agents.yamlis shared, so a file-level default would mean someone else'sapply -ysilently ships credential values. Three gates, and every refusal still prints aneeds-secretreminder so a skipped device is never silent — the flag must be set, the device must be reachable, and its host key must be pinned (the same baragents exec --copy-credssets, EXEC-34).Backend follows the platform:
fileon Linux,keychainon macOS/Windows. That is the load-bearing default — a headless Linux box has no keychain and its file store auto-provisions its OWN machine-local key, so each device gets an unshared at-rest key and no passphrase is forwarded. That is the direct alternative to the fleet-wide shared secret this ticket is about.With provisioning on,
applyruns one extraagents secrets list --jsonper device (metadata only — names and timestamps, never values) and skips a bundle the device already has; without that, every run re-resolves the bundle locally and a resolve can prompt for Touch ID, so a converged fleet would nag on every apply. It compares presence, not content —--forcere-pushes regardless. The--planmatrix gains asecretscolumn, shown only when the manifest declares bundles, and names the flag when the capability is available but off. Source:apps/cli/src/lib/secrets/push.ts(extracted from theexport --hostaction so a lib no longer needs a command module),apps/cli/src/lib/fleet/apply.ts,apps/cli/src/commands/apply.ts.agents teamsauto-scheduling is health-, harness-, and load-aware, and fails loud when no pool device can run the agent (RUSH-2002). Placing an unpinned teammate onto a--devicespool used to be a pure roster count that could land it on an unreachable box, an overloaded one, or one where its agent isn't installed.teams startnow probes the pool once (reachability + load from the same snapshotagents devicesshows, plus whether the teammate's agent is installed there) and: excludes unreachable / overloaded (loadedheadroom) /agents.max-concurrent-capped / not-installed devices, then ranks the survivors by agent installed + signed in, then lower load, then fewer running teammates. If no pool device can run a pending teammate's agent,teams startfails loud —No device in the team pool can run [email protected]. Run 'agents devices ping' to see which devices have the agent installed + signed in.— instead of stranding the teammate or silently falling back to a local run;--forcedowngrades it to a warning. A probe that could not reach the pool does not trigger the failure (no false positives). The pick stays pure and fully unit-tested;teams addis unchanged (no probe on the add path). Source:apps/cli/src/lib/teams/scheduler.ts,apps/cli/src/lib/teams/placement-probe.ts,apps/cli/src/lib/teams/agents.ts,apps/cli/src/commands/teams.ts.agents reconnect [session-id]re-enters a dropped remote agent terminal, and the auto-reconnect no longer dead-ends on a dead pane (RUSH-2085). When the network dropped duringagents run --device <box>and the peer's tmux pane was gone by the time the link came back, the reattach ranagents sessions focus <id> --local --attach-only, which hard-failed withNo live session matching …and dropped the user at a bare shell with the id scrolled off screen. Two fixes: the auto-reconnect reattach now runsagents sessions focus <id> --local(no--attach-only), so a surviving pane is joined and a dead one RESUMES in place instead of dead-ending; and a newagents reconnect(alsoagents sessions reconnect) is the manual companion for after the auto-loop gave up or a VS Code terminal tab closed — attach the live pane if it survived, else resume the session. With no id it targets the most recent session started from the current directory (the terminal that most likely just dropped), not the full fleet picker. The exhausted / remote-exit notices now print the exactagents reconnect <id>command instead of a raw id and a shell prompt. Source:apps/cli/src/lib/hosts/reconnect.ts,apps/cli/src/commands/reconnect.ts.agents routines addnow rejects an agent the local daemon can't fire, at add time (RUSH-2102).--agent opencode(or any real, installable agent outside the daemon'sAGENT_COMMANDStable — currentlyclaude,codex,gemini,cursor,kimi,droid,muse) used to passvalidateJobbecause it only checked the agent against the full agent registry, not the daemon-runnable subset — the routine was written to disk and only failed once the scheduler fired it (Unsupported agent for daemon jobs: opencode).validateJobnow rejects it immediately for the default local placement, with an error naming the supported agents. Routines placed withhostStrategy: host/fleet/cloudare unaffected — those dispatch throughagents run/a cloud provider, not this table, so a wider agent set is legitimately supported there. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/agents.ts.OpenClaw's capability table no longer claims
hooks: truewith zero hooks ever installed (RUSH-2122).registerHooksToSettingshas noopenclawbranch and silently returned{ registered: [], errors: [] }for it, soagents sync openclawreported success while installing nothing, andagents doctortreated the agent as hooks-capable with no way to detect the gap. OpenClaw only exposes a fixed set of internal, named hooks (e.g.boot-md, which runsBOOT.mdon gateway restart) — there is no general event->shell-command registration surface an agents-clihooks.yamlmanifest could target — socapabilities.hooksandsupportsHooksnow readfalse, matching what the CLI can actually do. A new completeness test (hooks-capability-completeness.test.ts) pins everyhooks: trueagent to a real branch inregisterHooksToSettingsso a capability flip can never ship again without a registrar behind it. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/hooks-capability-completeness.test.ts.agents browser streamkeeps one Node process and browser-daemon IPC socket warm across repeated actions (RUSH-2149). The newline-delimited JSON interface sends every request through the existing browser daemon and its cached CDP connection, so screenshot/click loops no longer need a freshagentsprocess or IPC connection per action.--task(orAGENTS_BROWSER_TASK) supplies the default task, astartresponse becomes the default for later lines, and malformed input returns an error response without ending the stream. Fleet-remotestartrequests enforce the same device-localbrowser remote-controlconsent gate as the ordinary command. Source:apps/cli/src/lib/browser/ipc.ts,apps/cli/src/lib/browser/stream.ts,apps/cli/src/commands/browser.ts.agents sessions backfill toolsreads a growing transcript incrementally, and the search index compacts itself (RUSH-2208). Incremental discovery already appended tool calls for a Claude/Codex session that grew; the backfill did not. EveryensureToolIndexpass re-read the transcript from byte 0 and deleted the session's stored evidence to rewrite it, so a session backfilled N times cost N full parses of an ever-larger file — measured on a 4.5 MiB, 4000-call transcript that grew by 20 calls: 344 ms and 4020 calls re-parsed, now 12 ms and 20 calls. Schema v36 adds a resume point totool_scan_ledger(parsed_offset, the byte just past the last complete record consumed, andparser_state, the collector snapshot at that offset), so a transcript that only grew is read from where the last pass stopped and merged into what is already stored; the batch byte budget now counts the bytes a pass actually reads rather than the file's size, so one bounded batch covers far more growing sessions. A different extractor version, a mismatched ledger path, a file shorter than what was parsed, or an unreadable snapshot still re-reads the whole file. Two related fixes ride along:tool_call_textrows are addressed by therowidof the call they describe instead of the UNINDEXEDcall_key, which made every delete a full scan of the FTS index, and the scan path now runs a bounded, threshold-gated FTS'merge'after each batch of writes, so index health no longer depends on someone runningagents sessions optimizeby hand. Source:apps/cli/src/lib/session/tool-index.ts,apps/cli/src/lib/session/tool-store.ts,apps/cli/src/lib/session/db.ts.OpenCode session scans re-index only the sessions that changed (RUSH-2210). OpenCode keeps every session in one shared
opencode.db, and the scanner stamped each session with that whole file's mtime/size. Any write to any session therefore invalidated every indexed session, so a single new turn re-emitted up to 1000 sessions — and the indexer re-openedopencode.dbonce per re-emitted session to re-parse a transcript that had not moved. Each session is now stamped with its own newest write time (across itssessionrow, its messages, and its parts) and the byte length of its message + part payloads, so an unchanged session is skipped; the file-level stat stays only as the cheap "nothing changed at all" short-circuit, and a scan opensopencode.dbonce instead of once per session. The stamp deliberately does not rely onsession.time_updatedalone, which real databases leave hours behind the session's newest part. A side effect:sessions.file_sizefor an OpenCode row is now that session's payload size instead of the whole database's size, so the tool-backfill byte budget and its 16 MiB in-memory parser cap finally reflect the real cost of parsing that one session. Source:apps/cli/src/lib/session/discover.ts.Session query hot path is indexable again (RUSH-2211). The default
agents sessionslisting sort (ORDER BY IFNULL(last_activity, timestamp) DESC) wrapped the sort column inIFNULL(), which defeatsidx_sessions_last_activityand forces a full table sort on every list/resume query; a new migration backfillslast_activityso it's unconditionallyNOT NULLand the sort now runs on the bare column (EXPLAIN QUERY PLANconfirmsUSING INDEX idx_sessions_last_activity). The post-query existence check now batchesfs.existsSyncper directory instead of one stat syscall per row — real transcript trees put many sessions in one project directory, so this collapses thousands of stats into a handful ofreaddirSynccalls with the same result. Interactive label search (ftsSearch) no longer runs a leading-wildcardLOWER(label) LIKE '%q%'scan of the wholesessionstable on every keystroke; it now queries the already-indexed FTS5labelcolumn. Source:apps/cli/src/lib/session/db.ts.The standalone
browserbinary now routes--host/--device(RUSH-2214).browser start --host <box>dispatches to the remote over SSH, exactly likeagents browser start --host <box>already did — previously the standalone bin dropped the flag withunknown option '--host'because it never entered the top-level router. A self-named or absent host still runs locally. Source:apps/cli/src/browser.ts,apps/cli/src/lib/hosts/passthrough.ts.agents run --deviceno longer reports a host as unreachable when the SSH probe times out (RUSH-2249). The ready probe (readyProbeinhosts/ready.ts) now disables SSH multiplexing so a stale control socket cannot hang the local client, and it checksr.timedOutbefore parsing stdout — a slow login shell (nvm/sdkman init, cold node startup) producing an empty stdout was silently treated as "not reachable". A timeout now surfaces a distinct, actionable error that names the cause and suggestsagents ssh <host> agents viewto confirm manually, rather than the misleading "not reachable over SSH" message. Source:apps/cli/src/lib/hosts/ready.ts.agents teams startnudges the operator toward feed milestones (RUSH-2250). After launching teammates,teams startnow prints a one-line tip — teammates are briefed to post IMPORTANT milestones to the feed (watch them withagents feed timeline), and team progress is watched withagents teams status <team>. Print-only in both the single-wave and--watchpaths (suppressed under--json); no engine behavior changes. Pairs with the.agents-systemguidance that instructs teammates to post those milestones. Source:apps/cli/src/commands/teams.ts.secrets listno longer skips every biometry-ACL'd item, sohold/always-policy bundles are readable again (RUSH-2251). A regression first shipped in v1.22.10 addedkSecUseAuthenticationUI: kSecUseAuthenticationUISkipto the keychain helper'slistdata-protection pass.UISkipmakesSecItemCopyMatchingsilently omit every item protected by a biometry access control — which is exactly the value itemssetwrites — so enumeration returned only the no-ACL metadata andnever-policy items. Every consumer that builds its keychain read set from that enumeration (secrets exec/get/unlock/view --reveal/export,agents run --secrets,ssh,browser,share) then reported the real secrets asstored item '…' not found, andunlockcould not even warm the broker to work around it. The DP pass is now attributes-only with nokSecUseAuthenticationUIkey:kSecReturnAttributeswithoutkSecReturnDatanever evaluates the ACL, so it neither prompts for Touch ID nor filters the ACL'd items out — restoring the design the code comment already described. The RUSH-2233 timeout bound on that pass is unchanged. Source:apps/cli/src/lib/secrets/keychain-helper.swift.Hook-cache background refresh recovers from an orphaned single-flight lock (RUSH-2259). The stale-while-revalidate lock (
<cache>.bg.lck) was only released by the background refresh'sEXITtrap, so a hard kill (SIGKILL, OOM, reboot) that skipped the trap orphaned the dir and every future refresh'smkdirfailed — permanently stalling background refresh while stale cache was served forever. The shim now reclaims a lock older than a 5-minute TTL before acquiring, so a dead lock self-heals on the next fire. Source:apps/cli/src/lib/hooks/cache.ts.agents doctornow fails loud when this box cannot reach the owner-delivery lane (RUSH-2262). The feed/notify owner lane (agents notify,agents feed post --level important/--blocked) delivers over the rush-backed owner channel (iMessage), which only works from a context that hasrushon PATH and can read its keychain-bound session — so a headless Linux fleet box (no rush) or a non-GUI SSH session on a mac (login keychain locked) silently could not escalate a blocked agent, surfacing only as an after-the-factowner failed: …line.agents doctorhad no signal for it. A new critical finding,owner-sink-unreachable, probes the same transport from the same context doctor runs in (which rush+rush whoami, never~/.rush/user.yaml, since the token is a keychain item) and reportsowner → unreachable: rush CLI not on this box's PATH/rush has no usable session herewith the fix. It fires only when owner delivery is configured for the fleet, so an un-opted-in box is never flagged;agents notify --dry-runis not this check (it short-circuits before thewhich rushpreflight and reports success even where rush is absent). Source:apps/cli/src/lib/channels/owner-sink.ts,apps/cli/src/lib/devices/doctor-findings.ts,apps/cli/src/commands/doctor.ts.agents insightsowns counter mix;agents trendsis a deprecated alias. The former top-leveltrendstree (harness/model mix, tools-per-session, token ratios, secrets/browser recipes, raw usage query) now lives underagents insights mixandagents insights <recipe>/query/recipes. Bareagents insightsremains the behavioural report (transcript content, account split).agents trendsstill works but prints one deprecation line and runs the same mix tree — no second implementation. Why: two peer "analytics" verbs (insights+trends) taught agents and humans to guess; one verb, two engines (content vs counters). Latency stays onagents perf; quota onagents usage; skill/slash popularity onagents sessions stats. Source:apps/cli/src/lib/analytics/mix-commands.ts,commands/insights.ts,commands/trends.ts,docs/06-observability.md.The keychain reaper no longer kills the auto-lock-on-sleep watcher (RUSH-2232 follow-up). The reaper (shipped in 1.22.23) classified a process as a reap target purely by the helper binary path, which also matches the broker's deliberately long-lived
watch-lockwatcher — a healthy child of the live daemon that wipes the in-memory secret store on sleep. Its class-(b) rule ("helper child of a live parent, older than 90s") therefore killed the watcher on its second sweep (~10 min after the daemon started hosting the broker), silently disabling auto-lock-on-sleep. Reap-eligibility now matches the full command line and excludes thewatch-lockverb, so only the short-lived keychain reads/writes a wedgedcoreauthdcan hang are ever reaped. Source:apps/cli/src/lib/secrets/reaper.ts(isReapableHelperCommand).
1.22.25
issue: 2108 type: fixed
agents sessions focus ... --active now excludes closed and crashed registry rows; explicit lifecycle filters such as --closed and --crashed still select them. Per-device latest / oldest selectors also wait for the peer's filtered index result instead of widening to live rows whose version is unknown.
agents doctornow reports the file-store master key when it is live in the process environment, not just when it is exported from a shell rc file (RUSH-1968).rc-hygiene.tsscans FILES, which leaves a real hole: a value inherited by a long-lived process outlives the rc line that set it, so an operator who deletes~/.zshenv:8gets a cleanrc-secret-exportwhile every shell, editor and agent started before the edit still carries the key and hands it to everything they spawn. Confirmed onyosemite-s1, which has zero rc exports and still had the value in its environment. The newenv-secret-exportwarning names that state and says the deletion is not sufficient; like every other finding it reports only that the variable is set, never its value. Expected inside a release sign context (headless-sign-context.shsets it deliberately), which the message says so an operator on the home base does not chase it. Source:apps/cli/src/lib/secrets/rc-hygiene.ts(masterPassphraseInEnv),apps/cli/src/lib/devices/doctor-findings.ts,apps/cli/src/commands/doctor.ts.agents doctor --devicesnow reports a remote box's secret hygiene, and its inventory probe no longer times out on a slow box (RUSH-1968). The fan-out ran each remote's owndoctor --jsonbut parsed only.fleet, discarding thefindingsthe remote had already computed — so a leaking box read as clean from anywhere but itself. The remote'src-secret-export/env-secret-exportrows now ride back, but the remote contributes exactly one thing: the kind. Severity, message and remediation are generated locally, anddeviceis overwritten with the name that was dialled. Each of those matters separately — a remediation is a command a human copies and runs, a message is the one place a secret value could re-enter a readout that otherwise never prints one, a severity decides the CRITICAL section, and a device name decides which box gets blamed. The cost is detail: the fleet row names the box and the kind, and says to runagents doctorthere for the file and line. Only those two kinds forward — sign-in and divergence rows are recomputed centrally and would otherwise double.Separately, the probe's 30s timeout was below the real cost of the command it runs —
doctor --jsonmeasures 57s onyosemite-m0and 136s on an idle box — so every slow device silently contributed nothing at all: no inventory, no sign-in, no divergence. Raised to 180s, matchingChildProcess.doctorTimeout, which was set to 180 for this same command for this same reason. Source:apps/cli/src/commands/doctor.ts(asRemoteSecretFindings,probeFleetInventory).Feed and menu-bar session counts now use the
agents sessions --activelifecycle model (RUSH-1993). Feed outcome headers count distinct mailbox agents in every state, so repeated open blocks from one agent cannot makeneeds youexceedagents. Feed rows replace genericterminalwith a known host app and label routine-spawned sessions asroutine:<name>. The menu-bar snapshot backfills the same routine metadata as active-session JSON and renders the full canonical status set (working,waiting,idle,queued,orphan,crashed,closed,abandoned,unknown) without reclassifying warm rows from attention files. Source:apps/cli/src/commands/feed.ts,apps/cli/src/commands/sessions.ts,apps/cli/src/lib/feed-outcome.ts,apps/cli/src/lib/menubar/snapshot.ts,apps/cli/menubar/Sources/MenubarHelper/{LocalState,StatusItemController}.swift.A typo'd command with
--hostsaid the opposite of the truth (RUSH-2022). The--host/--devicerouter runs before commander parses, soagents session resume --host <box>(one letter offsessions, which does accept--host) reported`agents session` does not support --host/--device— a true statement about a command nobody typed. Unknown names now fall through tounknown command '<name>'with a did-you-mean, and the spellcheck can suggest the lazily-registered groups (sessions/teams/cloud/…) it previously could not see. A real command with no remote semantics still gets the flag-support error. Source:apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/startup/command-registry.ts.agents publishis in the lazy command table.commands/packages.tsregisters it at top level but the registry did not list it, so it only resolved through the unknown-command fallback that loads the whole command tree. Found by the new test that pins the command-name set against the real tree.A mistyped command keeps its
--host. The distance-1 auto-correct now re-checks routing after correcting the name, soagents docto --host <box>corrects todoctorand actually routes to<box>— previously the corrected command re-parsed locally with a--hostit silently dropped (a regression caught in review: it used to fail loudly with the wrong message, corrected-but-unrouted made it fail silently instead).apps/cli/src/index.ts.Cursor now supports
--mode planheadless and interactive (RUSH-2101). The capability registry previously listed onlyeditandskip, soagents run cursor --mode planand routine jobs silently degraded to writableedit.cursor-agenthas supported--plansince the 2026-01-16 CLI release; the registry,AGENT_COMMANDSflag mapping, and the routine runner now forward it correctly. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/exec.ts,apps/cli/src/lib/runner.ts.agents sessions resumepicker now filters in-memory on every keystroke instead of re-querying the database (RUSH-2212). Thefiltercallback in the multi-select picker was callingfilterSessionsByQuery, which runs a full-text search scan againstsessions.dbon every character typed. It now uses the same cheap in-memory substring match (sessionMatchesQuery) the session browser already applies, so the picker stays responsive regardless of library size. Source:apps/cli/src/commands/sessions-resume.ts.
type: minor
Add Cursor Cloud Agents as a native cloud provider so agents run cursor --cloud and agents cloud run --agent cursor dispatch through Cursor's v1 REST API, with status, streaming, cancellation, and follow-up runs.
A starved
coreauthdcan no longer hangsecrets listforever (RUSH-2233). The keychain helper'slistruns two passes, and the data-protection pass omitskSecUseAuthenticationUI: …Failon purpose (that flag drops every biometry-ACL item even when authentication is healthy). Omitting it means the query still reaches LocalAuthentication/coreauthd, which has no deadline of its own — a wedgedcoreauthdleftSecItemCopyMatchingblocked for the life of the process. That is why helpers accumulated for the bounded-spawn + reaper work in RUSH-2231/2232 to clean up. The pass now runs on a background thread behind a 3-second wait: on timeout the helper logs one line to stderr, skips the pass, and prints whatever the file-keychain pass produced — the same handling the screen-lockederrSecInteractionNotAllowedcase already got. SetAGENTS_KEYCHAIN_LIST_TIMEOUT_MSto override the deadline;AGENTS_KEYCHAIN_BOUNDED_TEST=1runs the helper's headless self-test for the bounded wait. Source:apps/cli/src/lib/secrets/keychain-helper.swift.Use one three-minute AGI Menu snapshot and one watchdog executor (RUSH-2260). The menu bar now reads routines, 40 indexed recent sessions, the daemon-warmed active-session cache, and persisted watchdog state in one subprocess every three minutes;
doctor --jsonremains independently limited to 15 minutes. The menu no longer executes watchdog ticks. The daemon is the sole automatic watchdog executor, warms active sessions on the same three-minute cadence, is gated by device-localwatchdog.enabled, and cleans failed/timeout routine process groups that are still alive before they can overlap a replacement run. Source:apps/cli/src/lib/menubar/snapshot.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/routine-process-cleanup.ts.Add Warp Agent CLI (Oz) as a harness (RUSH-2261).
warpis now a first-class harness:agents add warp,agents run warp,agents teams add <team> warp, resource sync, andagents viewall work. The CLI command isoz(the shared Warp binary); install is self-updating viabrew install --cask oz(macOS) /oz-stableapt|yum|pacman (Linux), config lives under~/.warp/, the rules/context file isAGENTS.md, and auth isoz login(browser OAuth) or aWARP_API_KEYtoken for headless/CI. Capabilities are truthful and gated bysupports(): MCP (Claude.mcp.jsonschema at~/.warp/.mcp.json, stdio + http with headers) and skills are on; hooks, allowlist, commands, plugins, subagents, workflows, and memory are off (Oz exposes no matching install surface). Sessions are not indexed — Oz stores conversations server-side (retrieved with auth viaoz run conversation get <id>), so there is no local transcript foragents sessions. No usage bar — Oz exposes no usage/limits endpoint. Source:apps/cli/src/lib/{agents,exec,mcp}.ts.
scripts/release.sh now runs orchestration in a fresh release-owned worktree, so a dirty shared checkout or feature branch no longer blocks or contaminates a release.
Plugin discovery warns instead of silently skipping a directory with no
.claude-plugin/plugin.json(RUSH-2270). Aplugins/<name>/directory missing (or with a malformed) manifest was invisible toagents plugins list/info/sync,agents doctor, and the marketplace materialize-into-version-homes step, with zero diagnostic anywhere in the chain — noagents sync/agents repo pullcould surface it, since discovery dropped it before any of those ran. Found via a real case: theworkplugin merged tophnx-labs/.agents-systemwithout its manifest and sat invisible for a full merge cycle; the same fix run against this box's own~/.agents/plugins/duckfound it missing one too.discoverPluginsInDirnow writes oneagents-cli:warning to stderr naming the directory and the missing manifest path, matching the existing advisory-not-fatal patternsyncMarketplaceManifestalready uses for a malformed (as opposed to missing) manifest. Source:apps/cli/src/lib/plugins.ts.Muse login is recognized under
providers.meta(balanced no longer signed_out). Livemuse loginwrites~/.config/muse/auth.jsonas{ schema_version, providers: { meta: { access_token, user_email, … } } }. The presence detector only walked one object level, so it never saw the token underproviders.metaand reported signed-out after a successful OAuth —agents run museunder balanced then failed with "excluded: 0.1.0 (signed_out)". Detection now recurses into nested provider slots and surfacesuser_emailwhen present. Source:apps/cli/src/lib/agents.ts.File-backed secret resolution now fails on decryption errors instead of treating unreadable ciphertext as an absent value (RUSH-2264). A wrong
AGENTS_SECRETS_PASSPHRASEor tampered encrypted value now reports the affected file-store item and stops before launching a consumer with an empty secret such asHCLOUD_TOKEN. Source:apps/cli/src/lib/secrets/filestore.ts,apps/cli/src/lib/secrets/bundles.ts.
1.22.24
agents run --leasenow shares one warm pool across repositories by default (RUSH-2225). Repo sandbox/CIprofile:labels no longer split lease reuse into one idle box per repo; a dedicated lease pool is explicit with.crabbox.yamlleaseProfile:. An empty pool keeps its newly warmed box for later callers. Concurrent runs attach with crabbox--reclaimand launch with separate working trees, agent homes, and credential files, so callers share compute without clobbering run state. Switching repos re-syncs the checkout, trading cache latency for lower idle-compute cost. Source:apps/cli/src/lib/crabbox/config.ts,apps/cli/src/lib/crabbox/lease.ts,apps/cli/src/commands/exec.ts.agents sessions insightsturns multi-harness session history into an action list (RUSH-2280). The existingagents insightscommand is now also nested under the sessions noun, accepts repeatable--agentfilters, and reports deterministic offline friction/thrash, owner corrections, automatable repeats, harness split, and ranked rule/skill/automation/product actions with evidence counts and shortened sample session ids./sessions-insightsis a thin agent entry over the same CLI implementation;--narrativeremains opt-in and receives aggregate data only. Source:apps/cli/src/commands/insights.ts,apps/cli/src/lib/session/insights.ts,.agents/commands/sessions-insights.md.agents sessions focusrecovers dead panes and shares the sessions browser's selectors (GH-2108). A retained tmuxremain-on-exitpane is probed through#{pane_dead}immediately before attach, so dead or missing panes no longer open aPane is deadscreen.focusaccepts session ids, topic/path searches,agent@versionselectors (including per-devicelatest/oldest), device, project/time, team/routine, skill/plugin, favorites, and the complete live-state union. Focus, resume, attach, andrun --resumenow use one recovery decision on the origin device: a healthy exact origin performs native resume; otherwise balanced selection chooses a healthy version of the same harness and sends/continue <id>to read the indexed transcript, including transcripts retained under version trash. Host-dispatched rows persist the dispatch host as their origin, andattachroutes its detach-record cleanup there before resuming. No usable same-harness version fails with the device, origin version, and account-health reason. Source:apps/cli/src/commands/focus.ts,apps/cli/src/commands/sessions-browser.ts,apps/cli/src/lib/session/recovery.ts.agents secrets setupno longer tells you to setAGENTS_SECRETS_PASSPHRASE, anddocs/secrets.mdstops recommending the shell-rc export it flags as a leak (RUSH-1968). The file-backend note readset AGENTS_SECRETS_PASSPHRASE for headless encrypted-file reads, implying a requirement; headless reads have worked with no passphrase since the store began auto-provisioning a 0600 machine-local key, so it now says so and names the real path. The docs were worse than merely stale: they called an rc export "Recommended for shared/CI machines" and the 0600 key file "identical to" it, which is how a master key ended up in~/.zshenvon seven worker boxes. That equivalence is inverted — the key file is read by one process, an rc export is inherited by every child and readable from/proc/<pid>/environ— and the section also named a pre-#479 key path (~/.agents/.cache/secrets/.passphrase, now~/.agents/.secrets-key/passphrase) and a TTY prompt stepgetPassphraseno longer has. A newdocs-hygiene.test.tspins those claims against the shipped doc so the advice cannot drift back. Source:apps/cli/docs/secrets.md,apps/cli/src/commands/setup-secrets.ts,apps/cli/src/lib/secrets/filestore.ts.agents secrets push/pull,agents sync --secrets, andagents secrets export --to-file/import --from-filereadAGENTS_SYNC_PASSPHRASEnow;AGENTS_SECRETS_PASSPHRASEis the file store's master key and nothing else (RUSH-1968). One variable meant two different secrets: the local file store's master key, and the passphrase that seals a bundle for transport. The store stopped needing a passphrase once it auto-provisioned a machine-local key, but headlesspush/pullstill hard-failed without one — so the only way to get unattended sync on a worker box was to export the master key fleet-wide, handing every same-user process the key to the whole store. Splitting them means a box that only needs headless sync setsAGENTS_SYNC_PASSPHRASEand never has the master key in its environment. The old name still works for sync as a deprecated fallback — warned exactly once per process, so apush --allover many bundles does not flood stderr — so scripted CI and release automation keep working across the upgrade. The headless error now names the new variable (A sync passphrase is required. Run from a TTY, or set AGENTS_SYNC_PASSPHRASE.), andagents sync's skip line with it — it previously readno passphrase available, naming nothing an operator could act on. Note the legacy fallback only works where that value is also the store's master key, since the old name still keys the store; that coupling is the thing being retired. Resolution moved to one chokepoint so the once-per-process promise holds rather than being per-call-site. Also correctsSEC-29a, which claimed the variable applied "exclusively" to the file and age-vault backends: the age-vault backend never reads it (it is gated byagents login), and sync plus the portable--to-fileenvelope were two more consumers — the newSEC-29bstates the split as a normative invariant. Source:apps/cli/src/lib/secrets/sync-passphrase.ts,apps/cli/src/commands/secrets-sync.ts,apps/cli/src/commands/sync.ts,apps/cli/src/lib/sync-umbrella.ts.Added
--teamas an alias foragents sessions --teams. (RUSH-1995)Added absolute
agents secrets unlock --until <date>expiry, mutually exclusive with relative--ttl. (RUSH-1960)Added persisted per-agent-version reasoning effort defaults and surfaced them in
agents view. (RUSH-2005)agents feedno longer crashes when the session index is locked (RUSH-2006). Outcome enrichment callsdiscoverSessionsagainstsessions.db; under concurrent scanner/daemon pressure that open can throwSQLITE_BUSY/ "database is locked" and take down the whole feed. A lock error now degrades to an empty meta set with a stderr warning so blocks still render. Source:apps/cli/src/commands/feed.ts.agents events --event/ filtered activity reads no longer miss matches under--limit(RUSH-2093). The unified reader capped activity records before applying the event-type filter, so a rare match older than the newest-N routine rows (e.g. onepr.openedunder twentyfile.edited) was silently dropped. Event types are now passed intoreadRecentActivityso the cap counts matching rows; a non-activity--moduleskips the activity half entirely. Source:apps/cli/src/lib/event-stream.ts.agents mcp addno longer strips comments fromagents.yaml. The write path (writeManifest/serializeManifestinsrc/lib/manifest.ts) used plainyaml.stringify, which dropped every hand-written comment on every add. It now round-trips viayaml.parseDocumentand edits only the keys that changed — the same approachserializeCentralalready uses for the central meta file (RUSH-2090).Hook cache refresh is now single-flight, backs off on failure, and logs its real exit code (RUSH-2121). The background refresh in the hook cache shim acquires an atomic
mkdirlock, so concurrent hook invocations no longer stampede into N parallel refreshes; a persistently-failing refresh records a failure timestamp and is skipped until a 60s backoff elapses instead of re-firing on every invocation; and the refresh now emits ahook.cache.refreshevent carrying the subshell's real exit code instead of a hardcoded0. Source:apps/cli/src/lib/hooks/cache.ts.agents sync --host allno longer fails every peer withunknown option '--json'. Fleet fan-out injects--jsonon each remote so the roster can parse per-device results, butsyncnever registered the flag. Register--jsononagents syncand emit a machine-readable umbrella/agent/repo payload so peers accept the flag and return parseable stdout (RUSH-2216). Source:apps/cli/src/commands/sync.ts.A wedged keychain can no longer pile up
agentsprocesses (RUSH-2231, RUSH-2232). A stalled macOScoreauthdused to hang the signed keychain helper's XPC receive forever, so every secrets-touchingagentscommand blocked and dozens of helper processes plus their<defunct>zombies accumulated and made the machine sluggish. Two fixes: (Layer 1) every keychain-helperspawnSyncis now bounded and hard-killed (SIGKILL) on timeout — 8s for never-prompt verbs (has/set/delete/list*), 60s for the may-prompt reads (get/get-batch/migrate-*) — surfacing a typed timeout error and arming the read back-off instead of hanging. (Layer 3) the daemon reaps the backlog: a 5-minute tick kills orphaned helpers (reparented to PID 1, past a 30s grace) and, two-sweep-debounced, the helper child of anagentsproc stuck past 90s (child first, escalating to the parent only if it stays wedged) — never touching a process whose start-time can't be captured or whose path doesn't match the helper. Any keychain touch now also opportunistically starts the daemon so the reaper runs even on a secrets-only box. Source:apps/cli/src/lib/secrets/index.ts,apps/cli/src/lib/secrets/reaper.ts,apps/cli/src/lib/daemon.ts.agents inspectandagents doctornow report harness-scoped hook inventory as capable, on-disk, wired, and unmanaged state. Their JSON output carries the same state sets fromgetResourceInventory, so a harness with hook files or native wiring no longer collapses to a misleadingHooks (0)summary. Inventory is keyed by agent harness and installed version, not by configured model. Source:apps/cli/src/lib/resource-inventory.ts,apps/cli/src/commands/inspect.ts,apps/cli/src/lib/doctor-diff.ts.Fix: the keychain-helper reaper never reaped on macOS. The daemon reaper shelled
ps -o etimes— a GNU/Linux procps keyword that macOSpsrejects with a non-zero exit — soexecFileSyncthrew on every tick andreapOrphanedKeychainProcessesreturnedreaped: 0on its only supported platform, leaving orphaned/wedgedAgents CLIhelper processes to pile up. Switched to the portable BSDetimekeyword ([[dd-]hh:]mm:ss) with a parser to seconds. This also un-quarantines the darwin integration test that #2153 had toit.skipto unblock releases: that test's symlink fixture was correct all along — it readreaped: 0only because the reaper's ownps etimescall threw before parsing anything. Verified on macOS 15.4.1: 20/20 reaper tests pass, the integration test reaping a real orphaned sleeper. Source:apps/cli/src/lib/secrets/reaper.ts,reaper.test.ts.agents modes [agent[@version]]lists the permission modes a harness accepts. The modes analog ofagents models: for Claude / Codex / Cursor / …, shows which--mode plan|edit|auto|skipvalues work, the native CLI flags, the native default (*), any configuredrun.defaultsmode, and degrade notes (e.g. plan→edit on Antigravity).agents inspectalso prints the mode list next to capabilities, andagents run --help/agents modelspoint at the new command. Source:apps/cli/src/commands/modes.ts,apps/cli/src/lib/agent-modes.ts.agents insights --allnow works as an alias for--since all. It previously exited withunknown option '--all'— a hard failure in the middle of a report the user asked for, on the spelling most people reach for first. An explicit--sincestill wins, so--all --since 7dresolves to 7d rather than silently contradicting itself. Source:apps/cli/src/commands/insights.ts.Coexisting agents-cli installs no longer fight over the menu-bar helper. The helper lives at one path in Application Support, but every install on the box runs the startup self-heal, and both the version stamp and the plist's baked
AGENTS_ENTRYrecord whichever copy acted last — so each copy read the others' marks as drift and recopied the app bundle over them. Recopying replaces the executable under the running helper and kills it, launchdKeepAliverestarts it, and the next copy repeats it: a new pid every 5-15 seconds, 578 launches in one observed helper log, and a status item that never stayed visible whileagents menubar statusstill reportedrunning: yes(a pid always existed). The plist'sAGENTS_ENTRYis now treated as the owner and only the owner reinstalls freely; a same-install upgrade keeps its entry path, sonpm updatestill installs the new helper normally. Another install still gets there — immediately if the recorded owner is gone from disk, otherwise at most once an hour — so a stale copy that merely still sits on disk can't freeze the menu bar for whichever install the user actually upgrades. Repairs (a missing helper executable, a Developer-ID heal) are never gated, andagents menubar setupbypasses the gate as the immediate manual fix. An ad-hoc/dev-signed build never wins the timed takeover — recopying an un-notarized bundle over a good one gets it rejected as "damaged" — though it can still adopt a helper whose owner is gone. Two installs that are both invoked regularly still trade ownership at the cooldown, so the helper restarts about once an hour until one is removed; that is bounded rather than converged, and the real fix remains a single install.agents menubar statusno longer promises that a stale helper "runs on nextagentsstartup", which is not guaranteed on a multi-install box. Fixes #2109. Source:apps/cli/src/lib/menubar/install-menubar.ts.Muse Code global binary is visible to
agents view/isVersionInstalled. Muse installs a single self-updating launcher at~/.local/bin/muse(same shape as droid), butgetBinaryPathstill resolved a version-homenode_modules/.bin/musethat never exists. Afteragents import muse/agents add muse, version dirs now resolve to that global path so managed view, collapse, and live-version bookkeeping match what actually executes; install no longer writes a self-referential shim symlink. Source:apps/cli/src/lib/versions.ts.Muse multi-version isolation matches Claude/Codex (XDG pin, not bare symlink). Claude/Codex isolate per version with
CLAUDE_CONFIG_DIR/CODEX_HOME. Muse has no dedicated config env; it reads$XDG_CONFIG_HOME/museand$XDG_DATA_HOME/muse. Afteragents import muse,~/.config/museis a symlink into the version home — Muse refuses that path withAgent Definition filesystem source failed: SymlinkOrReparseand exits 1 (the Zionagents run [email protected]failure). Managed launches now pinXDG_CONFIG_HOME+XDG_DATA_HOMEinto the version home frombuildExecEnv, the main shim, and versioned aliases ([email protected]), the same way Claude pinsCLAUDE_CONFIG_DIR. Muse is also listed inCONFIG_ENV_ISOLATED_AGENTSso--isolatedinstalls are honest. Source:apps/cli/src/lib/{exec,shims}.ts.Scheduled routines now run on a per-account
claude setup-tokenwhen one is provisioned, instead of throwing it away.buildRoutineSpawnEnvunconditionally deletedCLAUDE_CODE_OAUTH_TOKEN, so even afterbuildExecEnvinjected a long-lived, non-rotating setup-token from the reserved file-backedauthbundle (resolveClaudeSetupToken), a routine still fell back to the version home's rotating.credentials.jsonlogin. With one Claude account signed into several version homes (or several fleet boxes), that rotating login is the single-use-refresh-token revocation storm — one home's refresh silently revokes every sibling copy, so an unattended routine keeps landing on a just-revoked token and dies withauth_failed: OAuth access token has been revoked/Please run /login, even thoughagents viewshows the account healthy. The delete now distinguishes the two flavours: a per-account setup-token (keyed to this home's own account) is re-asserted and KEPT; an inherited ambient token (a shared value the daemon env happened to carry — the RUSH-1822 fleet-logout path) is still stripped so a routine never runs on it. Provision the token with/fleet:mint-auth. Source:apps/cli/src/lib/runner.ts(buildRoutineSpawnEnv).Resolved secrets no longer appear in
psfor tmux-launched agents (RUSH-2100). An interactiveagents runwraps the agent in tmux viaexec env K=V … <agent>, which put the entire exec env — every resolved secrets-bundle value — into the pane's command line, readable by any process of the same user. On one fleet box six live processes carriedAGENTS_SECRETS_PASSPHRASE, the key that decrypts every file-backed bundle on that machine. The pane now sources a0600env file and unlinks it beforeexec, so only the file path is argv-visible; a missing file aborts the pane rather than launching half-configured. Every key routes through the file, not a curated "secret-bearing" subset, so a newly added credential is covered without anyone maintaining a list. Source:apps/cli/src/lib/exec.ts,apps/cli/docs/specifications.md(SEC-8a).
1.22.23
Daemon-warmed cross-surface session-status cache (RUSH-2062). Menubar, Factory, watchdog, and CLI used to each re-run a full
sessions --activegather (~9s / ~170MB) with no sharing. The daemon now warms a local active- session snapshot every 15s; those surfaces read the warm file when fresh.session/remote.tsis cache-first too — a reachable host skips SSH while its cache is within the freshness window (it used to replay only on unreachable). Immutable per-session fields (topic, label, cwd, …) are memoized by transcript mtime; live status never rides that memo. Source:apps/cli/src/lib/session/session-cache.ts,apps/cli/src/lib/session/remote.ts,apps/cli/src/lib/daemon.ts.agents sessions <id>/agents resume <id>resolve across the fleet fast, with early-exit and SSH cancellation (RUSH-2203). The cross-machine resolve fanned out withawait Promise.alland no cancellation, so one slow or unreachable peer stalled the whole lookup to the 12s per-peer timeout even after a fast peer already returned the exact match — a full-UUID resolve on a fleet with an offline box took ~16-40s. A full-UUID resolve (globally unique) now opts into a cancellable, early-exit fan-out: the first peer holding it resolves the sweep and SIGTERMs every still-outstanding peer, and a local UUID hit resolves with zero SSH.agents resume <label>auto-resumes the one exact-label match; labels are not globally unique, so they stay all-settle (a same-label session may live on another peer) — a unique label resumes, a cross-machine collision surfaces as an ambiguity. The not-found message reports the actual sweep result (devices searched, which were unreachable) instead of the misleading "search with--device <host>" — fleet search is already the default.--device all/--devicesare accepted and mean "the whole fleet". The all-settle default is unchanged for the shared fan-out's other callers (tool-search, program-count) and for labels / ambiguous short-id prefixes, which still wait for every peer. Source:apps/cli/src/lib/remote-agents-json.ts,apps/cli/src/lib/session/remote-list.ts,apps/cli/src/commands/sessions.ts,apps/cli/src/commands/resume.ts.agents sessions focusgains device + live-state scoping and multi-select tabs (RUSH-2206).focusnow honors--device/--host <name>(scope the picker to a device's live sessions) and the same live-state filterssessions --activedefines (--orphan/--crashed/--waiting/--idle/--working/--closed/--abandoned/--queued/--unknown) — they compose, soagents sessions focus --orphan --device yosemite-s0lists only that host's orphans. The interactive picker is now multi-select: check several sessions and each opens as a new tab in the terminal you're in (Ghostty / iTerm / tmux, auto-detected), reusingresume's batch open and flood guard. Per tab, live semantics hold — a tmux session is joined (a second client, no fork), local or remote over SSH; a session with no attach rail resumes a copy in the tab, reported never silently dropped.focus <id>direct-jump and--attach-only(oldgo) behavior are unchanged. Source:apps/cli/src/commands/focus.ts,apps/cli/src/commands/go.ts.agents harness editis now wizard-capable, on a shared step engine (RUSH-2219). Runagents harness edit <name>with no flags in a terminal and it walks each field (model, endpoint, auth, version, fallback, description) pre-filled with the harness's current values — where before it was flag-only and errored without one. Bothadd/forkandeditnow drive one shared step engine: every step is skippable by the matching flag, so scripting stays non-interactive and a flagless non-interactiveeditstill errors as before. Endpoint and version prompts are gated by the host's API format (a host with no custom-endpoint slot, or one that self-updates, shows the field disabled with a reason instead of silently accepting a value the run would drop). This is the foundation the model catalog, connection test, edit matrix, and cross-host portability build on. Source:apps/cli/src/commands/harness-wizard.ts,apps/cli/src/commands/harness.ts.Codex launches now default to safe writable access instead of an offline read-only sandbox. When no configured run default exists, omitting
--modeselects a managed workspace profile with network access, on-request approvals,~/.agents, and regenerable build caches. Explicit--mode planremains filesystem-read-only but now retains network access. The same policy coversagents run, fallback attempts, resumes, routines, directcodexshims, versioned aliases, and Windows shim passthrough; only explicitskipdisables the sandbox and approvals. Source:apps/cli/src/lib/codex-policy.ts.Phone forwards are shaped to a text, not a wall.
agents feed post(--level important/--blocked),agents notify, andagents send --to ownerall forward to the owner's phone through one seam,composeBroadcastMessage; it now keeps the post title (the scannable headline) and truncates a long body to a short excerpt marked… (full in feed)(caps: 500 chars / 8 lines). The full post is untouched in the feed — only the outbound phone copy is shortened — so a requested long write is not lost, and enforcing it at this seam covers every sink (owner alias, in-processchannel:, spawnedcommand:via{message}), which a per-command shell hook cannot. Source:apps/cli/src/lib/feed-broadcast.ts(truncateBroadcastBody).agents run --leaseself-cleans its warm pool. Each lease now auto-stops expired, idle boxes in the run's profile pool (an expired box can never be reused, so it was pure cost the 1h-idleagents lease gcwindow left running). Conservative: only a box that is running, in the same profile+netMode pool, past its lease expiry, and untouched for a grace window is stopped — a mid-boot or in-use box is never touched. Skipped when an explicit--box <slug>is named. Source:apps/cli/src/lib/crabbox/lease.ts.Muse Code (Meta) harness support.
museis a first-class agent: install viacurl -fsSL https://dev.meta.ai/install.sh | sh, run withagents run muse, and use in teams. Modes map to Muse safety flags (--disable-write/--disable-approval/--yolo);--modeland--reasoning-effortare forwarded; headless ismuse execwith--json; interactive resume ismuse resume <id>(id immediately after the verb), headless resume is--session-id. Sessions under~/.local/share/muse/sessions/(plus version homes) are discovered and parsed; usage shows Meta Model API rate limits when a key is present, otherwise local 7-day token totals. MCP writes to~/.config/muse/settings.json(mcp_servers,schema_version: 1). Model catalog:muse-spark-1.2(default),1.1,1.2-contributorwith offline pricing. Aliases:muse-code,meta-muse. Hooks write Claude-shaped matcher groups into~/.config/muse/settings.json(schema_version: 1); plugins use the Claude marketplace layout under the XDG data plugin store (~/.local/share/muse/plugins) with.muse-pluginmanifests. Allowlist stays false (Muse uses approval-mode + sandbox, not tool-name allow/deny). Source:apps/cli/src/lib/{agents,exec,models,usage,mcp,hooks,plugins, plugin-marketplace,runner,shims}.ts,apps/cli/src/lib/session/*.Release CI no longer re-runs the six-job cross-platform matrix on
v*tags. The expensiveci.ymlmatrix (ubuntu + macOS + Windows × Node 22/24) still runs onrelease/**branch pushes and manualworkflow_dispatch, but not when a version tag is pushed.release.shtags the exact commit that already passed the release-branch matrix, so the post-tag matrix was pure cost. Source:.github/workflows/ci.yml.claude-opus-5andclaude-sonnet-5were unpriced, so every session using them cost $0. The pricing table carriedclaude-opus-4,claude-sonnet-4,claude-fable-5andclaude-mythos-5but not the Opus/Sonnet 5 line. Matching is dash-bounded (getModelPricing), soclaude-opus-5cannot fall back to theclaude-opus-4entry — it resolved to null, and an unpriced model contributes nothing rather than erroring. On one real index that silently zeroed 526 sessions, 478 of them the current default model, understatingagents cost,agents outputandagents insightsalike. Rates from the published table: Opus 5 $5/$25 per MTok (cache write $6.25, cache read $0.50); Sonnet 5 $2/$10 (cache write $2.50, cache read $0.20). Source:apps/cli/src/lib/pricing/prices.json.Schema v34 reprices the sessions that were zeroed. Adding prices alone fixes nothing already indexed:
cost_usdis computed at scan time and the scanner skips any transcript whose(file_mtime_ms, file_size)is unchanged, so those rows would keep their NULL forever. They cannot be repaired in place either — the row storestoken_countandoutput_tokensbut not the uncached-input / cache-read / cache-write split the price table needs. So v34 flushesscan_ledger, the same remedy v5 → v6 used for this exact column when cost was introduced. One slower scan, then correct numbers. Source:apps/cli/src/lib/session/db.ts.Sonnet 5's $2/$10 is introductory pricing that ends 2026-08-31. From 2026-09-01 the standard rate is $3/$15. The table holds one current rate per model with no notion of an effective date, so that entry must be updated then or Sonnet 5 spend reads ~33% low. Called out at the top of
table.ts.agents sessions --orphan/--active: cross-harness live discovery + richer rows (RUSH-2205). Two fixes. (1) The headlessps-scan recognized only 6 agent executables, so a bare-headlessgrok/kimi/antigravity/openclaw/hermes/rushrun was silently dropped from the live views; the comm→kind map is now derived fromSESSION_AGENTS× the AGENTS registry (cliCommand), so every discoverable harness surfaces. (2) Each live row now shows the agent version and a human created · idle time cell, and backfills the ticket/PR/label from the indexed session history onto orphan rows that lack them, with the label/topic on its own line — grouped-by-directory layout unchanged, rows stay width-safe. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/commands/sessions.ts.agents doctor --checkverdict lines now carry acheck:prefix and a total version count, so CI logs and log-scrapers get one consistent, grep-alike shape whether the result is clean or drifted:check: ok — 3 version(s) in sync/check: drift — 2 stale, 1 never-synced across 3 version(s). The per-version status badge for a never-synced version now readsnever-synced(its real status) instead of the unrelatedcoldlabel, and all three badges (stale/never-synced/unwired) share one fixed-width column so the rows align. Source:apps/cli/src/commands/doctor.ts(runCheckGate).agents secrets export --host --remote-backend fileno longer requiresAGENTS_SECRETS_PASSPHRASE. Since the file store became passphrase-free (auto-provisioning a 0600 machine-local key under~/.agents/.secrets-key/), the remoteimport --backend filereads headlessly under that key with no passphrase and no Touch ID — but the export path still hard-failed unless a passphrase was set, and forcing one made the remote bundle require that shared passphrase to read, defeating the headless use it was for. The passphrase is now optional: with none set, the remote command carries noAGENTS_SECRETS_PASSPHRASEread/export prologue and the .env is the only stdin, so the remote keys the bundle under its own machine-local key (headless reads); setAGENTS_SECRETS_PASSPHRASElocally only to opt into a shared off-disk key, which is still forwarded over ssh stdin (never argv). This unblocks hands-off Linux-driven releases that provisionapple.comon a headless macOS sign host. Windows targets are still refused cleanly. Source:apps/cli/src/lib/secrets/remote.ts,apps/cli/src/commands/secrets.ts.Docs: the per-command secrets Touch-ID contract is now written down and accurate. The
view --reveal(1.22.14) andexec(1.22.21) interactive-unlock changes shipped in code + CHANGELOG only; the reference docs still implied every value command behaves the same.docs/secrets.mdand thesecretsskill now carry an explicit Touch-ID matrix (deliberateview --reveal/execat a real terminal → one sheet on a locked bundle;get/exportautomation primitives → never prompt, fail closed toagents secrets unlock; anything an agent launches → broker-only),specifications.mdadds SEC-13b + aPrompts?column on the materialization table +GWT-S2b, and the skill's stale hold-window facts are corrected (7-day default, screen-lock does not drop the hold). Docs-only; no behavior change. Source:apps/cli/docs/secrets.md,apps/cli/docs/specifications.md,skills/secrets/SKILL.md.Resume session identities through one live-first path across the fleet.
agents sessions resume <selector>accepts a full UUID, unique UUID prefix, durable tmux name (ag-codex-c1f3d813), or unique alias suffix (c1f3d813). It resolves the owning device, rechecks whether the process and pane are alive, attaches a live pane, and otherwise resumes the harness-native conversation with its recorded version, cwd, and mode. Retained dead tmux panes no longer count as attachable, and native resume no longer collides with an existing live wrapper. Source:apps/cli/src/commands/focus.ts,apps/cli/src/commands/sessions-resume.ts,apps/cli/src/lib/exec.ts.agents forkis nowagents sessions fork. Forking is a session operation, so it lives under thesessionsgroup next toresume/focus—agents sessions fork <id>branches a conversation into a new, independent copy you can continue separately, leaving the original untouched. The old top-levelagents forkkeeps working as a hidden alias, so nothing that scripted it breaks. For a harness without a native copy (anything but Claude today), the command now fails loud and names the manual branch — start a fresh agent and seed it with/continue <id>— instead of only saying "unsupported". Source:apps/cli/src/commands/fork.ts,apps/cli/src/commands/sessions.ts.Muse Code global binary is visible to
agents view/isVersionInstalled. Muse installs a single self-updating launcher at~/.local/bin/muse(same shape as droid), butgetBinaryPathstill resolved a version-homenode_modules/.bin/musethat never exists. Afteragents import muse/agents add muse, version dirs now resolve to that global path so managed view, collapse, and live-version bookkeeping match what actually executes; install no longer writes a self-referential shim symlink. Source:apps/cli/src/lib/versions.ts.
1.22.22
New:
agents insights— how you work, split by the Claude account that did the work. Tool and language mix, friction (interruptions, tool-error classes, your own reply latency), what you changed (line deltas, files, commits), an hour-of-day rhythm, and how often two accounts ran at once. Modelled on Claude Code's/insights, with the difference that motivated it: that command reads one account's directory, whilebalancedrotation sprays sessions across every signed-in account, so it describes a fraction of the work and credits all of it to one org. Source:apps/cli/src/commands/insights.ts,apps/cli/src/lib/session/insights.ts.Deterministic and offline by default.
--narrativeis opt-in and adds a written read by piping the aggregate — never raw transcripts, unlike/insights— through a headlessclaude -p. Facets are cached per session in a newsession_insightstable keyed on(file_mtime_ms, file_size), so the first run parses every transcript once and later runs re-read only what changed.The Claude parser can surface interruption markers on request.
[Request interruptedtext was dropped outright, so the signal was unrecoverable downstream.parseSession(..., { includeInterrupts: true })now emits a dedicatedinterruptevent. It stays OFF by default because the event array is a versioned consumer contract:agents sessions <id> --jsonserializes it verbatim,computeSummaryStatsfolds every timestamp into the session duration, and the live-state reader inspects a fixed window of trailing events.agents insightsis the only caller that opts in. Source:apps/cli/src/lib/session/parse.ts.digest.tsnow classifies droid'sCreateas a file write. Its tool-vocabulary set claimed cross-harness coverage but omitted it, so droid file creations classified as nothing. Source:apps/cli/src/lib/session/digest.ts.Codex usage bars no longer show a previous account's numbers after you switch accounts.
agents viewderives a Codex version's usage from that home's session transcripts, which carry no account identity and are not removed on logout — so after logging a version out and into a different ChatGPT account, the bar kept showing the old account's last-seen percentage (e.g. "S: 99%") until the new account ran a session. Usage is now scoped to the current login: only sessions written at/after the id_token'sauth_time(the OIDC authentication time) count, and a signed-out home reports no usage.auth_timeis used rather than the auth.json file mtime because Codex rewrites auth.json on every token refresh, but a refresh does not re-authenticate, soauth_timestays at the real login — an actively-refreshing account keeps its bar, only a real re-login or account switch moves the floor. Source:apps/cli/src/lib/usage.ts.The interactive session picker's detailed preview no longer collapses to empty.
agents sessions,agents sessions <query>, andagents sessions --activeopen the picker with the rich preview pane (prompt, files, hooks, errors, tests, last response) on by default, but the preview had no guaranteed height: a 15-row list (PICKER_RECENT_COUNT) on a short terminal consumed the whole viewport, the computedavailablePreviewRowswent to zero, and the pane silently vanished — worse when fleet-unreachable warnings and the hidden-session footer had scrolled lines above the prompt. The picker now caps the visible list page so the preview keeps a floor ofPREVIEW_MIN_ROWS(6) rows, and accounts for the lines printed above the prompt so those notices and the preview stay on screen together. Applied consistently acrossitemPicker,dynamicPicker, andmultiItemPicker, so the bare browser, the query picker, and the--activebrowser all behave the same; the space/tab preview toggle is unchanged. Source:apps/cli/src/lib/picker.ts,apps/cli/src/commands/sessions-picker.ts,apps/cli/src/commands/sessions.ts.
1.22.21
agents secrets exec <bundle> -- <cmd>now resolves a locked keychain bundle interactively at a real terminal. The local resolve hardcodedagentOnly: true(commands/secrets.ts), so runningexecon a locked bundle at a terminal failed closed with "runagents secrets unlockfirst" instead of raising the one Touch ID sheet the human just implied by asking to use the values. It now gatesagentOnlyonisHeadlessSecretsContext() || !isInteractiveTerminal(): an unlocked bundle still runs silently, a locked bundle at an interactive terminal resolves with a single sheet and then runs the command with the secrets injected, and under an agent (AGENTS_RUNTIME) or headless (no TTY) it stays broker-only — release/CI scripts never prompt. Mirrors the same fix forview --reveal.--hostremote resolves andexport/getare unchanged. Source:apps/cli/src/commands/secrets.ts.
1.22.20
Claude sessions are attributed to the account that produced them. The session scanner resolved ONE account email process-globally (
cachedClaudeAccount) and stamped it on every Claude row of a scan, so a machine with several signed-in accounts reported all of its history under whichever resolved first — on a three-account machine, 982 of 2,736 indexed sessions carried the wrong email and most of the per-account cost was misplaced. Attribution now resolves per transcript, from its path plus the version recorded inside the file. Two orgs sharing one email (a Team seat and a personal Max plan) stay in separate buckets, keyed on the orgusageKeythe same wayagents run's balanced rotation keys quota. Sessions whose account cannot be established are reported asunattributed:<reason>rather than folded into a real account. Source:apps/cli/src/lib/session/claude-accounts.ts,apps/cli/src/lib/session/discover.ts.Rows under the mutable
~/.claudesymlink are attributed by their recorded version, not by wherever the symlink points now: only 684 of 1,334 such rows came from the version the symlink currently names. Retired (trash/) homes keep their.claude.json, so their transcripts stay attributable. A transcript in a home that exists but is signed out stays dark and is named after that home — its location proves which config dir Claude used.Harness scope: Claude only. Attribution depends on the per-version home carrying an
oauthAccount. Other harnesses have their own per-version credential files, so the mechanism generalizes, but each needs its own identity extractor and quota-bucket notion. Until then a non-Claude session has noaccount_keyand rolls up underunattributed:<agent>.account_key/account_orgon indexed sessions, and--by accountrollups. Schema v33 adds both columns plusidx_sessions_account_key, andqueryUsageRollupacceptsgroupBy: 'account'— surfaced byagents cost --by accountandagents output --by account, which render the org and email rather than the raw uuid. The migration repairs existing rows in place fromfile_pathandversionand deliberately does not flushscan_ledger: attribution needs no transcript re-parse, so a 2,736-row index migrates in ~200ms with every ledger entry still warm.getDBalso runs a guarded self-healing repair for rows an older CLI left unattributed. Source:apps/cli/src/lib/session/db.ts.upsertSessionsBatchbound its named parameters from an untyped literal. bun binds named parameters in strict mode, where a MISSING key throws, while node binds NULL — so a key omitted from that literal broke only the shipped standalone binary, and the per-row guard swallowed it into a silently skipped session. The literal is now typed againstSessionRow, so the compiler rejects the next omission. Source:apps/cli/src/lib/session/db.ts.readClaudeHomeConfig()inapps/cli/src/lib/agents.ts— the single place a Claude home'soauthAccountidentity is read.getAccountInfonow uses it, so identity extraction is no longer duplicated. UnlikegetAccountInfoit does not apply the credential floor, because a revoked token does not change which org produced a past transcript.agents routines addaccepts--project <name>(repeatable) and--all-projectsto tag a routine to one or more projects. Project names are validated againstagents projects listat creation time; unknown names are rejected with a suggested fix command. The flag sets the newprojects?: string[]field in the job config YAML — metadata-only, no effect on scheduling or execution.--all-projectssetsprojects: ["*"](the "all defined projects" sentinel) and is mutually exclusive with--project. Source:apps/cli/src/lib/routines.ts(JobConfig,validateJob,computeProjectGroup,writeJob),apps/cli/src/commands/routines.ts.agents routines listnow groups by project by default. The human terminal view buckets routines under their associated project name, All projects (projects: ["*"]), Cross-project (multiple project entries), Operations (noprojects:field), or Unknown projects (project names not found inagents projects). Pass--group-by deviceto restore the previous device-placement grouping. The--jsonpayload gainsprojects(array) andprojectGroup(string) fields. Source:apps/cli/src/commands/routines.ts(groupRoutineJobsByProject).
1.22.19
Fixed
agents sync --local -yrefreshes every installed version, not only the default. Unattended reconcile (refresh({ skipPrompts })) previously wrote resources and registered hooks into each agent's default version alone, so non-default homes kept stale hooks after a system update. Unattended refresh now loopslistInstalledVersionsfor both resource sync and hook registration. Interactive refresh still targets the default only. Source:apps/cli/src/lib/refresh.ts.
1.22.18
Fixed
agents syncre-copies nested system hooks after content changes.listResources('hooks')treated event-group directories (pre-tool-use/) as resource names, sosystem:*pattern expansion never included nested scripts likegit-guard.sh. Force sync then left stale flat copies in version homes forever. Hooks discovery now expands one-level group dirs the same way asgetAvailableResources/listHookEntriesFromDir. Source:apps/cli/src/lib/resources.ts.Route owner iMessage notifications through Rush's verified owner message endpoint instead of requiring a live daemon channel registration. (RUSH-2193)
agents sessions --activenow carriesterminalIdon tmux-hosted rows (RUSH-2192). Grok/Codex (and everyag-*tmux pane) get theirAGENT_TERMINAL_IDfrom the launch registry's by-pid entry. The ps-scan path already setterminalId; the tmux source — which wins dedupe for interactive agents — omitted it, so Factory could never join a tab to its live session even when SessionStart preserved the key. Source:apps/cli/src/lib/session/active.ts.
1.22.17
Codex versions no longer share one account — each keeps its own login. Installing a new Codex version used to copy the current default version's
.codex/auth.jsoninto the new version home, soagents viewreported the same ChatGPT account for every installed Codex and you could never sign two versions into two accounts. The credential is now excluded from settings carry-forward (config, prompts, and rules still carry), matching how Claude omits.claude.json. A fresh Codex version installs signed-out; runcodex login(oragents run codex --version <v>) inside it to authenticate that version's own account. Source:apps/cli/src/lib/settings-manifest.ts,apps/cli/src/commands/versions.ts.agents feed postnow resolves owner delivery and session identity from their canonical indexes (RUSH-2193).agents notifyandchannel: ownerconsume the normal addressable channel fromhumans.yaml, so the phone destination is no longer duplicated inagents.yaml. When a harness leavesAGENT_SESSION_IDempty, feed posting joinsAGENT_LAUNCH_IDto recent activity before requiring--session.agents feed post --helpnow states that the defaultmilestoneis recorded but a sink gated atminLevel: importantonly texts for--level important;--blockedremains reserved for work that cannot continue. Source:apps/cli/src/lib/humans.ts,apps/cli/src/lib/feed-post.ts,apps/cli/src/commands/feed.ts.The macOS menu bar keeps its ACTIVE project accordion open and opens Quick Dispatch without blocking. Project headers now mutate their session rows inside the existing
NSMenutracking session instead of closing the dropdown and attempting a synthetic reopen.Cmd-Shift-Oprebuilds and focuses its panel before any session-history, attachment scan, thumbnail decode, or Linear-cache work; those sections hydrate asynchronously from warm data. Source:apps/cli/menubar/Sources/MenubarHelper/{StatusItemController,PromptPanel,AgentsCLI}.swift,apps/cli/docs/menubar.md. (#2051)agents doctornow accepts symbolic version qualifiers;agents viewno longer prompts to sync resources.agents doctor claude@latest,@oldest,@pinned,@all, and exact semver qualifiers are resolved through the sharedresolveAgentTargetsengine instead of falling through to a "not installed" error. Bareagents doctor <agent>still sweeps all installed versions withversionExplicit: false(so--fixexcludes isolated copies). Separately, the implicit resource-drift scan and interactive sync prompt that ran at the end ofagents view <agent>have been removed —viewis read-only and must not mutate version-home state. Source:apps/cli/src/commands/doctor.ts,apps/cli/src/commands/view.ts. (#2058)agents run <agent> --cloud— the vendor cloud becomes a run placement.--clouddispatches the run to the agent's native cloud through the existing provider registry (claude→rush, codex→codex, droid→factory, antigravity→antigravity) — the exact same dispatch asagents cloud run --agent <agent>, tracked byagents cloud list/status/logs/cancel/message. It sits alongside--host/--device/--leaseas the third placement (local, machine, cloud) and is mutually exclusive with them;--where cloud[:provider]is the one-door spelling.--provideroverrides routing;--repo(repeatable),--branch, and--cloud-envrefine the task (run's--envstays the KEY=VAL passthrough, so the Codex Cloud environment id gets its own flag). Agents without a native cloud (kimi, grok, cursor, opencode, …) fail loud with the capable list unless--provideris given, and local-run flags (--loop,--resume,--secrets,--terminal,--cwd, account strategy, …) are rejected rather than silently dropped. The dispatch core is now shared:agents cloud runandagents run --cloudboth callsrc/lib/cloud/dispatch.ts(executeCloudDispatch), so capability checks, the missing-target picker, persistence, streaming, and the budget kill-switch cannot diverge. Source:apps/cli/src/commands/run-cloud.ts,apps/cli/src/lib/cloud/dispatch.ts,apps/cli/src/lib/placement.ts,apps/cli/src/commands/exec.ts.
1.22.16
Resume exact sessions locally or across the fleet with
agents resume <id>andagents run <agent|auto> --resume <id>. Full IDs use the local SQLite index before any SSH fan-out; remote owners route to the recorded device and version home. Session metadata now records launch mode alongside harness, version, account, cwd, and machine so strict resume reconstructs the original run. Claude, Codex, Grok, Kimi, Droid, and Cursor use their verified version-specific native resume syntax;run auto --resumecan select another healthy harness/account and continue through/continuewhen native resume is unavailable. Source:apps/cli/src/commands/{exec,resume,sessions}.ts,apps/cli/src/lib/{exec,session/db}.ts,packages/session-tracker/src/hook.sh.Hooks: one-level event dirs (
hooks/<event-name>/<script>) are first-class. System hooks organize by harness event (session-start/,pre-tool-use/, …). Install names stay the file basename. Dirs with top-level scripts expand into individual hooks; fixture-only dirs remain directory bundles. Manifestscript:may be a relative path underhooks/. Source:apps/cli/src/lib/hooks.ts,apps/cli/src/lib/staleness/writers/sources.ts,apps/cli/src/lib/versions.ts,apps/cli/src/lib/__tests__/hooks-nested-groups.test.ts.agents humans show owner [--json]— new command to display the owner config from~/.agents/humans.yaml. The file is written automatically on first run whennotify.ownerexists inagents.yaml. Source:apps/cli/src/lib/humans.ts,apps/cli/src/commands/humans.ts.humans.yaml— typed, versioned owner config.~/.agents/humans.yaml(version: 1) now stores owner identity (name, timezone, quiet hours, severity), notification channels, and escalation policy.notify.ownerinagents.yamlis migrated into it on first run and thenotify.ownerkey is removed fromagents.yaml; unrelated keys are preserved.agents send --to owner/agents notifypreferhumans.yamlwith a fallback toagents.yamlduring the migration window. Source:apps/cli/src/lib/humans.ts,apps/cli/src/commands/humans.ts,apps/cli/src/lib/migrate.ts.agents memoryignoresAGENTS.md,CLAUDE.md,GEMINI.md, andMEMORY.md. These rule/index files lived in~/.agents/memory/but were incorrectly surfaced as memory facts.isFactFile()now excludes them by name (case-insensitive). Source:apps/cli/src/lib/memory.ts.Permissions write path fixed —
groups/subdirectory.installPermissionSet,removePermissionSet, andsavePermissionSetnow all write to thegroups/subdirectory (matchingdiscoverPermissionGroups()which already reads fromgroups/). Source:apps/cli/src/lib/permissions.ts.Stop eagerly creating webhooks directories.
ensureAgentsDir()no longer creates~/.agents/webhooks/or~/.agents/.system/webhooks/on startup — both dirs are created on first actual use. Source:apps/cli/src/lib/state.ts.Terminals canonically under
.cache/. The stale migration comment that blockedterminals/from moving to~/.agents/.cache/terminals/is replaced by the actual move. Factory already writes to.cache/terminals/(foreman.registry.ts:9), so no app-level change is needed. Source:apps/cli/src/lib/migrate.ts.Menu bar warns when a device is under high load (local or remote). The agents-cli menu bar now shows a
⚠ <device> — high load N%row in NEEDS YOU when a machine's load or memory crosses theheadroom()"loaded" threshold (≥75%), and a red✕when critical. The local machine is probed natively viagetloadavg(zero subprocess); fleet peers come from the daemon-warmed.fleet-stats.jsoncache with a freshness guard — never the slowagents doctorpath. Action-required rows are now emphasized so items that need you stand out. Source:apps/cli/menubar/Sources/MenubarHelper/LocalState.swift,apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.Projects canonicalization contract.
agents projects import --from-factory/--min-confidence/--allare gone; import is--from-linearonly, and~/.agents/factory/projects.jsonis never read or migrated.agents projects list --jsonreturns definitions only (zero session scan / SSH);--with-agentsis an explicit opt-in for local active counts. Newagents projects save --jsonreads one completeProjectDeffrom stdin, validates, writes atomically under~/.agents/projects/, and prints the saved def.agents projects rm <name> --jsonreturns machine-readable success/error. Factory'smanagedProjects.tsshells only throughagents projects list|save|rm— never reads or writes project YAML/JSON directly, never seeds or migrates legacy Factory state; errors stay explicit for inline UI display. Source:apps/cli/src/commands/projects.ts,apps/cli/src/lib/projects.ts,apps/factory/src/core/managedProjects.ts,apps/cli/docs/11-projects.md.ProjectDefYAML gainsdispatchblock andlinear.name.~/.agents/projects/<name>.yamlnow accepts adispatch:block (enabled,maxAgents,provider,host) that opts a project into auto-dispatch and is read directly byagents __auto-dispatch— previously these fields lived only in Factory's own registry.linear.namestores the Linear project display name alongside the existingprojectIdandurl. Both fields are optional; existing YAMLs are unchanged. Source:apps/cli/src/lib/projects.ts,apps/cli/src/lib/auto-dispatch.ts.agents secretsno longer pops a generic "Agents CLI needs to authenticate" Touch ID sheet on every agent launch.listBundles— which runs on essentially every secrets touch (session-title generation,agents devices list, everyagents run, every remote launch that resolves secrets on the host) — could not ask the keychain for just the bundle metadata items: with hashed service names (#316) those names are opaque, so it fell back to a broadagents-cli.keychain scan that also matched the ACL'd secret value items. On machines where a bundle carries a biometric ACL (e.g. ahold-tier bundle holding an SSN or a password), macOS evaluated that value ACL during the attributes-only scan and raised a generic, context-less Touch ID prompt — on every launch, so a busy fleet felt like a machine-wide bombardment. NeitherkSecUseAuthenticationUIFailnorLAContext.interactionNotAllowedcan list the no-ACL items while skipping the ACL'd ones (both return nothing), so the fix is to stop doing the broad scan: a per-machine no-ACL metadata-name index (opaque hashes only, in the regenerable helpers dir — it leaks nothing #316 didn't) is read as a silent file instead. The write paths keep it current; an absent/stale index self-heals by rebuilding from the one-time scan, and a missing entry only makessecrets listcosmetically incomplete — it never affects a resolve-by-name. Your sensitive bundles keep their biometric gate on real value reads; only the bundle listing goes silent. Source:apps/cli/src/lib/secrets/bundles.ts.
1.22.15
Separate routine definitions from device activation (#2023). Enable a routine by listing its name in
~/.agents/devices/<hostname>/agents.yaml; built-in Watchdog setup andwatchdog on|offnow update that host-owned manifest without rewriting the routine definition. Source:apps/cli/src/lib/routine-activation.ts,apps/cli/src/commands/setup-watchdog.ts.agents devices listno longer shows the "Leased boxes" section by default — it moves behind a new--allflag (RUSH-2190). Loading the section routes through crabbox's bundle auto-detect, which scans the keychain and can raise a macOS Touch ID sheet after the device table has printed, hanging non-interactive callers (observed: the.agents-systemSessionStart topology hook). The default list now renders only registered devices, which are reachable without any secrets; the load/mem/headroom columns are unchanged (the stats probe was already broker-only).agents devices list --allrestores the section;--no-statsremains a hard "instant, no provider calls" opt-out even with--all. Source:apps/cli/src/commands/ssh.ts(showLeasedBoxesSection),apps/cli/src/commands/ssh.test.ts.Install:
plugin:prefix on the unified path (Phase 5 packaging).agents install plugin:<spec>uses the same grammar and trust gate asagents plugins install(name@url, local path,--allow-exec-surfaces). Specialized verbs still work;agents installis the one add path for mcp, skill, plugin, and GitHub sources. Source:apps/cli/src/commands/packages.ts,apps/cli/src/lib/registry.ts.
1.22.14
agents secrets view <bundle> --revealnow resolves a locked keychain bundle interactively at a real terminal. The command hardcodedagentOnly: trueon both reveal call sites (commands/secrets.ts), so an explicit human--revealon a locked bundle went through the broker-only path and errored with an unlock hint instead of raising the one Touch ID sheet the human just asked for. TheagentOnlyflag is nowisHeadlessSecretsContext() || !isInteractiveTerminal()— under an agent (AGENTS_RUNTIME) or with no TTY it stays broker-only and never prompts, but a deliberate--revealtyped at an interactive terminal resolves the value with a single biometric sheet. This mirrors the existingreveal && !isInteractiveTerminal()guard a few lines up.export --plaintextandexecare untouched — they stay intentionally silent for release/CI scripts. Source:apps/cli/src/commands/secrets.ts.agents sessions optimize— compact the FTS5 session search index. The scanner delete+inserts a session's docs into thetool_call_text/session_textfull-text indexes on every rescan, and FTS5 never merges the resulting segments on its own — so over thousands of sessions the%_datashadow tables bloat with hundreds of thousands of unmerged segments (observed on a real fleet box: 701 MB of index for ~69 MB of content, 196K segments) andagents sessionsslows to a crawl / hangs. The new command runs FTS5'optimize'(merge all segments, purge tombstones), non-destructively — no searchable content is lost. Reclaimed space frees as reusable pages inside the DB file (VACUUM with the daemon stopped returns it to disk); wireable to a weekly routine so the index never re-bloats. Source:apps/cli/src/lib/session/db.ts(optimizeSessionSearchIndex),apps/cli/src/commands/sessions-optimize.ts.
1.22.13
agents sessionsaccepts direct live-state flags and remains fleet-wide by default.--working,--idle,--waiting,--orphan/--orphaned,--crashed,--closed,--abandoned,--queued, and--unknowneach imply the live scan; multiple flags form a union.--workingis narrower than--active: it excludes idle, waiting, and lifecycle-failure rows. Cross-device collection was already the default and stays that way;--localopts out, while--allcontinues to widen historical directory and time scope. Source:apps/cli/src/commands/sessions.ts,apps/cli/src/commands/sessions.test.ts.Workflows:
name@sourcedisambiguation (Phase 5 packaging). When two plugins (or a plugin and an extra repo) ship the same workflow name, pin the source:agents run deploy@ship-toolsoragents run workflow:deploy@social. Bare names keep layered precedence (project > user > plugin > extra > system); a missing source returns no match instead of silently falling back. Source:apps/cli/src/lib/workflows.ts,apps/cli/src/lib/resources/workflows.ts.
1.22.12
Store operational events in daily history directories, retain 7 days and at most 50 MiB automatically, and make
agents logs audituse theagents events --auditreader.agents clirenamed toagents clis; resource directorycli/renamed toclis/. The CLI resource kind and its subdirectory are now plural throughout:ResourceKindchanges from'cli'to'clis', manifests live atclis/<name>.yaml,agents clisis the only command surface (noagents clialias), andagents view --clisreplaces--cli. A startup migration renames any existingcli/directory toclis/in the user, system, and project.agents/layers; if bothcli/andclis/are present the migration fails with a clear error rather than silently merging. Source:apps/cli/src/lib/resources.ts,apps/cli/src/lib/cli-resources.ts,apps/cli/src/commands/cli.ts,apps/cli/src/lib/startup/command-registry.ts,apps/cli/src/commands/repo.ts,apps/cli/src/commands/view.ts,apps/cli/src/lib/migrate.ts.agents.yamlno longer silently loses top-level keys across a version-skewed fleet.serializeCentral(lib/state.ts) rewrote the syncedagents.yamlwith a delete-any-key-not-in-the-in-memory-object pass. An older CLI version whoseMetatype predated a key (beta:,notify.owner,feed:, importedprojects) would parse the file, never surface that key, delete it on the next write, and sync the deletion to every machine — the recurring "my config vanished" data-loss (see the restore in commit04295e3). The delete pass now consults aRecord<keyof Meta, 'central' | 'device'>scope map (compile-time exhaustive — a newMetafield that isn't classified fails the build) and deletes only keys this version knows (a cleared central key, or a device key that is legacy cruft in the synced file); a key it doesn't know is preserved verbatim. Once a machine runs a CLI carrying this fix, it can never drop a newer version's key again. Source:apps/cli/src/lib/state.ts,apps/cli/src/lib/__tests__/state.test.ts.Watchdog files a feed block only when a session genuinely needs the human. When the smart brain concludes a stalled session must be left for the human (
needsHuman), the watchdog now surfaces that on the owner's feed instead of dropping it in a menubar-only flag. Two cases: if the session is addressable it injects a self-file reminder into the agent ("You appear stuck. File it:agents feed post … --blocked") so the agent declares its own block; if it is un-addressable — the case where the watchdog can't even reach the terminal to remind it — the watchdog files a declared block on the agent's behalf so the owner is still paged. Paging fires only on this confirmed-needs-human path: a plain nudge-worthy drive-forward poke (un-addressable or under a hands-off policy) is flagged for the tray but never texts the owner. Both paths are gated by the existing cooldown ledger (at most once perWATCHDOG_COOLDOWN_MSwindow) and are no-ops when a block for the session already exists, so no double-paging. Source:apps/cli/src/lib/watchdog/runner.ts(NudgeDecision.needsHuman,WatchdogTickOptions.publishBlockFn, the needs-human skip branch),apps/cli/src/lib/watchdog/runner.test.ts.
1.22.11
--blockediMessage notifications are now phone-actionable. The forwarded message dropped the block's--options,--default, and timeout and instead showedagents focus <id>— a CLI command that is useless on a phone. It now shows the choices (Options: publish / wait) and the safe-default fallback (Default in 15 min: wait) and omits theagents focusline, so a--blockedpost that carries a--defaultself-resolves when the owner can't reply. Source:apps/cli/src/lib/feed-broadcast.ts.Move operational event logs from the git-backed
~/.agents/root into~/.agents/.history/events/, including existing numbered gzip archives.agents projectsis out of beta — noagents beta enable projectsneeded. The command tree (list / add / import / status / link / …) is always registered now;projectsis dropped from the beta registry (ALL_BETA_FEATURES,BetaFeatureName) and thepreActionbeta gate is removed. Any lingeringbeta.enabled: [projects]entry is harmlessly ignored, andagents beta enable/disable projectsprints a friendly "graduated out of beta" note and no-ops instead of erroring (so old scripts survive). Source:apps/cli/src/lib/beta.ts,apps/cli/src/lib/types.ts,apps/cli/src/commands/beta.ts,apps/cli/src/commands/projects.ts.agents projects statusshows every project across the whole fleet by default; scope it with--device/--devices. The old--fleetflag is gone — status now dials every registered device's workspace (presence, branch, drift) in one parallel SSH round without being asked.--device <name...>(repeatable) or--devices a,b,cnarrows the fan-out to a subset; with no filter the whole fleet is dialled. Reuses the shared--host/--devicetarget resolution. Source:apps/cli/src/commands/projects.ts.scripts/release.shhome-base hop: pass a single remote argv toagents ssh. Multi-arg forms (bash -lc '…') are joined without re-quoting bywrapRemoteCommand, so the remotecdnever ran and publish failed withfatal: not a git repository. One shell string keeps the command intact. Source:apps/cli/scripts/release.sh.
1.22.10
Plugins package workflows (Phase 5 packaging slice). A plugin’s
workflows/<name>/WORKFLOW.mdis discovered and resolved byagents run <name>with precedence project > user > plugin > extra > system — no separate install into~/.agents/workflows/required. Plugin inventory / resource groups listworkflows. Source:apps/cli/src/lib/workflows.ts,apps/cli/src/lib/plugins.ts,apps/cli/src/lib/resources/workflows.ts.scripts/release.shroutes the home-base publish hop viaagents ssh. Plainssh mac-minifails host-key checks on headless Linux workers;agents sshuses the devices registry and brokered credentials. Falls back to plain ssh only whenagentsis not on PATH. Source:apps/cli/scripts/release.sh.Touch ID is now raised in exactly one place —
agents secrets unlock.agents secrets list,agents run <agent>,secrets get/export/view, and every background read resolve from the secrets broker / durable session / no-ACL layer and never raise a biometric sheet; a locked keychain bundle fails with an actionable "runagents secrets unlock <bundle>" hint instead of prompting. TheAGENTS_SECRETS_NO_PROMPTenvironment override and the "a human at a TTY, so prompting is fine" heuristic are deleted — the prompt decision is structural, not an ambient env var. The macOS keychainlist/list-syncedenumeration queries now passkSecUseAuthenticationUISkip(enumeration itself was evaluating the biometry ACL, soagents secrets listprompted and silently dropped keychain bundles when the sheet was cancelled), and the one-time hash-rekey + metadata-ACL heal run only inside the singleunlocksheet so nothing on the run/list path can storm. Source:apps/cli/src/lib/secrets/keychain-helper.swift,apps/cli/src/lib/secrets/index.ts,apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/lib/secrets/headless.ts,apps/cli/src/commands/secrets.ts.The Factory VS Code extension (
swarm-ext) no longer decrypts secrets or shells rawssh. Device health, reachability, and sync route every remote command throughagents ssh <host>(broker-owned credentials, no prompt); the extension's own secret-resolution path (resolveSecret/discoverSecretsReadCmd/extractCredentials) is removed, so rendering the devices list never raises Touch ID. Source:apps/factory/src/vscode/deviceHealth.vscode.ts,apps/factory/src/vscode/settings.vscode.ts,apps/factory/src/vscode/extension.ts.
1.22.9
agents ssh autoandagents teams add --device autono longer reject with "Unknown device 'auto'" (RUSH-2185). Theautoaffinity sentinel was arun-only preprocessing step (applyDeviceAutoToOptionsinsmart-launch.ts, wired only fromagents run's exec path) — every other--host/--devicecaller went straight to the shared resolver, which had no idea whatautomeant and reported it as an unregistered device.matchHost(the one core every--host/--devicecaller shares) now resolvesautodirectly via the sameresolveDeviceAffinityenginerunuses, soagents ssh,agents teams add, and anything else routed throughmatchHost/resolveHost(including the generic--host/--devicepassthrough) pick a device the same way.agents teams add --device autolanding on the local machine now just runs the teammate locally, matchingrun's "null pick = local" outcome;agents ssh autorefuses a local pick with a clear message instead of self-SSHing, sinceagents sshexists to dial OUT to a remote box. Source:apps/cli/src/lib/hosts/registry.ts,apps/cli/src/lib/devices/resolve-target.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/commands/teams.ts,apps/cli/docs/00-concepts.md,apps/cli/docs/hosts.md,apps/cli/docs/teams.md.agents harness editgains--auth-provider,--fallback-model, and--from-secrets;add/forkgain an interactive wizard and--from-secrets.edit(already shipped with--model/--base-url/--version/--description) now also repoints auth at a different keychain-backed provider, sets or clears (--fallback-model "") the same-host fallback model retried on a rate limit, and — likeadd/fork— accepts--from-secrets <bundle>[:<key>]to copy a value out of an existingagents secretsbundle into the harness's own keychain item once, instead of retyping a key already stored elsewhere (the item it writes to,agents-cli.<provider>.token, is never gated behind the biometry-required prefixes, so later reads stay silent).agents harness add/forknow accept[name]/[source] [name]as optional positionals — run either with insufficient flags in an interactive terminal and a picker (fork from a native host or existing harness → a built-in preset or "build custom" → the harness's name, pre-filled with the preset's own name → how to get the key) replaces the old hard error; flags remain fully supported for scripts, and a non-interactive shell still gets the original error. Source:apps/cli/src/commands/harness.ts,apps/cli/src/commands/profiles.ts,apps/cli/docs/profiles.md.agents run --leaseis reuse-first against the crabbox profile pool, with a--freshopt-out. A bare--leaseused to always lease a brand-new box, so bursts of runs (e.g. resumed sessions) stacked up idlekeep=trueboxes at full monthly cost. Now, before warming a new box, the run looks for a warm box carrying the sameprofilelabel the warmup would use (read from the repo's.crabbox.yaml, matchingscripts/sandbox.sh'spick_ready_box) and the same network mode — a tailnet box is never handed to a public run or vice versa — and reuses the first onecrabbox statusreports SSH-ready, keeping it after the run. A not-ready pool box is skipped, never stopped.--freshforces the old behavior (brand-new box, torn down after the run);--box <slug>is unchanged. Source:apps/cli/src/lib/crabbox/lease.ts,apps/cli/src/lib/crabbox/cli.ts,apps/cli/src/lib/crabbox/config.ts,apps/cli/src/commands/exec.ts.The menu bar now notices and reports when the scheduler dies — instead of staying silent forever. The only proactive "routines overdue / scheduler down" signal was
notifyOverdue(src/lib/overdue.ts), fired from insiderunDaemon()— so it could never fire while the daemon itself was down, the exact outage it exists to report.MenubarHelperis a separate launchd KeepAlive service that stays alive when the daemon dies, so its 10s tick now polls daemon liveness independently of the dropdown ever being opened; once it has been continuously unreachable for ~30s (debounced past a routine restart blip), it fires one native notification ("Scheduler stopped — routines won't run") through its ownNSUserNotificationCenterdelivery — no daemon, no CLI spawn required — and lights the always-visible menu-bar badge (⏻) until the scheduler comes back. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.Observe umbrella aliases:
inbox,timeline,roster(Phase 3 surface consolidation). Thin doors onto existing readers (no store merge):agents inbox≡feed,agents timeline≡feed --filter updates,agents roster≡sessions --active. Root help gains an Observe section;agents auditstays the tamper-evident run log (not an events alias). Source:apps/cli/src/lib/observe-aliases.ts,apps/cli/src/commands/feed.ts,apps/cli/src/commands/sessions.ts.Daemon usage refresh is a fixed 5-minute per-host schedule, concurrency-safe, and Touch-ID-free. Each machine's daemon still owns its own usage cache (no fleet-wide store). Account live fetches are now scheduled every 5 minutes (was adaptive 90s–15m), with a 60s wake to notice due accounts after backoff ends. Cache writes use a file lock + atomic rename so a concurrent
agents viewbackground refresh cannot tear or drop rows. The daemon path loads Claude credentials withfileOnly(setup-token / no-ACL cache /.credentials.jsononly) and never opens the ACL-bound macOS keychain item, so a background tick cannot pop Touch ID. Refresh still skips a provider under 429 backoff and still never rotates single-use Claude refresh tokens.
1.22.8
agents browsernow gates cross-machine drives behind per-device consent.agents browser <cmd> --host <device>already routes a browser command to another fleet machine over SSH and drives its browser — but nothing asked that machine's permission, so any box you could SSH to, you could drive. A new device-localbrowser.remote-controlsetting (off by default, never synced) fixes that: a fleet-remotebrowser --host <this-machine> startis refused with an actionable message until the owner runsagents browser remote-control onhere. Local starts (no--host) are never gated. The fleet passthrough marks every--hostdispatch withAGENTS_FLEET_REMOTEso the far side can tell a cross-machine drive from a local one. New command:agents browser remote-control [on|off](no arg prints status;--jsonsupported). Source:apps/cli/src/lib/browser/remote-control.ts,apps/cli/src/commands/browser.ts,apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/device-config.ts,apps/cli/docs/browser.md.agents browsertasks are now attributed to the caller that ranstart, not to the browser daemon.Task.owner(RUSH-2020) was resolved withresolveActor()inside the shared, long-lived browser daemon, so every task — no matter which agent or person opened it — was stamped with the identity of whoever happened to start the daemon. The caller's identity is now forwarded over IPC: the CLI (the caller's own process) putsactor(resolveActor().id) andlaunchId($AGENT_LAUNCH_ID, the per-run idexec.tsinjects for every harness) on thestartrequest, and the daemon stamps exactly those. AddsTask.launchId— which run created a task — the scope a laterbrowser status --mineand the no-flag current-task default will filter on. Source:apps/cli/src/lib/browser/types.ts,apps/cli/src/lib/browser/service.ts,apps/cli/src/lib/browser/ipc.ts,apps/cli/src/commands/browser.ts.agents harness editandagents harness renameare now real commands.editProfileandrenameProfilealready existed inlib/profiles.tsbut nothing on the CLI surface reached them, so changing a custom harness meant hand-editing its YAML.agents harness edit <name>applies--model,--base-url,--version, and--descriptionin place, preserving fork lineage (an edit never marks a harness as forked from itself).agents harness rename <name> <new-name>renames the YAML file and itsnamefield, and rewritesforkedFromon every harness that pointed at the old name so the fork graph stays accurate. There is deliberately no--label: the headeragents viewprints is derived from the harness name, so renaming is how you change it.Run-time messages call a custom harness a "custom harness", not a "profile". When you
agents run <name>a custom harness (created withagents harness add), the CLI now saysResolved custom harness '<name>'and, for a discarded cost tier,cost tiers don't apply to custom harness '<name>'— instead of the legacy internal noun "profile". The--strategyand account-picker notices on a custom-harness run are aligned too. Behavior is unchanged; the legacyagents profilesalias still works. Source:apps/cli/src/commands/exec.ts.Placement model +
agents run --where(Phase 2 surface consolidation). "Where does the body run?" is one shared object (local | device | fleet | cloud | lease) insrc/lib/placement.ts.agents run --where device:<name>|auto|lease[:backend]|localexpands into the existing--host/--leasepaths; mixing doors fails loud. Docs (00-concepts.md§ Placement,hosts.md) and help on run / routines / monitors teach the matrix — including that monitors--deviceis owner, not body placement. Old flags remain aliases. Source:apps/cli/src/lib/placement.ts,apps/cli/src/commands/exec.ts.agents harness forkno longer accepts--label(breaking change). The--labelflag was used to set a human-facing display name for a custom harness. Display names are now always derived from the profile'snamevia a curated vendor/brand table (deepseek-flash→DeepSeek Flash,spark→Spark), so the flag is superfluous. Any script that passes--labeltoagents harness forkwill receive a CLI error; remove the flag to migrate.agents runno longer auto-picks an account whose token the server has already rejected. Account rotation judged an account "signed in" from a local heuristic — a credential file is present and its email decodes — which cannot tell a good token from a revoked-but-unexpired one, sobalanced/available/run autocould route into arevokedaccount and die at spawn ("session expired"). Eligibility now also reads the daemon's live auth-health probe (auth-health.ts): arevoked(401/403) account is excluded from the pick, reported asrevokedby the pre-flight readiness check, shown as "needs re-login" in the account picker, and named in the teams throttle warning. Fail-open: a missing probe or any non-revoked verdict never blocks a launch (a cachedrevokedkeeps gating until the daemon's next probe clears it). Source:apps/cli/src/lib/rotate.ts,apps/cli/src/commands/run-account-picker.ts,apps/cli/src/commands/teams.ts,apps/cli/docs/hosts.md.Scheduled routines no longer overlap or outlive their configured timeout (RUSH-2186). Detached cron, catchup, and monitor launches now take a cross-process per-routine claim and refuse a second fire while the prior run is alive. The configured deadline is persisted in run metadata; both the live runner and the restart-recovery monitor kill the owned process tree and record
timeoutwhen it expires. Source:apps/cli/src/lib/runner.ts,apps/cli/src/lib/routines.ts,apps/cli/docs/03-routines.md.agents snapshot— one-process poll for inventory + active sessions (Phase 4 surface consolidation). Consumers (Factory, scripts, menubar) were forkingview --json× N harnesses plussessions --active --json(and sometimes feed) on every tick.agents snapshot --jsonreturns the same shapes in one invocation:inventory(view),sessions(active rows), optional--with-feed/--with-sync. Default sessions scope is this machine;--all-hostsmatches fullsessions --activefan-out. Does not redefineagents status, which stays the UnifiedSyncStatus sync contract. Source:apps/cli/src/commands/snapshot.ts,apps/cli/src/lib/snapshot.ts.
1.22.7
agents feed --project <name>scopes the whole feed to one project. Open blocks, the updates view (--filter updates), and the trailing activity lane are all filtered to the requested repo/project using the same worktree-aware project key asagents perf(lib/project-key.ts). The masthead becomes<project> needs you/<project> updates. Filtering is applied locally after the fleet fan-out, so older peers that do not recognize--projectstill contribute correctly. Source:apps/cli/src/commands/feed.ts,apps/cli/src/lib/feed-ranking.ts.Feed blocks are now stamped with their project. The
feed-publishhook derives project from the session cwd, andagents feed post --blockedstamps it on the declared block. Live-session enrichment backfillsprojectonto older blocks that lack it. Source:apps/cli/src/lib/feed.ts,apps/cli/src/lib/feed-outcome.ts,apps/cli/src/lib/session/active.ts.agents activityis removed. The standalone milestone timeline is gone; its stream is now read throughagents feed --filter all(blocks + updates) oragents feed --filter updates(updates only).activity --project <name>is replaced byfeed --project <name>. Source:apps/cli/src/index.ts,apps/cli/src/startup/command-registry.ts,apps/cli/src/commands/activity.ts(deleted),apps/cli/docs/06-observability.md,apps/cli/docs/11-projects.md.agents browser startno longer fails with "Custom binary not found" when thedefaultprofile came from another OS.~/.agents/agents.yamlsyncs across the fleet, so adefaultprofile auto-created on macOS carried a/Applications/Google Chrome.app/...binary path that doesn't exist on a Linux box — a barebrowser startthere died withCustom binary not found, the top browser roadblock (one session burned six commands working around it).ensureDefaultBrowserProfilenow validates that the resolved default can actually launch on THIS machine and, if its browser/binary is missing, regenerates thedefaultfrom the installed-browser auto-detect instead of handing back the broken profile. A configured default (profiles set-default) that can't launch here warns and falls through to auto-detect; remote (ssh://) defaults skip the local binary check since their browser lives on the far host. Source:apps/cli/src/lib/browser/profiles.ts,apps/cli/docs/browser.md.The stray "Agents CLI needs to authenticate to continue" Touch ID sheet now actually heals on an already-hashed machine (SEC-13/#1938 follow-up). 1.22.5 added a one-time no-ACL re-store for a stale-ACL'd
agents-cli.hmackeyitem — the internal HMAC key read before every hashed keychain lookup, whose damaged copy pops a generic, context-less Touch ID sheet on nearly any command that touches secrets. But it wired the heal only intomaybeAutoRekey, which is bypassed for the hmackey and hashed-name lookups themselves (prepareServiceNamereturns early forHMAC_KEY_ITEMbeforemaybeAutoRekeyruns). So the exact hot paths that read the key — theagents devices liststats probe a SessionStart hook runs, and every background hashed read — never triggered the heal, and an already-migrated machine prompted forever. The documentedagents secrets rekeyremedy is also a no-op on such a machine: with no cleartext names left to re-key, it returns without re-storing the key. The heal now runs on the read path itself (readHmacKeyRecord): the first hashed lookup in the first process re-stores the record no-ACL exactly once (guarded byhealedNoAcl, so it never churns the keychain afterward) — one last prompt on the read that heals it, then silent forever, on every path. Source:apps/cli/src/lib/secrets/index.ts.Add Pi (Oh My Pi,
omp) as a native harness. agents-cli now installs, runs, and syncs resources for Oh My Pi (@oh-my-pi/pi-coding-agent, binaryomp) under idpi. Pi is a Bun-based, terminal-first, multi-provider coding agent; its cross-provider model catalog (OpenRouter, OpenAI, Anthropic, xAI, DeepSeek, …) surfaces inagents viewandagents models piviaomp models --json. It is Claude-compatible: MCP (.mcp.json, stdio + http + headers), skills, file commands, and Claude-shaped subagents all sync into~/.omp/agent/. Hooks, allowlist, and plugins are intentionally off (omp's hook/approval/plugin models don't map to agents-cli's). Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/exec.ts,apps/cli/src/lib/models.ts.agents run <profile> --model <tier>now resolves the cost tier against the profile's own harness, not its host's. A custom-harness profile (e.g. a DeepSeek model routed through theclaudehost binary) resolved a tier token (cheap|default|best|ultra) by callingresolveTier(options.agent, ...)withoptions.agentalready overwritten to the HOST agent's id — sobestresolved against Claude's own catalog and could push a real Claude model id as the--modelflag, clobbering the profile's ownANTHROPIC_MODELenv value. Profiles can now declare amodels:block (per-tier model ids for the harness's own catalog); a requested tier resolves against it first — clamping an unset tier down to the next cheaper one that IS set — and the concrete id is substituted into both the env and the forwarded--modelvalue before exec.ts's native (host-catalog) tier logic ever runs. A profile with nomodels:configured degrades gracefully to today's behavior — the harness's single pinned model, with an informational note instead of an error. Source:apps/cli/src/lib/profiles.ts,apps/cli/src/commands/exec.ts.agents projects statuscard: host-grouped agents, focus units, and a warnings footer. Live agents render under@hostrows so the same harness on two machines is not collapsed into one cell. Focus counts are labeledfile-touches (Nd)instead of bare integers. Repo drift, dirty trees, missing checkouts, slug mismatch, unmeasurable schedule, and crash piles land at the bottom with 🔴 critical / ⚠️ continue. Local workspace probe always feeds the footer (full fleet table still requires--fleet). Source:apps/cli/src/lib/project-status.ts,project-focus.ts,project-probe.ts,commands/projects.ts.agents projects statusandview/showshare one body. Named form is the full card (every milestone + definition); unnamed is the multi-project rollup. No second implementation to drift. Source:apps/cli/src/commands/projects.ts.agents sessions --helpand05-sessions.mdnow teach one session-lifecycle matrix.focus/focus --attach-only/detach/attach/resumeare listed as distinct intents (not synonyms), so operators stop guessing amonggo/focus/attach/resume. Source:apps/cli/src/commands/sessions.ts,apps/cli/src/commands/focus.ts,apps/cli/docs/05-sessions.md.Cost tiers are ignored (with a clear warning) for profile runs. A profile's model comes from its endpoint (e.g. Kimi/DeepSeek/GLM via
agents run <profile>), not the host harness's catalog — so passing--model cheap|default|best|ultrato a profile used to resolve against the host harness and forward an incompatible model id to the profile's endpoint. Now a tier on a profile run is discarded with a standout warning and the profile's configured model is used. Concrete--model <id>on a profile is unchanged. Source:apps/cli/src/commands/exec.ts.agents viewharness rows now lead with the version number. Custom harness rows previously showedvia <host> <version>— the host CLI name came first, which buried the version in the middle of the line. The format is now<version> (forked from <host>)for pinned harnesses and<version> (forked from <host>, tracks default)for unpinned ones that follow the host's global default. Thetracks defaultlabel is shown in green so it stands out at a glance.Chained fork lineage in harness headers. When a custom harness is itself a fork of another custom harness (which in turn forks a native host), the block header now shows the full two-hop chain:
custom · forked from <intermediate> -> <native-host>. Single-hop forks continue to showcustom · forked from <parent>.BYOK budget bar in
agents view. Custom harnesses backed by an OpenRouter key now show a live spend bar (amount used, remaining, and limit) inline on the model/auth row. Keys are deduplicated so multiple harnesses sharing the same keychain entry trigger exactly one API call. The bar is rendered only when a budget is available; harnesses without a BYOK key are unaffected. Source:apps/cli/src/lib/byok-usage.ts,apps/cli/src/commands/view.ts.agents run autowith no prompt no longer silently attaches a dead pane or leaves an orphan session (RUSH-2185 / EXEC-23a). Three latent bugs combined to produce this failure whenautopicked a harness likecursor-agentthat exits immediately without a prompt: (F1) the auto-picker had no gate for whether a harness can open a bare interactive REPL —cursor-agentwas a valid candidate even though its CLI requires a prompt and exits onargv = []; (F2)surfacePaneFailurewas guarded bystatus !== 0, so a clean exit-0 death produced only a bare[detached]line with no diagnostic; (F3) the "pane still alive → keep session" fall-through relied onpaneExitStatusreturning{dead:false}, which it also returns on any query error (a race right after the pane-died hook), leaving the session alive as an orphan. Fixed: (F1) a newinteractiveReplcapability bit inAgentConfig.capabilitiesmarks every harness;autonow filters to REPL-capable candidates before picking, and fails loud naming the installed harnesses when none qualify; (F2)shouldRecapDeadPane(status, interactive)surfaces the pane tail any time the run is interactive, regardless of exit code; (F3)isPaneKnownAliveFromQueryResult(code, stdout)is now required as positive proof before keeping a session — an ambiguous result tears the session down viakillSessioninstead. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/agents.ts,apps/cli/src/lib/types.ts,apps/cli/src/lib/capabilities.ts,apps/cli/src/commands/exec.ts,apps/cli/docs/specifications.md(EXEC-23a).agents run autocan pick the Pi (omp) harness for prompt-less interactive runs again, andmainbuilds green. TheinteractiveReplcapability bit added by the RUSH-2185 / EXEC-23a fix landed at the same time as the new Pi harness, and neither change saw the other — sopiwas the one agent inAGENTSthat never declared the bit, and the completeness test that pins the registry to the capability list went red onmain(pi missing interactiveRepl). Pi now declaresinteractiveRepl: true: bareompruns the TUI, andomp -pis the one-shot form that answers a prompt and exits.projects status --fleetno longer labels this box@local. Local sessions fromgetActiveSessions()lackedmachine; host-grouped agents stamped remotes only. Locals are now filled withmachineId()before rollup so the agents roster and fleet lines agree. Source:apps/cli/src/lib/project-status.ts,commands/projects.ts.agents sessions stats— which skills/commands you actually invoke, and which are dead weight. A cheap, db-backed rollup ofsession_resource_usage(the skill/Skill-tool + slash-command tallies already recorded at index time), joined tosessionsfor attribution so--agent/--project/--since/--machinenarrow the window and--kind/--pluginnarrow the resources. A both-ends view: the most-invoked resources (--bottomflips to least-invoked,--top <n>caps), and the installed-but-never-invoked ones (cross-referenced againstlistResources/discoverPlugins) — the productized form of a manual transcript-scan audit.--jsonemits a versionedsessions-statsenvelope. The signal captures EXPLICIT invocations only (slash commands +Skilltool calls) — an auto-triggered skill emits no event and reads as 0, and only Claude transcripts expose the signal today; both caveats are surfaced in help and output. A newagents sessions backfill resourcesfolds historical sessions (indexed before the signal shipped) into the usage index, re-parsing each transcript from byte 0, gated by a newresource_scan_ledger(schema v31) so reruns skip completed transcripts — mirroringagents sessions backfill tools. Source:apps/cli/src/commands/sessions-stats.ts,apps/cli/src/commands/sessions-backfill.ts,apps/cli/src/lib/session/db.ts,apps/cli/docs/05-sessions.md,apps/cli/docs/specifications.md(SES-IF-4b).agents shareserves screenshots and recordings with a real content-type. Publishing a PNG/JPEG/GIF/WebP/AVIF image, an MP4/MOV/WebM video, or a PDF now sets the matchingcontent-typeinstead ofapplication/octet-stream. GitHub's image proxy (camo) only renders an inlinewhen the asset is served as a real image/video type, so this is what lets an agent drop a screenshot or a screen recording straight into a PR body viaagents share <file>. HTML, SVG, CSS, JS, JSON, and text were already typed correctly. Source:apps/cli/src/lib/share/publish.ts.agents trends tools-per-sessionnow counts every scanned session, not justagents teamsruns. The recipe readsessions.tool_call_count, a column nothing populates except the teams summarizer (apps/cli/src/lib/teams/summarizer.ts) — the general session indexer never computes it. So every session that did not come from a team was scored 0 or excluded outright byWHERE tool_call_count IS NOT NULL, pinning the fleet-wide p50 at 0 however many tools ran and leaving onlyclaudein the table. It now readstool_scan_ledger.call_count, the per-session count the tool indexer writes for every session it scans — the same index behindagents sessions --include tools, so the two surfaces stop disagreeing. Sessions with genuinely zero tool calls still count as 0 instead of vanishing. On a real 7-day window this took the sample from 400 to 570 sessions and surfacedgrok,rush,codex,kimi,droidandantigravity, none of which had ever appeared. Runagents sessions backfill toolsonce if historical sessions were never indexed. Source:apps/cli/src/lib/analytics/recipes.ts.
1.22.5
agents eventscan now filter by--session <id>and--bundle <name>— trace which agent/session triggered a secret access. Every event already carries the provenancesessionId, and secrets events carry thebundlein their payload, but neither was queryable: you could see that thesharebundle was read, not which session read it.--session(wired to the engine's existingsessionIdfilter) and--bundle(a new payload filter across both the operational log and the activity stream) close that gap.agents events --module secrets --bundle share --session <id>answers "which agent read the share bundle" — the attribution the Touch ID storm investigation needed, since the macOS biometric sheet itself emits no event. Source:apps/cli/src/lib/event-stream.ts,apps/cli/src/commands/events.ts,apps/cli/docs/06-observability.md.agents feed post --blockedrecords now survive the agent's next Stop. The feed-publish hook cleared the per-session block file on everyStop/SessionEnd/PostToolUse, which silently dropped a declared (--blocked) block the moment the agent parked it and its turn ended — exactly when the owner still needs to see and answer it. Declared blocks are now exempt from the lifecycle clear and stay inagents feeduntil they are actually answered (a terminal reply orrecordAnswer); question/notification/approval blocks still clear as before. Source:apps/cli/src/lib/feed.ts.agents add grok@latestno longer lets a second grok account silently displace a first account's install. Grok's version directories are keyed by upstream release number alone, not by account — so two different grok accounts that both self-update to the same identical release ("latest") were landing on the SAME on-diskversions/grok/<version>/directory. The second account's credentials would overwrite the first's in that shared directory, even thoughagents view grokstill listed both accounts as separately installed.installVersionnow detects this before finalizing the install: if the target version's home already has a signed-in account whose identity differs from the account driving the current update, it refuses with a clear error instead of silently corrupting the first account's install. Source:apps/cli/src/lib/versions.ts.The stray "Agents CLI needs to authenticate to continue" Touch ID sheet now heals itself. An old keychain helper (before the metadata/hmackey no-ACL migration fix) could re-stamp the internal HMAC-key item (
agents-cli.hmackey) with a biometry ACL. That item is read before every hashed keychain lookup, so a damaged copy popped a generic, context-less Touch ID sheet on nearly any command that touched secrets —agents devices list, a background agent, a session hook — at seemingly random times. The migration fix stopped the re-stamping but never un-stamped an already-damaged item, and once hashed naming is active nothing re-stored it, so it prompted forever. Now, on the first read where hashing is active, an un-healed HMAC-key record is re-stored no-ACL exactly once (healHmacKeyNoAclOnce, gated by ahealedNoAclflag so it never churns the keychain afterward) — one last prompt on the read that heals it, then silent. Existing damaged machines can also fix it immediately withagents secrets rekey. Source:apps/cli/src/lib/secrets/index.ts.agents inspectandview --jsonnow report isolation honestly. Found by diffing every command's output between an isolated-only and a normal install.inspectprinted the bare-shim path unconditionally, so an isolated copy — which deliberately has no shim, that being the guarantee — was shown sitting on the user's PATH; it now reports(none — isolated installs stay off PATH)andshim: nullin JSON.inspectalso showed onlydefault: falsefor an isolated copy, hiding that it was the selected one; it now carriesisolatedandisolatedDefault, and the header reads[isolated default].view --jsonhad no isolation signal at all, so tooling could not distinguish a sandboxed copy from one that owns the launcher and real config — its version entries gainisolatedandisIsolatedDefault. Source:apps/cli/src/commands/inspect.ts,apps/cli/src/commands/view.ts.agents modelsis now a scannable tier menu, and you can override a tier with a command. The tier map (cheap|default|best|ultra→ model +~$/Mtok) prints for every installed harness by default; the raw model list moved behind--all. When the auto-guess is wrong (subscription harnesses with no price signal), pin the right model without hand-editing YAML:agents models tier set <agent[@version]> <tier> <model>(e.g.agents models tier set kimi best kimi-code/k3),tier clear,tier list. Overrides live undermodel.tiersinagents.yaml(same selector shape asrun.defaults) and resolve most-specific-first —<agent>:<version>→<agent>:*→ auto; an overridden id a version doesn't ship falls back to auto, andagents modelsmarks a pinned tier[override]. Ships a curated Kimi ladder (k2.7-highspeed<k2.7-coding<k3) so it's right by default. Also fixes two extraction bugs: bumps the model-catalog cache schema so a freshly-upgraded box re-extracts instead of serving a stale "No models extracted" for 24h, and stops the id-scan from listing bare legacy ids likeclaude-opus-4(#1892). Source:apps/cli/src/lib/model-tier-overrides.ts,apps/cli/src/lib/model-tiers.ts,apps/cli/src/commands/models.ts,apps/cli/src/lib/models.ts,apps/cli/docs/model-tiers.md.agents run --secrets <bundle>never raises a Touch ID sheet on launch — even with a tty (a second storm source). The--secretsinjection read gated onisHeadlessSecretsContext(), which is FALSE for an interactive run — so anagents run … --interactivelaunch (the watchdog firesagents run auto --interactiveevery ~2 min via routine + menu-bar tick) could pop Touch ID for a keychainholdbundle, piling up helper sheets. An agent launch must never prompt regardless of tty (SEC-13): the read is now alwaysagentOnly— it resolves from the broker (or a no-ACL bundle) and otherwise fails fast namingagents secrets unlock <bundle>, matching the behavior the code's own comment already described. Unchanged: the explicitagents share/agents share setupcommands (readWriteTokenFromBundle,readCloudflareCreds) still honor the interactive/headless gate — those are user-initiated, not agent launches. Source:apps/cli/src/commands/exec.ts.Background and read-only secret reads never raise Touch ID (SEC-13). Every non-user-initiated secret read now resolves
agentOnly— from the secrets broker or a no-ACL bundle — and, on a lockedhold/alwayskeychain bundle, fails fast namingagents secrets unlock <bundle>instead of popping a Touch ID sheet on the interactive launcher. This closes the rest of the per-launch prompt storm that #1905 fixed only for--secretsinjection. Covered: session-sync (r2.backups, read on every daemon cycle — degrades to no-transport when locked, re-checked each cycle for fast pickup once unlocked), the--leasecrabbox provider token (resolved once up front and memoized, so a locked bundle fails loud once and the ready-wait poll never re-issues the read), browser-profile secrets on launch (skipped silently, launch proceeds), cloud dispatch (cloud:antigravity, fails loud with the unlock hint), thewebhook servereceiver, and theget_secretMCP tool (throws as the tool error). Unchanged and still interactive-gated: the user-initiatedagents secrets get/export,agents browser type,agents exec --secrets, thesshaskpass, and the explicitagents share/agents share setupprovisioning reads — a human running those in a plain terminal still gets a prompt. Source:apps/cli/src/lib/session/sync/config.ts,apps/cli/src/lib/crabbox/cli.ts,apps/cli/src/lib/browser/chrome.ts,apps/cli/src/lib/cloud/antigravity.ts,apps/cli/src/lib/secrets/mcp.ts,apps/cli/src/commands/webhook.ts.
1.22.4
- Background processes no longer storm macOS Touch ID sheets (secrets-touchid-storm).
Raw keychain item reads — a profile's provider token on
agents run <profile>, the Claude OAuth read behindagents view, anygetKeychainTokencaller — now fail fast with an actionable error naming the item when the process is non-interactive (an agent runtime, or a TTY-less background spawn like the Factory extension host'sagents viewpoll), instead of raising a sheet nobody is watching. A cancelled or failed interactive read opens a 5-minute back-off memo (~/.agents/.cache/keychain-read-backoff/) so a polling caller can't re-prompt every few seconds; any successful read or write clears it. Reads that are prompt-free by construction (bundle metadata,never-policy bundles, the unlock session store, the OAuth token cache) attest their no-ACL write and are unaffected. crabbox's tailscale key is now read at most once per process instead of on everycrabboxEnvcall (list/wait/spawn/stop). Source:apps/cli/src/lib/secrets/index.ts,apps/cli/src/lib/secrets/headless.ts,apps/cli/src/lib/secrets/read-backoff.ts,apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/lib/crabbox/cli.ts,apps/cli/docs/specifications.md(SEC-13, SEC-27).
1.22.3
- Menubar home-base self-test accepts
MenubarHelper-universal. The signed-helper gate requiredexecutablePathto end in exactlyMenubarHelper, but lipo production builds name the binaryMenubarHelper-universal, so every 1.22.2 publish on mac-mini failed after the release PR had already merged and tagged. Source:menubar/Sources/MenubarHelper/ChildProcessSelfTest.swift.
1.22.2
Menu bar recovers from a stale single-instance lock instead of staying dead. The helper's lock fd is now opened
O_CLOEXEC, so a spawneddoctorchild can never inherit it and hold themenubar.lockflock after the helper crashes; andSingleInstance.acquirenow self-heals — when the flock is held but no liveMenubarHelperowns it (a leaked orphan / dead pid), it reaps the orphan and retries rather than exiting as "already running". Previously a leaked orphan bricked the menu bar until reboot. The headless Swift self-tests (single-instance- child-process) now run as a build gate (
menubar/scripts/test-menubar.sh), which nothing invoked before. Source:apps/cli/menubar/Sources/MenubarHelper/SingleInstance.swift.
- child-process) now run as a build gate (
agents projectsdefinitions can now carry goals — the OKR-shaped "why". A project serves one or moregoals[], each anobjective(the outcome) plus an optionalmeasure(the key result). Set them at scaffold time withagents projects add <name> --goal "objective:measure"(repeatable), replace them later withagents projects set <name> --goal …, or hand-edit the YAML. Goals show on thestatuscard (compact) and inprojects view(in full), and survive a--from-linearre-import like every other hand-set field. Milestones (pulled from Linear) remain the dated checkpoints toward these goals. Source:apps/cli/src/lib/projects.ts,apps/cli/src/commands/projects.ts.Balanced rotation no longer picks version homes that only inherit the active login.
getAccountInfofalls back to the active/global HOME credential soagents viewstill shows who is signed in when a version home has no auth file of its own. Launch paths isolate config (GROK_HOME,CODEX_HOME, …) to the per-version home, so those empty homes died at spawn with "Not signed in" after balanced picked them (observed:[email protected]with noauth.jsonlooking signed-in via~/.grok→0.2.32). Rotation now requires a real per-version credential when we know where it lives (credentialPresence.perVersion). Source:src/lib/rotate.ts(isLaunchableSignedIn,collectRunCandidates).A
never-policy secrets bundle now actually stays silent — no more Touch ID for a bundle you set to silent, and no more double prompt. Two bugs madeagents secrets policy <b> nevera lie. (1) The command rewrote only the bundle metadata, never the value items — but macOS gates each read on the item's own ACL, not the tier label, so a bundle created underhold/alwayskept its biometry ACL and kept popping Touch ID forever after the switch.policynow reconciles the value items to the new tier (reAclBundleItems): tightening toneverre-stores them no-ACL (a single last prompt to read them once), and loosening back re-attaches the gate. (2) The signed keychain helper's just-in-time migration (migrateInline/rehomeOrphan) re-stamped a biometry ACL onto everyagents-cli.*item it touched on read — including bundle metadata and the HMAC key, which are supposed to be silent — so a metadata/hmackey read in its own helper process raised a second Touch ID sheet on top of the value read. The migration now re-adds silent items (metadata, hmackey) without an ACL, so metadata enumeration and the pre-value hmackey read never prompt. Net: unlock/read aneverbundle once and it stays silent through sleep, reboot, 30+ days, an agents-cli upgrade, and a macOS upgrade — with no Touch ID and no passphrase. Thenevertier's durability and attribution guarantees are now written into the§Secretsspec (SEC-19, SEC-27, SEC-28). Source:apps/cli/src/commands/secrets.ts,apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/lib/secrets/keychain-helper.swift,apps/cli/docs/specifications.md.The per-run Touch ID storm is fixed:
agents runno longer pops a sheet to auto-inject the share token. On everyagents run,shareRuntimeEnvauto-reads thesharebundle's R2 write token to hand it to the spawned agent — and becauseshareis a keychain bundle that is rarely broker-held, an interactive read spawned the helper and raised Touch ID on EVERY launch. Auto-injecting a token on an agent launch is a background convenience, not a user-initiated secret access, so it must never raise a sheet (SEC-13): the read is now alwaysagentOnly— it resolves the token from the injected env or an already-held / no-ACL bundle and silently returns nothing otherwise (the agent can still publish via its own explicitagents share). And a NEWsharebundle now defaults to thenevertier (no biometry ACL) — the write token is low-sensitivity automation infra — so auto-share is silent with no unlock at all; an existing bundle keeps its tier (change it withagents secrets policy share never, which now actually strips the ACL). Source:apps/cli/src/lib/share/config.ts,apps/cli/src/commands/exec.ts.The daemon watchdog now rotates rate-limited sessions in place. A stalled session whose transcript tail shows a hard account limit ("You've hit your weekly limit · resets …", "usage limit reached", "out of credits") is rotated instead of nudged: the tick gates on the same first-party healthy-account selection
agents run automakes (collectHarnessCandidates+pickHarnessWeighted— zero healthy logs onerotateskip event per cooldown window towatchdog.logand leaves the terminal untouched), injects the harness's exit sequence (claude: Esc, Ctrl+C, Ctrl+C; codex/gemini/cursor/opencode: Ctrl+C twice), relaunchesagents run auto --interactive --session-id <uuid>in the SAME tab via the inject rail, then — once the new session's TUI is live — injects the resume replay for the old session. Readiness is the new session's transcript (primary) or a fresh active session correlated by cwd + machine, never an unrelated one; the wait is bounded (60s), and on timeout the session is flagged with a bare-shell message pointing at a manualagents run autoand suppressed for 15m before retry — never blind-typed into. The state machine (exiting → launching → awaiting-tui → replaying → done | failed) persists at~/.agents/.cache/state/watchdog/rotate/<sessionId>.jsonand spans ticks via a post-loop sweep. Config:agents watchdog rotate on|offwriteswatchdog.rotatein~/.agents/agents.yaml(default on; rotate-only, nudging is unaffected), honored per tick;agents watchdog status/--jsonreport the rotate config and every persisted rotate state. This replaces the Factory extension's own watchdog rotate loop, which is being deleted in the companion change. Source:apps/cli/src/lib/watchdog/rotate.ts,apps/cli/src/lib/watchdog/runner.ts,apps/cli/src/commands/watchdog.ts.
1.22.1
agents doctorde-noise: never-synced and cross-version hook drift are warnings, not criticals (RUSH-2162). The CRITICAL section now holds only "needs you now" problems — a logged-out account, or a hook/plugin missing from a version you keep synced. A version that was never synced (an old/unused install with nothing installed) and a hook that merely differs across versions (installed but stale) are surfaced as WARNINGs instead, cutting the critical count on a busy machine from ~11 to the handful that actually need action. Source:apps/cli/src/lib/devices/doctor-findings.ts.The "… is damaged and can't be opened" dialog stops — both helper
.appbundles now install atomically and serialized. The secrets keychain helper (Agents CLI.app) and the menu-bar helper (MenubarHelper.app) are each (re)installed on the hot path of ordinaryagentsinvocations, and both did a non-atomicrm -rf dest+cp -R src deststraight onto the live bundle. On a busy box dozens of concurrent invocations raced that path, so a reader (Gatekeeper, or an exec of the bundle) could see a half-written.app— a truncated Mach-O / mismatched code signature — which macOS reports as damaged. A new shared installer (lib/app-bundle-install.ts, replacing the two duplicated copy functions) stages the copy in a sibling dir and swaps it in with renames (the live bundle is only ever a complete, signed.app, and a failed copy never touches it), and serializes concurrent installers behind the sharedwithFileLockwith a double-checked skip so a burst copies once instead of stampeding. Source:apps/cli/src/lib/app-bundle-install.ts,apps/cli/src/lib/secrets/install-helper.ts,apps/cli/src/lib/menubar/install-menubar.ts.agents doctor --jsonno longer stampedes into dozens of concurrent runs — the overview is singleflighted and cached, with a new--refreshto force a live recompute. The baredoctor --jsonoverview probes every host CLI, every agent's sign-in, and every agent×version diff — seconds on an idle box, minutes on a loaded one. The menu-bar helper polls it on a timer with only a per-process in-flight guard, so a helper relaunch (or any second poller) each launched its own live compute, and a helper killed mid-run orphaned adoctor --jsonthat kept spinning — stacking to dozens of concurrent runs pinning the CPU. Now a fresh snapshot (< 90s) serves instantly from a disk cache, and when a live compute IS needed exactly one runs while every other caller serves its result (a lock-directory singleflight that self-heals if the computer dies).agents doctor --json --refreshbypasses the cache. Source:apps/cli/src/lib/devices/doctor-overview-cache.ts,apps/cli/src/commands/doctor.ts.doctor --jsonreleases its singleflight lock before it returns. The overview gate fired the lock release without awaiting it on the path where a waiter serves the winner's fresh snapshot, so the call returned with the lockfile still on disk. The next caller then retried against a lock that was already logically free — the pile-up the gate exists to prevent, narrowed to the window between return and unlink. The release is now awaited. The existing coalescing test failed 5 times in 15 runs before this and 0 in 15 after. Source:apps/cli/src/lib/devices/doctor-overview-cache.ts.agents add grok@latestno longer strands the freshly-downloaded binary in the old version's home. When the post-install version probe (<cli> --version) transiently failed right after grok's self-updating installer exited,installVersionsilently fell back to the literal string'latest'as the resolved version — creating a bogusversions/grok/latest/directory and defeatingrelocateGrokBinaryToVersionHome's exact-filename match (its regex could never matchgrok-latest-..., since the real file is namedgrok-<semver>-<platform>). The real multi-hundred-MB binary was left behind in the PREVIOUS default's downloads dir, andagents view groknever listed the new version as installed even thoughagents addreported success. The probe now retries briefly instead of silently falling back, and fails loudly if it still can't resolve a version rather than corrupting the version bookkeeping. Relocation also now self-heals: if the current~/.groksymlink target has nothing matching, it sweeps every other installed grok version home for a binary stranded by a past occurrence of this bug. Source:apps/cli/src/lib/versions.ts.A blocked menu-bar row now takes you to the session (RUSH-2110). A NEEDS-YOU row exists because an agent is waiting on you, but its only action was "Reveal working dir", which unblocks nothing — you still had to go find the session by hand. Blocked rows now lead with Focus session, which runs
agents focus <id>: attach the live terminal, or open a new tab and resume, cross-host. Reveal stays underneath. Both render paths are covered — the single inline row and each entry inside a collapsed multi-waiter group. A row the engine could not identify (a cloud task, a stale sentinel) simply omits the item rather than offering an action that would do nothing. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift,AgentsCLI.swift.Two projects sharing one monorepo checkout are no longer indistinguishable. Session, activity, and feed attribution anchored a project on
root ?? defaultPath, so a subproject whoserootis the monorepo and whosedefaultPathis a subdir collapsed onto the same path as its umbrella — the longest-match tiebreak had nothing to separate them, and work inrush/apps/clicounted toward whichever definition happened to be listed first. AdefaultPathnested underrootnow takes precedence over thatroot(the root says where the checkout is;defaultPathsays which work is this project's), and each bound repo's checkout and subpath anchor too. A narrowedrootstill covers the rest of its checkout as a fallback, so a lone project defined with--pathkeeps attributing work across its own repo instead of only inside the subdir. Source:apps/cli/src/lib/projects.ts.agents projects view <name>now shows more thanstatus, not less. The command you open to learn everything about one project built its own short list — root, repos, a raw Linear project id, an issue count, milestones — and never called the card renderer, so it omitted the agents roster, merged PRs and release, focus areas, the schedule verdict, tickets, and artifacts thatstatushad shown all along.viewandstatusnow gather through one function and render through one card;viewadds every milestone (instead of just the next) and the stored definition in full underneath — each repo with its subpath and checkout, each context with its purpose, each integration with its URL. It also takes--window <days>to matchstatus. Source:apps/cli/src/commands/projects.ts.The
agentsroster on the card lists live sessions only. It included every matched session, so a card headed23 livewent on to printclaude · crashed ×25— the corpses thedeadrow already reports, counted twice and contradicting the headline. Both now derive from oneisDeadStatuspredicate, pinned by a test across everyActiveStatus. Source:apps/cli/src/lib/project-status.ts.--host <self>and the fleet-health fan-out now short-circuit ALL of the local machine's names, not just its short hostname (RUSH-2114). A--hosttarget or fleet probe that referenced this box by its tailscale dnsName (zion.tail1a85a1.ts.net) slipped past a=== machineId()check and SSH'd to the local box over its own name; on a loaded machine that self-SSH'ddoctor --jsonorphaned on timeout and piled up until the host was crushed. A newisSelfHost()matches every identity the box answers to (short id, loopback, tailscale dnsName + its short form) and gates all four self-checks — the generic--hostpassthrough (maybeRunOnHost), the--devices-all fan-out (runFleetPassthrough),remoteFleetTargets, andrunFleet— so a self-reference runs locally instead of self-SSHing. Source:apps/cli/src/lib/devices/self-host.ts,apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/devices/fleet.ts.
1.22.0
agents run auto— full-auto dispatch (RUSH-2132).run autocomposes all three routing layers: host (14d launch affinity, unless--hostis given), harness (installed CLIs weighted by best-account headroom), and account (the configured strategy).balanced/availablenow exit nonzero when every installed account is unhealthy — naming each excluded account, the earliest window reset, and the--strategy pinnedescape hatch — instead of warning "falling back to defaults" and launching the exhausted pinned default. The error text is a machine-readable contract (no healthy+resets <iso-time>) the Factory watchdog tail-detects for rotate cooldowns. Source:apps/cli/src/lib/rotate.ts,apps/cli/src/commands/exec.ts,apps/cli/src/lib/runner.ts.Bash-command summaries are faster and recognize more of what actually ran (#1830).
classifyBashCommand(behindagents sessions/agents activitysummaries) tokenized the entire command — every pipeline segment, multi-KB heredoc bodies included — just to read the leading executable, costing up to ~1ms on a bigcat <<HEREDOC …. It now tokenizes only the head of the first simple command. Coverage gaps that dumped commands into a rawotherpile are closed too: acdprefix separated by;or a newline (not just&&) unwraps to the real command, a path/tilde executable (~/.agents/skills/linear/scripts/linear) resolves by basename, and the repo's own toolchain (agents,linear, plusrmdir) is recognized —agentswas the single top unrecognized token.agstays the silver searcher, not anagentsalias. Source:apps/cli/src/lib/session/bash-command.ts.agents computer describenow counts towardusedComputer. Every other verb (click,type,key,screenshot,run, …) fires thecomputer.actionevent viaemitComputerAction;describenever did, so a session that only ranagents computer describeread backusedComputer=false— a false-negative in the sessions preview. A new completeness-guard test pins every registeredagents computerverb command to a matchingemitComputerActioncall so a future verb can't ship the same gap silently. Source:apps/cli/src/commands/computer-actions.ts,apps/cli/src/commands/computer-actions.test.ts.Pick a model by cost tier —
--model cheap|default|best|ultra— onagents runandagents teams add. Instead of a concrete id that churns per release and differs per harness, a tier resolves per(harness, installed version)to a model that version actually ships, ranked by the provider's own lineup (opus/sonnet/haiku/fable; Codex "frontier/balanced/fast" → Sol/Terra/Luna), then price, then size tokens. Single-model harnesses (Grok) map the tiers to reasoning effort; Droid uses a curated credit-multiplier map capped at 2x. An unsupported tier clamps to the nearest lower one; an unresolvable tier drops the flag and falls back to the harness default. Concrete model ids keep working unchanged.agents models [agent[@version]]now prints the per-harness tier map (with~$/Mtokwhere priced) and emitstiersin--json, and Droid joins the model-capable set. Also fixes the Claude catalog extractor returning 0 models on the newest native-binary format (a fallback id scan), and refreshesprices.jsonwith the GPT-5.6 Sol/Terra/Luna series. Source:apps/cli/src/lib/model-tiers.ts,apps/cli/src/lib/models.ts,apps/cli/src/lib/exec.ts,apps/cli/src/commands/models.ts,apps/cli/docs/model-tiers.md.agents projects statussays what was worked on and what the dates prove. Two new lines.focusranks the directories the window's commits landed in, read from the local checkout withgit log --name-only— no API call, no credential, no rate-limit budget, measured at 0.23s over a 897-commit week. Changelog fragments and lockfiles are excluded from the ranking: this repo files one fragment per PR, so.changelogotherwise ranked second and presented PR count as an area of focus.schedulestates what the milestone dates prove —overdue by N days,due in N days,N milestones, no issues filed against any, ornone dated. Source:apps/cli/src/lib/project-focus.ts,project-schedule.ts.The schedule line will never say "on track". That verdict needs either project start and target dates to interpolate expected progress, or a scope-history series to extrapolate a finish date. Probed against a live workspace, all of them are absent (
health: null,startDate/targetDatenull,scopeHistoryandcompletedScopeHistoryempty), so an on-track or at-risk chip would be fabricated — and a confident wrong answer on a status card is unfalsifiable from the card. When a human posts a Linear project health update, it is relayed and attributed (per Linear: atRisk), never synthesized.The
--device/--hostauto-reconnect loop no longer trusts a remote-origin exit code of 255 as "the SSH link dropped."reattachRemoteSession'sconnectedflag is set as soon as the fast SSH preflight probe succeeds, before the actual reattach runs — so if the remote command it drives (agents sessions focus <id> --local --attach-only) ever exited 255 for a reason that had nothing to do with the SSH transport, that would be indistinguishable from the link itself dropping, refill the retry budget every cycle, and loop forever — printing "attempt 1/6" on every cycle and leaving the terminal full of aborted-TTY escape codes. The remote invocation is now wrapped inbash -lcso that whatever exit code it decides on, a 255 is remapped to 254 before this process sees it, closing that gap in the exit-code channel regardless of which remote-side path or peeragentsversion might produce it. A genuinely recurring local SSH failure can still refill the retry budget on every attempt by design (unchanged, tracked separately: phnx-labs/agents-cli#1884). Source:apps/cli/src/lib/hosts/reconnect.ts.agents sessionscan query distinct tool calls and count static Bash program occurrences locally or across the fleet. Use--include tools, repeat--querywithtool:,program:,input:,output:,status:,exit:, orerror:fields, and add--fleetfor live SSH fan-out.--countreports exact occurrence, containing-call, and session totals from orderedwrapper/effectiverows without reparsing; synced mirrors are partitioned by origin so fleet evidence and totals do not duplicate sessions. Historical parsing is explicit and resumable throughagents sessions backfill tools; normal scans index new and changed sessions once. Codex orchestration wrappers are parsed statically so only literaltools.exec_commandcommands reach the Bash AST, never wrapper code. Each device keeps a redacted, bounded relational SQLite/FTS5 cache, queries perform no transcript I/O or index writes, and no embeddings, vector database, or model calls are used. A sampling script explicitly backfills then extracts redacted shell-command origins from 50–100 sessions over the last seven days into a 16 MiB maximum artifact.Local team worktrees base on freshly-fetched
origin/<default>, notHEAD.createWorktree(andagents worktree provisionfor new branches) nowgit fetch originthenworktree add -b … origin/<default>, matchingcreateRemoteWorktree. Previously local teammates forked from the orchestrator's currentHEAD, so a stale checkout made every teammate write on old code and only surface the conflict at merge. Source:apps/cli/src/lib/teams/worktree.ts,apps/cli/src/commands/worktree.ts,apps/cli/docs/teams.md.
1.21.3
agents projects import --from-factorystops printing raw git errors. Reading each checkout's real remote is done per registry row, and a checkout with nooriginmakes git writeerror: No such remote 'origin'straight to the terminal — its own stderr, which the surrounding try/catch never sees. Importing 12 rows printed two of them between the progress lines. The probe now discards git's stderr; an absent remote is an expected answer, not something to report. Source:apps/cli/src/commands/projects.ts.Sessions now track browser/computer tool use and skill/plugin/slash-command usage, queryable with
agents sessions --skill <name>/--plugin <name>.browser.navigate,browser.screenshot, and a newcomputer.actionevent fire on everyagents browser/agents computeraction, carrying session identity for free. The sessions index persistsusedBrowser/usedComputer(from a scoped events-log read, not a transcript re-scan) and a newsession_resource_usagetable records every skill and slash-command invocation with its owning plugin, source repo, and git commit — resolved againstresolveResource()/discoverPlugins()at scan time. The sessions picker preview surfaces both asbrowser/computerandSkills:tags. Source:apps/cli/src/lib/browser/service.ts,apps/cli/src/commands/computer-actions.ts,apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/highlights.ts,apps/cli/src/lib/session/discover.ts,apps/cli/src/commands/sessions.ts.ResolvedResourceandDiscoveredPlugincarry provenance:repoRootand a lazily-resolvedsnapshotSha. Every resource/plugin resolution can now answer "which DotAgents repo, which commit" without an extra lookup; the git shell-out is memoized per repo root and only runs when a caller actually readssnapshotSha. Source:apps/cli/src/lib/resources.ts,apps/cli/src/lib/plugins.ts,apps/cli/src/lib/git.ts.SessionEvent.slashCommandcaptures a typed or model-invoked slash command (both the<command-name>wrapper and theSlashCommandtool call), andagents sessions's perf sample forcommand.endnow carries the session id and agent instead of being anonymous. Source:apps/cli/src/lib/session/prompt.ts,apps/cli/src/lib/session/parse.ts,apps/cli/src/index.ts.agents routines statusno longer reports "stopped" for a live scheduler, andagents routines startcan't spawn a second one. The daemon writes its pid file once (on claim/start) but rewrites the heartbeat every tick. If the pid file was lost while the daemon kept ticking — an earlier status check clearing a stale/reused pid, or the file removed out from under a live daemon —statusread only the pid file and reportedstoppedfor a scheduler that was in fact running and firing jobs, whileclaimDaemonInstance()would start a concurrentJobSchedulerthat double-fires every routine.isDaemonRunning()and the single-instance claim now also trust a fresh heartbeat whose pid is alive, re-adopting the pid file to heal the desync. Source:apps/cli/src/lib/daemon.ts.
1.21.2
agents trends— resource and session analytics dashboard. Baked recipes (harness/model mix, tools per session, token ratio, secrets/browser hot lists) readsessions.dbplus a new value-free warehouse at~/.agents/.history/analytics/usage.db. Secrets usage migrates once fromsecrets.db; agent run and browser launch/close emit into the warehouse. Quota stays onagents usage, latency onagents perf. Source:apps/cli/src/commands/trends.ts,apps/cli/src/lib/analytics/.The macOS menu bar app is now named AGI Menu in System Settings and Accessibility prompts. Privacy & Security previously showed the executable name
MenubarHelperbecause the bundle had noCFBundleDisplayName. The bundle now shipsCFBundleName/CFBundleDisplayName=AGI Menu, andagents menubarstatus/enable/disable copy uses the same name. An install that was left ad-hoc-signed by an older heal path is also replaced from the Developer-ID source on the nextagentsrun, so Accessibility stops re-prompting for a new identity every upgrade. Source:apps/cli/menubar/scripts/build.sh,apps/cli/src/commands/menubar.ts,apps/cli/src/lib/menubar/install-menubar.ts.Cursor usage bars now show Auto/API/Total, and Cursor sessions carry live todo progress.
agents viewreads Cursor's dashboardget-current-period-usagefirst for the Auto + Composer (A) / API (API) / Total (T) percent breakdown, falls back tousage-summaryfor accounts without a usableplanUsage, and only drops to the legacy monthly request bar (M) for request-capped free/legacy plans.agents sessionsalso now folds a Cursor session'sTodoWritecalls intoSessionMeta.todos, so the checklist progress shown for Claude/Codex/Kimi sessions renders for Cursor too. Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/session/discover.ts.feed.broadcastgains an in-processchannel:sink and an implicit owner fallback (RUSH-2123). Afeed.broadcastsink can now declarechannel: <name>(plusto:for a non-owner destination) instead ofcommand: [argv...]— it delivers through the same channel-provider registryagents send/agents notifyuse (deliverEnvelope()), no spawn.channel: owneris the address alias, expanding tonotify.owner.{channel,to}. When an operator hasnotify.ownerconfigured but never wrote afeed.broadcastblock at all, an important-level post (--level important, or any--blockedpost) now falls back to that owner address automatically instead of reaching nobody — previously afeed post --blockedwithnotify.ownerset and nofeed.broadcastlooked recorded but delivered to no one. A routine milestone post still stays record-only even with the fallback available, and an operator-declaredfeed.broadcastalways wins outright.command:argv sinks (the tracker/webhook escape hatch) are unchanged. Source:apps/cli/src/lib/feed-broadcast.ts,apps/cli/src/commands/feed.ts.agents runno longer stalls on a live usage fetch, and the daemon keeps the quota cache warm instead (RUSH-2061). The router's candidate collection (collectRunCandidates) used to block on a live provider HTTP read whenever an account's usage snapshot was older than 5 minutes — one round trip per account added to cold-start. It now reads the usage cache cache-only (readOnly) and never touches the network; an unconfirmable snapshot is simply routed around by the existing freshness guard (isUsageVerified). A new daemon refresher (runUsageRefresh) keeps that cache fresh in the background: it refreshes only accounts signed in on THIS host (sole-writer, no cross-host coordination), on an adaptive cadence from each account's session-window burn rate (90s when racing toward the 5h cap, up to 15min when idle), capped at ~6 provider calls per account per hour and skipped entirely while a provider is under a 429 backoff. Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/usage-refresh.ts,apps/cli/src/lib/rotate.ts,apps/cli/src/lib/daemon.ts.Balanced routing now deprioritizes an account projected to cap soon, not just one already maxed (RUSH-2061).
deriveUsageHeadroomprojects minutes-to-limit from the session-window burn rate; balanced weighting scales an account's headroom weight down as that projection shortens (capacityWeight), so a launch avoids an account racing toward its 5-hour cap instead of only skipping a 100%-maxed one. Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/rotate.ts.The daemon no longer SSH-probes the whole fleet every 3 minutes — fleet status is publish-own / read-union now (RUSH-2061, RUSH-2114). The daemon's fleet-cache warm force-probed every registered device over ssh on every tick; with N daemons each probing N devices that was N² remote resource probes across the fleet every 3 minutes, and the source of the orphaned fleet-doctor probe pile-up. Each daemon now probes only itself (no ssh) and publishes its own row — resource stats plus live-agent workload (running-agent count and a per-context / per-agent breakdown) — to a shared local mirror (
~/.agents/.cache/.fleet-status.json). Cross-host rows are unioned on demand by the reader:agents devices statusgathers peers cache-first, ssh-reading a stale/missing peer viaagents devices status --local --jsonthrough a bounded, kill-on-timeout fan-out.agents devices status(and--json) now shows how many agents are running on each box. Source:apps/cli/src/lib/fleet-status.ts,apps/cli/src/lib/fleet-cache.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/devices/health-report.ts,apps/cli/src/commands/ssh.ts.agents doctor --jsonis no longer a ~136-second stall (RUSH-2136). The overview probed every host-CLI manifest with a blockingspawnSync(10s timeout each) one after another, so a dozen-plus slow checks summed into minutes. The checks now run concurrently (listCliStatusAsync), so total time is the slowest single check, not their sum; the per-check 10s kill-on-timeout is preserved. Source:apps/cli/src/lib/cli-resources.ts,apps/cli/src/commands/doctor.ts.Metrics foundation: hook/command instrumentation + routine metrics. Every hook now instruments through a generated shim —
matcher:-only hooks like git-guard/rm-guard/git-require-clean-tree previously fired with zero perf samples;agents perf hooksnow reports them.agents perfgains--project <key>(scope to one repo), aP95column alongside P50/P99, and anERR/TIMEOUTrate column. Newagents perf frictionsurfaces sessions stuck repeatedly hitting the same guard block instead of adapting. Newagents routines stats [name]reports run count/failed/missed/avg/p50/p95 duration per routine;agents routines runs --jsonnow includesduration. Routine session transcripts are now archived for gemini/antigravity/droid/ kimi/grok routines, not just claude/codex/cursor. Source:apps/cli/src/lib/hooks.ts,apps/cli/src/lib/perf/db.ts,apps/cli/src/commands/perf.ts,apps/cli/src/lib/routines.ts,apps/cli/src/lib/runner.ts.The Linear line on
agents projects statusis cached, and stops vanishing. The card paged every issue in a project on every invocation — up to 10 requests per project — against a 2500/hour request budget that an agent runningstatusin a loop exhausts. Answers are now cached on disk for 10 minutes (~/.agents/.cache/linear-projects/, one file per project written by atomic rename so concurrent agent sessions cannot clobber each other), so a repeatedstatusspends zero Linear requests. More importantly, a failed or rate-limited fetch now serves the last good answer marked stale instead of dropping the line: a populated Linear row silently disappearing on one 8s timeout was the observed defect, and it is the same rulemergeAuthHealthEntriesalready keeps for account health. A 429 records itsx-ratelimit-requests-resetso later runs don't spend a request to be told there are none left. Source:apps/cli/src/lib/linear-cache.ts.The compact
projects statuscard shows the milestone it callsnext. Milestones are listed in date order, and Linear can flag a later-dated one as next — so slicing the front of the list showed an earlier milestone while burying the actual next under+N more, which is the one thing that row exists to say. The next milestone now leads, and identity is matched on name plus target date rather than name alone (two milestones can share a name, which put thenextlabel on the wrong row). Source:apps/cli/src/commands/projects.ts.agents projectsstops reading the wrong GitHub repository. Factory derives a project'sowner/repofrom the checkout path's last two segments, so a repo cloned to~/src/github.com/<you>/agents-cliwhose origin isphnx-labs/agents-cliimported as<you>/agents-cli. Both are real repositories, so nothing errored — the card's merged-PR and release lines simply reported a stranger's repo (0 merges in 7 days instead of 100).import --from-factorynow reads the checkout's actualoriginand only falls back to the path guess when there is no remote to ask, andstatus/showprint a warning with the fix when a stored slug disagrees with the remote. Source:apps/cli/src/lib/project-doctor.ts.agents projects set <name>changes one field without destroying the rest. Previously the only ways to correct a field were$EDITORon raw YAML oradd --force, which rebuilds the definition from flags alone and silently dropslinear,contexts, anddescription.setloads, patches the named field, and writes back. Flags:--repo,--root,--path,--description. Source:apps/cli/src/commands/projects.ts.Merged-PR counts say when they are a lower bound. The
ghfetch caps at 100, and a busy repo where all 100 land inside the window has more — the count now renders100+rather than presenting the cap as a total, matching the existing Linear2500+contract. Source:apps/cli/src/lib/project-status.ts.agents projects view <name>replacesshow(kept as an alias) and now renders the project's full plan: every declared Linear milestone with its date and progress, issue counts, and a warning when no issues are assigned to any milestone — a milestone nothing is filed against cannot report progress, and a row of silent0%s hid that. Sixteen other command groups already useview <name>;projectswas the only one that did not. Source:apps/cli/src/commands/projects.ts.The status headline counts live agents, not corpses. It read
39 agentson a project where 19 had crashed. It now reads19 live, with a separatedeadrow breaking down what finished or was lost — 19 crashed sessions is a thing to go fix, not throughput.orphanedcounts as live:session/active.tsdefines it as "alive, but no client is attached", and the repo's own dead rule isclosed+crashedonly. Source:apps/cli/src/lib/project-status.ts.planPctis gone from the card and from--json. It summed each matched session's most recent checklist snapshot, so one agent opening a fresh 40-item plan rendered the whole project0% plan, and a project where nobody had written a checklist showed no figure at all. A cross-session sum of ad-hoc checklists does not measure project progress.liveanddeadcounts replace it in--json.The next milestone comes from Linear's own
status: "next"when Linear sets it, falling back to earliest-dated-unfinished only when nothing is flagged — Linear's answer is the one shown in its UI, ours is a guess.A regression guard for the distributed
--active --local/--hostsession-query paths, wired into CI (#1866). RUSH-2118 fixed a--localquery dialing remote-host teammates over real ssh, but nothing bench-guarded the fix's latency, and the--hostcross-fleet fan-out had no bench at all.bench/sessions-active-perf.tstimesAgentManager(..., localOnly=true).listAll()against N synthetic remote-host teammates (asserting zero ssh calls and sub-500ms latency, with a positive-control run proving the ssh-PATH shim actually intercepts) and thegatherActiveSessions({ hosts })fan-out against N synthetic peers (asserting it stays parallel, not sequential). Wired into.github/workflows/bench.ymlas the one gating step in that workflow — every other bench step stayscontinue-on-error. Documented with measured baselines inapps/cli/docs/05-sessions.md#benchmarks. Source:apps/cli/bench/sessions-active-perf.ts,.github/workflows/bench.yml.agents viewcolumns stay aligned across agents, and usage no longer piles up (view-ui-perf). The multi-agent overview padded every row to the widest usage string — an Antigravity account with four model quotas forced ~194-column lines that wrapped sorate-limitedand last-active drifted under the version column. Overview now caps compact meters to two windows (+Nfor the rest), always emits fixed account/usage/status/lastActive columns (empty cells space-padded), and measures padding withstringWidthso chalk + block bars don't skew gutters. Usage fetches go through one unified core: 5-minute fresh cache (was 2), concurrency-capped live reads (USAGE_FETCH_CONCURRENCY=3), single-flight per identity, and a background SWR queue capped at 2 so delayed HTTP responses cannot stack. Spinner stays up through account+usage load. Source:apps/cli/src/commands/view.ts,apps/cli/src/lib/usage.ts,apps/cli/src/lib/agents.ts.
1.21.1
Feed posts require a title + body; phone
{message}ends with a Sent-from footer.agents feed post --title "Short subject" "body text"— title is the phone first line (~4–5 words), body follows after a blank line, thenSent from <agent>/<session-chunk> on <host>(like "Sent from my iPhone"). Em/en dashes in title/body are scrubbed to ASCII-. Source:apps/cli/src/lib/feed-broadcast.ts,feed-post.ts,commands/feed.ts.Hook
timeoutin agents.yaml now accepts duration strings, not just bare seconds (#1555). A hook can be writtentimeout: 5s/timeout: 2m/timeout: 1h30minstead of onlytimeout: 30— self-documenting at the call site. A bare number still means seconds, so every existing manifest keeps working.parseHookManifestnormalizes the value to a seconds number once, so all harness serializers keep consuming a number; an unparseable timeout is dropped with a warning rather than silently coerced. Source:apps/cli/src/lib/hooks.ts(normalizeHookTimeoutSeconds,parseHookManifest),apps/cli/docs/hooks.md.Owner notifications route through the one channel seam. The feed urgent-block dispatch and the monitor
notifyaction now send through the registered channel provider (lookupTransport→ChannelProvider.send) instead of shelling out toopenclawdirectly. The recipient comes fromnotify.ownerin agents.yaml — the hardcoded owner chat id is gone, so changingnotify.owneris honoured by every path. A bare--notifyon a monitor now targetsnotify.owner;--notify <channel>overrides the owner channel. The monitor path also gains the provider's missing-binary guard (a clean error instead of a raw ENOENT). A channel name that resolves to no registered provider (a typo innotify.owner.channel, or--notify <channel>) fails that one send with a clean error — it does not exit the monitor daemon or abort theagents feed --dispatchloop. Source:apps/cli/src/lib/notify.ts,apps/cli/src/lib/monitors/dispatch.ts,apps/cli/src/lib/channels/resolve.ts.
1.21.0
A clone of your own DotAgents repo no longer hijacks project-layer rule resolution (RUSH-2037). Cloning
~/.agentsto the canonical~/src/github.com/<you>/.agentspath (to edit rules in an editor) made that checkout eligible as a project layer whenever you worked from its parent directory. Because project outranks user, a stale clone'srules/subrules/*then silently shadowed the live user rules by filename, and the compile planted an out-of-dateAGENTS.mdin an ancestor dir that every session beneath it ingested. Project-layer discovery now identifies a DotAgents repo by repo identity (git origin), not path: a.agents/that is itself a git checkout whose origin matches the user's or system's DotAgents repo is skipped, so the live user layer wins. Legitimate project.agents/layers (a plain subdirectory of a project, or a git repo with an unrelated origin) are unaffected. Source:apps/cli/src/lib/state.ts.agents sessions --localno longer dials remote-host teammates over ssh, in the default listing or--active(RUSH-2118).--localis supposed to mean this-machine-only, but the underlyingAgentManagerpoll still fired a real ssh round-trip for every teammate dispatched viaagents teams add --device— even a teammate that had already finished. On a box with 30 completed remote-host teammates that measured out to 180 realsshexecve calls (6 per teammate: two ssh calls insyncRemoteMirror, run three times per poll) and a ~4.3s--active --localcall. A--localquery now reads a remote-host teammate's last-persistedmeta.jsonstate instead of dialing it, and a teammate that has already reached a terminal status (completed/failed/ stopped) is never re-dialed by ANY--activequery, local or not — its final log bytes and exit code were already captured on the poll that resolved it. The same gate now covers every--localsurface: the bare default listing's live-glyph enrichment (maybeLiveIndex) and--preview(renderSessionPreview, freely combinable with--local) both called the local-onlygetActiveSessions()with nolocalOnlythreaded through, despite the--localhelp text already promising this-machine-only for all of them. Source:apps/cli/src/lib/teams/agents.ts(syncRemoteMirror,readNewEvents,updateStatusFromProcess,AgentManager),apps/cli/src/lib/session/active.ts(listTeamsActive,getActiveSessions),apps/cli/src/commands/sessions.ts(gatherActiveSessions,maybeLiveIndex,renderSessionPreview).A rules preset now applies at
agents runtime, not only afteragents rules switch(RUSH-2128).setActiveRulesPresetused to take effect only on the next explicitagents rules switch/agents add/agents use— a preset change made any other way left the harness launching against a stale rules file until someone remembered to re-sync.agents runnow re-applies the active preset for the resolved agent+version immediately before dispatch, every time, with a skip-fast sentinel so an unchanged preset costs no recompose or rewrite. Version-scoped only; per-model preset scoping is a follow-up. Source:apps/cli/src/lib/rules/run-sync.ts,apps/cli/src/commands/exec.ts.Activity events now carry the same actor and session lineage as operational events. The TypeScript activity writer and the embedded PostToolUse hook stamp actor kind, launch id, and parent session id from the shared execution provenance floor, so
agents eventsno longer invents an agent name as the activity record's OS user. Source:apps/cli/src/lib/event-provenance.ts,apps/cli/src/lib/activity.ts.agents viewno longer re-scans every installed Claude binary on each run. When a Claude model extractor produced zero models (a broken regex, or a mid-install CLI), the result was never cached — sogetModelCatalogre-ran a fullreadFileSyncscan of the 230-270MB Claude binary for every affected installed version, on every invocation (~1.85s each). With 4 affected versions installed, that was ~7.5s added to everyagents view. A 0-model extraction is now cached too, stamped with when it was attempted, and served for 24 hours before self-healing by retrying; an upgrade/reinstall (a new source mtime) still re-extracts immediately, as before. Measured on a real install with 7 Claude versions (4 of them hitting the broken extractor): the cold first-call cost (~12.5s, unavoidable) drops to ~1-2ms on every subsequent call. Source:apps/cli/src/lib/models.ts.Per-device and fleet-wide config keys now have a home: the
config:block in the two-tier agents.yaml store. Three new subcommands underagents devices(no new top-level noun):agents devices set-interactive <name>records the one device agents show YOU artifacts on (browser opens, dashboards) asconfig.interactiveHostin the central, synced agents.yaml — skills no longer guess "the online macOS box", and the host is marked★ interactiveinagents devices list.agents devices configure <name> --max-agents N --scheduler on|offandagents devices note <name> "…"(repeat to append,--clearto empty) write device-scope keys underconfig:in~/.agents/devices/<name>/agents.yaml— targetable for any device from any box (the devices/ tree syncs; each machine reads only its own). The default browser profile joins the same registry asbrowser.profile, routed to the existing device-localdefaultBrowserProfilefield (no duplicate key, resolution order unchanged). Unset keys always mean today's behavior; everything is scriptable with--json, anddevices list --jsonnow carries each row'sconfigand aninteractiveflag. agents.yaml files the CLI writes now carry ayaml-language-serverhint pointing at the newapps/cli/schema/agents-yaml.schema.json.The keys are live inputs, not just stored values.
--scheduler offstops the routines scheduler from starting on that device —routines addskips the auto-start with the reason, a manualroutines startrefuses, and the daemon re-evaluates the gate on every SIGHUP reload (boot it again withagents devices configure <host> --scheduler on+ any reload, no daemon restart).--max-agentsfeeds host ranking: Factory auto-launch excludes a device at its cap (counting device-wide running agents) and names the cap when a pool is exhausted; teams placement excludes it from the least-loaded auto-pick (counting the team's own roster, local teammates included) and an all-capped pool fails loud instead of over-filling a machine. Setup asks instead of guessing: bareagents setupends with a skippable preferences step (which machine you sit at → interactive host; which browser agents drive here → device default),agents setup fleetoffers the interactive host after a sync, and theagents setup browserpicker highlights the auto-detect winner. Source:apps/cli/src/lib/device-config.ts,apps/cli/src/lib/state.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/teams/scheduler.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/commands/setup-preferences.ts,apps/factory/src/core/launchHost.ts,apps/cli/schema/agents-yaml.schema.json.Mailbox messages now expire and dead boxes are reaped automatically. Messages enqueued without an explicit TTL used to sit in the spool forever, so pending mail would outlive the session that needed it. They now get a 24-hour default TTL (
AGENTS_MAILBOX_TTLoverrides the default;agents message … --ttl 2hsets it per message). When a message expires, a live-but-idle box archives it with adropped: expiredreceipt. The watchdog tick also runs a liveness sweep using the same live-session set asagents sessions --active, archiving pending mail in dead boxes asdropped: deadand pruning stale consumed entries. Dropped messages tied to a feed block surface a failure receipt (status: dropped/expired) so the sender sees the bounce instead of silence. Run the sweep manually withagents mailboxes gc(--jsonsupported). Source:apps/cli/src/lib/mailbox.ts,apps/cli/src/lib/mailbox-gc.ts,apps/cli/src/commands/message.ts,apps/cli/src/commands/mailboxes.ts,apps/cli/src/commands/watchdog.ts,apps/cli/src/lib/feed.ts.The menu-bar helper can no longer leak CLI processes until the machine is unusable. Its poll shelled
agents doctor --jsonthrough an unboundedProcess+readDataToEndOfFile(). Two properties composed badly: the call had no deadline (doctor --jsonmeasures 136s on an idle box, against a 60s poll interval), and a helper that died mid-call left the child reparented to launchd (PPID 1) with nothing to reap it — along with thenode -eversion probes that child had forked. Both fire together, because the helper crashes under exactly the conditions that make the CLI slow:NSApplication.sharedsegfaults insideSLSNewConnectionwhen WindowServer is too starved to hand out a connection, launchd'sKeepAliverestarts it, and the restart spawns a new doctor while the old one keeps burning a core. Observed on a real machine: 38 orphaned doctors + 92 orphaned probes, ~13 of 18 cores consumed, load average 490, keystrokes visibly lagging. The crash itself cannot be prevented from inside the app — it is AppKit dereferencing a null connection before any of our code runs — so a crash no longer costs anything permanent: every child carries a deadline (30s; 180s fordoctor --json, above its real measured cost); it is spawned as its own process-group leader so a timeout kills the whole subtree rather than just the CLI; and each live child is recorded on disk so the next launch reaps whatever a crash abandoned (no exit handler runs on SIGSEGV). The doctor refresh also drops from every 60s to every 15 minutes, and the launchd job gainsThrottleInterval30 so a startup crash-loop cannot respawn every 10s. A poll that blows its deadline now shows a stale menu instead of taking the machine down with it. Source:apps/cli/menubar/Sources/MenubarHelper/ChildProcess.swift,AgentsCLI.swift,StatusItemController.swift,main.swift,apps/cli/src/lib/menubar/install-menubar.ts.The macOS menu-bar helper is now notarized, ending the "app is damaged" dialog and the per-run
no valid code signature; skipping launchspam (RUSH-2134). The helper shipped Developer-ID signed but not notarized, so Gatekeeper on macOS 26+ rejected it as damaged and the install path tried to heal it by re-signing ad-hoc on everyagentsinvocation — which can never satisfy Gatekeeper, so the dialog and the noise persisted. The release now notarizes + staples the helper (menubar/scripts/build.sh, mandatory for any Developer-ID build, run under the release'sagents secrets exec apple.comcontext), theprepackgate refuses to pack an un-notarized bundle (scripts/verify-menubar-helper.shnow requires a stapled ticket), and the runtime ad-hoc re-sign band-aid is deleted — a notarized + stapled bundle survives npm's tarball round-trip untouched, so the helper launches with no per-machine healing. The launch guards now verify Gatekeeper acceptance (not justcodesign --verify) and fail loud pointing at an upgrade rather than re-signing over it. Source:apps/cli/menubar/scripts/build.sh,apps/cli/scripts/verify-menubar-helper.sh,apps/cli/scripts/release.sh,apps/cli/src/lib/menubar/install-menubar.ts.agents projects importgains Linear as a source, and gates the Factory guess.import --from-linearturns the workspace's Linear projects into definitions via thelinearCLI, binding a local checkout only on an exact name match so it never silently points a project at the wrong repo.--from-factorynow imports onlyhigh-confidence rows by default (--min-confidence low|medium|high,--allto take everything), and prints why each row was skipped — the auto-detected registry used to absorb every stale clone it found. Source:apps/cli/src/lib/project-import.ts.agents projects listcolumns line up again. Widths are computed from the rows being printed instead of a fixed 32-character path pad that every home-relative root ran straight through. Source:apps/cli/src/commands/projects.ts.agents projects statusshows the next Linear milestone. A newnextline names the project's earliest unfinished milestone with its progress and a human due date (Beta cut · 3/8 · due in 6 days,overdue by 3 days,due Aug 21) — a percentage says how far along a project is, the milestone says what it is due to hit next. The milestone list comes from the project rather than from issue assignments, so a milestone with nothing filed under it yet still shows; it rides along on the first page of the existing issue fetch, costing no extra request. Source:apps/cli/src/lib/linear-project-counts.ts.agents publish --branch <b>now pushes the index to<b>, not just the printed URL (#1061). The flag rewrote the printedraw.githubusercontent.com/.../<b>/skills-index.jsonURL, but the commit still landed on the checked-out branch — so--branch devfrom amaincheckout published the index tomainwhile advertising adevURL that didn't resolve.commitAndPushnow takes an optional target branch and pushes<current>:<target>, reporting back the branch the index actually landed on so the URL references it. Omitting--branchstill publishes to the repo's current branch. Source:apps/cli/src/lib/git.ts(commitAndPush,pushOrigin,getCurrentBranch),apps/cli/src/commands/packages.ts.Remove the unused
agents hqcommand.agents hq floor --jsonwas a machine-readable bridge for an interactive Agents HQ floor UI that was never built —apps/factoryhas zero references to it and it had no other consumer. Typingagents hqnow prints a clear removal notice and exits non-zero instead of silently disappearing. Source:apps/cli/src/index.ts,apps/cli/src/lib/startup/command-registry.ts(removedapps/cli/src/commands/hq.ts,apps/cli/src/lib/hq/).Removed
agents driveand the R2/CRDT background session-sync beta. Both predateagents sessions export/import, which now cover the same ground without a daemon:agents drive(rsync-based session/config mirroring) and the opt-insession-syncbeta (agents sessions sync, the daemon's ~90s R2 push/pull loop,agents sync --sessions) are gone. If you hadsession-syncordriveenabled, re-enable is no longer possible — use--hostfor live cross-machine reads oragents sessions export --encrypt/agents sessions importfor portable, encrypted transcript bundles instead. The R2 network client and CRDT merge machinery are removed entirely with the rest of the background sync. Export/import's own encrypted-bundle path survives unchanged: it never talked to R2 over the network — it only reuses ther2.backupsbundle's sharedR2_SYNC_ENC_KEYfor local AES-256-GCM encryption, falling back to a printed ephemeral key when that bundle isn't configured. Source:apps/cli/src/commands/drive.ts,apps/cli/src/commands/sessions-sync.ts,apps/cli/src/lib/session/sync/crdt.ts,apps/cli/src/lib/session/sync/sync.ts,apps/cli/src/lib/session/sync/r2.ts,apps/cli/src/lib/daemon.ts.A routine that fires less often than weekly can now be caught up at all. Overdue detection walked a fixed one-week window for the most recent expected fire, so any cron whose gap exceeds that returned nothing and the routine was skipped entirely — never flagged overdue on any device, never caught up, no
missedrecord, silently. Monthly, semi-monthly, quarterly and annual routines were all in that class. Measured on a real schedule (0 9 1,13,25 * *, 12-day gaps): on 10 of every 28 days the routine could not be evaluated. The lookback now widens (week → month → quarter → year) only when the narrower window finds nothing, so a dense schedule never walks more than a week of occurrences.Catch-up no longer resurrects a retired routine.
detectOverdueJobsnever checkedendAt, and the scheduler only auto-disables lazily inside a live cron tick — so a routine whoseendAtelapsed while the daemon was down was still enabled on disk, rescheduled on restart, and executed by the catch-up pass.One-shot detection matches the scheduler's. Overdue used the raw
runOnceflag while the scheduler usesisOneShotRoutine, so a one-shot-like schedule (a fixed minute/hour/day/ month) that never carried the flag could be replayed by catch-up.The creation floor now covers built-in routines.
routineEffectiveStartresolved a routine's file through a user-layer-only lookup, butlistJobsreads the system layer too — so a built-in shipped in the system repo had neither acreatedAtstamp nor a resolvable path, the floor was skipped, and it read as instantly overdue on first daemon start. AddedresolveJobFilePath, which resolves across every layer the loader reads.A
createdAtin the future is clamped to now. Left unclamped (clock skew, a hand-edited year) it sits after every possible expected fire, so the routine could never be flagged overdue until wall-clock time caught up.A routine now runs on exactly one device, instead of once per device listed.
devices:was an allowlist where every listed device fired independently, so a routine pinned to two boxes ran twice on every schedule — two full agent sessions doing identical work and burning double the agent quota. On one live fleet seven routines were in that state:security-sweepran at 15:30:02 on one box and 15:30:03 on the other, both completing. Ownership is now a pure function of the config (the first device in normalized sort order), so every daemon reaches the same answer with no lease, no cross-device coordination, and no split brain when the fleet partitions. Omittingdevicesstill means fleet-wide, which is whatwatchdogandcheck-updateswant.agents routines add --devices a,banddevices --set a,bare now rejected. A routine belongs to one machine; the error names the fix. Routines already on disk with a multi-device pin keep running — on their owner only — rather than being dropped.agents doctorlists any routine still carrying a multi-device pin, with the devices it names, the one that now fires, and the command to make it explicit. Also indoctor --jsonasambiguousDevicePins. The remediation deliberately offers the candidates rather than prescribing the owner: the lowest-sorted name can be a registry alias that matches no live machine, and cementing that would keep the routine dead.
1.20.93
agents sendis a real delivery envelope;notifyis just--to owner(RUSH-2123). Flag-first form:--to,--text,--channel,--attach,--url.--to ownerexpands fromnotify.ownerin agents.yaml. Positional text still works. Help names the three planes (deliver / record / control) so send is not confused withfeed post,activity, ormessage/sessions inject. Source:apps/cli/src/commands/send.ts,apps/cli/src/lib/channels/send.ts.agents events emit— record events produced outside the CLI. In-process code callsemit()directly, but the producers that most need to record events are not agents-cli processes: the Factory VS Code extension host, shell guards, external tools. They now pipe JSONL on stdin —… | agents events emit --source factory.--sourceis stamped asmodule, soagents events --module factoryfilters to one producer. Routing is forced by the stores rather than chosen: a milestone kind requires asessionIdand lands in that session's activity log, everything else lands in the operational log. A milestone with nosessionIdis rejected, not quietly written elsewhere. Rejection is per line, so one bad line never discards a batch, and the exit code is 1 if any line was rejected.--dry-runvalidates without writing. Source:apps/cli/src/lib/events-ingest.ts,apps/cli/src/commands/events.ts,apps/cli/docs/06-observability.md.Four
factory.*event kinds.factory.command,factory.action,factory.uriandfactory.launchdescribe what a user did in the Factory VS Code extension.factory.launchis a milestone — it carries thesessionIdandterminalIdthat later events join through — andfactory.uriis audit-level, since an external process driving the user's editor is a "who reached in from outside" fact. Source:apps/cli/src/lib/events.ts,apps/cli/src/lib/activity.ts.emit()accepts a caller-supplied timestamp. A batched producer records when each event happened and flushes later; without this, every event in a flush was stamped at flush time, collapsing their order and corrupting--sinceboundaries.tsstays reserved against payload injection — only the explicit override can set it. Source:apps/cli/src/lib/events.ts.Fixed:
agents _internal frictionrecorded its own invocation. The command exists precisely because shell guards run before anyagentsprocess exists and so cannot emit in-process, but it still fired thecommand.start/command.endaudit hooks, writing two records on top of every friction record. Recorder commands are now exempt. Source:apps/cli/src/index.ts.Desktop notifications now show the agent on the right, not a second copy of the app icon. macOS draws two images on a banner: the sending app's icon on the left and
contentImageon the right (a YouTube notification uses the slots for "YouTube" plus the channel avatar). agents-cli was putting its own lime mark in the right slot, so both slots said the same thing. The right slot now carries the harness the notification is about — a brand-colored tile with a two-letter mark (CLclaude,CXcodex,GKgrok, …), two letters because four harnesses start withcand two withg.agents run --notifyand agent/workflow routines pass their harness through; a daemon heal, an overdue sweep, a command routine, or a fan-out across several agents has no single agent and leaves the right slot empty. Source:apps/cli/menubar/Sources/MenubarHelper/AgentAvatar.swift,apps/cli/src/lib/menubar/notify-desktop.ts,apps/cli/src/lib/run-notify.ts,apps/cli/src/lib/routine-notify.ts,apps/cli/docs/menubar.md.MenubarHelper --notifygains--agent <id>. The one-shot notifier accepts the harness id that drives the right-hand avatar; omitting it is how a caller says "no single agent owns this event".agents projects status --fleet— per-device workspace drift (beta). Projects are natively multi-device;--fleetadds afleetline to the status card showing, for each project, whether its workspace repos are present on every fleet device, on which branch, ahead/behind their upstream (↑/↓), and how many uncommitted changes they carry — plus a hiddenagents projects probe --json <path...>subcommand that is the peer half of the fan-out. One parallel SSH call per device (12s timeout), nogit fetch— drift is measured against each peer's last-fetched upstream, and a repo with no upstream reports no drift rather than zero. Peers that are unreachable or run an older CLI are named once in a trailing note;probeitself is not beta-gated so peers answer whenever their binary carries it. The schema gainsrepos[].path(home-relative local checkout) to opt additional repos into probing beyond the primaryroot, and--jsongains per-projectworkspaces[]with the host-tagged probe rows. The card'sliveline also counts agents on every box under--fleetvia the existing sessions fan-out. Source:apps/cli/src/lib/project-probe.ts,apps/cli/src/commands/projects.ts,apps/cli/src/lib/projects.ts.agents projectsoutcomes on the card — agent×project members, releases, Linear counts, andprojects link --linear(beta). The status card gains anagentsline underlivenaming WHICH harness is on each project (claude · running · RUSH-2107 @zion, sorted running-first, capped at 6 with a+N moretail; under--fleetremote agents carry their peer's hostname), a latest-release tag on theshipsline (primary repo only, best-effortgh release list), and alinearline counting the bound Linear project's issues by state type (12/30 done · 5 in progress) — best-effort with an 8s budget, skipped by--no-remote, and omitted when the def has nolinear.projectId. The newagents projects link <name> --linear [query]writes that binding: no query auto-suggests from the def name + repo slug via the normalized-key matcher (ported from Factory'slinearProjects.ts), ambiguous/none prints the candidate list and exits 1.--jsongainsmembers[],latestRelease, andlinear. Source:apps/cli/src/lib/project-status.ts,apps/cli/src/lib/linear-projects.ts,apps/cli/src/lib/linear-project-counts.ts,apps/cli/src/commands/projects.ts.Project sync no longer spams a warning per file it left alone. Syncing a project whose
.claude/commands/you wrote yourself printed one wrappedSkipping project resource target …: already exists and is user-ownedline per file — six hand-authored commands meant twelve lines of terminal noise in the middle ofagents view claude. Those files are the normal steady state, not a warning, so the sync now reports them once, grouped, in plain words:Kept 6 of your own files in .claude/commands: debug.md, doc-gaps.md, image-nbp.md, +3 more. The list also rides out onSyncResult.projectSkippedfor callers that want it. Source:apps/cli/src/lib/project-resources.ts.agents secretsnow tracks per-bundle usage and surfaces it. Every secret lifecycle/access event — create, import, export, view, access (a value read for injection), unlock — funnels through the oneemitSecretAuditchokepoint, which writes to BOTH the append-only~/.agents/events.jsonlaudit log AND a derived, value-free read-model at~/.agents/secrets/secrets.db(never a secret value — bundle name, event kind, key count, resolving agent/host, status only).agents secrets view <bundle>now shows whether the bundle is currently unlocked (held by the secrets-agent, so reads are prompt-free), a usage summary ("accessed 42× (last 2h ago) · exported 3× (last 1d ago)"), and per-agent attribution, and nudges when a bundle has no description (also atcreatetime).agents secrets listgains--sort uses(most frequently accessed) alongside the existing--sort used, and the--jsonpayloads carryuses,usage, andheldExpiresAt. A newagents secrets activity [bundle]prints the recent value-free event timeline (bounded to 90 days). Naming guidance is taught in the help and skill: name a website bundle after its domain (stripe.com,openai.ai), a desktop-app bundle after its binary suffix (slack.app,photoshop.exe). Recording is best-effort andAGENTS_NO_USAGE_TRACK=1disables it. Source:apps/cli/src/lib/secrets/usage-db.ts,apps/cli/src/lib/secrets/audit.ts,apps/cli/src/commands/secrets.ts,apps/cli/src/lib/secrets/list-filter.ts.
1.20.92
agents sessions render <id...>produces shareable, redacted Markdown instead of raw harness JSONL. Claude, Codex, Kimi, Grok, Cursor, and Droid transcripts flow through the existing normalizedSessionEvent[]parsers, then render with the same session-browser preview at the top, ordered user/assistant turns, fenced shell commands, JSON tool arguments, and explicitly truncated tool results. Redaction remains on by default through the canonical redactor, now also masking Unix/macOS/Windows home-directory identities and live secret values;--no-redactis local-only. Reasoning defaults to omitted and can be folded or included explicitly. One or several selected sessions can be written to a mode-0600Markdown file, and--jsonexposes the rendered documents for machine callers. The repo session skill and transcript-sharing guidance now require rendering.mdbefore creating a confidential gist. Source:apps/cli/src/commands/sessions-render.ts,apps/cli/src/lib/session/render.ts,apps/cli/src/lib/redact.ts,.agents/skills/sessions/SKILL.md.agents sessionsrows show creation time as well as last activity (RUSH-2107). The trailing time cell used to carry one unlabeled "X ago" — last activity — so a row could not say when the session began or how long it had been alive. It now reads3d → 1 hour ago: the compact creation age, then the last-activity label the listing sorts by. Both the interactive picker and the flat/tree listings render it. A session that ran for under a minute keeps a single field (the two halves would name the same moment), and a terminal too narrow for both drops the creation age rather than squeezing the topic below its floor, so rows never wrap. The picker's detail pane spells the same facts out ascreated X ago · last active Y ago · lasted Z, and now derives them from the indexed session metadata when no local transcript exists — so remote and not-yet-indexed sessions report their timing instead of showing none. Source:apps/cli/src/lib/session/relative-time.ts,apps/cli/src/commands/sessions.ts,apps/cli/src/commands/sessions-picker.ts.Stop fleet health probes from orphaning remote processes on timeout (RUSH-2114).
sshExecAsyncnow uses a direct ssh connection whenever atimeoutMsis set, because a control-master outlives the local client and keeps the remote command running after we kill it.agents doctoralso normalizes host names before excluding the local machine, sozion.localcan no longer be self-SSH'd. Source:apps/cli/src/lib/ssh-exec.ts,apps/cli/src/commands/doctor.ts.Harden menubar install against Gatekeeper rejection.
ensureValidSignaturenow checksspctl --assess; a Developer-ID-signed but un-notarized bundle is stripped of quarantine and re-signed ad-hoc so the launchd service does not crash-loop with "app is damaged". The release build script gained optional notarization viaMENUBAR_HELPER_NOTARIZEandMENUBAR_HELPER_NOTARIZE_KEYCHAIN_PROFILE. Source:apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/menubar/scripts/build.sh.agents activitynow shows the whole fleet, grouped by project. The question the command answers is "what are my agents doing", and agents run on every box — but it read only the local logs unless you remembered--devices-all, and printed one flat newest-first stream. Both defaults are inverted: every run fansactivity --jsonout to each reachable device and merges the peers' streams host-tagged, then buckets them by project, one level, no sub-grouping.--localscopes back to this machine,-H/--hostto named boxes, and--flat(or--group-by none) restores the single stream;--devices-all/--hosts-allremain accepted so existing scripts keep working. A peer answering the fan-out still carries the recursion guard, so it never re-fans the fleet.Each project header names the machines its work ran on. A bucket reads
▸ agents-cli 12 events · 4 milestones · zion, yosemite-s0— up to three machines by name plus a+Ntail, so a project touched by a dozen boxes stays one scannable line; individual rows keep their own[host]tag. Peers that never answered are reported once at the end (· 2 devices unreachable: …) rather than a line each above the timeline, so a missing machine is visible but not noisy.A project is now the repository, not whatever directory the agent sat in. A cwd resolves to the git repository containing it, so
<repo>/apps/clifiles under<repo>instead ofcli, and a worktree under<repo>/.agents/worktrees/<slug>folds back into the repo it branched from. A directory in no repo groups as itself, and a dotfiles repo at$HOMEis not treated as a project. Theagents sessionsoverview andagents feed postnow share this one resolver (lib/project-key.ts), so a project reads identically everywhere instead of each view folding cwds its own way.--limitis spent on milestones, not on collapsed churn. The default view rolls routinefile.editedwork up to a count, so a plain slice let one busy machine's 40 file edits hide every other device's PRs behind a singlefile edited ×40line. The cap now bounds the milestones shown, with the routine events inside that window riding along for the counts.--allstill shows routine work inline and caps every event.The activity header no longer carries other subsystems' hook warnings. Registering the activity-log hooks surfaced every unresolved entry in the hook manifest — a missing
inject-session-idscript, someone else's half-installed plugin — printing five wrapped yellow lines above the timeline on every run. Those areagents doctor's job; only a failure that would leave the activity log unwritten is reported here.The menu bar's New Session opens in the terminal you actually work in. It hardcoded AppleScript at Terminal.app, so a Ghostty or iTerm user got a Terminal.app window every time. It now shells
agents run <agent> --terminal, and the CLI resolves the terminal from the user's own live sessions — the host appagents sessions --activealready attributes every session to (ActiveSession.host). Order: the terminal the caller is in, then the host of the most recent live session, then the first available backend. Hosts map to backends only where the engine can really drive them, so an undrivable host (Warp, kitty, Cursor) falls through instead of opening the wrong app. A tmux-hosted session (every interactiveagents run) resolves to the app its attached tmux client is in, via the same resolver behindagents sessions' "viewing in Ghostty tab 2" — without that it would name the multiplexer and no terminal at all. Source:apps/cli/src/lib/terminal/preferred.ts,apps/cli/src/lib/terminal/backends/terminal-app.ts,apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift.agents run <agent> --terminalopens a run in a real terminal tab. For a caller that cannot host a TUI (the menu bar, a script). Without a value the terminal is detected as above;--terminal <backend>forces one (iterm | ghostty | terminal | tmux | vscodium-agent) and errors on an unknown id rather than silently auto-detecting. The tab re-invokes the same argv with the flag stripped, so--mode,--cwd, and a--passthrough ride along. Cannot combine with--host. Source:apps/cli/src/lib/terminal/run-surface.ts,apps/cli/src/commands/exec.ts.Terminal.app is a real launch backend now (
terminal). Registered last, so it is the every-Mac floor without outranking a terminal the user chose to install, and reported unavailable over SSH whereosascriptcannot reach the GUI login. It has no scriptable split, so a split request opens a tab, andagents sessions resume --splitsnow says so instead of quietly producing tabs.detectCurrentBackendalso recognizesTERM_PROGRAM=Apple_Terminal. Source:apps/cli/src/lib/terminal/backends/terminal-app.ts.agents sessions resume/sessions focusreach Terminal.app too. Adding it to the backend registry changes both: on a Mac with neither iTerm, Ghostty, nor VSCodium installed they used to fall back to resuming in the current process, and now open a Terminal.app tab;resume's interactive picker gains a Terminal row, and--terminal-appforces it (named apart fromrun --terminal, which means something different). Source:apps/cli/src/commands/sessions-resume.ts,apps/cli/src/commands/focus.ts.New Task… in the menu bar. A row above New Session that opens the quick-dispatch bar — the same panel as
Cmd-Shift-O, now reachable without the chord (and without the Accessibility grant the chord needs). The status item owns the one panel instance, so an interrupted capture is restored whichever entry point you return through. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.Activity, feed posts, and the sessions overview now speak defined project names. One resolver (
resolveProjectNameForCwd,lib/projects.ts) backs all three: a cwd inside a defined project (~/.agents/projects/<name>.yaml) reads as the project's name — a multi-repo project is a single bucket inagents activity, not one per repo — and anything else falls back to the repository-level key, so nothing changes without definitions. Each peer resolves its own cwds against its synced definitions before events cross the wire. Source:apps/cli/src/lib/projects.ts,apps/cli/src/commands/activity.ts.agents activity --project <name>narrows the fleet stream to one project, exact-matched on the resolved label — one project's PRs, plans, and worktrees across every box without the rest of the fleet's noise. Source:apps/cli/src/lib/activity.ts(filterActivityByProject).agents projects— named multi-repo projects with a project progress rollup (beta). Define a project once in~/.agents/projects/<name>.yaml(name, home-relative root/defaultPath, multiple repos with monorepo subpaths, describedcontexts[]starting points, externalintegrations[], Linear link) andagents run --project <name>resolves the definition before the old<root>/<slug>convention — undefined slugs behave exactly as before. The headline isagents projects status: instead of a per-agent activity line, it renders one card per project — live agents by state, plan completion, open and recently-merged PRs, tickets in flight, and the artifacts agents produced — by rolling up signals already on disk (live agents matched to a project by this machine's session cwd; the merged-PR count is repo-global viagh).--window <days>and--no-remotetune the PR/artifact lookup. Alsolist/add(infers root + origin slug) /show/edit/import --from-factory(absorbs the Factoryprojects.jsonregistry) /rm. Enable withagents beta enable projects. Source:apps/cli/src/lib/projects.ts,apps/cli/src/lib/project-status.ts,apps/cli/src/commands/projects.ts,apps/cli/src/lib/project-root.ts.The cross-fleet session sweep no longer hides sessions on manually-registered devices.
agents sessionsfan-out (and therefore--resolve, cross-machine resume, and--active) picked peers with a stricttailscale.online === truetest. A device registered withaddress.via: "manual"never gets a Tailscale peer entry at all, so itsonlinestayedundefinedand the sweep skipped it permanently — every session on that box was invisible and could not be resolved or resumed from any other machine. Peer selection is nowisDialableDevice, a union of both liveness signals: a device with no Tailscale block is unknown-not-offline (the rulessh.tsrenderDeviceTableand Factory'sisDeviceOnlinealready used, so the picker and the sweep finally agree on who exists), and a positive live SSH probe (DeviceProfile.reachability, RUSH-1965) additionally rescues a device whose snapshot says offline. A failed probe deliberately does not remove a peer — the probe runs on a short SSH budget and returns false negatives on a congested tailnet (observed calling the local machine unreachable), and letting that shrink the sweep would hide sessions on healthy boxes. Applied to both sweeps that share this shape. Source:apps/cli/src/lib/devices/registry.ts(isDialableDevice),apps/cli/src/lib/session/remote-list.ts,apps/cli/src/lib/remote-agents-json.ts.
1.20.91
An agent can now say it is stuck:
agents feed post --blocked(RUSH-2110). The feed carried benign progress but had no way to signal "I cannot proceed", so agents hand-rolled it into the status text (NEEDS MUQSIT: …) and it reached nobody. A blocked post writesstatus.blockedto the shared activity stream and opens an answerable block in the ledger, so the ask stays open until someone resolves it instead of scrolling away. It is a flag on the existing verb, not a new command — one thing for an agent to learn, and one stream where most posts are benign and some need a human. Blocked is a state, not a volume: it always broadcasts atimportant, so passing--leveltoo is a usage error rather than a silent override. Pair it with--optionfor an answerable choice or--defaultfor a safe fallback policy may apply. Source:apps/cli/src/commands/feed.ts,apps/cli/src/lib/feed.ts.Feed blocks are actually delivered.
publishBlockwrote every "needs you" record to the ledger and stopped there —broadcastPostedEventran only forfeed post, so a block was durable and invisible at the same time. Blocks now reach the configuredfeed.broadcastsinks, carrying the ask and the literalagents focus <id>command that unblocks it, and a block that reaches nobody exits non-zero instead of looking like a success. Source:apps/cli/src/lib/feed-broadcast.ts.New
desktopchannel provider.agents send --channel desktop(andnotify.owner.channel: desktop) posts a native notification through the branded menu-bar helper. It is the only channel with no external dependency — no network, no login, no vendor CLI — so it still reaches you at your Mac when a messaging gateway is down. It reports real deliverability rather than always succeeding: on Linux it probes fornotify-sendinstead of trusting the platform name. Source:apps/cli/src/lib/channels/providers/desktop.ts.agents perf— disposable SQLite latency warehouse. Indexed p50/p99 rollups for hooks, CLI commands, andagent.runtimings without scanning the audit JSONL. Warehouse lives at~/.agents/.cache/perf/perf.db(safe to wipe); identity columns reuse sessions/events string shapes (session_id,agent,machine, …) for soft cross-reference — no foreign keys. Hook shims spool into the same DB;agents hooks profilereads it first. Source:apps/cli/src/lib/perf/db.ts,apps/cli/src/commands/perf.ts.A routine that misses its fire now runs late instead of being silently lost. Fires are in-process croner timers, and croner only ever schedules forward from "now" — so a daemon that was down, asleep, or wedged when a routine came due dropped that fire outright, and
loadAll()rebuilt every timer looking only at the future. Detection existed but ran once, at daemon startup, and only logged a warning plus a notification; catching up was a manualagents routines catchup. Observed cost: zion's daemon was down from 02:03Z to 08:23Z while the laptop slept,weekly-fleet-retrowas armed for exactly 04:00Z, never ran, and the restart logged2 routine(s) overdueand did nothing. The daemon now re-scans every 5 minutes as well as at startup and runs each missed routine via the same detached pathcatchupalready used. Source:apps/cli/src/lib/catchup.ts,apps/cli/src/lib/daemon.ts.New
catchup:routine field, andagents routines add --no-catchup. Defaults to true — a routine you scheduled is one you expect to have run. Setcatchup: falsefor a routine whose worth expires with its slot (a 9am brief is useless at 3pm); the miss is still recorded, it just is not re-run.agents routines list --jsonreports the effective value ascatchup.New
missedrun status. A missed fire previously left no trace anywhere — no run record, no log line in the routine's history — soagents routines listkept showing the previous run'scompletedas though it were current, sometimes for weeks. A miss is now written as a real run stamped at the moment the fire was due, soagents routines runs <name>shows the gap, and the listing renders it distinctly fromfailed(a miss is an infrastructure problem, not a task failure). That record is also what makes catch-up idempotent: it advances the overdue comparison, so the same missed fire is never reconsidered across ticks or a daemon restart storm, and its directory is created with a non-recursivemkdir— an atomic claim, so if the daemon's timer and a manualagents routines catchupoverlap, only one of them runs the routine. Source:apps/cli/src/lib/routines.ts(RunMeta),apps/cli/src/commands/routines.ts.A routine is never caught up for a fire that predates it.
detectOverdueJobswalks back a week for the most recent expected occurrence, and a routine with no runs is overdue by definition — so before this,agents routines addon any daily or weekly schedule whose slot had already passed made the routine instantly "overdue". That was cosmetic while catch-up was a manual command; with the daemon now catching up automatically it would have run every newly created routine once, within five minutes of creating it. Routines gain acreatedAtstamp (written once, likeactor), and overdue detection floors the expected fire at it — falling back to the routine file's mtime for routines written before the field existed. Observed on the live fleet:agents-cli-updates, created Aug 1 and never run, was flagged overdue for a Jul 27 fire. Source:apps/cli/src/lib/overdue.ts(routineEffectiveStart),apps/cli/src/lib/routines.ts(writeJob).agents secrets listcan be filtered. It had no filtering at all —--hostpicks a machine and--jsonpicks a format, but nothing selected over the bundles themselves, so "which of these read with no Touch ID?", "which still store a raw value inline?", "what have I not touched in three months?" meant piping the table throughgrepor went unanswered. There is now an axis per question: a[query]positional over name and description,--policy,--backend,--type,--kind,--held/--not-held,--expired,--expiring [days],--unused <duration>, plus--sortand-n/--limit. Every axis narrows independently, so they compose. Following theagents sessionshouse style, an unknown value is a loud error naming the valid set rather than an empty list, filters apply before--jsonso the payload is the exact twin of the table, and they are forwarded over--hostso a remote list narrows the same way.--held/--not-heldread live broker state and so refuse to run off macOS instead of reporting every bundle as unheld. An empty result names the filters that emptied it and the total it started from. Source:apps/cli/src/lib/secrets/list-filter.ts,apps/cli/src/commands/secrets.ts.The EXPIRING column no longer hides keys that have already expired.
countExpiringSooncounted only keys due in the next 30 days — the guard isd >= 0— so a bundle whose token died last month rendered-, identical to one with no expiry at all. The only places a lapsed key surfaced wereagents secrets viewand a hard abort at inject time, i.e. after it had already broken a run. The column now counts lapsed and upcoming together and turns red once anything has lapsed, andsecrets list --jsongains anexpiredcount alongside the existingexpiringSoon. Source:apps/cli/src/commands/secrets.ts.agents secrets listnow states the hold window instead of the bare wordhold. Theholdtier is a duration — prompt once, then stay silent forsecrets.agent.holdMs(7 days by default) — but the POLICY column printed only the tier name, so a reader could not tell it meant a window, let alone which one; finding out meant runningagents secrets status. The column now readshold 7d, andhold 7d · held 6dwhile the broker is actually caching the bundle. It follows the configured window, so a 24-hour hold readshold 1d.alwaysandneverare unchanged — neither has a window, and annotating one would repeat the mistake thedailyrename fixed. Two adjacent bugs go with it:agents secrets viewprinted "7d by default" as a string literal and so misstated the window for anyone who had configuredholdMs, and a stale broker entry past its expiry rendered ashold · held expiredbecause the column tested the entry for presence rather than liveness.secrets list --jsonandsecrets view --jsongain an additiveholdMsfield (null onalways/never) so a machine caller gets the window too. Source:apps/cli/src/commands/secrets.ts.Richer session previews: skills, hooks, links, artifacts, repos, todo status. The
agents sessionsquick preview and full summary now show the skills a session invoked (with counts), the hooks that fired (Claude transcripts, with repeat counts and failures), a clickable Links section (Linear/Jira/GitHub/GitLab URLs harvested from the conversation), the documents the session produced (.agents/artifacts|plans|reportsand other*.md/*.htmlcreations), the repos it worked in (via.gitwalk-up), and an error tally in the picker. The full summary's Plan section now marks checklist items[x]/[>]/[ ]and renders the checklist alongside the ExitPlanMode text instead of hiding it. Changes/Dirs lines collapse.agents/worktrees/<slug>prefixes to⧉ <slug>/…, are width-capped, and no longer list shell junk (2>&1,$VARpaths),node_modules, or agents-cli internal archives. Source:apps/cli/src/lib/session/highlights.ts,apps/cli/src/lib/session/parse.ts,apps/cli/src/lib/session/render.ts,apps/cli/src/commands/sessions-picker.ts.
1.20.90
Bash commands are now parsed and classified for richer activity summaries. The
11-activity-log.pyhook tokenizes every Bash tool call and emits a structuredbash.executedactivity record withcategory,bashTool, andbashAction. High-signal commands also raise milestones:video.rendered/video.convertedforffmpeg,image.upscaledforrealesrgan/waifu2x/swin2sr, andmetadata.editedforexiftool/id3v2/metaflac/vorbiscomment. The session renderer and digest use the sharedlib/session/bash-command.tsclassifier. Source:apps/cli/src/lib/session/bash-command.ts,apps/cli/src/lib/activity.ts,apps/cli/src/lib/session/digest.ts,apps/cli/src/lib/session/render.ts.agents sessions --activenow shows one row per agent, not one per directory. A live tmux agent pane whose durable identity records were missing (the common case once meta/pid-registry entries age out) was dropped, then re-surfaced by the ps-scan under the newest transcript in its cwd — so many distinct sessions collapsed onto one stranger's id with an inflated×Nbadge, andagents sessions focus <id>could not find them. The scanner now recovers the session id straight from theag-<agent>-<shortid>tmux pane name (resolved to the full UUID via the short-id index in one batched query), and refuses to borrow a co-located sibling's transcript when no id is known — so every live session surfaces as its own row and is focus-able again. Also adds arunTmuxtimeout so a wedged tmux server can't hang the scan. Source:apps/cli/src/lib/session/active.ts.agents viewnow shows live usage bars for Antigravity. Theagyaccount row renders one bar per model quota bucket (3.1P: ███░░ 42% (1d)style), sourced from the same Google Code Assist:retrieveUserQuotaendpointagyitself talks to. Auth reuses the storedagyOAuth credential (macOS Keychain itemgemini/antigravity, Linux Secret Service, or the~/.gemini/antigravity-cli/antigravity-oauth-tokenfile fallback), refreshing the access token in memory when expired — safe from a read path because Google's refresh tokens are non-rotating, and never written back to the keychain. Each per-model bucket also flows into the throttle badge, run rotation eligibility, andagents view --json(whose usage windows now carry alabelso same-keyed per-model bars are distinguishable). Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/agents.ts,apps/cli/src/commands/view.ts.A custom harness is now its own agent type in
agents view. A harness created withagents harness add(oragents profiles add) used to render as an indentedprofilerow under whichever host CLI executes it. It now gets its own block beside Claude and Codex — a bold name header, then one row carrying the pinned model, the account/auth state, andvia <host> <version>naming the native harness underneath. That matches how it is already launched:agents run <name>treats a custom harness exactly like a native agent id. A harness whose host CLI has no install is flagged(host <id> not installed)rather than listed as runnable, and the separate "Profile-only Agents" section is gone — those harnesses now render in the main list like every other one. Source:apps/cli/src/commands/view.ts.agents view <harness>describes a custom harness — host, model, provider, auth, fork lineage, YAML path — instead of failing with "unknown agent";agents view <harness> --jsonemits its summary. Source:apps/cli/src/commands/harness.ts(renderHarnessDetail).New
agents harness fork <source> <name>. One verb over both starting points: fork a native harness (agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter) or copy a custom one you already tuned and change only what you name (agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3). Forking a custom harness is a full copy — env, endpoint, auth binding,fallback_model, host version pin — so the two diverge and deleting the source never affects the fork; forking a native harness requires--modelbecause there is no model to inherit. Flags:--model,--base-url,--auth-provider,--version,--label,--description,--key-stdin,--force. Source:apps/cli/src/lib/profiles.ts(forkProfile).Profile YAML gains optional
label:andforkedFrom:.labelsets the nameagents viewprints for the harness (defaults to the file name);forkedFromrecords the parent as display-only lineage. Existing profiles keep working untouched. Source:apps/cli/src/lib/profiles.ts.Breaking (
--json): inagents view <agent> --json, the per-agentprofileskey is nowharnesses, and each entry carries newlabel,hostVersion,description, andforkedFromfields alongside the existing ones. Source:apps/cli/src/commands/view.ts(ViewJsonAgent).Menu bar ACTIVE: project accordion + session detail submenu. Projects are collapsed by default as a status strip (
▶ agents-cli ●8 ◐1 zion); click▶/▼to fold agents open under the project (idle-row caps removed — collapse is the wall protection). Focusing an agent opens a side submenu with linkable detail (work title URL, cwd, Linear ticket, GitHub PR, duration, copy session id) from the warmsessions --activecache. Accordion reopen rebuilds from cache only (no teams walk / no CLI schedule). Local/remote uses the same host normalize as CLImachineId()so local rows are not mislabeled remote. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift,LocalState.swift,Models.swift.An offloaded editor tab no longer displays another session's id. A Factory tab launched with
agents run --host <device>has no local agent process, but the extension still resolved its "live" session id by reading the SessionStart hook's~/.agents/.cache/state/sessions/<pid>.jsonfor the local pid tree — the pid of the ssh client. Those files are keyed by pid alone and are only pruned when the pid is dead, so once the OS recycled a pid the tab adopted whatever session had last held it: one remote tab showed the id and version of an unrelated synthetic run from 20 days earlier while/statusinside it reported the truth. An offloaded tab now takes its identity from the device instead of local disk, and a local tab rejects any state record whose SessionStart timestamp predates the tab itself.AGENT_TERMINAL_IDnow rides the SSH hop.agents run --hostforwarded actor provenance but not the launching tab's terminal id, so the remote pid registry recorded no terminal — leavingagents sessions --active --host <device>unable to answer "which session is this tab running?" once the agent moved on (a/clear, or an exit and rerun in the same tab).agents sessions --active --jsonnow carriesterminalId. The pid registry has always recorded it; the emitted row dropped it, so no consumer could join a live session back to the editor tab that launched it.Balanced routing no longer launches into an account it only thinks has headroom. Account usage is cached per machine under stale-while-revalidate: a snapshot up to 24h old was served instantly, and the background refresh that should have corrected it lands after the pick is already made. On a box whose refresh is failing that state is permanent — measured on
yosemite-s1, every Claude snapshot sat 26 hours to 2.7 days old, so balanced read[email protected]as 48% used and launched into it while the account was at its weekly cap; the session answered "You've hit your weekly limit" on its first turn. Routing now caps how stale a snapshot may be when it is about to decide (5 minutes), blocking on one bounded, parallel live read past that — and no read at all inside the existing 2-minute fresh window, which back-to-back launches hit. Display paths (agents view) keep the full 24h window and stay off the network.A pick made on unconfirmed data says so. When no account on the machine could be refreshed, routing still launches — a broken refresh must not make a box unusable — but the banner now reads
… (2 of 5 healthy, usage unverified — no account could be refreshed)instead of presenting a guess as a fact. An account with a verified snapshot always wins over one with a stale snapshot, even when the stale number looks emptier. This applies to--strategy availableas well asbalanced— both route on the same cache, andavailable's headroom sort was inverted by a stale number in exactly the same way. An explicit version preference is an instruction, not a ranking signal, so it still wins.The mid-run failover chain is unchanged. Declining to pick an account on unconfirmed data and declining to fail over to it after the primary already hit a 429 are different risks — by then the alternative is not launching at all. Every eligible account stays in the failover chain; only the initial pick prefers verified ones.
agents routines listno longer reports another device's routine as failed. Run records are written into the runs dir of whichever machine fired the routine and carry no device attribution, but the listing resolved Last Status from any local record and rendered it even on rows for routines pinned elsewhere. A routine re-pinned to another device therefore kept reporting the old machine's leftover records forever — on zion,security-sweep,review-open-prsandhetzner-lease-gcall readfailedfrom late July whileyosemite-s0/s1, the devices that actually fire them, had completed them that morning. The macOS menu bar reads this JSON, so it painted a column of redexit 1rows for routines that were green. Last Status is now scoped to the device that owns the run: a routine this device does not fire shows-, and--jsonreturnsnullforlastStatus,exitCode,failureReason,lastRunStartedAtandlastRunCompletedAt(runsHere: falsealready says why). A routine pinned to several devices renders one row per device but carries a status only on its This machine row. Read a peer's status withagents routines list --device <name>; the local history is untouched and still readable viaagents routines runs <name>. Source:apps/cli/src/commands/routines.ts(localLatestRun,groupRoutineJobsByDevice),apps/cli/docs/03-routines.md.agents watchdognow tracks per-session presence (RUSH-2007 Layer C). Each tick reconciles a per-session presence record —{location, device, transport, lastSeen, status}at~/.agents/.cache/state/watchdog/presence.json— from the tick's active scan, derivingconnected/disconnectedby diffing consecutive ticks. A session that was tracked but is now absent (its SSH link dropped or the peer went unreachable) flips todisconnected, and the flip is surfaced inagents watchdog --jsonunderpresence.transitions— an interactive drop as areconnect-nudgecandidate, a headless remote askeep-alive. Folded into the existing tick (no revived daemon, no extra SSH fan-out); additive and does not change the tick's nudge decisions. Source:apps/cli/src/lib/session/presence.ts,apps/cli/src/lib/watchdog/runner.ts.agents setup secrets --policy holdno longer fails, andagents secrets statusstops naming the retireddailypolicy. The 1.20.79daily→holdrename swept the help, docs, and thesecrets listPOLICY column, but two surfaces were never migrated. The worse one was functional: the onboarding wizard carried its own copy of the policy vocabulary, soagents setup secrets --policy hold— the canonical name every other secrets command prints — exited withInvalid --policy 'hold'. Use daily, always, or never., and its interactive prompt still offereddailyas the default choice. It now sharesparsePolicyOptwithagents secrets policy, so the two commands can't disagree about what a policy is called;daily/sessionstay accepted as aliases and the wizard's default is unchanged (the hold tier). The second was cosmetic:agents secrets statusprinted "a daily bundle prompts once…" and "the next read of each daily bundle…" — the one command a user runs to answer why did it prompt again, naming a policy its sibling commands no longer emit. Both lines now sayholdand are pure values pinned by tests, so the vocabulary can't drift again. Source:apps/cli/src/commands/setup-secrets.ts,apps/cli/src/commands/secrets.ts.Favorite sessions from the browser.
*stars the highlighted session inagents sessionsandffilters the list to the starred ones; outside a TTY,agents sessions favorite <id>(--remove/--list/--json) andagents sessions --favoritesdo the same. Stars live in~/.agents/.history/favorites.jsonkeyed by session id, so they survive a reindex of the session cache. They are per-machine — session sync carries transcripts, not this file. Source:apps/cli/src/lib/session/favorites.ts,apps/cli/src/commands/sessions-favorite.ts.Detect sessions that lost their host — two new statuses,
crashedandorphaned. A session whose editor window or connection went down hard used to just VANISH fromagents sessions --active(its dead-pid registry entry was filtered out), and one still running in tmux with nobody attached reported a plainidle. Both now say so:✗ crashedwhen the host window stopped republishing and the agent died with it,◍ orphanwhen the agent is alive with zero clients attached. Derived from tmux's#{session_attached}and the IDE window's registry heartbeat — never from a deliberateagents sessions detach, and never over a session that is still working. Source:apps/cli/src/lib/session/host-link.ts,apps/cli/src/lib/session/active.ts.agents sessions --active --favoritesnow actually filters. The flag was wired into the interactive browser only, so every path that skips it —--json,--waiting, a pipe, a multi-host scope, an SSH-fanout peer — silently returned the whole fleet. Source:apps/cli/src/commands/sessions.ts.agents sessions --active --waitingno longer counts a dead session.activityis not rewritten when a session dies, so one that crashed mid-question reported "needs your input" forever — what it needs is a relaunch. Source:apps/cli/src/commands/sessions.ts.Resolve historical sessions safely across the fleet (#1757).
agents sessions --resolve <full-id|prefix|keywords> --jsonuses a versioned safe peer protocol, returns only resolver metadata, reports every full-ID candidate on ambiguity, treats synced copies as one match, and exits 2 without deciding when a peer fails, returns malformed output, or runs an older CLI. Source:apps/cli/src/commands/sessions.ts.A rate-limited usage endpoint is now backed off instead of hammered. The daemon warms auth-health every 3 minutes and probes every installed version home in one parallel batch, so a machine with five Claude accounts sent five concurrent requests to
api.anthropic.com/api/oauth/usageevery three minutes — roughly 100/hour — before the usage refresh added its own. Nothing readRetry-After. Measured onyosemite-s1: the endpoint answered429 rate_limit_errorwithretry-after: 2678(about 45 minutes) for every account while the credentials themselves read healthy, and the next tick fired three minutes later, deep inside the penalty window, re-arming it. The box never recovered, every usage read failed, and its cache froze — the permanently-stale state balanced routing was already having to defend against.A 429 now records its deadline and every read honours it. Usage fetches and health probes for that provider short-circuit until the window passes — no request, no renewed penalty — and report
Claude rate-limited this machine — not retrying for 45 minutes.The state is on disk, because the callers are separate processes: the long-lived daemon and every one-shotagents view/agents run— one empty file per penalty under~/.agents/.cache/usage-backoff/, named<agent>.<deadline>, so two processes recording the same provider at once cannot displace each other and a read takes the furthest deadline. A server delay is capped at an hour, and a missing or unparseableRetry-Afterstill backs off.A usage read that fails now says so, instead of returning a silent null. Four branches in every networked usage fetch — Claude, Kimi, Droid and Cursor — returned
{ snapshot: null, error: null }: no readable credential, a locally-expired one, a rejected request, and a request that threw (timeout, DNS/TLS, an unparseable payload). The caller could not tell any of them apart from a healthy read, so it fell back to whatever the stale-while-revalidate cache held and drew those bars as fact. Measured onyosemite-s1: every Claude account's stored access token had expired (one of them eleven days earlier), so no read could succeed, andagents view claude --refreshprinted a full, healthy-looking table twice while writing nothing to the cache. A usage read never refreshes a token (RUSH-1822), so an expired credential does not heal on its own — the account stays unreadable until that agent actually runs. A rate-limited endpoint (429) now reads differently from a rejected credential (401), because re-authing fixes one and not the other.agents viewmarks bars the live read could not confirm. A row whose snapshot came from the cache after a failed live read renders the reading plusunverified, rather than looking identical to a confirmed one. The number still shows — it is the last thing we saw — but it no longer reads as current.agents view --refreshreports what it could not refresh. It now lists each account it failed to reach and why, instead of rendering a table that looks fully refreshed regardless.
1.20.89
Webhook handler layer for one-off agent/workflow/command/routine triggers. Routines still fire from signed webhooks, but a new
~/.agents/webhooks/*.ymllayer can also run one-off actions:run.agent,run.workflow,run.command, or delegate to an existingroutine. Handlers support the same source/event/ action/label/repo/branch filters as routine triggers, plus LinearstateTo/stateFromstate-change filters. Prompts and commands can use{{issue.identifier}},{{updatedFrom.state.name}}, etc. The receiver emitswebhook.received,webhook.authorized,webhook.rejected,webhook.matched,webhook.fired,webhook.handler.start, andwebhook.handler.endevents. Source:apps/cli/src/lib/triggers/handlers.ts,apps/cli/src/lib/triggers/webhook.ts,apps/cli/src/lib/routines.ts,apps/cli/src/commands/routines.ts,apps/cli/docs/03-routines.md.agents routines addgains--state-toand--state-fromfilters for Linear triggers. A Linear routine or handler can now fire only on a specific state transition (for example--state-to Plan), instead of on every issue update.Values substituted into
run.commandare shell-quoted. A webhook context is built from an external payload, and fields likeissue.titleor a GitHubpull_requesttitle are free text any outside contributor can set — pasted raw into a shell command they would be a command-injection sink. Substituted values are now single-quoted (POSIXsh), so a payload stays one inert argument while the operator's own template keeps its pipes, redirects, and&&. On Windows, whereexecruns throughcmd.exeand these quoting rules do not hold, arun.commandcontaining{{…}}is refused with a clear error rather than run.run.promptis unaffected — it never reaches a shell. Source:apps/cli/src/lib/routines.ts(substituteWebhookCommand,assertShellSubstitutionSupported),apps/cli/src/lib/triggers/handlers.ts.The
Cmd-Shift-Oquick-dispatch bar now lists the repo's open Linear tickets, and dispatches one on a click (RUSH-2098). The panel only captured NEW work; it now also shows what already exists. Switching the repo dropdown switches the Linear project (the repo name is matched againstlinear projectsreduced to lowercase alphanumerics, soagents-clifinds "Agents CLI" with nothing to configure; a worktree resolves to its parent repo, and a repo that matches no project says so and lets you pick one, remembered per repo). Rows are ranked urgent-first — Linear priority, then overdue, then in progress, then newest — and typing filters them, so an existing ticket surfaces before Return files a duplicate. Clicking a row (or⌘1–⌘5) dispatches that ticket to the selected agents in the picked repo: Run claims it and implements it, Plan posts a plan as a ticket comment.⌘-click opens it in Linear instead. The list renders from a 90-second warm cache so the panel still appears instantly. Source:apps/cli/menubar/Sources/MenubarHelper/LinearTickets.swift,apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift,apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift.Fixed: a menu-bar dispatch whose child printed more than ~64 KiB hung forever and never notified. The helper read a monitored child's stdout only from the process-termination handler, so a child that filled the pipe buffer blocked on write, never exited, and the completion callback never fired — two
linearprocesses were left wedged by a single ticket fetch. Both monitored paths (the ticket agent andlinear create) now drain stdout, and feed stdin, on a background queue while the child runs. Source:apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift.The daemon warns when it was launched from an ephemeral root. A daemon started from a temp dir (
/tmp,/var/folders,/dev/shm) or a git worktree resolves its own job modules by dynamicimport()rooted at the launch entry (getAgentsBinPath→process.argv[1]). When that directory is later removed — a/tmpcleanup, a review/verify checkout teardown,git worktree remove— the long-lived daemon keeps ENOENT-ing on every routine's imports (auto-dispatch.ts,routines-placement.ts,devices/fleet.ts), silently wedging until restart.anchorDaemonCwdalready rescues the cwd, but nothing can re-root a deleted module tree.runDaemonnow callswarnEphemeralDaemonRootat startup, so the risk is logged the moment the daemon comes up — including a directagents __daemon-runthat never passes through the launch-timevalidateDaemonBinarycheck. That launch-time check is also broadened from git-worktree-only to any ephemeral root via the shareddescribeEphemeralDaemonRootpredicate. The fix for a wedged daemon is unchanged: run it from the globally installed binary (npm i -g @phnx-labs/agents-cli) so its entry roots at a stable version home. Source:apps/cli/src/lib/daemon.ts(describeEphemeralDaemonRoot,warnEphemeralDaemonRoot,validateDaemonBinary).A
README.md/AGENTS.mdsitting in a resource directory is no longer installed as a resource.listResourcesskipped only dotfiles, so every.mdbeside the actual resources was materialized as one:commands/README.md— which the system repo has shipped for months — installed a bogus/READMEslash command into every agent home, and adding per-directoryAGENTS.mddocs would have added/AGENTS,/CLAUDE, and/GEMINIalongside it.README,AGENTS,CLAUDE, andGEMINIare now filtered from bothlistResourcesandresolveResourcefor every kind exceptrules, whereAGENTS.mdis the resource (the composed ruleset that syncs as each agent's memory file). The check tests!entry.isDirectory()rather thanisFile(), because aDirentfor a symlink reportsisFile() === falseandCLAUDE.md/GEMINI.mdare symlinks toAGENTS.mdby convention — a resource directory namedagents/is still a real resource. Verified against the real installed layers: 30 commands withREADMEleaking before, 29 with none after.agents commands listand the command picker no longer offer a name that cannot be opened.listCentralCommandsanddiscoverCommands(src/lib/commands.ts) run their ownreaddirSyncscans rather than going throughlistResources, so they kept offeringREADMEwhileagents commands view READMEanswered "not found" — a listed-but-unopenable name. Both now share the one exportedisDirectoryDocpredicate, so every enumerator agrees. Verified: 27 names withREADMEbefore, 26 with none after.agents commands add/remove/viewno longer suggestREADMEas the example command name. WithREADMEreserved as a directory doc, the six hardcoded examples in the help text and non-interactive hints named a command that can never exist. They now useplan, which actually ships.File-backed secrets bundles no longer require
AGENTS_SECRETS_PASSPHRASEon macOS. The encrypted file store now silently auto-provisions a stable machine-local key (a 0600 file under~/.agents/.secrets-key/, kept outside the encrypted store) on first use on every platform, macOS included — no prompt, no Touch ID, nothing to set or remember. Previously a file-backed bundle on a Mac hard-failed unlessAGENTS_SECRETS_PASSPHRASEwas exported, which blocked headless reads (e.g. theauthbundle the usage/auth reader consults) and frequently hung. SettingAGENTS_SECRETS_PASSPHRASEstill works and takes precedence — use it to hold the key off disk or to share one bundle's ciphertext across boxes under a common key. Source:apps/cli/src/lib/secrets/filestore.ts,apps/cli/src/lib/secrets/bundles.ts.Menu-bar & daemon notifications now use the current agents-cli mark, not the legacy logo. A desktop notification from the menu-bar helper or the routines daemon showed the old
assets/logo.pnggradient "A" — outdated, and blank in the notification's left-hand app-icon slot.MenubarHelper.app'sAppIcon.icnsis now generated from the current brand mark (assets/app-icon.svg→app-icon.png: the lime-tile lowercaseashared with the agi-cli web favicon and the menu-bar glyph), which drives both the notification's right-handcontentImageand its left-hand app icon. The installer also registers the bundle with LaunchServices (lsregister -f) at its~/Library/Application Supportpath so the OS can resolve that app icon. Source:apps/cli/menubar/scripts/build.sh,apps/cli/src/lib/menubar/install-menubar.ts,assets/app-icon.svg.The menu bar is a single instance, always. Two copies of the helper could run at once — launchd's
KeepAliveservice plus a LaunchServices/openlaunch of the same.app— putting two agents marks in the menu bar, and the second copy could holdCmd-Shift-V/Cmd-Shift-O(RegisterEventHotKeyis first-come). The helper now takes anflockon~/.agents/.cache/state/menubar.lockat launch and holds it for its lifetime; a helper that cannot take the lock pops the running helper's menu open and exits 0, since re-launching a menu-bar app means "show me the one I already have". Anflockrather than a pid file: the kernel releases it when the holder dies, so aSIGKILLed helper cannot leave a stale "already running" that blocks every later launch. Source:apps/cli/menubar/Sources/MenubarHelper/SingleInstance.swift,apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.agents menubar setupconfigures the menu bar end-to-end. One idempotent command for a machine that is wrong — never configured, helper down, or showing a duplicate icon. It ends every running helper, installs/refreshes the bundle, checks its code signature, writes the launchd login item (RunAtLoad+KeepAlive), clears a previousagents menubar disable, and verifies exactly one helper came back up — reporting each as its own step and exiting nonzero if it cannot reach that state.--checkreports without changing;--jsonemits the step list. Source:apps/cli/src/commands/menubar.ts,apps/cli/src/lib/menubar/install-menubar.ts.agents menubar statusnow shows a duplicate. Live helper processes were collapsed to a booleanrunning, so two copies of the installed bundle — the duplicate a user actually sees — reported as healthy.--jsonnow carries aninstancesarray (copies of the installed bundle) beside the existingforeignInstances, and the text readout names every extra pid and points atagents menubar setup. Source:apps/cli/src/lib/menubar/install-menubar.ts(classifyMenubarProcesses).Quick-dispatch ticket list: one-row filter + sort, and a scrollable list. The ticket controls sit on a single row of popups next to the Linear project (project · filter · sort) — not a chip matrix or two-column block. Quick filter options: All open, Todo, Doing, Backlog, P1 only, P2 only, Overdue. Quick sort options: Urgent first, Newest, Oldest, Due date, Priority (flat list, no status grouping). Filter and sort picks are remembered across summons. Ticket rows scroll inside a fixed viewport so more than five matches stay reachable without growing the panel. Source:
apps/cli/menubar/Sources/MenubarHelper/LinearTickets.swift,apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift.release.shnow takes a release lease, and refuses to bump past an unpublished tag. Releases run from whichever fleet box an agent happens to be on, so two agents could enter the pipeline at once; the collision only surfaced at the publish gate (merged tree != built tree -- refusing to publish), after one run had already merged and tagged, leaving the version merged but unshipped. A newscripts/release-lease.shholds mutual exclusion onoriginas an orphan commit atrefs/release-lock/held— a second claimant's push can never be a fast-forward, so git's rejection is the failed lock acquisition. The lease is claimed before the first mutation and dropped by the existing cleanup trap on every exit path. Because a healthy release routinely outlives any sane expiry — the CI matrix alone has run 57 minutes and release 1.20.77 took 186 minutes — the lease is renewed by a background renewer for the whole run, and the squash-merge, the tag, and the publish each verify ownership first, failing closed if it can no longer be proven. A lease that stops being renewed is reclaimable after 30 minutes, and reclaiming names the dead holder instead of silently overwriting it. Separately,release.shnow refuses to cut a new version while an olderv*tag exists that npm never received, and points at the re-run that finishes it — bumping past an unpublished tag is what turned a one-version gap into npm 1.20.78 vs main 1.20.81. Source:apps/cli/scripts/release-lease.sh,apps/cli/scripts/release.sh.agents funnel downdisables a public Funnel port from the same wrapper used to enable ingress. Webhook ingress now has a complete local receiver runbook: keep GitHub/Linear signing keys inagents secrets, bind the receiver to127.0.0.1, expose it withagents funnel up, rotate one source secret at a time, and turn the public port off withagents funnel downbefore stopping or moving the receiver. Source:apps/cli/src/commands/funnel.ts,apps/cli/src/lib/funnel.ts,apps/cli/docs/03-routines.md.New
agents secrets rotate-passphrasere-keys the encrypted file store under a new master passphrase, atomically (RUSH-1975). Until now there was no supported way to rotate the file-store passphrase —rekeyonly renames macOS keychain service names androtate <bundle> <key>replaces a single secret value, so a leaked passphrase (RUSH-1968) could only be remediated by a hand-rolled non-atomic script or an export-to-plaintext round-trip (the exact exposure being fixed). The new command decrypts every<item>.encunder the current key, re-encrypts under a freshly generated one, and swaps both the ciphertext and the 0600 key file by directory rename after verifying every item round-trips. A crash at any point self-heals on the next rotate run to a single readable store — content-aware recovery probes which key actually decrypts the live store (not merely which files are present) and classifies the WHOLE store: it completes the rotation forward or rolls back only when one key opens every item, and if a latersecrets setcontaminated a crashed rotation into a MIXED store (items under two keys at once, or a store dir recreated by an interstitial write after the crash left it absent, so its backup holds items the live dir does not) it refuses with an actionable error and preserves every recovery artifact rather than sweeping the only copy of a key or the backed-up ciphertext — so a crash anywhere in the swap can never orphan the store, even when a write landed in between. The rotation and every store write run under one cross-process lock, so asecrets setor a second rotation can never interleave with a swap in the first place. No plaintext secret or passphrase is ever written to disk, argv, or a log. Items that don't decrypt under the current key (orphan caches, stale test artifacts) are carried through verbatim, never re-keyed. Dry-run by default (--committo apply). A dry run never re-keys, but it does heal an interrupted rotation — that is how a crashed store becomes readable again without re-keying it — and it says so instead of claiming nothing was written. Refuses while the secrets-agent holds live unlocks or whileAGENTS_SECRETS_PASSPHRASEis exported in the environment, unless--force. Headless-safe and Linux-first. Source:apps/cli/src/lib/secrets/filestore.ts,apps/cli/src/commands/secrets-rotate-passphrase.ts.agents sessions --active --jsonnow reports who is watching each session. TheviewingInfield carries the same string the table prints —codium tab 3,ghostty tab 2, ordetachedfor a live tmux pane with no client attached (its terminal was closed or crashed). It isnullboth for a session that isn't tmux-hosted and for one whose pane the locator could not resolve —detachedis claimed only when the pane was actually located, so absence of evidence is never reported as evidence of absence. Previously the JSON path returned before the locator pass ran, so the field never appeared and a machine consumer could not tell a session someone is looking at from an orphaned one — which is exactly what the Factory extension'sAgents: Resumepicker ranks by. The JSON path resolves tmux clients only — no osascript — so scriptable output keeps the cheapness the old ordering was protecting; a Ghostty-attached client resolves asghosttywithout its tab number. Peers running an older CLI that still emits the{app, tab}object are normalized at the fan-out boundary, so a mixed-version fleet sweep stays correct. Source:apps/cli/src/lib/session/viewing-in.ts(viewingInLabel,parseViewingIn),apps/cli/src/commands/sessions.ts(serializeActiveSessionsForJson,enrichTmuxLocators),apps/cli/src/lib/session/remote-active.ts.Webhook handlers gain
run.envandhostplacement. A handler can now inject environment variables into the process it spawns (run.env), and choose where that run executes (host).hosttakes a device name (yosemite-s0), orfleetto pick any eligible online worker, orfleet/<platform>/<platform>/fleet(also a barelinux/macos/windows) to restrict that pick to one platform. A fleet expression that matches no eligible device fails loudly rather than silently falling back to the local machine, sofleet/linuxcan never land on a macOS box. Omittinghostruns locally, as before. Source:apps/cli/src/lib/triggers/handlers.ts(resolveHandlerHost),apps/cli/src/lib/routines-placement.ts(pickFleetDeviceplatform filter),apps/cli/src/lib/routines.ts(JobConfig.env),apps/cli/src/lib/runner.ts.
1.20.88
agents doctorredesigned into a prioritized, fleet-aware, per-version readout (RUSH-2069). Comprehensive by default (no--verbose): a top✗ CRITICAL — needs you now (N)section lists every critical across the whole fleet worst-first (device · harness@version · account · message → remediation), then a─── by computer ───section gives each device its warnings plus a compact accounts/versions line showing every installed version and its account (provable ✓ / ✗). A single-machineagents doctorcollapses to the CRITICAL section plus one▸ <machine>block. Severity: provable logged-out, a missing hook/plugin, a broken CLI, and a never-synced version whose declared resources are therefore absent are CRITICAL; drift, version-skew, repo-behind/-drift, orphans, and an unprovable logout are WARNINGS. Sign-in is now probed per installed version (each version's own home + the global credential via the newcredentialPresence), so a per-version logged-out claim is made only when both are absent, agents with no inspectable identity never report logged-out, and the login remediation is version-targeted (agents run <agent>@<version>for the isolated set; gemini/antigravity/droid/cursor say the login is shared). Older fleet boxes that can't report per-version sign-in surface an "older agents-cli — upgrade" warning. The readout is de-duplicated so one root cause is one line: a version's missing hooks/plugins and drifted resources collapse to a count plus two examples (32 hooks missing (incl. 'a', 'b')), the same problem on several versions of one agent reads asclaude (5 versions)with an agent-wide fix (isolated copies stay on their own line, since the sweep skips them), every orphan row on a machine folds into one cleanup-only line, and a version that already listed its drifted resources no longer also says "sources changed since last sync". The two advisories that predate the redesign are findings now, not separate blocks: credential-shaped exports in shell rc files (RUSH-1968) and the Windows execution policy that blocksagents.ps1. The duplicate-version-home hook check keeps its text output too — differing copies are critical, identical ones a warning, one row per agent rather than one per hook — and so does the Host CLIs check, as ahost CLIswarning namingagents cli install <name>. Remediations reach every version in their row: a row collapsed across versions usesagents sync <agent>@all --yes(a bareagents sync <agent>would fix only the default version), a cross-device resource gap saysagents repo pullrather than the central-to-homeagents doctor --fix, and a diverged config repo names its own alias instead of always sayinguser.agents doctor --jsonadds afindingsarray and a per-versionfleet.signInmap; the existingclis/sync/orphans/fleet/signIn/reposfields are unchanged. Source:apps/cli/src/lib/devices/doctor-findings.ts,apps/cli/src/lib/devices/fleet-inventory.ts,apps/cli/src/lib/agents.ts,apps/cli/src/commands/doctor.ts.Run Cursor routines safely (RUSH-2080). Routines configured with
agent: cursornow reuse the same-device login under the default sandbox, trust the configured workspace without--yolo, warn when a requested read-only plan is elevated to writable edit mode (includingloop:jobs), and record successful runs correctly. Source:apps/cli/src/lib/runner.ts.agents sessionsnow discovers, indexes, and renders Cursor agent transcripts (RUSH-2081). Cursor writes its conversation toprojects/<encoded-cwd>/agent-transcripts/<uuid>/<uuid>.jsonland metadata tochats/<workspace-hash>/<uuid>/meta.json. Discovery starts from the transcript and joins metadata by UUID, so abandoned chats with no transcript never become empty rows. Cursor is installed outside agents-cli's managed version homes, so users with any managed agent version must pass--unmanagedto include Cursor rows. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/session/parse.ts.Fix Cursor usage and account inspection (RUSH-2082).
agents usagenow derives support from the usage library so Cursor, Grok, and future usage sources cannot drift from the command, andagents run cursor@can inspect Cursor's active account. Source:apps/cli/src/commands/usage.ts,apps/cli/src/lib/agents.ts.Sync Cursor commands to the IDE and cursor-agent CLI (RUSH-2083). Shared commands now remain available as typed IDE slash commands and are also generated as Agent Skills for cursor-agent, while preserving user-authored files in
.cursor/commands/. Source:apps/cli/src/lib/command-skills.ts.Daemon routines resolve
agentson~/.local/bininstalls. The generated daemon service (systemd + launchd) now puts theagentsshim's own directory onPATH, not only the Node runtime dir. On a box where the shim lives outside the Node bin dir (a~/.local/binglobal install, a separate npm prefix), the daemon'sPATHpreviously carried only the Node dir, so every scheduledcommandroutine — the always-on watchdog included — shelled out to a bareagentsthat resolved to nothing and died withexit 127. Source:apps/cli/src/lib/daemon.ts.
1.20.87
agents devices enable|disable|prefer|unprefer <name>control which machines Factory auto-launches onto. A disabled device is skipped byNew <Agent>and the balanced launch, but stays available throughNew <Agent> (Pick Host). A preferred device wins ties against otherwise equivalent machines — worth about two running agents in the ranking, so a preference never sends work to a box that is genuinely swamped. Every device is enabled and unpreferred by default, and an unregistered name is now rejected instead of writing a preference that matches nothing. Preferences live in~/.agents/.history/devices/auto-launch.json, written by the CLI and read by the extension. Source:apps/cli/src/lib/devices/registry.ts,apps/cli/src/commands/ssh.ts,apps/factory/src/core/deviceAutoLaunch.ts,apps/factory/src/core/launchHost.ts.Point-of-use friction events for
agents teamsfailures. The CLI'sdie()chokepoints inteamsnow emit a structuredfrictionevent (surface,failureId,error) before exiting, so the nightly factory-metrics routine can rank recurring failures without re-parsing transcripts. A hiddenagents _internal frictionrecorder lets shell guard hooks (git-guard, rm-guard, large-file-add-guard) self-report blocks into the same stream. Source:apps/cli/src/lib/events.ts,apps/cli/src/lib/format.ts,apps/cli/src/commands/teams.ts,apps/cli/src/index.ts.agents viewno longer reports a working Claude install as "logged out". Claude'ssignedInis!!emailread from a version home's.claude.json(lib/agents.ts), so a version that authenticates from an ambientCLAUDE_CODE_OAUTH_TOKEN— no account ever written to that home — rendered "(logged out — log in with: claude, then /login)" while every run against it succeeded. On one fleet box five of seven versions read as locked out and all of them answered a live prompt. Those now render "(no per-version login — using ambient CLAUDE_CODE_OAUTH_TOKEN)", which is both accurate and the more useful warning: an ambient token is ONE account, so balanced rotation across those versions rotates nothing. Source:apps/cli/src/lib/signin-badge.ts,apps/cli/src/commands/view.ts.Claude per-account run tokens.
agents runnow injects the Claude setup token keyed to the selected version home's own account email, so balanced Claude rotation no longer inherits one ambient shared token across accounts. Source:src/lib/exec.ts.agents sessionsnow shows which session spawned which team. The link already existed on disk and was discarded twice.SessionMeta.spawnedTeam— the team name read off theagents teams create/addcommand at scan time — had no column insessions.db, so the writer dropped it and no consumer had ever seen a non-undefinedvalue; a newspawned_teamcolumn (schema v21, which forces one full rescan) persists it, and orchestrator rows now carry a greenteam:<name>badge. Separately,classifyTeamSessionwas already opening each teammate'smeta.jsonand throwing away itstask_nameandparent_session_id, so a teammate row could not name its team or point back at its orchestrator; teammate rows now read[<team>/<handle>]and the preview pane carries aTeam:line from either end of the lineage. New--in-team <name>(and athotkey in the browser) filters to one team's orchestrator plus its teammates,agents teams status --parent-session <id>lists the teammates a given session spawned, andagents teams listgains aby <id>column. Source:apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/team-filter.ts,apps/cli/src/commands/sessions.ts,apps/cli/src/commands/sessions-browser.ts,apps/cli/src/commands/teams.ts.agents sessions --device <box>no longer opens an empty browser. The interactive one-host listing kept the browser's default this-repo scope, but every row it fetches is the peer's and no peer cwd is under the localprocess.cwd()— so the filter dropped all of them. A host scope now implies all-directories (and thephotkey is a no-op under one). Three more scope bugs on the same path:--device <this machine>fanned out to the whole tailnet, becausegatherRemoteListreads the resulting empty peer list as "no hosts given" and sweeps;--local --device <box>rendered a silent empty list instead of reporting that the two flags ask for opposite things; and--device <box> --cloudfell through to the cloud listing, which has no host scope and silently ignored the device. An unreachable peer now says so in the browser header — the fan-out's stderr note is repainted away by the full-screen picker, so "that box is asleep" used to read as "no sessions match". Source:apps/cli/src/commands/sessions.ts,apps/cli/src/commands/sessions-browser.ts,apps/cli/src/lib/session/remote-list.ts.Teammate records are found by the session id they actually produced. A teammate's directory under
teams/agents/is named for its agent id, but the harness mints its own session id and the spawn records it separately asremote_session_id.classifyTeamSessionlooked only under the directory name, so most teammates were unreachable — on a live box, 14 of 16 records resolved only viaremote_session_id— and their rows could not name their team however complete the record was. Both keys are now registered in one index built per process, which also replaces theexistsSync+readFileSyncthe old path paid for every row in the pool. Source:apps/cli/src/lib/session/team-filter.ts.detectSpawnedTeamno longer indexes prose or flag values as team names. Rendering the value exposed that it had been wrong for most of the rows that had one: on a live index of 4627 sessions, 11 carried a team and 6 of those read2ort. It matched documentation and echoed output rather than only executed commands; its flag-skip used\sand so ran across a newline to capture a word from the next line; and a value-taking flag did not swallow its value, so--device autoleftautolooking like the team name — which was corrupting real detections, not just adding false ones. After the fix the same index resolves ten teams, every one of them a real team name. Source:apps/cli/src/lib/session/state.ts.--in-teamreturns a team's whole lineage, not the slice inside the default window. It filtered in memory after the query, so a team older than the default top-50 / 30-day / current-directory scope came back empty with no message — and a team's teammates run in their own worktrees, which the directory scope hid. The flag now widens its own scope the way--alldoes. It is also refused with--active, whose live rows carry no lineage to match on, rather than being silently ignored. Source:apps/cli/src/commands/sessions.ts.The session preview pane sanitizes peer-supplied
planand directory text. A remote row's metadata is JSON the peer sent andparseRemoteListhands over verbatim;sanitizeMetacoveredtopic/label/cwd/todosbut not these, so a terminal escape in another machine's plan text reached the TTY. The remote preview also renders more of what already rides across the hop: the checklist items, the directories the scan recorded, and a one-line plan summary (never the full markdown blob).directoriesTouchednow reads the realrecentDirectoriesTouchedfield instead of adirsTouchedthat nothing in the repo ever wrote. Source:apps/cli/src/commands/sessions-picker.ts.
1.20.86
agents sessionsnow shows a Kimi session's todo list and its file-touching tool calls. Kimi writes its checklist withTodoList(items shaped{title, status}, where finished isdone) rather than Claude'sTodoWrite({content, status: "completed"}), so the checklist registry matched nothing and every Kimi session rendered with no todos — in the picker preview, the session detail, and the--activefan-out that carries progress off remote devices. Kimi also names the file argumentpathwhere Claude names itfile_path, soRead/Write/Editcalls summarized as a bareReadwith no file. Both spellings are now handled, and the snapshot-checklist tool names live in one exported registry (SNAPSHOT_TODO_TOOLS) that the picker and the state engine share instead of each hardcoding its own pair. Source:apps/cli/src/lib/session/parse.ts,apps/cli/src/lib/session/state.ts,apps/cli/src/commands/sessions-picker.ts.agents viewnow shows Grok's default model (e.g.grok-4.5). Claude, Codex, Antigravity, and Kimi already filled the model column via their catalogs; Grok was missing fromlocateModelSource, soresolveConfiguredModelreturned null and the column stayed blank. Grok has nosettings.jsonmodelfield (its config isconfig.toml+models_cache.json); the authoritative default isgrok models→Default model: <id>. The catalog extractor now spawns that command against the version-home binary (skipping failed-download stubs) and flags the default, soagents view,agents view --json(configuredModel), and the other identity-cluster surfaces show it. Source:apps/cli/src/lib/models.ts,apps/cli/src/commands/models.ts.agents events --limit 0now reads the whole stream, and a capped read says so.--limitparsed asMath.max(1, parseInt(raw) || 50), so--limit 0collapsed back to50(0 || 50) and there was no way to read past the default cap at all. The cap is applied after filtering and before the caller sees anything, so every aggregation over--jsonsilently ranked the newest 50 records instead of the matching set — measured against a real 7-day corpus of 2,135 CLI failures in 9 classes, 8 of 9 ranks came out wrong with counts off by roughly 100x, and nothing warned.--limit 0now means no cap (29,649 records on a 30-day stream here, against 50 before), a truncated read printsShowing the newest 50 — more events matched. Pass --limit 0 for all.(on stderr under--json, so a| jqpipeline still receives clean JSON), and a non-numeric, negative, or empty--limitexits 2 rather than quietly becoming 50 — an empty one (--limit "$LIMIT"with the variable unset) would otherwise have read as "no cap" and returned the whole stream unannounced. Source:apps/cli/src/commands/events.ts,apps/cli/tests/events-limit.test.ts,apps/cli/docs/06-observability.md.Desktop notifications now show the current agents-cli mark, not the old logo. The menu-bar helper's app icon — the icon macOS puts on the left of every notification banner it posts (the menu bar helper's own notices and every
agents run --notifyfinish notice) — was generated from the retired gradient "A" logo, so notifications carried stale branding while the menu-bar status item already used the new lowercasea. The shared master logo (assets/logo.png) is now the currentamark, so the menu-bar helper, theagents computerhelper, and the keychain helper all regenerate theirAppIcon.icnsfrom it on the next build. Source:assets/logo.png,apps/cli/menubar/scripts/build.sh.
1.20.85
agents feed postcan now be mirrored to the systems you actually watch. A post was durable but local: an operator away from every terminal never saw it, and the tracker that owns the work heard nothing. Declare sinks underfeed.broadcastinagents.yaml— argv templates, not built-in integrations — and each post is fanned out to them.--level importantmarks a post worth interrupting someone over, so a sink withminLevel: importantnever fires on a routine "CI green"; a template referencing{ticket}is skipped when no ticket is known, and the ticket is joined from the session index rather than asked for as a flag.{message}composes the human line a messaging sink wants —<project> · <text>plus the first attached URL — so an out-of-band ping leads with the project and carries a clickable link. Delivery is best-effort and reported per sink; a mirror that fails never costs you the post. Source:apps/cli/src/lib/feed-broadcast.ts,apps/cli/src/commands/feed.ts,apps/cli/docs/06-observability.md.agents feed --filter updatesnow shows the progress posts agents actually wrote, across the fleet. The view read the most recent N activity events and then keptstatus.posted, so routinefile.editedchurn filled the whole slice — a box with six real posts rendered "0 posts" (and--jsonreturned one of six).readRecentActivitygainedevents/tierfilters that apply before the limit, so the limit counts posts; the same fix restores the milestone lane underagents feed. The updates view also fans out over SSH like the block view (-H/--host,--device,--localto opt out), because an agent posts on whichever box ran it. Source:apps/cli/src/lib/activity.ts,apps/cli/src/commands/feed.ts.agents run --notifyposts a desktop notification when a headless run finishes, and menu-bar quick dispatch now uses it. The dispatch panel used to post its "finished"/"failed" notice from the MenubarHelper's own process-termination callback, so a helper that restarted mid-run — an upgrade replacing the bundle, a crash — took the callback with it while the run carried on reparented to launchd, and the dispatch could never report back. The run process owns the notice now: armed on its ownexit, so it covers local,--hostand--leasedispatch alike and survives anything that happens to the launcher. The helper's click actions also accepturl:<https…>so a completion notification can open the PR or ticket the run produced. Source:apps/cli/src/lib/run-notify.ts,apps/cli/src/commands/exec.ts,apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift,apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift.
1.20.84
Agent onboarding cheat sheet and docs drift guard. Added
apps/cli/docs/AGENT-CHEATSHEET.mdas a one-page on-ramp for agents, wired it fromapps/cli/AGENTS.mdandapps/cli/docs/README.md, and addedscripts/verify-docs.sh(plus averify-docsnpm script and CI job) to catch broken relative links and missing entry-point wiring before merge.Codex can now build, test, and install without escalating to YOLO. Codex's
workspace-writesandbox blocks$HOME, socargo build,go build,npm/bun install,pip installetc. failed on their out-of-workspace cache writes (~/.cargo,GOCACHE,~/.npm,~/.cache, …) — which is what pushed people to--mode full(--dangerously-bypass-approvals-and-sandbox). agents-cli now writes a platform-resolved baseline of regenerable toolchain caches into Codex'sconfig.toml([sandbox_workspace_write].writable_roots) on permission sync —~/.cargo,~/.rustup,~/.npm,~/.bun,~/go,~/.deno,~/.gradle,~/.m2,~/.gem, plus~/Library/Caches~/Library/pnpmon macOS or~/.cache+~/.local/{share,state}on Linux. Credential dirs (~/.ssh,~/.aws,~/.gnupg,~/.config,~/.netrc) are deliberately excluded, so--mode autostays a real sandbox — far narrower than danger-full-access. Anywritable_rootsyou set yourself are preserved (unioned, never clobbered). Source:apps/cli/src/lib/permissions.ts(codexDefaultWritableRoots,mergeCodexSandboxWrite).
agents sessionsteam rows now show the team's target and teammate, not just the slug. Each teammate row reads<team> · <teammate> · by <orchestrator> · <live turn | mission>, where the mission is a one-line summary of the teammate's spawn prompt (assignedTask, shown even before it has a transcript). Several teams from one orchestrator stay legible (distinct team names) and each says what it is for.--active --jsoncarriesassignedTask. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/commands/sessions.ts.
1.20.83
Routines now treat date-specific cron schedules as one-shot jobs (RUSH-2074).
agents routines add --schedule "0 14 29 7 *"now warns, persistsrunOnce: true, marks the routine as one-shot inroutines list, andagents routines cleanupremoves completed expired one-shots that still have user-layer YAML. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/scheduler.ts,apps/cli/src/commands/routines.ts.agents routines listgroups terminal output by device and placement (RUSH-2075). The default table is bucketed under this machine, fleet-wide, cloud, named devices, and named hosts with offline/unknown registry hints;--flatkeeps the legacy single table and--jsonremains a flat payload. Source:apps/cli/src/commands/routines.ts.agents.yamlno longer churns on every meta write.writeMetaUnlockedwrote the central config withyaml.stringify, which strips all comments — so the freshly-serialized bytes never matched the comment-annotated file on disk,writeIfChangedrewrote it on every meta write, and the perpetually-dirty tree wedgedagents sync("Blocked by local changes") across the fleet. It now serializes via ayaml.Documentround-trip (serializeCentral) that edits only the keys that actually changed, so comments, key ordering, and untouched top-level blocks (e.g.hosts:) are byte-stable — and a write that changes no central field leavesagents.yamluntouched. Source:apps/cli/src/lib/state.ts.agents run codexcan now reach the fleet from inside its sandbox. Codex'sworkspace-writesandbox blocks$HOME(verified against the live CLI and OpenAI's sandbox docs), but the model routinely shells out toagents ..., whose runtime state lives under~/.agents— the SSH askpass shim (~/.agents/.cache/devices/askpass.sh), the device/stats cache, secrets, session writes, config tunings. Those inner writes hitEROFS(agents sshdied before connecting, so a remoteagents run codexcould not SSH or self-tune), and the fix was previously left to the caller (teams pass--add-dir ~/.agentsexplicitly; a plainagents runnever did).buildExecCommandnow grants~/.agentsas an extra writable root whenever Codex runsworkspace-write(--mode edit/auto) — via--add-diron fresh runs (deduped against user--add-dirs) and via-c sandbox_workspace_write.writable_rootson resume forms (which reject--add-dir). This is the officially-recommended way to widen scope "without removing the sandbox entirely" — far narrower than--mode skip(danger-full-access).plan(read-only) andskip(sandbox already dropped) are unaffected. Source:apps/cli/src/lib/exec.ts(buildExecCommand,codexWritableRootsConfig).Fix headless release signing (
errSecInternalComponent).headless-sign-context.shnow runssecurity set-key-partition-listright after unlockingrush-signing.keychain-db, authorizingcodesign/apple-toolto use the Developer ID key non-interactively. Without it, the key's ACL prompts for UI approval that a headless SSH release session can't answer, socodesignfails and the npm publish halts. Idempotent; runs every release. Source:apps/cli/scripts/headless-sign-context.sh.Cmd-Shift-V clip paste no longer breaks with an "sshd-keygen-wrapper would like to control this computer" prompt. A menu-bar helper started from an ssh session registered the global chords but could never service them: macOS attributes its Accessibility request to the responsible process,
/usr/libexec/sshd-keygen-wrapper, not to the helper's bundle, so the prompt named a process whose grant does nothing for the paste (and, if granted, hands keystroke synthesis to everything any ssh session spawns).RegisterEventHotKeyis first-come, and the prompt naming sshd-keygen-wrapper is itself the evidence that this copy — not the trusted launchd-managed one — had registered Cmd-Shift-V and was servicing it. The interactive mode now refuses to start over a remote shell, and refuses unrecognized arguments: an unknown flag used to fall straight through to the status-bar app, which is how a strayMenubarHelper --self-testfrom a verify run became a permanent second helper.launchctl bootstrap(agents menubar enable) is unaffected, including when run over ssh. Source:apps/cli/menubar/Sources/MenubarHelper/Guards.swift.agents menubar statusnow names a second helper process instead of reporting a healthyrunning: yes. The check waspgrep -f MenubarHelper, which matches any process with that name, so a stray copy holding the global chords looked identical to a working install. Status now identifies the helper by its resolved executable (ps -o comm=), reportsrunningonly for the installed bundle, and lists every other live copy with its pid underforeignInstances(also in--json). Source:apps/cli/src/lib/menubar/install-menubar.ts.The menu bar now says so when a hotkey is unavailable or the paste is not permitted. A
RegisterEventHotKeyconflict only wrote a line to a launchd log, and a missing Accessibility grant madeClip.injectreturn silently — both looked exactly like a dead hotkey. A stolen chord now posts a notification naming it, and a denied grant copies thehost:pathreference to the clipboard and says which setting to grant, so the clip is never lost. Source:apps/cli/menubar/Sources/MenubarHelper/Hotkey.swift,apps/cli/menubar/Sources/MenubarHelper/Clip.swift.Fix the release catch-up path aborting on an unbound variable. When a release PR had already merged and only the tag + publish remained,
release.shre-validated CI and then aborted withline 933: RELEASE_COMMIT: unbound variable, so the retry never reached npm. The catch-up block that runs whenmainsits exactly at the release merge commit never setHISTORICAL_CATCHUP, so phase 4 took the normal-release branch and readRELEASE_COMMIT, which only the branch-creating path defines. It now sets the flag, and phase 4 resolves the release commit from the merged PR (MERGED_RELEASE_SHA+CI_TESTED_HEAD) as intended. This is why 1.20.79, 1.20.80, and 1.20.81 were tagged but never published. Source:apps/cli/scripts/release.sh.Removed
agents check/agents resourcesnow forward to their replacements instead of erroring (RUSH-1234). After the command consolidation, running the removed names produced a bareunknown command(their edit-distance todoctorwas too far to even trigger a "did you mean"). They are now hidden tombstone commands that print a one-line deprecation notice to stderr and re-run the replacement, preserving flags and exit codes:agents check …runsagents doctor --check …(so--json/--quiet/--devicesand the CI drift-gate exit code carry through), andagents resources …runsagents view --merged …(withagents inspect <target>pointed to for per-agent/per-repo detail). The notice goes to stderr so a--jsonconsumer's stdout stays clean. Source:apps/cli/src/index.ts.agents sessions --host/--device <box>now opens the interactive fleet browser instead of a raw text dump. A bare remote listing on a TTY folds the named box into the same preview-rich, selectable picker as the local view (it previously short-circuited to the legacy per-host stream — non-interactive, no previews). A--hostquery, a render/filter flag,--json, or a non-interactive caller keep the streamed output. Source:apps/cli/src/commands/sessions.ts.agents sessionsnow shows which orchestrator spawned each team. A teams teammate row was keyed off its orchestrator's session id (captured fromAGENTS_SESSION_IDat spawn), which both hid the lineage and mislabeled the teammate with the orchestrator's id/topic. The teammate now keys off its own transcript, exposes the orchestrator asorchestratorSessionId(+ a resolvedorchestratorLabel) in--active --json, and the listing renders<team> · by <orchestrator>so "which session spun up this team" is answerable at a glance. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/commands/sessions.ts.
1.20.82
Codex hook sync no longer leaves startup warnings after upgrades. The Codex hook registrar now prunes hook commands from sibling Codex version homes before writing
hooks.json, so removed versions such as0.142.0cannot leave dead PreToolUse/Stop handlers that exit127. It also writesSessionEndhook timeouts at Codex's 3-second limit instead of emittingtimeout: 5and making Codex warn that it is clamping the value on every startup. Source:apps/cli/src/lib/hooks.ts.agents secrets export/list/viewnow accept--device/--devicesas aliases for--host/--hosts, and a keychain-backedexport --hostpush is verified.--device mac-miniused to fail with "unknown option" on the secrets commands even though the rest of the fleet vocabulary (agents activity,agents run --device) accepts it; it now resolves identically to--host. And a default keychain-backend push to a macOS host over headless SSH — the sign host a Linux-driven release offloadsapple.comprovisioning to — used to land the bundle metadata but no readable secret items (the remote login keychain is locked over SSH), then fail every later read with the confusingBundle 'X' key 'Y': stored item '...' not found. The push now reads the bundle back the way a headless release will and fails loudly when the keys didn't persist, naming the locked-login-keychain cause and steering to--remote-backend file(headless-readable) or unlocking the remote keychain. This unblocks headless Linux-driven releases. Source:apps/cli/src/commands/secrets.ts,apps/cli/src/lib/secrets/remote.ts.agents sessions resumeshows session previews immediately and opens one tab per session by default (RUSH-2023). The multi-select picker now starts its preview pane open whenever the caller supplies preview content;tabstill toggles it. Batch resume now uses full-width tabs across terminal backends, with side-by-side two-per-tab packing available explicitly via--splits. Source:apps/cli/src/lib/picker.ts,apps/cli/src/commands/sessions-resume.ts.agents sessions --activenow distinguishes dead and abandoned sessions (RUSH-2066). The active-session engine computes lifecycle from PID liveness and transcript mtime: a dead process reportsclosed, a transcript stale forABANDONED_STALE_MSreportsabandoned, and a live opaque harness still reportsrunningas its honest floor. The default list, grouped active tallies, andagents hq floorrenderclosed/abandoneddistinctly, and Factory mapsclosedto done andabandonedto failed so dead work no longer appears idle. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/commands/sessions.ts,apps/factory/src/core/remoteSessions.ts.agents viewnow surfaces the Cursor account and its usage. Cursor was absent from the account view; it now shows the signed-in account (email/authId from~/.cursor/cli-config.json, token from~/.config/cursor/auth.json) and, for request-capped (free/legacy) plans, a monthly request bar (M) fromcursor.com/api/usage. Usage-based plans have no request cap, so they render the account row without a bar. Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/agents.ts.The routines daemon now anchors its working directory to
$HOMEon startup, so a deleted launch directory no longer crashes every scheduled routine. The daemon is long-lived and inherited whatever cwd it was launched from — commonly a git worktree under.agents/worktrees/. When that directory was later removed (git worktree remove,rm -rf), the daemon kept the deleted inode as its cwd (a process cannot chdir out of a deleted directory on its own), and every job it spawned inherited the dead cwd —spawnJobAttemptand command runs pass no explicitcwd. Bun then failedgetcwd()at startup and every routine died at 0 seconds withENOENT: Bun could not find a file(orThe current working directory was deleted) before the agent ran — a fleet-wide routine outage from a single removed worktree.runDaemonnow re-anchors to the home directory once at startup (anchorDaemonCwd), making the scheduler immune regardless of how it was launched. Source:apps/cli/src/lib/daemon.ts(anchorDaemonCwd,runDaemon).agents repo refreshis deprecated in favor ofagents sync. The command is now hidden from help and prints a deprecation notice on use, pointing at the replacement:agents sync --local(reconcile all installed agents, no git) oragents sync <agent>(one agent). It still runs for now so existing scripts and muscle memory don't break —refreshwas a partial variant ofsync(it only ever materialized the single global-default version, and silently no-op'd for an agent with installed versions but no global default), whereassynccovers every installed version. Internal callers (crabbox bootstrap, theagents pullredirect,agents setuphelp) now useagents sync --local. The underlyingrefresh()function stays — it is the reconcile stage behindagents sync. Source:apps/cli/src/commands/repo.ts,apps/cli/src/lib/crabbox/.agents viewnow shows Grok usage limits. Grok's network usage endpoints 404, so usage is parsed from the local~/.grok/logs/unified.jsonllog instead — the latest billing-period config and subscription tier render as aWwindow, matching the other agents' live-usage display. Source:apps/cli/src/lib/usage.ts.agents run --hostnow starts in the same project you launched it from, not the remote$HOME. A host run with neither--cwdnor--remote-cwdsent nocdat all, so the remote agent opened in the home directory with no project context — every launch from a repo (including every Factory "Pick Host" tab) began with a manualcd. The dispatch now derives a working directory from the local cwd when the caller named none: a cwd under the local home is re-rooted onto the remote home (~/src/x→ the host's$HOME/src/x), which is the normal fleet layout where the same checkout sits at the same home-relative path on every box. Because a derived directory is a best-effort mirror rather than something the user asked for, a host that lacks that checkout falls back to its home instead of failing the run; an explicit--cwd/--remote-cwdis never mirrored, so a directory you named that does not exist still fails loudly. A cwd outside the local home is not mirrored — a path like/opt/thingsays nothing about the target's filesystem. Source:apps/cli/src/lib/hosts/dispatch.ts(deriveMirroredCwd,remoteCdPrefix),apps/cli/src/commands/exec.ts.The Cmd+Shift+O quick-dispatch bar is now Plan / Run, and never runs an agent in your home directory. The two spotlight modes were renamed from File Ticket / Fix to Plan (investigate → file a Linear ticket) and Run (headless
agents run). A new repo dropdown is populated from your recent session working directories with$HOMEdropped, and the pick is passed as--cwdto both modes, so an agent is always scoped to a real repo instead of the too-broad home dir; the last-picked repo is remembered. Run now always uses--strategy balanced(auto load-balance across signed-in versions with headroom, skipping rate-limited), and--nameis seeded from a slug of your task text instead of an opaquequick-<timestamp>. Source:apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift,apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift.The keychain Touch ID prompt now names the session that triggered the read (RUSH-1971). When a bundle read pops Touch ID, the operation prompt already named the requesting agent, bundle, and reason; it now also carries the triggering session's 8-char short-id — e.g. "Claude is requesting to unlock the 'prod' bundle (session e0a1b2c3) for 7 days …" — so an unexpected prompt is attributable when an interactive agent, headless workers, and
secrets execdeploys all run at once. The short-id is derived from theAGENT_SESSION_IDthe exec env already exports; no keychain-helper re-sign is required (the enriched string flows through the existingAGENTS_KEYCHAIN_PROMPTenv). Source:apps/cli/src/lib/secrets/index.ts,apps/cli/src/lib/secrets/bundles.ts.agents teams listrenders from cached team metadata instead of full status probes (RUSH-1996). The list and picker rows now read the team registry plus teammatemeta.jsonsnapshots, so listing teams no longer blocks on remote log pulls or unreachable hosts. Full teammate status is still loaded when a user picks a team or runsagents teams status <team>. Source:apps/cli/src/commands/teams.ts,apps/cli/src/commands/teams.test.ts,apps/cli/docs/teams.md.agents setup secretsnow guides first-run secrets onboarding (RUSH-1999). The setup command registers a newsecretscapability wizard that chooses a default storage backend (keychain, encryptedfile, or syncedvault), sets the existing default prompt policy (daily/always, withnevergated for explicit automation use), persistssecrets.backendso futureagents secrets create/importcommands use the selected backend when no backend flag is passed, optionally delegates imports toagents secrets import, and writes setup preferences under~/.agents/.history/setup. Source:apps/cli/src/commands/setup-secrets.ts,apps/cli/src/commands/secrets.ts,apps/cli/src/commands/setup.ts.agents setup fleetnow guides Tailscale device onboarding (RUSH-2000). The setup command registers a newfleetcapability wizard that verifies Tailscale, syncs discovered devices through the existingagents devices syncpath, applies SSH auth withagents devices set, optionally writes the managed SSH config include, tests connectivity withagents ssh <device> uname, and can runagents fleet updateafter registration. Source:apps/cli/src/commands/setup-fleet.ts,apps/cli/src/commands/setup.ts.agents feed postcarries artifacts and a project chip, and progress posts render rich (RUSH-2013 / RUSH-2014).feed postgains--attach <path-or-url…>(repeatable): a local file is copied under~/.agents/.history/attachments/<session>/<update>/so the link survives a worktree delete, and a URL is kept as a link — each classified to an image/audio/video/file/link kind by extension. Every post is now stamped with its project (basename of cwd, worktree-aware) on the activity event itself, so the chip shows without a live-session join. Astatus.postedevent renders as a multi-line update —agent · session · host · projectchips, the message, an attachment row with per-kind glyphs, and a↳ ag focus/sessionshint — wherever it appears (feed postecho, the feed activity lane,agents feed --filter updates, andagents activity).agents feed --filter needs|updates|all(RUSH-2015).needs(default) is the open-blocks inbox as before;updatesshows only deliberate progress posts over the local activity timeline (no block pipeline, no remote fan-out);allrenders the blocks then appends the updates view.--jsonunder--filter updatesemits the rawstatus.postedevents. Source:apps/cli/src/lib/activity.ts,apps/cli/src/lib/feed-post.ts,apps/cli/src/commands/feed.ts,apps/cli/src/commands/activity.ts.Fix: actor attribution now actually reaches
agents sessionsand--activefor real runs (RUSH-2018/2019). Two bugs, found by driving a realagents runend-to-end: (1) the session index'sactor/initiated_bywere kept out of the upsertON CONFLICTentirely, so any row indexed before its actor sidecar landed (an older scanner, or a scan racing the spawn-time write) was locked toNULLforever — nowCOALESCE(existing, incoming)backfills a null while still never clobbering a stored owner; (2) the live--activeowner read only the per-pid registry entry, which the SessionStart hook rewrites without an actor, so real runs showed no owner —--activenow falls back to the durable per-session actor sidecar. Verified with a realagents run: the actor reachessessions.dband the--activeowner resolves. Source:apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/active.ts(resolveOwner).Session lists expose the model and richer navigation metadata (RUSH-1981, RUSH-1991, RUSH-1992, RUSH-1994). Static flat rows add a compact model column only when the result set has model data, with width sized to that set so an 80-column terminal does not wrap. Local CWD and ticket/PR cells are clickable in supporting terminals, previews identify browser/computer use and sub-agent counts, and
agents sessions --active --jsonadds an always-presentprLinkkey. Existing session indexes migrate to schema v20 and rescan transcripts to backfill model data.
1.20.81
Generic
--device all/--host allfleet fan-out for every fleet-aware command (RUSH-1969). The passthrough now treatsallas a sentinel value on--host,--device,--hosts, and--devices. For any routable command (view,output,sync,doctor,list, …) it runsagents <cmd> --jsonon every registered device concurrently, then renders an OS-grouped roster (●installed,○offline/skipped,▸ … ← this machine). Offline and no-address devices render as rows instead of hanging the whole run. Add--jsonto get a device-keyed object. Commands that already own--all-hosts(output) keep their existing behavior. Source:apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/hosts/option.ts.agents applyno longer propagates single-use rotating refresh tokens (RUSH-1958). Droid (WorkOS) credentials use a refresh token that rotates server-side on every exchange; copying one credential file across N boxes caused the first refresh on any box to invalidate every other holder, collapsing the fleet to a single working login.agents applynow excludes droid — and any future harness added to the sharedSINGLE_USE_ROTATING_REFRESH_AGENTSset insrc/lib/fleet/auth-sync.ts— from credential propagation. The plan surfaces these asmanual login needed (single-use rotating refresh token)with the device name, routing the user to log in on the target box itself. Source:src/lib/fleet/auth-sync.ts,src/lib/fleet/apply.ts,src/commands/apply.ts.Non-Claude remote/tmux agent sessions now surface with their real id (RUSH-2007).
agents sessions --activeand theagents sessions focuspicker dropped every non-Claude tmux session (codex/gemini/kimi/grok/…) that lacked a launch-minted id, so a liveagents run --device <host> <agent>was invisible and un-refocusable after an SSH drop.listTmuxAgentSessionsnow backfills the id from the deployed SessionStart hook's own per-pid record at~/.agents/.cache/state/sessions/<pid>.json— the CLI previously only read the un-deployed session-tracker path (terminals/sessions/, empty on the fleet). A targeted per-pid read (never a scan of that graveyard dir), freshness-guarded by the launch's known start so a reused-pid record can't cross sessions. Source:apps/cli/src/lib/session/hook-sessions.ts,apps/cli/src/lib/session/active.ts.Releases publish the CI-tested tree, not a drifted merge. On a busy default branch, unrelated PRs merging during a release PR's CI window made the squash-merge tree diverge from what CI actually tested, so
release.shrefused to publish (merged tree != built tree) and the release stalled — every attempt merged a version bump it could never tag. The publish now tags the exact release commit the full matrix went green on (the PR head), letting the intervening commits ride the next release; the merge commit is still tagged when its tree matches (no drift). Thewait_for_ci_greengate is unchanged, so the published tarball is always a tree the full matrix validated. The tree-comparison decision is extracted intoscripts/select-publish-commit.shand unit-tested against a real git repo. Source:apps/cli/scripts/release.sh,apps/cli/scripts/select-publish-commit.sh.Consolidate the observability + inspection commands into one role each; remove
checkandresources(RUSH-1234). Two overlapping command clusters had grown ambiguous.agents check(the CI drift gate) is folded intoagents doctor --check— same drift engine, now with a scriptable exit code.--check --quiet,--check --json(backward-compatible payload: every field the oldcheck --jsonemitted, plus additiveunwiredHookVersions/sourceBehind), and--check --devicesall carry over; the standalonecheckcommand is removed.agents resources(the merged first-wins cross-layer resource table) is folded intoagents view --merged; the standaloneresourcescommand is removed. The observability surfaces (events,feed,activity,output,sessions) now have a documented one-role-each taxonomy —eventsis the raw unified audit stream,feedthe cross-agent decisions/status inbox,activitythe human milestone timeline,outputproductivity accounting,sessionsthe live roster + transcripts. Running the removedagents check/agents resourcesnow reportserror: unknown command. Source:apps/cli/src/commands/doctor.ts,apps/cli/src/commands/view.ts,apps/cli/src/lib/merged-resources.ts,apps/cli/src/lib/startup/command-registry.ts,apps/cli/docs/06-observability.md.agents sessions --activeshows who launched each run (RUSH-2018). New owner column on the active-sessions table (and anownerfield in--active --json), sourced from the resolved actor stamped at spawn into the per-pid registry and onto each teammate record. Displays the actor's short id (an email's local-part) and stays honest — an unresolved local run shows-, never a guessed box owner. The session index (sessions.db) also gains write-onceactor/initiated_bycolumns, kept out of the upsertON CONFLICTset so a content rescan never clobbers the original owner. Source:apps/cli/src/lib/session/pid-registry.ts,apps/cli/src/lib/session/active.ts,apps/cli/src/commands/sessions.ts(ownerLabel),apps/cli/src/lib/session/db.ts,apps/cli/src/lib/exec.ts.agents sessionsattributes historical sessions to a person, and teams carry spawn lineage (RUSH-2019). Each run now writes a durablesessionId -> actorsidecar at spawn (~/.agents/.history/by-session/, unlike the pruned pid registry), and the session scanner joins it while indexing — so the write-onceactor/initiated_bycolumns added in RUSH-2018 populate automatically and the durableagents sessionslisting (not just--active) shows who launched each session. Teammate spawns inherit the orchestrator's frozen actor and now record aparent_session_id(the orchestrator's ownAGENTS_SESSION_ID), so a team traces back to the one human who started it and the spawn chain is walkable. Source:apps/cli/src/lib/session/actor-sidecar.ts,apps/cli/src/lib/exec.ts,apps/cli/src/lib/session/db.ts,apps/cli/src/lib/teams/agents.ts.Actor provenance reaches events, routines, and browser tasks (RUSH-2020). Completes the actor layer's coverage beyond sessions: every emitted event now records
actor+kindthrough the audit origin (soagents eventsstats carry abyActorbreakdown inagents logs stats); a routine stamps its creator's actor id at creation and seeds it into each fired run's env (AGENTS_ACTOR), so an unattended cron's session and events attribute to the person who scheduled it instead ofUNRESOLVED@<host>— its run records gainactor(creator) andtriggeredBy(who kicked off that run); a browser task records theownerwho launched it, on the live task and in history. Source:apps/cli/src/lib/events.ts,apps/cli/src/lib/runner.ts,apps/cli/src/lib/routines.ts,apps/cli/src/lib/browser/{types,service}.ts.agents sessionsnow shows an accurate working / waiting / idle status for every harness, and shows it as text in the default list — not just a glyph. Two gaps are closed. (1) A live non-Claude/Codex agent (grok, droid, gemini, rush, kimi, hermes, opencode, antigravity) used to fall through to a blanketunknownbecausefindSessionFileForKind/computeLiveSignalsonly resolved and parsed Claude and Codex transcripts — a running Codex or grok session displayedunknown. Every tracked harness whose transcript is locatable + parseable is now wired into the same state engine:findSessionFileForKindresolves each kind's transcript through the session index (latestSessionFileForCwd), andcomputeLiveSignalsparses it with that harness's own parser and runs it through the sameinferSessionState, so it gets a realworking/waiting_input/idlethe principled way Claude/Codex do. For a genuinely opaque kind (cursor) or an unreadable transcript,resolveFallbackStatusnow reportsrunningfor any live process — a running agent never displaysunknown(that state is reserved for the sole un-answerable case: a dead process whose transcript vanished mid-read), and a live process is never downgraded to a fabricatedidle. (2) The defaultagents sessionslist (flat, tree, and the project overview) showed only a colored glyph for live rows; it now also prints the status word —working/waiting/idle— next to the glyph, the same three states the--activecolumn shows, withwaitingthe unmistakable "needs you" case. The single-session preview (agents sessions <id> --preview) leads with the same live status line, flagging← needs youwhen the agent is waiting on a question, permission, or plan review. Source:apps/cli/src/lib/session/active.ts(findSessionFileForKind,computeLiveSignals,resolveFallbackStatus),apps/cli/src/commands/sessions.ts(liveStatusWord,flatSessionRow,treeSessionRow,renderSessionPreview).agents sessions injectnow addresses VSCodium / Cursor / VS Code and iTerm sessions, not just tmux. It resolves targets through the same canonical resolver the watchdog uses (resolveInjectTargetForSession), so the manual unblock path and the watchdog agree on which sessions are reachable — and a failed resolve now surfaces the precise reason (host/rail) instead of a misleading "not running under tmux". Source:apps/cli/src/commands/sessions-inject.ts.Watchdog brain focuses on driving idle agents to completion, with context-aware, tool-pointing nudges. The decider prompt now reads the stalled agent's goal first, restates the conclusion it already reached, names the concrete next step (including a tool it forgot it has —
agents computer/agents browser/agents ssh <mac> "agents computer …"), splits do-it-yourself from ask-the-human, and treatsidleas its territory while leavingwaitingprompts to the user's feed. Design + normative spec:apps/cli/docs/watchdog.md,apps/cli/docs/specifications.md#watchdog. Source:apps/cli/src/lib/watchdog/watchdog.ts.
1.20.80
agents activitygoes fleet-wide, grouped, and session-enriched. The activity lane was a flat, local-only, newest-first list; it now shows progress-so-far across the whole fleet — who did what, where, on which project, for which ticket. New flags:--devices-all(alias--hosts-all) fans the sameactivity --jsonpayload out to every reachable device (feed-style, viagatherRemoteAgentsJson) and merges each peer's stream host-tagged;-H/--host/--devicescope to specific boxes;--localforces local-only (still the default).--group-by project|device|agentbuckets the stream (e.g. per project, what each agent did and for which ticket) and--filter <text>narrows by project/device/agent/event/ticket. Each item is enriched by JOINING to live sessions — the resolved project (repo/worktree slug from cwd), the execution host (provenance.host), and the Linear ticket (ActiveSession.ticket) — never by re-parsing transcripts. Milestone tiering (--milestones) and the default collapse are unchanged, and--jsonstays a mergeable per-host payload (now carrying the enriched fields). Source:apps/cli/src/commands/activity.ts,apps/cli/src/lib/activity.ts(enrichActivityEvents,mergeActivityEvents,parseActivityPayload,groupActivity,filterActivityEvents,projectFromCwd).Add
agents set— a short front door for per-version run defaults.agents set [email protected] --model opus-5pins the default model (and/or--mode) thatagents runuses for that agent version. It reads and writes the same store asagents defaults run set(agents.yaml->run.defaults), so the two stay consistent. Bareagents setlists every default;agents set <selector>shows one. Source:apps/cli/src/commands/set.ts.agents doctornow reads as a triaged health report, not neutral status. The verdict was terse status text ("Verdict: 1 divergent, source ~/.agents 16 commits behind…") a user had to decode. It is now a severity-ranked health block that leads with what is unhealthy, why it matters, and the exact fix — one row per finding, tagged with a restrained terminal glyph (✓✗⚠and a subtle info dot, colored via chalk to match the man-page voice):[email protected] ✗ unhealthy — 3 issues (1 critical · 2 warnings) ✗ critical ask-user-question-guard — on disk but not wired into settings.json; the hook never fires → agents sync [email protected] --yes ⚠ warning ~/.agents — 16 commits behind origin/main; you're running stale config → agents repo pull user ⚠ warning 11-activity-log — differs from source → agents doctor [email protected] --fix heal what's auto-fixable: agents doctor [email protected] --fixA clean install collapses to one green line —
✓ healthy — 34 resources reconciled · hooks wired · sources current. Each finding carries an agent-agnostic severity: critical (silent breakage — an unwired hook, a missing/unparseablesettings.json, a MISSING resource), warning (stale/drift — a source layer behind origin, a DIVERGENT resource, a stale/never-synced version), or info (an orphan/EXTRA resource →agents prune cleanup). Both surfaces get the same treatment: the target reportagents doctor <agent>@<version>and the bareagents doctoroverview, which now opens with aHealthbanner aggregated across every installed version. The existing per-resource detail rows are kept — the health block layers on top of them as the verdict.--jsongains averdictfield (target mode) and ahealthfield (overview), each carryingseverity/category/subject/impact/fixper issue; the existingsummary/kinds/hookWiring/sourceBehind/sync/orphansfields are unchanged. Source:apps/cli/src/commands/doctor.ts(computeVerdict,computeOverviewHealth,healthBlockLines,renderHealthBlock,verdictIsAutoFixable).The daemon's
MenubarHelper --notifyone-shots can no longer pile up in the menu bar. Each routine notification (start/finish/overdue/heal) spawned a fresh, detached, unsupervisedMenubarHelper --notifyprocess; on a stalled delivery — a locked screen or a WindowServer/XPC hiccup — the helper's runloop spin never reached its deadline and the process hung indefinitely, so duplicate "Agents" instances accumulated. The one-shot is now bounded by two independent watchdogs:runOneShotarms a background-thread force-exit at 3s (off the main queue, so a wedged main thread can't starve it — unlike the 0.6s runloop deadline it backs up), and the Node spawner (spawnDetachedQuiet) SIGKILLs the child at 4s if it never self-exits. A notifier that posts normally (the common sub-second path) is untouched; only a genuinely hung one is killed. Source:apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift(Notifier.runOneShot),apps/cli/src/lib/menubar/notify-desktop.ts(spawnDetachedQuiet,NOTIFY_TIMEOUT_MS).Removed the dead
commitOwnDeviceMetaauto-commit from the pull path. It committed this machine'sdevices/<host>/agents.yamlpin snapshot to the user repo'smainon nearly everypullRepo, without pushing — somaindiverged N-ahead per machine and wedgedagents syncacross the fleet. Now that per-device pins are gitignored (they are local runtime state — written bywriteMetaUnlocked, read on-disk by pinned-strategy resolution and the shim), the function only ever no-ops, so it and its solepullRepocall are deleted along with their tests.--strategy balancednever read pins; the only behavior removed is the never-reached auto-commit. Source:apps/cli/src/lib/git.ts(pullRepo),apps/cli/src/lib/git.test.ts.Remove Forge and hard-deprecate Gemini (RUSH-2060). ForgeCode is no longer an
AgentId, install target, resource-sync target, subagent target, MCP target, or permissions target. Gemini remains a legacy id so existing sessions/config can still be read, but it is no longer a managed harness:agents add gemini,agents import gemini, andagents sync gemininow fail and point users to Antigravity. Gemini is also excluded from capability-driven resource writers, staleness detectors, import choices, teams choices, model choices, fleet auth sync, and plugin/MCP/permissions/subagent sync. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/types.ts,apps/cli/src/lib/capabilities.ts,apps/cli/src/commands/{versions,import,sync}.ts, and the resource writers underapps/cli/src/lib/. (RUSH-2060)Every agent secret access and unlock is now captured in the raw event stream and the audit log. After the recent relaxation that lets agents read and unlock bundles more freely,
agents events --module secretsnow surfaces two typed, value-free events for the complete access picture.secrets.getrecords every path that resolves a secret VALUE out of a bundle (run --secrets,secrets exec/export,view --reveal, rawget <item>,sync push, remotebundle@host), and a newsecrets.unlockedrecords the deliberateagents secrets unlockgrant into the broker/durable session — the longer-lived grant a per-read event does not capture, carrying its TTL and the harness scope it was granted to (*= global). Every record is tagged with the resolvingagentscope and lands at audit level in the append-only~/.agents/events.jsonlaudit trail, but is non-milestone so it does not clutteragents activity/agents feed. The resolved value is never written — only bundle name, key NAMES, and counts. All value-read/unlock audits now funnel through one canonicalemitSecretAudithelper (apps/cli/src/lib/secrets/audit.ts), wired intolib/secrets/bundles.ts,commands/secrets.ts(reveal / raw get / unlock),lib/secrets/sync.ts, andlib/secrets/remote.ts; the new event type is registered inlib/events.ts. Source:apps/cli/src/lib/secrets/audit.ts,apps/cli/src/lib/events.ts,apps/cli/src/commands/secrets.ts.
1.20.79
A daemon bounce no longer orphans the secrets broker. Installing agents-cli ran
stopDaemon(), which sent SIGTERM, scheduled its hard-kill escalation on asetTimeout, and cleared the daemon pid file immediately — without waiting for the process to actually exit. In a short-lived process like the npm postinstall the timer never fired at all, and the cleared pid file madeisDaemonRunning()report false, sostartDaemon()launched a second daemon alongside the live one. Its hosted broker then found the socket in use, missed one 700ms ping against the busy owner, unlinked the live socket and rebound — leaving the first broker running with every unlocked bundle in RAM that no client could reach. On a machine with two installs (nvm + homebrew) this reproduced on every upgrade:lsofshowed two processes on one socket path at two different kernel socket addresses.stopDaemonnow waits for the process to actually stop serving before clearing the pid file (and escalates to a tree-kill only if it does not), andbindBrokerSocketprobes an in-use socket several times and refuses to reclaim it while a live process still owns the broker pid file, surfacing a clear error instead of silently starting a second broker. A zombie counts as exited — it holds no socket — so a daemon that is the caller's own child is not hard-killed after it has already gone. The socket owner is recorded in a dedicatedagent.ownerfile rather than the standalone service'sagent.pidsingle-instance claim, so the signal is present for the daemon-hosted broker (the primary configuration) without making a losing standalone service exit into a launchd restart loop;ensureAgentRunning's one-off fallback andteardownStaleBrokeralso wait for a broker to stop serving before unlinking its socket and ownership record, instead of destroying the evidence the check depends on. Source:apps/cli/src/lib/platform/process.ts(waitForExit,hasExited),apps/cli/src/lib/daemon.ts(stopDaemon),apps/cli/src/lib/secrets/agent.ts(ownerPath,brokerPidAlive,releaseBrokerPid,bindBrokerSocket,ensureAgentRunning,teardownStaleBroker).agents doctornow checks hook WIRING, not just hook files — and treats a stale source layer as unhealthy. Two blind spots let a version home read "healthy" while its hooks were dead. (1) Doctor only compared hook FILES against source, never thatsettings.jsonactually references each hook in the right event array — so a hook whose script was byte-identical to source but never wired intoPreToolUse/Stop/… reportedokand silently never fired (reproduced onyosemite-s1:[email protected]printedhooks 32 items 32 okwhile itssettings.jsonPreToolUse array omittedask-user-question-guard.sh). Doctor now inspects the version's nativesettings.json(Claude-family: claude, droid), verifying each hook per(event, matcher)group, and reports a present-but-not-wired hook asUNWIRED <hook> event=<event> matcher=<matcher>, counted against the verdict; a missing/unparseablesettings.jsonis surfaced too.--fixre-wires via the sameregisterHooksToSettingspathagents syncuses. (2) A source layer behindorigin/mainmeans the home is reconciled against stale truth, yet the "N commits behind" fact was a buried preamble while the verdict still said healthy — it now flips the per-version verdict to unhealthy with theagents repo pullremediation. Both checks run in every mode, not justagents doctor <agent>@<version>: bareagents doctor(overview) and the CI gateagents checknow flag a present-but-unwired hook and a behind-origin source layer, andagents checkexits non-zero on them. Source:apps/cli/src/lib/hooks.ts(checkVersionHookWiring),apps/cli/src/lib/drift.ts(checkSyncStatus/computeSourceBehind/computeDrift),apps/cli/src/lib/doctor-diff.ts,apps/cli/src/commands/doctor.ts(computeVerdict),apps/cli/src/commands/check.ts,apps/cli/src/lib/git.ts(commitsBehindUpstream).agents repo pullreconciles a diverged repo instead of wedging on it. It rangit merge --ff-only, which refuses any divergence — conflict or not — so a single local commit permanently blocked every later pull with nothing actually in conflict. SincepullRepoitself auto-commits the machine's owndevices/<host>/agents.yamlbefore pulling, every device eventually created that commit and stopped receiving updates: on one fleet, nine machines sat 9 commits behind and merged rule changes never reached any of them. It now rebases, which is what its own documentation has always described. Per-device paths are disjoint, so they replay cleanly. A genuine conflict aborts the rebase and rolls the checkout back untouched, so a failed pull can never leave the repo detached, mid-rebase, or with conflict markers in live config; a rebase already in progress is reported as itself rather than as a dirty tree.agents repo pull/pushexit non-zero when a repo fails. Both printed a failure line and returned 0, soagents fleet run "agents repo pull user"reported11 okacross a fleet that pulled nothing. Any automation gating on the exit code read a total no-op as success. Matchesagents sync <repo>, which already did this.agents repo statusreports across the fleet. New--devices-all(alias--hosts-all) fansrepo status/repo listout to every reachable device and renders one aggregated table (device · repo · sync · changes);--devices <who>(alias--hosts) takesallor a comma-separated device list. Unreachable peers are skipped with a clear marker, never failing the command, and a single--device/--hoststill streams that one box as before. Source:apps/cli/src/commands/repo.ts.Routines now always authenticate as the machine they run on, never on an inherited Claude token. The daemon was already forbidden from injecting a Claude OAuth token into a routine, but nothing stopped it inheriting one:
buildExecEnvspreads the ambientprocess.env, andsanitizeProcessEnvonly strips loader/interpreter variables, never credentials. So on any box whose daemon environment happened to carryCLAUDE_CODE_OAUTH_TOKEN— a provisioned fleet machine, a shell that exported it — every routine spawn silently ran on that one shared, rotating token instead of the host's own login. That is the fleet-wide-logout path the no-token design exists to prevent, reached by inheritance rather than injection: when the server rotates a refresh token, every other holder drops to "run /login". No CI runner has a token to inherit, so the existing test passed everywhere and the leak only appeared on a real machine (it surfaced on the release VM, halting a release).buildRoutineSpawnEnvnow drops the variable, and the routine still authenticates exactly as before —CLAUDE_CONFIG_DIRis pinned to that box's per-account version home, so a routine uses whatever agent login is set up there and needs no token of its own. Source:apps/cli/src/lib/runner.ts(buildRoutineSpawnEnv).The
dailysecrets policy is now calledhold, because it was never daily. The default prompt policy holds a bundle forsecrets.agent.holdMs— 7 days out of the box — yet it was nameddaily, soagents secrets policy --helpread as "you will be asked once a day" while the code comment beside it said "one Touch ID per ~7d". Both the CLI help anddocs/secrets.mdhad resorted to apologising for it in prose ("Name is historical", "Despite the name, it is not tied to one calendar day"), which is a name stating something false, not a name that is merely unclear. It is not one day, not one session, and not any fixed period — it is the configured hold window, so it is now named for that.dailyand the wire tokensessionremain accepted everywhere (agents secrets policy <bundle> daily,secrets.policy: dailyin agents.yaml, and thetier: sessionkey already written into every bundle on every synced machine), so no config or stored bundle changes behaviour on upgrade. One machine-readable surface does change:agents secrets list --jsonandagents secrets view --jsonnow report"policy": "hold"where a default-tier bundle previously reported"daily". Anything matching on that string needs updating — the CLI keeps acceptingdailyas input, but it no longer emits it, because a JSON field that reports a name the CLI itself has retired is a worse trap than a one-line change. The help text now also states what the tier actually depends on: the hold is a property of the running broker plus the durable session, not of the stored keychain item, so a broker that is down degradesholdto prompt-every-read; onlyneveris prompt-free independently of the broker. Source:apps/cli/src/lib/secrets/bundles.ts(SecretsPolicy,parsePolicy,secretsDefaultPolicy),apps/cli/src/commands/secrets.ts(parsePolicyOpt,policycommand help),apps/cli/src/lib/secrets/index.ts(legacy token mapping for the signed helper),apps/cli/docs/secrets.md.Claude usage/probe reads can authenticate with a file-based setup-token instead of the login keychain — no Touch ID. On macOS, reading a Claude account's usage went through Claude Code's ACL-bound
Claude Code-credentials-<hash>keychain item (loadClaudeOauth→/usr/bin/security), popping a Touch ID sheet on every cold read — per account, roughly every 8h, and again on the routines daemon's 3-minute auth-health probe (probeLocalFleetAuth), soag viewand the background warm both prompted.loadClaudeOauthnow first resolves a per-accountclaude setup-tokenfrom the reserved file-basedauthsecrets bundle (keyed by account email asCLAUDE_CODE_OAUTH_TOKEN_<slug>); when present, the usage endpoint is authenticated with that long-lived, non-rotating token and the keychain is never touched — killing the prompt. This applies only to the read-only usage/probe callers (accessTokenCache); the full-credential run/export path (which needs the refresh token) is unchanged, and an account with no provisioned setup-token still falls through to the keychain for now. Keyed strictly per-account (never a bare shared key) so one account's token can't be misapplied to another. Source:apps/cli/src/lib/usage.ts; design:docs/credential-management.md.
1.20.78
agents sessions <uuid>now resolves a remote session exactly, across the fleet. A full session id absent from the local disk used to fall back to an FTS content search — and because a UUID appears verbatim in other sessions' transcripts (a watchdog/continue <uuid>reference), that surfaced a list of unrelated "matches" instead of the one session, which actually lived on another machine. A UUID is now treated as an identifier: on a local miss the CLI fans the id lookup out to the online fleet (the existinggatherRemoteListSSH sweep), and when exactly one machine holds it, renders that session's summary from the owning peer viarunOnPeer(instead ofSession transcript not available). Same id on more than one box surfaces a machine-labeled conflict to disambiguate with--device <host>; a UUID found nowhere prints a clear "no session on this machine" message. There is no fuzzy/content fallback for a UUID anywhere — the peer's--jsonanswer id-resolves too, so a content mentioner can never masquerade as the session.--localstill restricts the lookup to the local machine, and a peer already answering a parent's sweep (AGENTS_SESSIONS_LOCAL=1) never re-fans-out. Source:resolveSessionAcrossFleet/fleetHitsById/shouldFanOutForIdinapps/cli/src/commands/sessions.ts(wired intorenderOneSession), and the id-only--jsonresolution at the sessions listing seam. (RUSH-2024)agents doctor --devicesnow detects cross-device harness divergence (RUSH-2027). The umbrella fleet diagnostic compares each registered device's installed harness inventory — resources (commands, skills, hooks, rules, mcp, permissions, subagents, plugins, promptcuts, workflows), per-agent installed versions, and.agents/.systemconfig-repo state (branch, HEAD, dirty) — against the local machine as the baseline, and flags anything present on one box but missing on another. A plugin likeswarminstalled onzionbut absent onyosemite-s0now surfaces as a clear warning (yosemite-s0 is missing plugin 'swarm' (present on zion)) instead of only being discovered at runtime asUnknown command: /swarm:run. Agent-version gaps (yosemite-s0 is missing [email protected]) and diverged config repos are reported too. Read-only by default — it never installs or syncs;--jsoncarries a stablefleetdivergence block for the VS Code extension to consume.agents fleet statusgained the same per-device divergence warning in its rollup. Every device's top-leveldoctor --jsonnow emits afleetinventory field so the comparison needs no extra probe. Source:apps/cli/src/lib/devices/fleet-divergence.ts(comparator),apps/cli/src/lib/devices/fleet-inventory.ts(collectLocalFleetInventory),apps/cli/src/commands/doctor.ts(runDevicesDoctor,renderFleetDivergence,--jsonfleetfield),apps/cli/src/lib/devices/health-report.ts(buildFleetHealthReportdivergence warning),apps/cli/src/lib/git.ts(readRepoState).agents doctornow shows repo-behind notices; they no longer appear on stderr during normal commands (RUSH-2048).printPendingUpdateNotices()— which wrote "agents-cli: ~/.agents/ is N commits behind origin/main" to stderr on every CLI invocation — is replaced byreadRepoBehindMarkers(), which returns the same data without printing.agents doctorreads these markers and renders a "Repo updates" section showing which repos are behind and theagents repo pull <alias>fix command.agents doctor --jsonemits areposarray so menubar helpers and other consumers can read the same data. Markers persist on disk until the next background fetch overwrites them, so the notice stays visible until the user acts. Source:apps/cli/src/lib/auto-pull.ts,apps/cli/src/commands/doctor.ts,apps/cli/src/index.ts.Session affinity data + host affinity resolver (RUSH-2049). Sessions index persists
machine(schema v18) so affinity canGROUP BY machine.queryAffinityRollupreturns launch counts by device (and harness/joint for analytics). Host affinity sampling lives insmart-launch.tsasresolveDeviceAffinity/applyDeviceAutoToOptions(weight ∝ launches^α; online hosts with no history still explore at weight 1). Account pick stays the existing balanced strategy (live session/week rate-limit windows). User-facing host pick shipped as--device auto/--host autoin RUSH-2059 (not a public--smartflag and not harness auto-pick). Source:apps/cli/src/lib/session/db.ts,origin-machine.ts,smart-launch.ts.agents sessions --allnow widens every non-status filter, not just the directory (RUSH-2055).--allused to only drop the current-project scope; it now also drops the 30-day window cap, so one flag means "all values for every non-status filter" — all directories AND all time.--activestill composes as a status filter, and-a/--device/--sincestill narrow their own axis (an explicit--sinceoverrides the all-time default). Applies to both the bare listing and--active. Source:apps/cli/src/commands/sessions-browser.ts.Device affinity is
--device auto(not--smart/ harnessauto) (RUSH-2059). Host pick from 14d usage affinity is a special value on the existing host flags:agents run claude --device autoor--host auto. The harness is always the agent you type — never auto-selected. Deprecated hidden--smartmaps to--device autofor one release. Extension New Agent unpinned launches use--device auto. Banner:device=auto → <host> (affinity …) · accounts=balanced. Source:apps/cli/src/commands/exec.ts,smart-launch.ts,apps/factory/src/core/agents.ts.agents sessions --activeno longer hides most of your running sessions. On a TTY,--activeopens the interactive browser, and the browser resolved "running" two ways that both dropped live sessions: its live scan called the local-onlygetActiveSessions()instead of the fleet sweep the static view uses, and it treated running as an intersection with the transcript index (pool.filter(r => live.has(r.id))) rather than a source of rows. Together they meant every session on another machine was invisible, as was any local one the index didn't already carry — a fleet with 32 live sessions across 7 machines showed 4. The browser now shares one gather with the static view (gatherActiveSessions) and folds live sessions the index lacks in as their own rows, keyed by session id, cloud task id, ormachine:pidso two id-less sessions never collapse into one. Picking a row that has no session id yet reports where the process is instead of trying to open a transcript that doesn't exist. Source:apps/cli/src/commands/sessions.ts(gatherActiveSessions,isIdlessLiveRow),apps/cli/src/commands/sessions-browser.ts(liveRowKey,indexLiveRows,liveSessionToMeta,mergeLiveIntoPool).The session browser now shows which program each running session is in. A new host column names the terminal or editor hosting the session —
codium,ghostty,tmux, ortmux→ghosttywhen a tmux session is being watched through another app (a baretmuxmeans it is running detached) — so a session in the list can actually be found. The column is live-only and appears just in the running view, since transcript metadata carries no host. The id column also truncates now, so a row named by a 7-digit pid can no longer shunt every later column out of alignment. Source:apps/cli/src/commands/sessions.ts(liveHostLabel,formatPickerLabel,PickerColumns.showHost).The routines daemon holds no Claude credential and injects no token. A scheduled or daemon-fired Claude run now authenticates exactly like an interactive
agents run claudeon the same machine: through the rotation-pinned account's ownCLAUDE_CONFIG_DIRlogin (.credentials.json), which Claude Code refreshes per-device. The daemon previously read a token from theclaudesecrets bundle and injected it into every routine spawn — first as one ambientCLAUDE_CODE_OAUTH_TOKEN(RUSH-1759), then also as per-accountCLAUDE_CODE_OAUTH_TOKEN_<account>setup-tokens — which shadowed each account's own on-disk login and made the daemon a second, competing credential store. Both paths are removed, along with the sandboxENV_ALLOWLISTentry that forwarded them; a sandboxed routine now stripsCLAUDE_CODE_OAUTH_TOKENfrom its environment and falls through to the per-account login. A box whose interactive login has expired is skipped up front by the auth-health preflight with are-login requiredhint instead of running on an injected fallback — log in once on that box (agents run claude) to restore it; no daemon restart is needed. This keeps the daemon out of the credential entirely, which is what avoids the fleet-wide rotation logout (a shared/injected token was the cause, not the fix). Removed:readDaemonClaudeOAuthToken/readDaemonClaudeBundleEnv/buildDetachedDaemonEnv(daemon.ts),resolveAccountSetupTokenandapps/cli/src/lib/secrets/account-token.ts,claudeHomeHasOwnCredential(agents.ts). Source:apps/cli/src/lib/daemon.ts,runner.ts,sandbox.ts,agents.ts.agents sessionsno longer over-counts test results from arbitrary stdout. The catch-up digest scraped any\d+ pass-shaped substring anywhere in a command's output, so a442 passwords generatedlog, agit status: 442 filesline, or a442 passes/secbenchmark was reported asTests ✓ tests 442 pass. It also treated any command merely containing a runner token as a test run, so npm-script sub-targets likebun test:setup,npm run test:watch, orpnpm test:ciwere counted. Test-run classification now matches only real invocations (bun/npm/yarn/pnpm testbare,vitest,jest,mocha,pytest,go test,cargo test,tsc) and rejects:sub-targetscripts, and pass/fail counts are read only from each runner's authoritative summary construct — vitest'sTests N passed/Tests N failed | M passedrow, jest'sTests:line, pytest's=== N passed[, M failed] in Xs ===rule, bun'sN pass/N failblock closed byRan N tests, and mocha'sN passing/N failing. A verdict is reported only when a real summary matched, so an ambiguous blob now shows nothing instead of a fabricated pass count. Source:apps/cli/src/lib/session/digest.ts(TEST_RUNNERSclassification with(?![:\w-])guard, newparseSummaryLine,parseTestOutput); consumed byapps/cli/src/lib/session/render.ts(renderTestsLine) andapps/cli/src/commands/sessions-picker.ts.Readable
Dirs:line inagents sessions. The session preview's touched-directories line no longer renders raw Claude project-slugs (-home-me--agents-…) or nested worktree paths. Paths under a git worktree collapse to⧉ <slug>/<remainder>; a Claude project-slug is matched in slug space (its cwd/.-encoding is lossy, so it is never decoded to a fake path) — a slug worktree shows⧉ <name>and a slug pointing at the session's own cwd (internal projects-storage scratch) is dropped; real paths still relativize against the session cwd and home (~). Source:apps/cli/src/commands/sessions-picker.ts.release.shis now a zero-config, self-routing release — runnable from any fleet box with an empty environment. No routing/secret environment variables:SIGN_HOST,SECRET_HOST,SIGN_HOST_REPO,FORCE_REMOTE_SIGN, thePREFERRED_SIGN_HOSTSlist, thezionfallback, and theagents devicesfleet discovery are all gone. The release has three self-selected homes: git/gh orchestration on the invoking box, the Linux test suite on a dynamic crabbox (scripts/sandbox.shselects an available Hetzner VM for the repo's.crabbox.yamlprofile or warms one — never a hardcoded instance), and build + sign + notarize +npm publish+ computer-helper on themac-minihome base (the one hardcoded name,RELEASE_HOME_BASE). The script detects its own host (scutil --get LocalHostName/hostname -s) and runs the privileged phase on the home base — locally if already there, else over ssh — always by checking out thev<version>tag into a throwaway worktree and running that worktree'srelease.sh --home-base-phase, so the publishing script is the one carried by the release tag, never the home base's stale on-disk checkout; the worktree is removed on exit on success or failure. The npm token is resolved on the home base and never borrowed to the trigger box. A new sharedscripts/headless-sign-context.shfactors the headless keychain-unlock +AGENTS_SECRETS_PASSPHRASEpreamble (no Touch ID) used by both the on-home-base publish andremote-sign-mac.sh. A phase tracker ([n/N], N=6 for a normal release, 4 for a catch-up publish) labels each phase with the box it runs on and a ✓/✗ result; a crabbox test failure prints the failing tests + the captured log path and halts before any PR/publish. Idempotency/catch-up/tree-verification guards are preserved. Source:apps/cli/scripts/release.sh,apps/cli/scripts/remote-sign-mac.sh,apps/cli/scripts/headless-sign-context.sh.agents secrets unlocknow grants globally, so one Touch ID actually covers everything. An unlock was silently scoped to the ambientAGENTS_AGENT_NAME: typed in a plain shell it was stored under a literalcliharness, while a read from inside an agent looked under its harness (claude,codex, …). The two never met, so a valid 7-day grant was invisible to every agent for its whole life —agents secrets exec <bundle>reported "not unlocked in the secrets agent" while the bundle sat unexpired in the store, and each miss cost another Touch ID or blocked a headless run outright. An unlock with no--foris now a global grant that every harness and a plain shell can read;--for <agent>still narrows it to one harness, and readers resolve own-harness → global so a narrow grant wins where it applies. The broker's in-memory store and the durable session store share one scope chain, so behavior is identical before and after a daemon restart. Grants already written under the oldcliscope migrate to global on the next broker start — an unlock you already paid Touch ID for keeps working across the upgrade instead of going unreadable. Source:apps/cli/src/lib/secrets/scope.ts(GLOBAL_HARNESS,bundleScopeChain),apps/cli/src/lib/secrets/agent.ts(gethandler),apps/cli/src/lib/secrets/session-store.ts(resolveSession,cli→global migration),apps/cli/src/lib/secrets/bundles.ts(readAndResolveBundleEnv),apps/cli/src/commands/secrets.ts(unlock --for).agents sessions export <id>now resolves a short id the same waysessions <id>does — by id only, never fuzzy content. The id-only fix landed for thesessionsview butsessions exportstill gated its index lookup onisCompleteSessionId, so a bare hex short-id liked3470b57absent from the discovered pool skipped the index and fell through to the text query — bundling every transcript that merely MENTIONED the id into the export. The one canonical id-shaped test,looksLikeSessionId, now lives besideisCompleteSessionIdinlib/session/discover.tsand is shared:sessions exportresolves any id-shaped selector through the index (exact -> prefix ->findSessionsById) and reports "No session with id …" on a miss instead of shipping the mentioner. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/commands/sessions-export.ts.Resolve a Claude transcript across every version home, not just the live
~/.claude. A session launched under an earlier agent version keeps its transcript under that version's home; resolving only the~/.claudesymlink (which repoints to the newest installed version) meant that installing a new version silently hid every still-running older-version session — nosessionFile, soagents sessionsrendered itunknownand the watchdog skipped it as "no activity timestamp".findClaudeSessionFilenow searches all version-home project roots viagetAgentSessionDirs, newest mtime winning. Source:apps/cli/src/lib/session/active.ts.
1.20.77
Interactive
agents run --hostnow tracks the real session for every agent, not just Claude. Codex, Kimi, Grok, and Gemini coin their own session id and reject a caller-supplied one, so an interactive host run of any of them showed a stale/absent id locally —agents sessionscouldn't surface it and a dropped link couldn't auto-reconnect it (RUSH-2033 fixed only the Claude--session-idpath). The launcher now forwards one correlation key it controls (AGENT_LAUNCH_ID); the remoteagents runadopts that key (resolveLaunchId), so its SessionStart hook records the agent's real session id under it. After the stream the launcher does one ssh read of the remote hook record, resolves the real id by launch id (resolveRemoteSessionId/pickRemoteSessionId), registers it in the local session index, and reconnects against it on a dropped link. Claude still forces its own id up front and is unchanged. Source:apps/cli/src/lib/hosts/remote-session-id.ts,resolveLaunchIdinapps/cli/src/lib/exec.ts, and the interactive--hostbranch inapps/cli/src/commands/exec.ts. (RUSH-2034)Project routines can opt into daemon firing with source tracking, sync, and host placement (RUSH-2035). Project YAML under
<project>/.agents/routines/*.ymlstays inspection-only untilagents routines enable-project(with interactive /--yesapproval) records the project onmeta.routines.projectsand materialises copies into~/.agents/routines/with asource:block (projectPath, gitrepo/branch/commit).agents routines sync(and daemon start/SIGHUP reload) refreshes those copies when project YAML changes;disable-project/projectsmanage the allowlist. NewhostStrategy: local|host|fleet|cloud(CLI--placement) chooses where the job body runs: local, a named--run-onhost, one online fleet device per fire (no cross-device double-fire — off-box strategies auto-pindevices), or the agent's native cloud provider.--hostremains the remote- management passthrough. List/JSON surfaces source repo/branch and strategy. Source:apps/cli/src/lib/routines.ts,routines-project.ts,routines-placement.ts,runner.ts,daemon.ts,commands/routines.ts,docs/03-routines.md.Factory interactive launches default to
--mode auto(RUSH-2038). The Factory VS Code extension no longer inherits the CLI'splandefault for interactive terminal launches. Codex, Claude, Gemini, Cursor, OpenCode, and Antigravity now start inauto(writable-but-gated) when opened from Factory without an explicit mode, so the agent can edit files instead of stalling in a read-only sandbox. Source:apps/factory/src/core/agents.ts.Codex approval blocks now notify you. A headless or terminal Codex agent blocked on an approval prompt used to stall silently — the feed/notification path only fired for Claude. Codex emits
PermissionRequest(not Claude'sNotification), which thefeed-publishhook now handles: it publishes an approval-class block with a high cost-of-delay and adenysafe-default, so the blocked agent surfaces onagents feedandagents feed --dispatchpages the phone as urgent. A Codex approval card clears once the approved tool runs, via a matcher-lessPostToolUseclear hook registered for Codex only — so Claude's card lifetime (itspermission_prompt/idle_prompt/elicitation_dialognotification blocks persist untilStop/SessionEnd) and per-tool overhead are exactly as before. The other feed hooks are now registered for Codex too, not Claude only. The Factory extension bridges the same waiting state to an edge-triggered VS Code notification with a "Focus terminal" action. Claude's path is unchanged. Source:FEED_PUBLISH_HOOK_SCRIPT/ensureFeedPublishHookinapps/cli/src/lib/feed.ts,apps/factory/src/core/waitingNotifier.ts. (RUSH-2039)agents fleet pingnow completes within ~15 s per device and ~30 s total, even when several fleet devices are offline or slow (RUSH-2041). The per-device remote auth probe timeout was lowered from 60 s to 15 s (matching thefleet statusversion-probe budget, which is enough for the ~8 s provider-fetch inside the local auth probe).fanOutDevicesgained an optionalperDeviceTimeoutMsthat races each probe against a deadline and records it asfailed: timed outinstead of hanging.runFleetPingnow also wraps the entire fan-out in a 30 s hard cap so the command can never outlast a reasonable budget. Offline devices are now reported promptly as failed/timed-out rather than left hanging in the spinner. Source:apps/cli/src/lib/devices/fleet.ts(fanOutDevices,FanOutDeviceOptions),apps/cli/src/commands/ssh.ts(probeRemoteAuth,runFleetPing).agents sessionssurfaces checklist progress in every list/preview (RUSH-2045). The picker preview,--activerows (local + cross-machine), flatdoingcell, and metadata-only previews now show compact✓done/total · current stepfromSessionMeta.todos/ActiveSession.todos, plus the originating prompt and a directories-touched activity line. Active/cross-machine rows also show label + clickable project/ticket alongside the agent short id. Covers interactive, headless, teams, and sub-agent sessions that share the preview infra. Source:apps/cli/src/commands/sessions-picker.ts,apps/cli/src/commands/sessions.ts.Checklist completions emit a feed event (RUSH-2046). When an agent marks a task-checklist item done, the
11-activity-log.pyhook now appends atask.completedmilestone to the session activity log (andchecklist.createdthe first time a checklist appears), so completions show inagents feedand the unifiedagents eventsstream with the item subject and runningN/M. Detection folds the transcript across harnesses — ClaudeTaskUpdate/TodoWrite, Groktodo_write, Codexupdate_plan— so a completion is recognized regardless of which agent produced it. Source:apps/cli/src/lib/activity.ts(incl. the embedded hook),apps/cli/src/lib/events.ts,apps/cli/src/commands/feed.ts.Actor provenance now survives the SSH hop. A run dispatched to another host (
agents run --host, a remoteagents teamssupervisor, or any--hostpassthrough) used to drop the resolved actor at the SSH boundary, so the remote re-resolved it from the originating box'sSSH_CONNECTIONand mis-credited the work to the shared machine orUNRESOLVED@<host>. The dispatch layer now forwardsAGENTS_ACTOR*/GIT_*across the wire (POSIXexportand Windows$env:alike), so the remote inherits the origin identity instead of re-resolving. A caller-supplied env value still wins on collision (mirrorsbuildExecEnv). Source:withActorEnvinapps/cli/src/lib/hosts/dispatch.ts, wired intolaunchDetached/runInteractiveOnHostand the--hostpassthrough. (RUSH-2028)A Linux-driven release now auto-discovers its macOS sign host instead of hardcoding
mac-mini.scripts/remote-sign-mac.shpreviously defaultedSIGN_HOSTtomac-mini, so a release from a Linux box failed outright whenever that one appliance was offline — the recurring reason a release stalled and a human had to finish it by hand. WithSIGN_HOSTunset the script now readsagents devices list --json, keeps the reachable/online macOS devices, and picks the first that answerssshin preference ordermac-mini→zion→ any other online Mac.mac-ministays first because it signs headlessly (no Touch ID);zion(the interactive Mac) is the fallback. An explicitSIGN_HOST=<host>still pins one and skips discovery, and when no reachable Mac qualifies the script fails with the ordered list it tried rather than hanging on a dead host. Source:apps/cli/scripts/remote-sign-mac.sh.An agent launch never raises a Touch ID sheet. On macOS, starting an agent terminal or firing a routine could pop several biometric prompts in a row, because each keychain read runs in its own helper process and the biometric assertion never reuses across processes. Two causes:
interactiveUnlockdefaulted to true whenever an agent name was present, which let an agent-initiated read fall through theagentOnlyguard; andisHeadlessSecretsContextrecognized theheadlessandteamsruntimes but notterminal, which is what an interactive run sets. Agent launches now resolve broker-only and a locked bundle fails fast namingagents secrets unlock <bundle>. Direct read commands use the same broker-only path even from a plain shell; only an explicit unlock may authenticate. This narrows the agent-triggered approval added in RUSH-2032, which is unreleased.release.shnow borrows the npm token from a primary device when the local box has none, so a Linux-driven release stops asking a human to approve a token. Token resolution was env → localnpmjs.combundle → die. On a fleet box whose own keychain holds no npm token, that dead end pushed agents to hand-move a credential between machines (and correctly get gated on it). A third step now resolves the bundle ephemerally from a primary device over SSH —agents secrets exec npmjs.com --host <host>, which resolves on the remote and injects into the run only, never storing the token locally. It triesSECRET_HOSTfirst, thenzion, thenmac-mini, and fails with the list it tried if none answer. Combined with the sign-host auto-discovery, a Linux box can now cut a full release end-to-end given a reachable Mac for signing and any reachable device that holds the npm token. Source:apps/cli/scripts/release.sh.Branded, actionable daemon notifications on the routine lifecycle (RUSH-2030). Daemon desktop notifications (overdue routines, config heal, the no-credential warning) now route through the
MenubarHelper.appcompanion instead of raw AppleScript, so they carry the agents-cli mark rather than the generic Script Editor icon; they degrade toosascript/notify-sendonly when the helper is not installed. The daemon also notifies when a routine starts and finishes (success/failure, with the report's first line or the error reason folded in), suppressing command-housekeeping start/success pings to avoid spam. Clicking a finish notification opens the run report/log; start/overdue open the runs folder. Source:apps/cli/src/lib/menubar/notify-desktop.ts,apps/cli/src/lib/routine-notify.ts,apps/cli/src/lib/daemon.ts,apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift.agents secrets statusnow suggests which bundles to unlock. It reads the existingsecrets.getaudit events and surfaces bundles you keep getting a Touch ID prompt for — read from the keychain (not served silently by the broker/session) 3+ times in the last 7 days and not currently held — with a readyagents secrets unlock <name>command.never/no-ACL bundles (which never prompt) are excluded, and the hint is best-effort so it never breaksstatus. Source:apps/cli/src/lib/secrets/unlock-hints.ts,apps/cli/src/commands/secrets.ts.agents sessions <id>with a short/partial id resolves by id only — no more "Multiple sessions match" from fuzzy content. A complete UUID already resolved by id, but a bare hex short-id liked3470b57was not caught byisCompleteSessionId, so it fell through to the ranked content search and surfaced every transcript that merely MENTIONED the string (a resume prompt echoes the parent id into the body of many later sessions) — a real view id returned a list of unrelated sessions. Any id-shaped query — complete id OR hex short-id/prefix (looksLikeSessionId) — now resolves through the index by id in bothresolveSessionQueryand therenderOneSessioncontent-widen gate, and reports "no session found" when nothing matches instead of content-searching. Free-text phrases keep the ranked search path. Source:apps/cli/src/commands/sessions.ts.agents sessions --activeattributes the initiating device for SSH-launched sessions. A session started by ssh'ing into a box (common for tmux-hosted runs) used to render aslocalwith no origin, because the tmux discovery path stamped atransport:'local'placeholder that made provenance enrichment skip it. Enrichment now probes the pane process's env and upgrades the row tosshwith the real origin, then resolves the SSH client IP against the device registry intoprovenance.origin({ device, user? }). Both the flat listing and the interactive browser readssh←<device>(e.g.ssh←zion); an unregistered IP stays baressh. Answers "which box launched this session" without scrapingps/who/tailscale. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/lib/session/provenance.ts,apps/cli/src/commands/sessions.ts,apps/cli/src/commands/sessions-browser.ts.agents sessions --include user/--first/--lastnow count genuine user turns, not harness-injected scaffolding (#1550). A Claude session opened with a!-prefix command (e.g.j <dir>) stores<bash-input>/<bash-stdout>asrole=userrecords, and<system-reminder>/<task-notification>/<command-*>/hook-feedback/skill bodies land the same way — so--include user --first 3returned the jump command and its shell output before the real ask, and every consumer of--include user(theverify-work-completeStop hook's "original request" self-audit,session-recall) inherited the noise.parseSessionnow flags these injectedrole=userevents_syntheticat its central post-parse chokepoint via one shared classifier (isSyntheticUserMessage), so turn slicing (applyTurnSlice) and role filtering (roleOfEvent) skip them; they stay in the default/--markdownstream for full fidelity. Claude-specific in practice — Codex/Gemini/OpenCode/Grok/Kimi/Rush route shell output totool_result, never torole=user, and Droid's<system-reminder>was already dropped — but the classifier is cross-harness by construction. Source:apps/cli/src/lib/session/{prompt,parse,render,types}.ts.Routine/daemon Claude runs authenticate the rotation-pinned account via its own long-lived setup-token — fixes fleet-wide daily logout. Claude Code's interactive OAuth session uses single-use rotating refresh tokens: when one fleet machine refreshes, the server invalidates that account's token on every other machine, so unattended boxes 401 and drop (Claude Code #25609/#56339). A
claude setup-tokenis a 1-year, non-rotating token that sidesteps this. The daemon now injects everyCLAUDE_CODE_OAUTH_TOKEN_<account>present in theclaudebundle (not just the one ambient token), and a routine spawn selects the token matching the account its version-home is pinned to (runner.tsbuildRoutineSpawnEnv→resolveAccountSetupToken), so each unattended account authenticates with its own setup-token instead of the rotating interactive session. Works on macOS too, where the prior drop-based path was inert. Inert (no behavior change) until per-account setup-tokens are stored in the no-ACLclaudebundle. Interactiveagents runand remote--hostdispatch are unchanged (out of scope; noted for follow-up). Source:apps/cli/src/lib/secrets/account-token.ts,apps/cli/src/lib/runner.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/sandbox.ts.
1.20.76
The routines daemon can read a
never/no-ACL secrets bundle headlessly again — fixes a false "no Claude credential" alert. The headless secrets guard (readAndResolveBundleEnv'sagentOnlybranch) threw for every keychain-backed bundle absent from the broker, but anever/no-ACL bundle carries no biometry ACL — its reads raise no Touch ID sheet, so blocking it served no purpose. That wrongly blocked the automation-onlyclaudebundle the routines daemon reads at startup (readDaemonClaudeOAuthToken), leaving every scheduled Claude routine token-less, and — on the new auth-failure alert path — firing "no Claude credential" on each daemon start even when the bundle was configured correctly. The guard now exemptsnever/no-ACL bundles (policy learned via a prompt-less metadata read), matching the existing file-backend exemption. Source:apps/cli/src/lib/secrets/bundles.ts.release.shcan now cut the next patch when main is ahead of an unpublishable version. The catch-up guard refuses to publish a merged release PR whose squash pulled in concurrent main commits — correctly, since the tree that would ship is not the tree CI tested (the hole that let 1.20.58 publish before its Windows matrix failed). Its refusal advises cutting the next patch through the normal release PR flow, but the version validator measured patch+1 from the REGISTRY, so with main at 1.20.75 and npm at 1.20.74 both 1.20.75 (blocked) and 1.20.76 (read as a skipped version) were rejected — leaving no patch-level path forward and a minor bump as the only escape. A newpatch-from-maincase accepts the version one patch abovepackage.jsonwhen main is ahead of the registry; it grants no bypass, and the release still earns its own release PR, full cross-platform matrix, merge, tag, and publish. The decision moved out ofrelease.shintoscripts/validate-bump.shso it can be tested directly —release.shitself cannot be run in a test, since it demands a clean main plus npm and gh auth long before it reaches the bump decision, which is why this arithmetic had no coverage at all. The rejection message now also lists the main-ahead options only when main really is ahead, instead of advising a version the script would then refuse. Source:apps/cli/scripts/validate-bump.sh,apps/cli/scripts/release.sh.Teams now run local teammates under one frozen actor. The orchestrator was spawning each local teammate through a raw shell without the actor env, so every teammate's inner
agents runre-resolved the actor independently instead of inheriting the orchestrator's — contradicting the "resolve once, whole tree shares one actor" contract. The local spawn env now carriesactorEnv(resolveActor())(process env < actor <--envoverrides), so all teammates inherit the single frozen actor. Teammate records also carry anactorfield, persisted tometa.jsonand emitted in the status dict. Remote teammates inherit the fix at the dispatch layer. Source:apps/cli/src/lib/teams/agents.ts.agents sessions <full-session-id>no longer answers with an unrelated session. A complete id that was not in the local index fell through to the FTS content search, which tokenizes the UUID and matches every transcript that merely mentions it. The miss surfaced as up to ten unrelated sessions underMultiple sessions match "<id>"plus the advicePass a longer ID to narrow it down— impossible to follow, since a full id is already the longest form. The same fallthrough made--previewrender a different session's transcript, let an 8-char short id lose to a content hit, and madeagents sessions export <id>bundle every transcript that mentions the id (14 unrelated sessions written into an archive meant to be handed to someone else). A query that is a whole session id now resolves by id alone: it reportsNo session with id <id> on this machine.and points at--device <host>for the fleet. Short-id prefixes and text searches are unchanged. The recognized shapes are the ones the index actually holds — a bare UUID,session_+ UUID (kimi, rush), andses_+ 26-char ULID (opencode); routine run ids and cloud execution ids stay out of scope and keep today's search behavior. Source:apps/cli/src/lib/session/discover.ts(isCompleteSessionId),apps/cli/src/commands/sessions.ts(resolveSessionQuery), andapps/cli/src/commands/sessions-export.ts(selectSessions).
1.20.75
Wire native file-based slash commands for Grok (RUSH-1851). Grok >= 0.2.111 discovers commands from the cross-agent
~/.agents/commands/dir (plus the legacy~/.claude/commands/symlink).agents sync groknow writes native.mdcommand files there instead of converting commands to skills, soagents view grokandagents commands list grokreport Grok as commands-capable. Source:apps/cli/src/lib/agents.ts.Document that Droid Factory Missions are invoke-only (RUSH-1864). Probed droid v0.177.0 (self-updating; ticket cited v0.161.0) and Factory docs: Missions run via
/missionsordroid exec --mission(optional-fis a prompt file, not a named template).~/.factory/missions/<sessionId>/is runtime state only — no auto-discovery dir agents-cli can populate — soworkflowsstaysfalsewith an evidence comment rather than inventing a writer target. Source:apps/cli/src/lib/agents.ts,apps/cli/tests/agents.test.ts.Reading state no longer writes
agents.yaml, which silently deadlockedagents repo pull(RUSH-1925). Registry presets fromSEEDED_REGISTRIES(todayskill.hermes) were seeded on the state read path, which wrote the registry entry plus aseededPresetsmarker intoagents.yaml. That file is git-tracked in the user's DotAgents repo, so the write left the working tree dirty and every subsequentagents repo pullaborted withWorking tree has uncommitted changes— naming neither the file nor the cause. Because everyagentsinvocation reads state, the dirt reappeared the instant it was cleared:git checkout -- agents.yaml && agents repo pullre-seeded before the pull ran, so the loop could not be escaped through the CLI at all. On a host with several live agent sessions even a rawgit pull --rebaselost the race, and one machine sat 27 commits behind for weeks as a result. Seeded presets are now resolved in memory bygetRegistries— the same wayDEFAULT_REGISTRIEShas always worked — so nothing is written and no later write can flush them into the file.seededPresetsbecomes a removal tombstone:agents registry remove skill hermesrecords the key and the preset stops being offered, which is the behaviour the marker existed to protect. Files seeded by the old code carry both the tombstone and an explicit entry in their ownregistries:block, and the explicit entry still wins, so upgrading changes nothing for them.setRegistryalso falls back toSEEDED_REGISTRIESwhen merging a partial update, soregistry disable/enable/configon a never-materialized preset can no longer persist a stripped entry that dropsurl. Source:apps/cli/src/lib/registry.ts,apps/cli/src/lib/state.ts,apps/cli/src/lib/registry.seeds.test.ts,apps/cli/src/lib/state.test.ts.agents fleet statusno longer hangs on an unreachable box (RUSH-1964). The cheap stats probe (~2.5s) already learns whether each box is reachable, but the dead-box skip that spares the expensiveagents --version(15s) +agents doctor --json(30s) dials was gated behind--refresh— so a default run still spent up to 45s per genuinely unreachable box, and one down box could stall the whole matrix. The default path now gates those dials on the same reachability verdict: a box the stats probe found unreachable short-circuits straight to anunreachablerow with zero further SSH round-trips. Measured on a single blackholed target,fleet statusdropped from 22.8s to 2.8s. (VPN-first transport and SSH key provisioning remain deferred.) Source:apps/cli/src/lib/devices/fleet.ts(fleetHealthSkip),apps/cli/src/commands/ssh.ts(runFleetStatus).Fleet reachability reflects the live probe (RUSH-1965).
agents devicesandagents fleet statusnow persist the live SSH probe's{reachable, via, checkedAt}verdict to the registry and read the online/offline word from it — freshest signal wins: a live stat this run, then the written-back verdict, then the cachedtailscale.onlinesnapshot. A reachable box (including avia:"manual"device with no Tailscale peer) no longer renders "offline" while its live load/mem sit one column over. Source:apps/cli/src/lib/devices/reachability.ts,apps/cli/src/lib/devices/registry.ts.agents fleet statusoutput redesigned — rollup + NEEDS ATTENTION, quiet when healthy (RUSH-1966). The old grid buried "is my fleet OK?" under duplicated columns and glyph soup (a "Health" column that just repeated Load/Mem,stale · coldcounts across every orphan version, an●5 ·8 ◐3auth cell, and warnings that re-listed all 12 devices three times). The default view now leads with a one-line rollup (● N online · ○ M offline), then a short NEEDS ATTENTION list where every item names its fix command — offline →check the box, config drift or a stark CLI gap →agents apply <box>, version skew →agents upgrade --fleet— then quiet per-device rows grouped by OS (macOS / Linux / Windows) showingname · capacity · load/mem · version, with this machine flagged▸ … ← this machine. Drift is reported on the active version only (not orphan versions); orphaned versions are demoted to a one-lineagents prunenudge in the footer; the freshness footer names the cache age and what--live/--verboseadd. A healthy fleet reads in a few lines. The full per-device auth/CLI/sync/version grid moves behind the new--verboseflag;--jsonis unchanged. Source:apps/cli/src/lib/devices/health-report.ts(renderFleetSummary,buildFleetAttentionItems),apps/cli/src/commands/ssh.ts(runFleetStatus).agents doctornow flags credentials exported from shell rc files (RUSH-1968). A secret exported from~/.zshenv/~/.zshrc/~/.bashrc/~/.profileis inherited by every process the login shell spawns and is readable from/proc/<pid>/environby any same-user process —.zshenvis sourced even by non-interactivessh host 'cmd', so the value lands in essentially everything the box runs. The doctor overview now scans the current user's rc files and prints aSecrets in shell configwarning that names each credential-shaped export byfile:lineand variable name (never the value), with the file-store master keyAGENTS_SECRETS_PASSPHRASEcalled out separately — its off-env home is~/.agents/.secrets-key/passphrase(chmod 600), and other credentials should move toagents secretsand inject viaagents secrets exec. The scanner reads only the variable name and line number, so a finding is safe to print or log. Source:apps/cli/src/lib/secrets/rc-hygiene.ts(scanUserRcFiles,scanRcExports,rcSecretWarningLines), wired intoapps/cli/src/commands/doctor.ts(renderRcHygieneAdvisory).agents devicesno longer forces a Touch ID prompt on a password-auth box (RUSH-1970). The read-only stats probe's live SSH to an uncachedauth.method === 'password'device used to drive the askpass shim to resolve the SSH password through the biometry-gated Keychain sheet under a TTY, popping Touch ID during what should be a silent probe. The probe now threads a broker-only signal (AGENTS_SSH_AGENT_ONLY) so it resolves from an already-unlocked broker or degrades to an unreachable row — never a biometric prompt. Source:apps/cli/src/commands/ssh.ts,apps/cli/src/lib/devices/health.ts,apps/cli/src/lib/devices/connect.ts.agents sessions migrate(aliasrelocate) relocates a RUNNING session onto another machine, then stops the source here (RUSH-1977).--autoscores the fleet and picks a target,--host <name>names one explicitly, and--leaseprovisions a fresh ephemeral box;--mode resume|rehydratechooses whether the target resumes the native transcript or replays it via/continue. Every migration is written to an append-only ledger, viewable withagents sessions migrations. Load-bearing invariant: the source session is never stopped until the transcript is confirmed live on the target, so a failed hop leaves the original running. (Not to be confused withagents sessions detach/attach, the unrelated background/foreground pair.) Source:apps/cli/src/commands/sessions-migrate.ts,apps/cli/src/lib/session/migrate-targets.ts,apps/cli/src/lib/session/migrations.ts.agents sessions --active --jsonnow emits flatticketIdandprojectkeys on every row. A supervising watcher joins active sessions on ticket + project, but the raw row nested the ticket underticket.idand carried noprojectat all, so a naive join silently dropped every session. Each row now carries top-levelticketId(from the detected ticket) andproject(the basename of the session's cwd — the same derivation the historical--jsonlisting uses), both always present andnullwhen unknown. The existing raw fields are unchanged. Source:apps/cli/src/commands/sessions.ts(serializeActiveSessionsForJson).Actor provenance — agent git commits are now credited to the human who started the run, not the shared account. One account across a shared fleet meant every commit, from anyone who SSH'd into a box, showed up as the same author.
resolveActor()now identifies who is behind a run: over SSH ittailscale whoises the client IP to the connecting tailnet identity (name + login email); locally it stays honest withUNRESOLVED@<host>and claims no identity. The resolved actor rides the agent's process env asAGENTS_ACTOR/AGENTS_ACTOR_KIND(inherited by the whole spawn tree, so it resolves once), and for a resolved human it also injectsGIT_AUTHOR_*/GIT_COMMITTER_*— so the agent's owngit commitis attributed to the person. An unresolved actor injects no git identity, so local runs keep their ambient git config unchanged. Source:apps/cli/src/lib/actor.ts, wired intobuildExecEnv(apps/cli/src/lib/exec.ts).New optional
actors:map inagents.yaml. Keyed by a short slug, each entry (kind/name/email/github/login) enriches or overrides whattailscale whoisresolves — pin a preferred git email, add a GitHub handle, override the display name, or mark an entry as an agent rather than a human. Entirely optional: a tailnet SSH identity already resolves without it. Source:apps/cli/src/lib/types.ts(ActorConfig,Meta.actors).agents run antigravity "prompt"now works headless without an explicit--headless. Antigravity's--printflag was gated on the raw--headlessflag, which defaults tofalseat the CLI layer — but headless is inferred from prompt presence. So a bareagents run antigravity "do X"builtagy <prompt>with no--print, launching the interactive TUI and dying withbubbletea: could not open TTY: /dev/ttyin any non-terminal shell (headless runs, teams, routines,--host). Print flags are now gated on the resolved headless state, matching the documented "--headlessauto-enabled when a prompt is provided" contract and the behavior of every other agent. Antigravity was the only agent affected — it is the sole harness whose prompt is a bare positional with no headless subcommand and no-pprint alias. Source:apps/cli/src/lib/exec.ts.Routine auth-failures are now detected, not silent. When a routine's agent is logged out or its token is revoked, the run is classified
failedwith anauth_failed:/auth_preflight:reason instead of a generic non-zero exit. The login-error text is no longer written intoreport.md, and{last_report}now only injects the last completed run's report — so a single logged-out run can no longer poison every subsequent run's prompt. Classification uses the Claude stream-json markers (error:"authentication_failed"and aresultevent withis_error:true), which is the reliable signal —terminal_reasonis"completed"on a logged-out run. Rate-limit still classifies first, so a 429 keeps triggering failover rather than being mistaken for an auth failure. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/runner.ts,apps/cli/src/lib/routines.ts.agents routines runnow exits non-zero when a run doesn't complete. A run that ends infailed,timeout, or an auth failure returns exit code 1 and--json { "ok": false, … }with the reason, instead of exiting 0 withok:true— so cron wrappers,&&chains, and--jsonconsumers actually see the failure. (Exit code is set viaprocess.exitCodeso the JSON payload flushes fully to a pipe first.) Source:apps/cli/src/commands/routines.ts.Auth preflight before dispatch. A routine whose (agent, version) has a cached
revokedauth verdict fails fast withauth_preflight: revokedwithout spawning a doomed agent. Fails open on any other verdict, so a stale/absent probe or a network blip never blocks a run, and agents with no live probe (codex/gemini/grok) are never blocked. Source:apps/cli/src/lib/runner.ts, reusingapps/cli/src/lib/auth-health.ts.The routines daemon no longer starts silently token-less. When no Claude OAuth token is available (e.g. a headless macOS daemon whose keychain was locked at start), the daemon now logs a
WARNand fires a desktop notification instead of quietly spawning Claude routines that all fail auth. Source:apps/cli/src/lib/daemon.ts.agents sessionsindexes again from the standalone binary. Every session write went through a named-parameter bind (INSERT ... VALUES (@id, @short_id, ...)inapps/cli/src/lib/session/db.ts), andbun:sqlitematches such an object only when its keys carry the SQL sigil — bare keys bound nothing, so all columns landed NULL andsessions.short_id(NOT NULL) rejected the row. The shims exec the Bun-compileddist/bin/agents, so no session reached the index from the CLI:agents sessionsprintedWarning: skipped unindexable session <id>: NOT NULL constraint failed: sessions.short_idper session and then listed only rows indexed earlier by the Node entrypoint. The suite runs under Node (vitest), wherenode:sqliteaccepts bare keys, which is why CI stayed green.apps/cli/src/lib/sqlite.tsnow opens the DB withstrict: trueunder Bun, so the bare-key call shape this codebase uses works on both runtimes (the edges still differ — the module doc lists what strict changes).sqlite.test.tscovers both the bind and a fullagents sessionsscan in a realbunsubprocess.The compiled binary no longer reports itself as a phantom
/$bunfsinstall, and can self-upgrade again. Since the standalone executable started shipping (1.20.53), the running copy located its own package root as<__dirname>/... Under a Bun standalone binary__dirnameis the embedded virtual filesystem, so that resolved to/$bunfs— a path that exists nowhere. Two symptoms followed on every machine running the compiled binary: the multi-install check reported one install as two (/$bunfs (running)alongside the real npm root, with the misleading advice to uninstall a stale copy that did not exist), andagents upgradefailed closed with/$bunfs is not an npm-managed installbecause no global prefix can be derived from a virtual path. A newresolveRunningPackageRoot()resolves the real on-disk root by walking up fromprocess.execPathto the directory whosepackage.jsonnames this package, and both sites use it. The PATH scan also recognizes<root>/dist/bin/agentsas an entrypoint, so a shim pointing at the compiled binary — typically first on PATH, and the copy that actually runs — resolves to the same root as its sibling npm bin instead of being invisible. Genuine multi-install warnings still fire, now naming a real, actionable path. Source:apps/cli/src/lib/self-update.ts,apps/cli/src/index.ts,apps/cli/src/lib/self-update.test.ts.Reading Claude usage no longer rotates the token and logs your fleet out.
getClaudeUsageInforefreshed the OAuth token just to read the usage endpoint (getClaudeAccessToken,usage.ts) — and Claude's refresh token is single-use and rotates server-side, so with one account signed into several machines that background refresh (fired by the stale-while-revalidate usage cache and byagents run's default "balanced" rotation on every unpinned run) invalidated every other box's copy, dropping the fleet to "run /login". This is the RUSH-1822 stampede, which was fixed for the 3-minute health probe but left live in the usage/run hot path. Usage reads are now strictly read-only: a new pureclaudeUsageAccessTokenNoRefreshuses the stored access token and, when it is within the refresh leeway, reports "no usage right now" instead of rotating — exactly mirroringprobeClaudeStatus. The single legitimate refresh stays on the realclauderun, never a usage read. Source:apps/cli/src/lib/usage.ts,apps/cli/src/lib/usage.test.ts.No more Touch ID storm from the usage view. On macOS, the usage-bar fetch (
agents view, the Factory watchdog that pollsagents view --jsonevery 60s per agent) and the daemon's every-3-min auth-health probe each read Claude's own ACL-boundClaude Code-credentials-<hash>keychain item on every refresh — each read popping a Touch ID prompt, so several running Claude agents meant a biometric prompt every couple of minutes. Those two access-token-only, high- frequency callers now opt into a device-local no-ACL access-token cache (the prompt-free mechanismsecrets/session-store.tsuses for unlocked bundles), bounded by the token's own expiry, so the ACL-gated read happens at most once per token lifetime and every agent process reads the cache silently. The cache is opt-in and caches only the short-lived access token — callers that need the full credential (isClaudeAuthValid's refresh,readClaudeCredentialsBlob's Rush Cloud export) still take the ACL read with the refresh token intact. Source:apps/cli/src/lib/usage.ts.The daemon no longer silently repoints your default agent version. The unattended 6-hourly launch-health pass (
healBrokenDefaultLaunches→ensureAgentRunnable) now runs withallowDefaultSwitch: false: it still repairs the current default in place, but if that default can't be repaired it no longer adopts another installed version or installslatestand pins it. A background default switch installs a fresh version home, which for Claude is a fresh, empty credential scope (macOS keychain keyed offCLAUDE_CONFIG_DIR; Linux per-version token file) — i.e. an "unprovoked logout" at a time uncorrelated with anything you did, and a leading cause of routine auth-failures on unattended machines. The daemon now logs aWARNnaming the version to pick instead; interactive callers (agents run,agents add) are unchanged and still repoint as before. Source:apps/cli/src/lib/versions.ts,apps/cli/src/lib/daemon.ts.agents sessions detach/agents sessions attach— send a running agent to the background and back.agents sessions detach <id>stops a live session's interactive process (killing the tmux session when tmux-hosted, else SIGTERM'ing the pid) and continues it headless, detached, via the existing version-pinnedagents run --resumepath — so it drives its task to completion without holding a terminal. The resumed run carries a nudge that tells the now-unwatched agent it is headless and to make the call rather than stall on a confirmation nobody can answer.agents sessions attach <id>stops that headless continuation and resumes the session interactively in the current terminal (resumeSessionInPlace) — the same session and full history, including whatever the background run did. They sit undersessionsnext tofocus/resume(the session-lifecycle verbs); the Factory extension exposes them as Agents: Detach (Cmd/Ctrl+K B) and Agents: Attach (Cmd/Ctrl+K A). Both verbs are agent-agnostic (native resume for Claude/Codex,/continuereplay for the rest). A session on another host is detached there over SSH rather than killed locally; cloud and team sessions are refused (they have their own lifecycles); the interactive process is fully awaited before the headless resume starts (no transcript race); and the background run's output is captured to~/.agents/.cache/logs/detach-<shortid>.logso a crash after detach is debuggable.agents sessions --active --jsonnow carries apresencefield (attached/background/parked), folded onto every row from a per-session detach record, so the menu bar and Factory show where each agent is. Source:apps/cli/src/commands/detach.ts,apps/cli/src/commands/attach.ts,apps/cli/src/lib/session/detached.ts. (agents sessions migrate's olddetachalias is renamed torelocateto free thedetachname for the background/foreground verb —migrateandrelocateboth still work.)agents devices syncno longer auto-registers tailnet nodes another user shared into the tailnet.tailscale statusincludes ShareeNode peers (for example a tagged relay shared in by a teammate); the parser ignored that flag, so bootstrap registered them as your own boxes and they surfaced inagents fleet ls. Parsing now carries ashareeflag,runDeviceSyncfilters those peers out of auto-registration and suggestions, and the interactive picker leaves them unchecked (labeledshared). Deliberate paths —devices register/addand afleet:manifest — still reach shared nodes when you name them. Source:apps/cli/src/lib/devices/sync.ts,apps/cli/src/lib/devices/tailscale.ts.Faster
agents sessionson large / unchanged session trees. Session discovery re-walked and re-stat'd every transcript directory on everyagents sessions/output/view/teamscall — for a heavy user the immutable version-home and backup roots dominated the cost yet never changed. A newdir_ledger(SQLite, schema v14) caches each leaf transcript directory's(mtime, entry_count); when both match, the per-filestatof that directory is skipped and its unchanged files are served straight from the DB, so those immutable roots cost one dir stat each instead of hundreds of per-file stats. Append safety is preserved: a file under the agent's live~/.<agent>root, or scanned within the last 10 minutes, is always re-stat'd (a parent-dir mtime bumps on create/delete/rename but NOT on an in-place append), so a growing live session is never missed; a create / delete / rename bumps the dir mtime and forces a full re-walk of that dir exactly as before. Wired into the Claude and Gemini scanners (the biggest win); the other scanners keep the existing per-file path. SetAGENTS_SESSIONS_NO_DIR_LEDGER=1to disable the short-circuit and force the old full per-file walk. Source:apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/discover.ts.agents feed post— agents announce progress without opening a “needs you” block. Free-text status posts append astatus.postedmilestone to the per-session activity log (same stream asagents activityand the feed’s recent-activity lane). Session/agent/host/runtime/pid/launch identity is auto-stamped from the process env and the per-pid launch registry — no domain-specific flags (tickets, URLs). Managed runs exportAGENT_SESSION_ID/AGENTS_AGENT_NAME/AGENTS_CWDso a Bash tool call needs no extra wiring. Source:apps/cli/src/lib/feed-post.ts,apps/cli/src/commands/feed.ts,apps/cli/src/lib/activity.ts,apps/cli/src/lib/exec.ts.PID-reuse protection now works on Windows, from one implementation.
captureProcessStartTime()existed twice — inpty-server.tsandteams/agents.ts— and neither copy had a Windows branch: both fell through tops, which does not exist there, so the function always returnednulland every caller silently skipped the guard. A dead session whose PID the OS had recycled read as alive, andagents teams stopcould signal an unrelated process group. Both copies now delegate to a single implementation inplatform/process.tsthat readsCreationDatefromWin32_Processas a culture-independent FILETIME, memoizes per PID (the listing path probes one PID per row), and bounds the spawn with a timeout. Source:apps/cli/src/lib/platform/process.ts(captureProcessStartTime).A session's recorded working directory is no longer rebased onto the local drive.
normalizeCwd()ranpath.resolve()over a cwd read out of a transcript, which may name a directory on another machine. On Windows that grafted the current drive onto a POSIX path (/Users/mebecameD:\Users\me), inventing a location that never existed. A foreign path is now normalized with POSIX rules and never realpath'd; local paths still normalize and resolve symlinks as before. Source:apps/cli/src/lib/session/discover.ts(normalizeCwd).agents cloud's task database can now be closed.cloud/store.tsopenedtasks.dband exported no closer, so nothing could release the handle — on Windows that leaves the file un-unlinkable. AddscloseStore(), the mirror ofcloseDB()insession/db.ts. Source:apps/cli/src/lib/cloud/store.ts(closeStore).Collapse indistinguishable worker processes into one active-session row. A daemon that spawns many agent binaries (an OpenClaw gateway running
codex app-server) produced onesessions --activerow per process, because a row with no session id and no transcript file skipped dedupe entirely — the Factory Floor showed ~40 identical.openclaw · bg · 0s agorows that buried every real session. Dedupe now falls back to the cloud/run handle and then to the worker's identity (agent binary + context + working directory), so N indistinguishable workers become one row carryingpidCount: N. Source:apps/cli/src/lib/session/active.ts.sessions --activenow stamps a start and last-activity time on every interactive session. Terminal, tmux, and headless agents discovered by the process scan carried nostartedAtMs— so the Factory Floor rendered every running agent as "0s ago" even when its transcript, topic, and progress had resolved. The scan now stampsstartedAtMs(the SessionStart hook's own timestamp, else the transcript's creation time) and a newlastActivityMs(the transcript's last-write) on each row, and the Floor renders "Xs ago" off the real last-activity instead of the session's age. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/lib/session/hook-sessions.ts.agents sessionsnow classifies Grok transcripts into real events, not a one-line stub. Grok sessions were indexed (title, timestamps, message count) but opening one showed a single placeholdersession_startevent —parseGrokwas a stub. It now reads the session'schat_history.jsonland normalizes every line into the sharedSessionEventshape:user/assistantmessages,reasoning→ thinking,assistant.tool_calls[]→ tool_use (withpathandcommandsurfaced), andtool_resultcorrelated back to its call bytool_call_id(anError:-prefixed result becomes an error event). The scanner recordssummary.jsonas the session path, so the parser resolveschat_history.jsonlbeside it; per-line timestamps aren't stored, so each event carries the session'screated_at(falling back to the transcript mtime). Verified end-to-end against a real Grok session (30 events: messages + thinking + tool_use + tool_result). Source:apps/cli/src/lib/session/parse.ts.agents harness— name a (host CLI + model) combo and run it like a native agent type.agents harness add spark --host opencode --model meta/muse-spark-1.1writes~/.agents/profiles/spark.yml, andagents run sparkthen dispatches OpenCode pinned to that model;--modelat run time still overrides it. A harness is a profile under the hood (same YAML, same run resolution, sameagents repo push userdevice sync), soagents profilesis unchanged;harnessadds the host+model one-shot (no preset required), owns its own--host(never remote-routed, unlikeprofiles --host), andagents harness listshows custom harnesses, addable presets, and the native harness registry in one view. The model lands on the host's model env var (OPENCODE_MODEL/ANTHROPIC_MODEL/GROK_MODEL/GEMINI_MODEL). Source:apps/cli/src/commands/harness.ts,apps/cli/src/lib/profiles.ts,apps/cli/src/lib/hosts/passthrough.ts.Fixed the Spark presets, which never ran.
claude-spark,opencode-spark, and theopencodepreset help all namedmeta/claude-spark-1.1— a model neither OpenRouter nor OpenCode serves; the live id ismeta/muse-spark-1.1. Separately, anauthOptionalpreset (opencode) still wrote a keychainauthblock thatresolveProfileEnvalways read, soagents run opencode-sparkdied with "Keychain item not found" even though OpenCode uses its own login.resolveProfileEnvnow skips optional auth when no token is stored, so those presets run on the host's own credentials. Source:apps/cli/src/lib/profiles-presets.ts,apps/cli/src/lib/profiles.ts.agents sessionsnow heals any pre-existing empty-shortIdrows on upgrade. The prior fix stopped producing empty shortIds (bare-prefix ids like asession_directory stripped to''), but a row already poisoned in the index did not self-heal — an empty shortId is not re-parsed unless its transcript changes, and an orphaned row whose file is gone never re-parses at all, so it stayed unaddressable in theshort_id LIKE ?picker lookups. A one-time schema migration (v16) repairs every such row in place (short_id = substr(id, 1, 8)), so upgrading users get a clean index without a full rescan. Source:apps/cli/src/lib/session/db.ts.Host and cloud runs are now mappable in
agents sessionsfor every agent, not just Claude. A--hostdispatch forced a session id only for Claude (the sole agent that accepts--session-id); every other agent's remote run coined its own id that the launcher never learned, so the run was orphaned inagents sessionsand couldn't be resumed by id. The remote run now prints its resolved session id as a one-line stdout sentinel (via a new internal--emit-session-idflag the dispatch forwards); the launcher parses it out of the followed log and stamps it on the host task, soagents sessions/resume-by-id work for Codex, Gemini, and the rest. Source:apps/cli/src/lib/hosts/session-marker.ts,apps/cli/src/lib/hosts/session-index.ts,apps/cli/src/lib/hosts/run-target.ts,apps/cli/src/lib/exec.ts.agents cloud runreconciles into the session index at dispatch. The cloud task store (tasks.db) and the session index were disjoint: a cloud run wrote only the store, andagents sessionslearned of it only later, via a proxy discovery. Now every cloud dispatch (and every status poll) registers a session row keyed by the real execution id with a[cloud/<status>]label, so a launch is mappable to a session immediately. Source:apps/cli/src/lib/cloud/session-index.ts,apps/cli/src/lib/cloud/store.ts.Codex Cloud dispatch no longer fabricates a task id. When
codex cloud execdidn't print a parseable id, the provider minted a syntheticcodex-<timestamp>— an id that could never match the real execution, silently breaking status, list, and session reconcile. It now also scans stderr for the id and, on a genuine miss, fails loud pointing atagents cloud listrather than persisting a bogus id. Source:apps/cli/src/lib/cloud/codex.ts.agents import <agent> --as <version>— the version flag now actually works. The option was declared as--version <version>, which the program-level.version(VERSION)claims globally:agents import codex --version 1.2.3printed the CLI's own version and exited without importing. It had been unreachable since it was introduced, and the "could not determine version" error advised passing it. Renamed to--as, which reaches the command. This also makesagents import <agent> --isolated --as <version>re-seed an existing isolated copy from your current local config, instead of only ever creating a new copy at whatever version happens to be installed locally.Fixed a silent no-op in config copying under the compiled binary.
fs.cpSyncdefaults toforce: true, but Bun drops that default when afilteris supplied — so copies that strip symlinks left existing destination files untouched.dist/bin/agentsis bun-compiled, so this affected a shipped path, not just tests. Now passed explicitly inconfig-transfer.tsandimport.ts.agents import --isolatedno longer misdescribes itself, chokes on codex, or copies your session history. Three defects found by using it: (1) the confirmation summary printedconfig: ~/.codex (will be moved into version home)even under--isolated— announcing the exact adoption the flag exists to prevent, though the code correctly copied; it now readswill be COPIED — your original stays put. (2) Seeding failed outright for codex withCannot overwrite non-directory, because its version home is a SUN_LEN-safe symlink to~/.agents/.codex-homes/<version>/.codexrather than a real directory; the seeder now follows the link and writes the home the agent actually reads. (3) The seed copied the whole config dir including sessions, logs, caches and sqlite — 757MB on a real machine, 349MB of itsessions— so runtime state is now skipped and reported (33MB for the same install), with--allto include it. Also skips the config copy when~/.<agent>is itself a managed symlink, which is another version's home rather than the user's real settings. Source:apps/cli/src/lib/import.ts,apps/cli/src/commands/import.ts.Incremental Claude transcript parsing on the live scan path. When an active Claude session grows,
agents sessions(and every consumer that scans:output/view/teams/ the watcher) now re-parses only the newly-appended bytes instead of re-reading the whole transcript from the top. The scan persists a resumable continuation (parser_state+content_text, schema v15) in thescan_ledger; the next scan resumes from the saved byte offset when the file merely grew and its mtime did not go backwards, and falls back to a full reparse from byte 0 on a cold start, a truncation / rewrite (size shrank), or a clock rewind. Both paths run through one shared reducer, so the indexed row an append produces is identical, field for field, to a from-scratch full reparse — token counts, cost, duration, topic/title, PR + ticket refs, and FTS content all match even when a signal straddles two scans (agh pr createin one write and its URL in the next). Only the Claude scanner is wired for now (Codex / Kimi are follow-ups); the other scanners are unchanged. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/session/db.ts.Incremental Codex + Kimi transcript parsing on the live scan path. Following the Claude incremental parse, the Codex rollout scanner and the Kimi wire.jsonl scanner now re-parse only the newly-appended bytes when an active session grows, instead of re-reading the whole file from the top every scan (
agents sessionsand every consumer that scans:output/view/teams/ the watcher). Each persists a resumable continuation in thescan_ledger(parser_state, reusing the schema v15 columns): the next scan resumes from the saved byte offset when the file merely grew and its mtime did not go backwards, and falls back to a full reparse from byte 0 on a cold start, a truncation / rewrite (size shrank), or a clock rewind. Both branches run through one shared reducer per scanner, so the indexed row an append produces is identical, field for field, to a from-scratch full reparse. For Codex that covers messageCount, the last-wins cumulative token snapshot (tokenCount / outputTokens / cost), duration, topic, and PR + ticket + team signals that straddle two scans (agh pr createfunction_call in one write and its URL in the next). For Kimi it covers the additive message + token counters. Both incremental paths apply only newline-terminated lines and defer a complete-but-unterminated trailing record to the next pass, so a record written before its'\n'is flushed is never double-counted. Grok is out of scope (it reads a wholesummary.json, not an append-only JSONL); Claude / Gemini and the shared helpers are unchanged. Source:apps/cli/src/lib/session/discover.ts.Isolated installs now resume sessions and resolve
@defaultlike any other install. Two places still assumed a managed version is reachable on PATH — which an isolated install deliberately is not. (1)agents sessionsresume looked up<cli>@<version>with a plain PATH lookup, never found it (the shims dir is intentionally off PATH under--isolated), concluded the version was uninstalled, and fell back to spawning<cli> "/continue <id>"— a slash command neither CLI has, so the session simply never resumed. It now resolves the versioned alias by absolute path, the wayagents runalready did, and the fallback is the agent's real resume verb against the current version rather than/continue. (2) The agent-spec resolver behind--agents/@default/@pinnedread only the global default, so an isolated-only agent threw "No default version set" even after an explicitagents use—resolveVersionhad gained the isolated-default fallback but this resolver had not. Both now consult it, and reportisolated-defaultas the source rather than claiming a global default.opencoderesume stays deliberately un-pinned, since its sessions are shared across versions. Source:apps/cli/src/commands/sessions.ts,apps/cli/src/lib/agent-spec/.Menu bar: prune orphan attention sentinels; group
NEEDS YOU; end silent truncation.LocalState.attentionMarksnow takes the caller's live-session set and unlinks sentinels whosesessionIdis not alive — the06-attention-sentinel.shhook already clears onStop/UserPromptSubmit, but leaks when a terminal is killed hard, a Claude version has no hook, or thesessionIddoesn't round-trip; the reader is the only layer withpidAliveground truth. Verified on mac-mini: 6 stale sentinels aged 1–22 days pruned to 0 on one dump run.addNeedsAttentiongroups blocked sessions by(agent, repo)and collapses groups of 2+ into a single<Agent> · <repo> · N waiting · oldest <t> ›row + submenu, dropping the generic— Claude is waiting for your inputfiller when the Notification message is empty.addActivecollapses the"other"bucket to a single clickableACTIVE · other · N idle ›row when idle-only, and replaces the silent 3-cap on idle rows with an explicit+ N more idle ›row + submenu so the header count always matches visible + explicit-hidden. No new session state — closed = hidden, as before.The menu-bar helper no longer crash-loops on macOS 26. npm's pack/extract strips the ad-hoc signature the release bakes into
MenubarHelper.app, leaving itcode object is not signed at all. macOS 26's code-signing monitor SIGKILLs an unsigned binary at launch (SIGKILL (Code Signature Invalid)), so under the launchdKeepAliveservice it restarted forever, and its unstable identity made the Accessibility grant (needed for the clip→paste keystroke inClip.swift) re-prompt every time. The install path now re-signs the copied bundle ad-hoc and verifies it before bootstrapping the service, so every machine gets a valid signature the kernel accepts — and a bundle that can't be made valid is skipped instead of spun in a crash loop. A Developer-ID-signed helper (which survives npm) is left untouched. Source:apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/menubar/scripts/build.sh.The configured model now shows wherever an agent is displayed.
agents view,use,add,status, andinspectsurface the model an agent+version actually runs with, beside the version (the identity cluster readsagent · version · model · account). The model is resolved agents.yamlrun.defaults→ the nativesettings.json→ the built-in default, andagents view --jsongains aconfiguredModel { model, source }field so downstream tools can read both the value and where it came from. Source:apps/cli/src/lib/models.ts,apps/cli/src/commands/view.ts.agents repo pullnow reloads the routines daemon so device pins refresh. The scheduler froze each routine's config — device pins included — in memory at daemon start. Arepo pullrewrites the synced routine YAML on disk (a routine re-pinned to another host, say), but without a reload the daemon kept firing the pre-pull pins, so a routine moved to another device still fired on the old host too — a phantom double-fire across the fleet. A successful pull now SIGHUPs the running daemon (scheduler.reloadAll()), re-reading the YAML so pins refresh. A no-op when the daemon isn't running or on Windows (no SIGHUP). Source:apps/cli/src/commands/repo.ts.Routines can pin a Claude account by identity to stop the OAuth-rotation logout storm. Unpinned
clauderoutines pick an account with the defaultbalanced(stateless weighted-random) strategy, so two concurrent unattended runs — on one box or across the fleet — can land on the same account; Claude's refresh token is single-use and rotates server-side, so the second refresh revokes the first run's token mid-flight (401 OAuth access token has been revoked). Across ~20 routines waking in one morning window that is a self-inflicted logout storm (RUSH-1957). A routine may now setaccount:(a login email or account key) to pin the run to the version slot holding that account — no rotation, no usage-read refresh, no failover onto other accounts — so each routine (or each device's routines) refreshes one credential nobody else touches. Prefer it overversion:, which pins a version number that is GC'd on the next upgrade, silently dropping the routine back tobalanced. An account that is not signed in on the box warns and falls back to the strategy rather than refusing to run. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/rotate.ts,apps/cli/src/lib/runner.ts.Balanced account rotation now works for scheduled Claude routines. The routines daemon injects one
CLAUDE_CODE_OAUTH_TOKENinto its environment so a token-less default account still authenticates (RUSH-1759). But Claude — and the Linux shim's own-z CLAUDE_CODE_OAUTH_TOKENguard — both prefer that env var over a pinned account'sCLAUDE_CONFIG_DIR, so once balanced rotation pinned a specific account the injected token shadowed it: the whole pool was inert and every fire authenticated as (and eventually 401'd on) the one token. A routine spawn now drops the injected token when the rotated account holds its own on-disk credential, so it authenticates as that account; when the account has no on-disk credential (the RUSH-1759 default) the injected token is kept. Source:apps/cli/src/lib/runner.ts(buildRoutineSpawnEnv),apps/cli/src/lib/agents.ts(claudeHomeHasOwnCredential).No more Touch ID prompt on every new agent session. Bundle metadata (names, descriptions, variable names + references, and non-sensitive
--valueliterals) is now stored WITHOUT the biometry ACL at every prompt-policy tier, not justnever. Metadata is non-sensitive by contract — real secret values live in separateagents-cli.secrets.*items that keep the bundle's policy ACL — so enumerating bundles no longer needs a keychain unlock. This kills the recurring Touch ID prompt that fired on every new Claude/agent terminal: a SessionStart hook runsagents devices list, which scans bundle metadata through crabbox, and that scan used to pop Touch ID once per broker window (~7 days) on every cold launch.agents secrets listis now silent too. Reading a bundle's actual values (run injection,view --reveal) still prompts. Existing bundles are migrated automatically and once: the first metadata scan after upgrade re-homes each bundle's metadata item no-ACL (reusing the read it already did, so it adds no extra prompt), and every scan after that is prompt-free. Source:apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/lib/secrets/index.ts.Wire workflows support for Grok (RUSH-1863). Grok Build ships native Workflows as of v0.2.111 (on by default): file-defined Rhai orchestration scripts in
~/.grok/workflows/<name>.rhai(user-global, underGROK_HOME) and.grok/workflows/(repo-level). agents-cli now declaresworkflows: { since: 0.2.111 }for grok, projects centralWORKFLOW.mdbundles into managed Rhai scripts viatransformWorkflowForGrok(with an// agents_workflow: <name>marker so user-authored scripts are never overwritten), and registers the writer + detector soagents workflows add --agents grok@…/ sync land files Grok can invoke as/<name>. Distinct from the grok commands gap (RUSH-1851). Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/workflows.ts,apps/cli/src/lib/staleness/{writers,detectors}/workflows.ts,apps/cli/src/lib/versions.ts.Dispatch-bar screenshots now upload via
linear create --imageinstead of landing as dead local paths. The menu-bar helper previously injected screenshot paths into the ticket-agent prompt, and the model echoed them into the issue description as/Users/…text. The agent now returns ticket fields as JSON, and the helper itself runslinear createwith--image <path>for each selected screenshot, so paths pass through Swift argv and survive spaces or@in CleanShot filenames. Coordinates with thelinear create --imagesupport added inphnx-labs/linear-cli#28. Source:apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift,apps/cli/menubar/Sources/MenubarHelper/IssueSelfTest.swift.Native helper bundles now ship the agents-cli icon and the computer helper is branded "Agents Computer". MenubarHelper.app, ComputerHelper.app, and the keychain
Agents CLI.apppreviously had noCFBundleIconFile/.icns, so Notification Center and System Settings → Privacy & Security showed a blank square. Each build script now generatesAppIcon.icnsfromassets/logo.pngand addsCFBundleIconFileto the bundleInfo.plist. The computer helper display name changed from "Computer Helper" to "Agents Computer" while keeping its bundle id and on-disk path, so existing Accessibility/Screen Recording grants remain valid. Source:apps/cli/menubar/scripts/build.sh,native/computer-mac/scripts/build.sh,apps/cli/scripts/build-keychain-helper.sh,apps/cli/src/commands/setup-computer.ts,apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift.One resolver for
--host/--device— every subcommand now dials the same box (RUSH-1967). A host token used to resolve through two disagreeing code paths:run --host(and the generic passthrough, teams placement, doctor, funnel, remote secrets) let a~/.ssh/configstanza win and dialed its bare name, whilesessions --host, session bundles, andagents sshdialed the device Tailscaleuser@dnsName. The same name could reach two different machines, and because the two emitted different target strings they never shared a multiplexed SSH connection. Resolution is now a single merged lookup (matchHost): the live devices registry supplies address/OS/presence, the agents.yaml overlay supplies capability tags, and ssh_config supplies hosts Tailscale has never seen — merged per-field, not one shadowing another. Fallout fixed with it: an enrolled device address always comes from the live registry (soagents devices synctakes effect without re-enrolling, no more frozen route), an enrolled device keeps its presence anddispatchableflags, a password-auth device cannot be made dispatchable by shadowing it with an inline entry, and a host present only in~/.ssh/configis now visible to thesessions --hostfan-out.agents run --device/--hostnow auto-reconnects when the network drops. A remote interactive agent runs in a detached tmux session on the peer, so an SSH blink kills only the local client — the agent keeps running. Previously the local side exited with ssh's connection-layer code (255) and you had to notice, find the session id, andagents sessions focusby hand. Now, when a tmux-hosted run with a known session id drops (exit 255), the client re-attaches the live remote pane automatically over SSH — reusing the peer's ownagents sessions focus <id> --local --attach-only(a live join, not a resumed copy) — with bounded exponential backoff (2s→30s, up to 6 attempts, and the budget refills after a genuinely live reconnection). A clean detach (Ctrl-b d, exit 0) or a real agent exit (any non-255 code) is left alone;--raw/no-tmux runs, which don't survive a drop, are not retried. This covers Claude and resumed runs today; capturing a resumable id for other agents on the--devicepath is tracked in RUSH-2007. Source:apps/cli/src/lib/hosts/reconnect.ts,apps/cli/src/commands/exec.ts.The secrets broker cache now actually works on the shipped macOS binary — one Touch ID per bundle per hold window, not one per read. The three synchronous broker clients (
agentGetSync,agentReachableSync,agentEvictSync) spawnedprocess.execPath -e <inline node program>, which is only correct whenprocess.execPathis node. Since 1.20.53 the macOSagentsis a bun-compiled Mach-O, soprocess.execPathis the CLI itself and the spawn becameagents -e …— rejected witherror: unknown option '-e'and a non-zero exit. Each client then took its own failure path (null/false/ no-op), which the caller reads as "broker down" and falls through to a real keychain read. Net effect: on every standalone install the hot cache was never hit, so thedailypolicy's one-prompt-per-7d never applied and every bundle read re-popped Touch ID —agents secrets statuswould report the broker running while holding nothing but explicitlyunlocked bundles (the durable session-store path, the only client that never spawned). Same defect class as the broker launch fixed in 1.20.56 and the PTY sidecar in 1.20.72; these three sites were simply never converted. They now spawn top-level__secrets-get/__secrets-ping/__secrets-locktokens built by the sharedgetCliLaunchprimitive and intercepted inindex.tsbefore commander — alongside__daemon-runand__vault-age-helper, and deliberately above the line where every normal command runscheckForUpdates()and forks a detached background sync, which would otherwise fire on every cache hit. Source:apps/cli/src/lib/secrets/agent.ts,apps/cli/src/index.ts.Let a bundle whose passphrase is lost be deleted, so the name can be recovered. A file-backed bundle that no longer decrypts bricked its own name:
view,add,delete, and bothimport --from icloudandimport --from 1passwordall calledreadBundle()first, so none of them could touch it — including the two commands that exist to restore it from a valid iCloud Keychain or 1Password copy.deletenow uses the newreadBundleIfDecryptable()and proceeds without the plaintext, reporting that the bundle's keychain items cannot be enumerated for purging instead of claiming a clean purge. Theviewhint no longer points atimport --from icloudfor a bundle that is still on disk — that command fails identically — and names the delete-then-import sequence that actually works. Only a genuine decrypt failure counts as deletable: a bundle that is merely locked for the run (headless macOS with noAGENTS_SECRETS_PASSPHRASE) still fails loudly and is left in place, sosecrets delete <name> --yesfrom a cron/launchd run that forgot to export the passphrase can't silently destroy a healthy bundle. Source:apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/commands/secrets.ts.Session tab titles no longer go missing or stale. A rescan that carried an empty or whitespace-only label used to clobber a good stored label, because
upsertSession/upsertSessionsBatchwrotelabel = excluded.labelunconditionally on conflict. TheON CONFLICTclause now preserves an existing non-empty label and only overwrites when a real label arrives, so a/rename, agent-generated title, or--namehandle survives later rescans. Headless runs launched with--namenow also surface that name as the session label (matching the terminal path) instead of showing only the topic. Source:apps/cli/src/lib/session/db.ts,apps/cli/src/lib/session/active.ts.agents sessions --activeno longer shows zombie sessions from recycled pids. Liveness was a bareprocess.kill(pid, 0)existence check, so once the OS handed a dead session's pid to an unrelated process, that session kept showing as alive — and the registry GC never pruned it.isPidAlivenow takes the session's recordedstartedAtMsand, when a start time is available, verifies the process at that pid did not begin meaningfully after the session started (a 60s window): a process that started later is a reused pid, so the session is dead. The start time is read once viaps -o lstart=(macOS + Linux); Windows and any unreadable start time fall back to the existence check, never worse than before. Applied to every registry-backed liveness path — the live-terminals filter, the terminal listing, the tmux-pane resolver, and the pid-registry prune. Source:apps/cli/src/lib/session/active.ts.agents sessionsno longer corrupts the index with emptyshortIdrows. Session ids that are only a known prefix — a baresession_Rush directory, an id of exactlyapi-(Hermes) orses_(OpenCode) — used to strip to''('session_'.replace(/^session_/, '').slice(0, 8) === ''). An emptyshortIdpasses theshort_id TEXT NOT NULLconstraint (empty string is not NULL) yet matches nothing in theshort_id LIKE ?picker lookups, so the row was silently unaddressable. All shortId derivation is now routed through one helper,deriveShortId, that guarantees a non-empty result by falling back to the unstripped id when the strip empties it. Every producer — the twelve parsers indiscover.ts,session/cloud.ts,cloud/session-index.ts,hosts/session-index.ts,session/fork.ts, andcommands/go.ts— uses it, replacing the duplicated inline.slice(0, 8)(some with a.replace(prefix, '')). Source:apps/cli/src/lib/session/short-id.ts.Honest live status: report
unknowninstead of a fakeidle(RUSH-1976).agents sessions --activenow reports an explicitunknownstatus (◌) for a live agent whose activity it cannot introspect — a running gemini/droid/cursor/opencode whose transcript format is not parsed — instead of the misleadingidleit showed before. Status resolution is standardized in one place (resolveFallbackStatus): a vanished transcript file no longer flips to a falserunning, an unanswered prose question with no mtime signal no longer sticks as "waiting on you" forever (the RUSH-1522 null-mtime hole), and theps/lsofprobes behind the scan now have hard timeouts so a hung syscall can't silently drop live sessions. Source:apps/cli/src/lib/session/active.ts(resolveFallbackStatus),apps/cli/src/lib/session/state.ts,apps/cli/src/commands/sessions.ts.Interactive session browser: preview-by-default with clickable ticket + PR links. In
agents sessions/agents sessions --active, the highlighted row's preview is now open by default (tabtoggles it off), and the preview's links line renders the ticket and PR as OSC 8 terminal hyperlinks — the ticket resolves to its Linear URL (workspace slug resolved config-first) andPR#resolves to its GitHub URL — so they are click-through in terminals that support them. Source:apps/cli/src/lib/picker.ts,apps/cli/src/commands/sessions-browser.ts,apps/cli/src/lib/session/render.ts.agents sessionsnow lists what agents-cli manages, not your own installs. Discovery scans the union of your real~/.<agent>and every managed version home, so once you had managed versions the listing mixed both — most visibly afteragents add --isolated, where keeping the two apart was the whole point. Listing is now scoped to managed versions (isolated or not);--unmanagedbrings your own installs back, and every render path prints what it hid (N sessions from your own unmanaged installs hidden) so nothing disappears silently. A user who has never runagents addsees exactly what they saw before — with nothing managed there is nothing to scope to. Scoping happens at query time rather than by narrowing the scan, so the index stays complete,--unmanagedneeds no re-scan, and watchdog /--roots/ the Factory watcher are unaffected. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/commands/sessions.ts.Attribute split-spawned agents on the authoritative tmux source (RUSH-1976).
agents sessions --activenow attributes each tmux pane to the agent actually running in it: an agent bare-spawned into a split of an existing shared-socket session (where$TMUXis already set, so no new session meta is stamped) is surfaced by the authoritative tmux source with its own exact identity and pane, instead of being dropped there and left to the weakerps-scan fallback. Attribution reads the per-pane launch registry (the id recorded at launch, or the SessionStart-hook join by launchId for a non-Claude agent), gated on the pid still being alive so a dead agent's pane can't linger. Source:apps/cli/src/lib/session/active.ts(resolvePaneIdentity,listTmuxAgentSessions),apps/cli/src/lib/session/pid-registry.ts(listPidSessionEntries).Watchdog v2 — the always-on watchdog now has judgment and delivers correctly into VS Codium. The 2-minute
agents watchdog --nudgeroutine no longer hard-skips a session that stopped to ask a question. The deterministic pass is now a cheap pre-filter (clearly-complete → skip, clear promise-without-toolcall → nudge) that ESCALATES the judgment-heavy cases — a session parked on a question, or an ambiguous stall — to a smart brain. The brain drives the agent to finish end-to-end when it asked a needless / already-authorized question or paused with work left, and leaves it for the human only for genuine cases (credentials/auth, an irreversible or outward-facing action, a real ambiguous product decision, or a finished task). Nudge messages restate the goal, tell the agent to use best judgment, and give one concrete next step. The brain is a customizablewatchdogworkflow: drop awatchdogWORKFLOW.md in your project or userworkflows/to override the prompt and pick themodel:; absent one, the improved built-in prompt runs viaagents run … --mode plan. Source:apps/cli/src/lib/watchdog/watchdog.ts,apps/cli/src/lib/watchdog/runner.ts.Watchdog delivery routes through the answer-router with the VS Codium rail working. A running agent is steered via its mailbox; a parked-on-question agent is answered into its EXACT split — including a VS Codium / Cursor / VS Code integrated terminal, which the answer-router's own resolver could not address — or, when headless, re-entered via resume; a parked agent with no addressable rail is flagged, never a guessed target.
Watchdog precedence + concurrency fixes. A long-idle (>15m) open question is no longer blindly force-nudged — waiting-on-user and completion now win over the 15-minute force-review short-circuit. The per-session cooldown ledger is written under a file lock (fresh-read + merge + atomic write), closing a lost-update race between concurrent ticks.
Watchdog decisions are logged to
~/.agents/.cache/logs/watchdog.login the JSONL shape the Factory Floor watchdog card reads, so it keeps working after the extension-side watchdog is retired.The always-on watchdog is now a daemon-fired routine, not a hand-rolled loop + sentinel.
agents watchdog enableused to flip a private~/.agents/.cache/state/watchdog/enabledsentinel that only meant anything to a manually-launchedagents watchdog --watchloop — so the auto-nudge only ran while some shell was babysitting it. It now creates and enables a plainwatchdogcommand routine (agents watchdog --nudge, every 2 minutes) and reloads the daemon, so the always-on watchdog is fired by the same scheduler that runs every other routine: it survives reboots, catches up if the daemon was down, and shows up inagents routines list.disablepauses that routine;statusreports whether it is enabled. The Swift menu-bar toggle andwatchdog status --jsonare unchanged. Bareagents watchdog(dry) andagents watchdog --watch(now dry unless--nudge) still work for ad-hoc runs. If you had already opted in under the old build, a one-shot migration folds that state forward — you stay enabled, now as the routine. Source:apps/cli/src/lib/watchdog/routine.ts,apps/cli/src/commands/watchdog.ts,apps/cli/src/lib/migrate.ts.Wire hooks support for OpenCode through generated plugins (RUSH-1850).
hooks.yamlentries now compile into~/.config/opencode/plugins/agents-cli-hooks.ts, mapping tool, prompt, and session lifecycle events to OpenCode's native plugin API and executing managed scripts with Bun's$shell primitive. OpenCode hooks are capability-gated to v0.3.130 and newer.
1.20.74
agents apply --agent claude@all --device <box>— replicate this machine's exact version set. The fleet roster is agent-granular, so a fresh box only ever got oneclaude@latest— losing a multi-version setup (e.g. several claude versions, one per Max account, to spread rate-limit quota). The new--agent <specs...>flag overrides the roster for the targeted device(s):claude@allexpands source-side to every version installed here ([email protected],[email protected], …) and installs each missing one on the target; a pinned[email protected]installs that exact version even if another claude is present. Version-pinned specs diff against a per-deviceagents view --jsonprobe, so the plan installs only what's missing and login still propagates once per agent. Source:apps/cli/src/commands/apply.ts,apps/cli/src/lib/fleet/apply.ts.Project resource manifests are now portable across Windows and POSIX. The managed-resource manifest
.agents-managed.jsonrecorded its paths with the host's native separator, so a sync run on Windows wrote entries likeskills\myskill. That file lives in the version-controlled project.agentsdir and travels between machines, and the cleanup pass matches manifest entries withpath.sep— so a manifest written on Windows silently failed to match on macOS or Linux and left previously managed files behind on the next sync (and vice versa). Manifest paths are now normalized to POSIX separators on write and on read, which also repairs manifests written by earlier Windows builds. Source:apps/cli/src/lib/project-resources.ts.agents import <agent> --isolated— bring your existing setup into a sandbox. Isolation was a cold start: a new isolated copy began empty, and the only way to get settings into it was by hand. A plainagents importis the opposite of what is wanted here — it adopts, moving~/.<agent>into a version home, symlinking the original away, setting the global default and creating a shim (and is now refused outright for an isolated-only agent).--isolatedcopies instead: your settings land in the isolated home, your real config stays exactly where it is, and the version is finalized the wayagents add --isolateddoes — versioned alias and marker, no default, no bare shim, no config symlink. Credentials are skipped by default and named in the output rather than silently included, since an isolated copy signs in as its own principal;--with-authopts in. Symlinks into~/.agentsare dropped so the copy does not depend on the CLI's tree. Source:apps/cli/src/lib/import.ts,apps/cli/src/commands/import.ts.agents use <agent>@<isolated>now works, and a bareagents run <agent>reaches your isolated copy. Isolated installs were unreachable by name:resolveVersionended at the global default, and an isolated install deliberately never becomes one — soagents userefused, and an isolated-only user had to type the fullagents run [email protected]every time while a bareagents run codexfell through to whatevercodexmeant on PATH.usenow records an isolated default instead of refusing, and resolution falls back to it (project pin -> global default -> isolated default). Strictly a fallback, so nothing changes for anyone who has a global default. The pointer lives inisolatedAgents:inagents.yaml, never in the globalagents:map — that separation is what keepsgetGlobalDefaultincapable of returning an isolated version, and with it the launcher, bare shim, config symlink and self-healshadowingcheck all stay out of reach. It is verified on read and re-pointed (or cleared) on removal, so it can never resolve to a version that is gone.agents viewlabels it(isolated default). Source:apps/cli/src/lib/versions.ts,apps/cli/src/commands/versions.ts,apps/cli/src/commands/view.ts.agents export <agent>[@<version>]— take an isolated install's config with you.--isolatedwas a one-way door: it builds a self-contained home under the version dir and nothing ever brings that work back, so a user who configured a sandboxed copy for a week had to copy files by hand to promote it — or to leave. Export is additive by default: it copies only paths you don't already have, and a collision is not silently skipped — the incoming file is written beside yours as<name>.from-agents-cliso you can--diffit and take the parts you want. Your files are never modified.--replacepromotes a sandbox wholesale (yours is moved tobackups/<agent>/<ts>, and it is the only mode that asks for confirmation);--stageddumps the tree into~/.<agent>/.agents-export-<ts>/and activates nothing. Every mode strips symlinks pointing back into~/.agentsso the result keeps working after agents-cli is gone, keeps your own symlinks, and writes a receipt to~/.<agent>/.agents-cli-export.jsonrecording exactly what came from the export — which makes "which of these files are mine?" answerable and the whole thing reversible. A~/.<agent>that agents-cli already adopted is refused, since writing there would mutate that version's home rather than your config. File contents are never auto-merged: the TOML parser here drops comments across parse+stringify, so unioning keys would silently delete them. Source:apps/cli/src/lib/export.ts,apps/cli/src/commands/export.ts,apps/cli/src/lib/config-transfer.ts.An isolated-only agent can no longer be adopted by anything.
--isolatedused to be defined by what it doesn't do — no global default, no bare shim, no config symlink, no PATH edit — which meant every code path that could adopt an agent had to remember to check first. It leaked three times that way. Protection is now derived from the.isolatedmarkers on disk (isIsolationProtected: at least one installed version, and every one isolated) and enforced inside the five primitives that can cross the boundary —setGlobalDefault,createShim,switchConfigSymlink,switchHomeFileSymlinks,adoptShadowingLauncher— so refusal is a property of the code rather than a convention. There is no mode to set and none to forget: installing with--isolatedis the opt-in, it is per-agent, and the escape hatch is inherent (remove the isolated copies and the agent is ordinary again).agents add,agents importanddoctor --adoptrefuse with guidance rather than a stack trace —importis additionally checked at its entry point, because it registers the adopted install as a normal version before adopting, which would otherwise un-protect the agent underneath the primitive gate. Clearing a global default stays allowed, since removal legitimately clears one as an agent becomes isolated-only. A completeness test pins the primitive list and scans for any new ungated mutator. Source:apps/cli/src/lib/shims.ts,apps/cli/src/lib/versions.ts,apps/cli/src/lib/isolation-boundary-report.ts.agents viewno longer hides your own CLI behind an isolated install. The listing was either/or per agent: any managed version at all suppressed the "Not Managed by Agents CLI" block, so a singleagents add <agent>@<v> --isolatedmade the user's globally-installed CLI disappear from the one command they'd run to confirm--isolatedhad left it alone. Nothing on disk was ever touched — the isolation boundary holds — but the report read exactly like the damage it was supposed to rule out. Isolated copies now render alongside the global install and are tagged9.9.4 (isolated); a normal (non-isolated) version still takes the launcher over and still suppresses the global row, since that row would just be our own shim. The global row is also resolved from PATH now (getUnmanagedCliState) instead of from the version dirs, which could otherwise report an isolated copy — deliberately unreachable from PATH — as(global). Source:apps/cli/src/commands/view.ts,apps/cli/src/lib/agents.ts.agents sessions --activenow resolves the exact session id for non-Claude and user-typed agents. Previously only Claude (launched with a known--session-id) got an exact id; every other agent fell back to "newest.jsonlin the cwd", which collapses co-located agents onto one row.ag runnow mints a launch id and exports it asAGENT_LAUNCH_IDon every launch path (bare spawn, tmux, and the Windows shim); the agent's own SessionStart hook already records that id, so the active-scan reconciles aps-discovered process to the hook's authoritative session id bylaunchId(robust even when the hook runs under a different pid — a tmux pane leaf orcmd.exewrapper), falling back toterminalIdand pid. This also attributes agentsag runnever launched (you typingclaudein a terminal). No on-disk directory moved — the CLI reads the existing hook state files read-only, so old installed hooks and a new CLI coexist safely. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/session/{pid-registry,hook-sessions,active}.ts.
1.20.73
agents cli install <binary-cli>no longer hardcodes/usr/local/bin(#1103). Binary-method installs downloaded (and extracted archives) straight into/usr/local/bin, which fails withEACCESon Apple Silicon Macs where that directory is root-owned and not user-writable. A newresolveBinDir()picks the install directory instead: honorAGENTS_CLI_BIN_DIRif set, else prefer~/.local/bin(created on demand — the same XDG user-bin dir shims already use), else fall back to/usr/local/binwith an actionable error pointing atAGENTS_CLI_BIN_DIR/~/.local/bininstead of a bareEACCES. Source:apps/cli/src/lib/cli-resources.ts,apps/cli/src/lib/cli-resources.test.ts.Stream host follows over one persistent SSH connection (RUSH-1407).
run --hostandhosts logs -fnow follow remote logs with a long-livedtail -fstream that reconnects from the saved byte offset and captures the remote.exitcode without per-cycle SSH spawns. Source:apps/cli/src/lib/hosts/progress.ts.Reuse warm crabbox boxes from
agents run(RUSH-1609).agents run <agent> "<task>" --box <slug>now targets an existing warm crabbox box, runs the same bootstrap and credential provisioning as--lease, and leaves the box running for reuse across repositories. Source:apps/cli/src/commands/exec.ts,apps/cli/src/lib/crabbox/lease.ts.Show reasoning in Factory progress timelines (RUSH-1634). Factory detail panes now interleave assistant prose and reasoning summaries with tool calls in the Progress rail, so agent activity explains intent instead of showing only file/tool touches. Source:
apps/factory/src/core/session.summary.ts,apps/factory/ui/settings/components/mission-control/Timeline.tsx.Expose an Agents HQ floor snapshot bridge (RUSH-1638).
agents hq floor --jsonnow emits a machine-readable floor snapshot that joins live sessions, teams, feed blocks, room placement, ambient events, and command-backed actions for HQ clients. Source:apps/cli/src/commands/hq.ts,apps/cli/src/lib/hq/floor.ts.Menu-bar Quick Dispatch attaches selected screenshots after ticket creation (RUSH-1693). The helper now uploads every selected quick-capture screenshot to the created Linear issue itself after parsing the
Created RUSH-###result, instead of relying on the ticket agent to run a second proof-upload command from its prompt. Source:apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift,apps/cli/menubar/Sources/MenubarHelper/IssueSelfTest.swift,apps/cli/docs/menubar.md.Move supported OpenClaw secrets into Keychain-backed refs (RUSH-175).
agents secrets openclaw-keychain migratenow stores supported OpenClaw plaintext credentials in macOS Keychain, rewrites OpenClaw config fields to exec SecretRefs, and refuses to delete top-level env secrets that have no supported SecretRef target. Source:apps/cli/src/lib/openclaw-keychain.ts,apps/cli/src/commands/secrets.ts,apps/cli/docs/secrets.md.Provision share workers completely (RUSH-1792).
agents share setupnow configures the R2 lifecycle rule and sets the WorkerWRITE_TOKENthrough Cloudflare's Workers Secrets API after deploying the R2-bound Worker. Source:apps/cli/src/lib/share/provision.ts.Extract a reusable share-publish endpoint seam (RUSH-1794).
agents share <file>now delegates its authenticated PUT (bearer token,--slug,--expire) throughpublishToEndpoint, decoupled from config/keychain loading, with a real-HTTP test asserting the wire contract. Source:apps/cli/src/lib/share/publish.ts.Map the default share domain automatically (RUSH-1796).
agents share setupnow mapsshare.agents-cli.shwhen the Cloudflare token can see theagents-cli.shzone, while keeping the workers.dev endpoint when it cannot and honoring--domainoverrides. Source:apps/cli/src/commands/share.ts,apps/cli/docs/share.md.Expire shared artifacts end to end (RUSH-1797).
agents share --expirenow storesexpires-atmetadata for the Worker to enforce andagents share setupinstalls a managed R2 lifecycle rule so old share objects self-clean. Source:apps/cli/src/lib/share/{publish,provision,worker-template}.ts.agents sharecentral mode now follows synced config plus injected write tokens (RUSH-1798).agents share joincan bind an existing synced endpoint without reprovisioning, publish readsSHARE_WRITE_TOKENfrom runtime env before falling back to the localsharebundle, and agent/team/supported cloud launches propagate the token when it is already available so ephemeral agents can publish durable links with no Cloudflare setup. Source:apps/cli/src/commands/share.ts,apps/cli/src/lib/share/config.ts,apps/cli/src/commands/exec.ts,apps/cli/src/commands/cloud.ts,apps/cli/src/commands/teams.ts,apps/cli/src/lib/cloud/rush.ts,apps/cli/src/lib/cloud/factory.ts,apps/cli/src/lib/cloud/codex.ts.Plan-render auto-publish plumbing (RUSH-1799).
agents share <file> --jsonnow emits a stable{ url, coverUrl, expiresAt }result so plan-render hooks can publish rendered HTML and post the returned link without scraping terminal output. Source:apps/cli/src/commands/share.ts,apps/cli/src/lib/share/publish.ts.Added
agents shareregression coverage for token storage, publish upload headers, expiry metadata, and Cloudflare provisioning request shapes without calling real Cloudflare in CI. Source:src/lib/share/{config,publish-file,provision}.test.ts,src/lib/share/provision.ts.agents repo pullnow fast-forwards the local checkout after fetch (--ff-onlysemantics) and reports when it is blocked by local changes or local commits instead of leaving the checkout behind origin. The system repo uses the same fast-forward path as user and extra repos.agents sharedefault links are much harder to guess (RUSH-1821). The random tail of an auto-generated share slug is now a 64-bit nonce (randomBytes(8), 16 hex chars) instead of the old 24-bit / 6-hex tail — closing a~16.7M-possibility space that was small enough to brute-force. Since share reads are public (the URL is the only capability), the nonce is the whole defense, so it now carries the full 64 bits. Passed-in--slugvalues and existing links are unchanged.docs/share.mdnow states the security model explicitly (unlisted-not-secret; reads are public; use--expirefor sensitive content; an opt-in auth-gated read is a future option). Source:apps/cli/src/lib/share/publish.ts(defaultSlug),apps/cli/src/lib/share/publish.test.ts,apps/cli/docs/share.md.agents secrets list/viewgain--json(RUSH-1834). Agents can now discover which secrets bundles and keys exist as machine-readable JSON before injecting one —list --jsonemits bundle metadata (name, key count, policy, backend, timestamps) andview <bundle> --jsonlists each key with its kind and stored/missing state. Values staynullunless--reveal(which keeps the same non-TTY--plaintextgate and audit event as the human view), so the discovery surface never leaks a secret. Gated on the explicit--jsonflag, notstdout.isTTY. Source:apps/cli/src/commands/secrets.ts.Per-user URL namespaces + privacy-first analytics for
agents share(RUSH-1835). Shares now publish under the publisher's GitHub username (share.agents-cli.sh/<user>/<slug>), with/<user>rendering a public gallery and legacy flat slugs still resolving. Every HTML publish also injects a cookieless Cloudflare Web Analytics beacon (opt out with--no-analytics). Configure the token duringagents share setup --analytics-token, and check status withagents share status/agents share analytics. Source:apps/cli/src/commands/share.ts,apps/cli/src/lib/share/{publish,analytics,worker-template}.ts,apps/cli/src/lib/git.ts,apps/cli/docs/share.md.Codex no longer breaks on macOS when its versioned
CODEX_HOMEoverflows the Unix-socketSUN_LENlimit. Codex binds an app-server control socket at$CODEX_HOME/app-server-control/app-server-control.sock, and macOS caps Unix socket paths at 104 bytes (SUN_LEN). agents-cli pointsCODEX_HOMEat the deep versioned home (~/.agents/.history/versions/codex/<version>/home/.codex), which for a typical user is long enough that the derived socket path exceeds 104 bytes — socodex app-server daemon startfailed withpath must be shorter than SUN_LENand every codex spawn on macOS died (this took down every OpenClaw agent on a mac-mini). Codex exposes no socket-path override and resolves symlinks before binding, so a short symlink to the deep home does not help. The codex shims andbuildExecEnvnow detect the overflow on macOS and relocate the home once to a short real directory under~/.agents/.codex-homes/<version>/.codex(leaving a symlink behind so the versioned path still resolves), keeping config, auth, and state intact. A caller-setCODEX_HOMEis always respected. Source:apps/cli/src/lib/codex-home.ts(new),apps/cli/src/lib/shims.ts,apps/cli/src/lib/exec.ts.Gate GitHub webhook routines on pull request labels (RUSH-203).
agents routines add --on github:pull_requestandagents cloud run --on prnow preserve GitHub--actionand--labelfilters, so a UX test routine can fire only when a PR receivesux-approved. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/triggers/webhook.ts,apps/cli/src/commands/routines.ts,apps/cli/src/commands/cloud.ts.Remove the deprecated
agents daemoncommand tree (RUSH-403). The legacyagents daemon start|stop|status|logsaliases are gone for v2.0; useagents routines start|stop|status|scheduler-logsfor scheduler controls. Source:apps/cli/src/lib/startup/command-registry.ts,apps/cli/src/index.ts,apps/cli/src/lib/daemon.ts,apps/cli/docs/03-routines.md.
Added top-level resource profiles via agents profile use, filtering synced resources and secrets bundles by the active profile.
Fixed source-qualified resource profile selectors for permission groups and workflows so project:, user:, and system: patterns match the real resource layer.
Fixed resolveResource to fall through to lower-precedence layers when a higher-layer match is excluded by the active profile, matching listResources behavior.
Support multiple accounts per secrets bundle (RUSH-668). Secrets bundle keys now accept
BASE.accountnames such asGITHUB_USERNAME.personal; selected account variants inject as the base env key and conflicting variants fail loud unless narrowed with--keys. Source:apps/cli/src/lib/secrets/bundles.ts,apps/cli/docs/secrets.md.Harden synced vault writes (RUSH-682). Synced secret mutations now lock the vault across the full read-modify-write cycle and persist through an atomic rename, preventing concurrent CLI processes from losing each other's bundle updates. Source:
apps/cli/src/lib/secrets/vault.ts.
Features
Added
agents login,agents logout, andagents whoamiplusagents secrets create --syncedfor age-encrypted synced secrets stored in~/.agents/vault.age.Protected synced secrets vaults from accidental replacement:
agents login --createandagents login --join <path>now require--forcebefore replacing an existingvault.age, and synced bundle writes batch metadata plus stored keys into one vault update.Synced vault encryption now runs without re-executing the
agentsbinary, so standalone macOS installs can encrypt and decrypt vault data reliably; new vault writes also use the age library's default scrypt work factor.Keep project resources in workspace config (RUSH-705). Project-scoped commands, skills, subagents, and workflows now sync into the current workspace's
.<agent>/directory instead of lingering in global agent version homes. Source:apps/cli/src/lib/project-resources.ts.Track Goose workflow subrecipes from first sync (RUSH-705). Goose workflow
.subrecipes/directories are tracked in the ownership manifest from the first sync, preventing workflows from being incorrectly skipped on later syncs. Source:apps/cli/src/lib/project-resources.ts.agents activity— an event-sourced view of what agents did. A new append-only, per-session event log (~/.agents/.history/activity/<sessionId>.jsonl) records agent-semantic milestones at hook time (plans, PRs, worktrees, sub-agents, artifacts) with no transcript re-parsing.agents activityrenders the stream newest-first (milestones individually, routine edits collapsed to a count);agents feedgains a compact recent-activity lane. Source:apps/cli/src/lib/activity.ts,apps/cli/src/commands/activity.ts.Lease command surface: reuse picker, devices section, step UI, and Tailscale net-mode (RUSH-1922/1923/1924). Wires the command layer onto the merged crabbox-core lib:
- Reuse (F3). On an interactive
agents run … --lease, a picker lists your warm boxes (ready+ unexpired, most-recently-touched first) and offers "Provision a fresh box" / "Always provision fresh (remember for this repo)". New flags:--reuse(scriptable — auto-pick the freshest warm box, else fresh) and--bare(skip copying your local~/.agentssetup onto the box, i.e.copySetup=false). Headless /--jsonnever blocks — it provisions fresh unless--reuse/--boxis given. New subcommandsagents lease list(--json) andagents lease stop <slug>. - Step UI (F2). The box-side setup now renders as a live checklist —
each
___PHASE_<name>___step from the lib'sonStepstream prints viarenderStepLine(✔ Step — detail (elapsed)). Non-TTY prints one line per step;--jsonemits{phase:"setup",name,elapsedMs}events. Host-side warmup/ready/teardown phases are unchanged. - Devices (F4).
agents devicesgains a live "Leased boxes (ephemeral · via crabbox)" section computed fromcrabboxList()— never written into the device registry.agents ssh <slug>now resolves a leased-box slug and connects tocrabbox@<tailnet-or-ip>:2222. - Net-mode (F5). New
--tailscale/--no-tailscaleonagents run.netMode = (--tailscale || reuse-context) && !--no-tailscale(a solo one-shot--leasestays public) is threaded into the lease so the lib leases onto the tailnet.agents lease setupnow also captures a Tailscale auth key (EPHEMERAL, pre-authorized,tag:crabbox) into thetailscale.comsecrets bundle asCRABBOX_TAILSCALE_AUTH_KEY; when Tailscale is requested with no key configured the run falls back to a public lease with an actionable hint. The final "box ready/kept" line surfaces the box's tailnet FQDN/IP.
Source:
apps/cli/src/commands/exec.ts,apps/cli/src/commands/lease.ts,apps/cli/src/commands/ssh.ts(+*.test.ts).- Reuse (F3). On an interactive
Add GitHub Copilot CLI permission sync, writing supported allow rules to
.copilot/permissions-config.json.Lease lifecycle: setup-copy, step progress, and tailscale plumbing (RUSH-1920/1921/1924).
agents run --leasegains a library core the command layer wires up:copySetupToBoxpushes the git-tracked subset of the local~/.agentsonto the box and refreshes it (never~/.claude); the box bootstrap now echoes___PHASE_<name>___sentinels parsed into a structuredLeaseStepstream (onStep+renderStepLine); and anetMode: 'tailscale'path leases boxes onto the tailnet (--network tailscale -tailscale-tags tag:crabbox,CRABBOX_TAILSCALE_AUTH_KEYfrom a secrets bundle) withCrabboxBox.tailscaleIPv4/tailscaleFQDNparsed from box labels. Source:apps/cli/src/lib/crabbox/setup-copy.ts,apps/cli/src/lib/crabbox/progress.ts,apps/cli/src/lib/crabbox/lease.ts,apps/cli/src/lib/crabbox/cli.ts.agents cloud run --jsonnow emits machine-readable failures.die()— the shared fatal-exit path — always wrote red text to stderr and left stdout empty, so an agent parsing--jsonoutput saw nothing plus a bare nonzero exit with no reason.die()gains an optional{ json, hint }and, in json mode, prints{"error", "hint"?}to stdout; a pureformatDie()makes the human/agent split unit-testable. Every failure path incloud runnow threads the resolved--jsonflag. Source:apps/cli/src/lib/format.ts,apps/cli/src/commands/cloud.ts. (RUSH-1830)Show live Droid quota bars in
agents view(RUSH-1357). Factory billing limits now render asS/W/Mwindows for Droid, matching Claude's live-usage display. Source:apps/cli/src/lib/usage.ts.agents eventsis now one unified stream — operational + agent activity. Agent-semantic events (plans, PRs, worktrees, sub-agents, artifacts) share the event vocabulary and read through the same reader as operational events (secrets, teams, commands), newest-first.--module activityshows agent events,--auditrestricts to operational only, and all existing filters (--event,--agent,--since,--command) apply across both. NewreadUnifiedEvents(apps/cli/src/lib/event-stream.ts) is the single read surface for higher-level features. Source:apps/cli/src/lib/events.ts,apps/cli/src/lib/activity.ts,apps/cli/src/commands/events.ts.The daemon no longer crash-loops on headless Linux when a routine is overdue. On an overdue routine the daemon fires a best-effort desktop notification via
notify-send(Linux) /osascript(macOS). A missing notifier binary — the default on a headless box withoutlibnotify-bin— surfaces as an asynchronousspawn'error'event, not the synchronous throw the surroundingtry/catchexpected, so Node re-threw it as an uncaught exception and killed the daemon. systemd then restart-looped it every ~10s, which also tore down the browser IPC socket (agents browser startfailed with "Timeout waiting for browser daemon socket"). Both notifier spawns now carry an'error'listener so the failure is swallowed as the "best-effort" contract already promised. Source:apps/cli/src/lib/overdue.ts,apps/cli/src/lib/overdue.test.ts.agents fleet apply— reconcile the fleet from under thefleetverb. The idempotent reconcile engine already shipped as top-levelagents apply, but users who reach forfleet/devicesas the noun (fleet capture,fleet login,fleet status) had no matchingfleet apply. This surfaces the identical command asagents fleet apply(andagents devices apply) via a shared configurator, so the two can never drift — same flags, same engine, same--plan/--device/--onlysemantics. Pure discoverability alias; no behavior change toagents apply. Source:apps/cli/src/commands/apply.ts(configureApplyCommand,registerFleetApplyAlias),apps/cli/src/commands/ssh.ts.agents fleet loginnow finds the agent CLIs on the remote box. The remote drive ran the login command over a non-login SSH shell (ssh <box> kimi), where the agents-cli shims (~/.agents/.cache/shims) are not on PATH — sokimi/droid/codexwere "command not found" and the device-code scrape always timed out. The remote command now prepends the shim dir (resolved on the box via$HOME) to PATH so the login program launches. Source:apps/cli/src/lib/fleet/remote-login.ts.New
agents fleet login— log agent CLIs into every fleet box over SSH from one browser page. File-copying one OAuth credential across N machines is fatal: a shared refresh token rotates server-side on first refresh and invalidates the other copies. The durable fix is a per-machine login (one interactive OAuth per agent x box), and this command makes that bearable. It drives each box's device-code flow through the PTY sidecar (ssh -tt <box> <loginCmd>), scrapes the verification URL + user code, and surfaces every pending login in ONE local dark/light dashboard with per-codeAuthorizedeep-links and TTL countdowns — so you enter codes back-to-back instead of babysitting N terminals. Default mode requests all codes concurrently;--interactivewalks one box at a time, requesting each code just-in-time so the ~15-min TTL can't expire while you work. Only true device-code flows are driven (droid, codex, kimi); loopback / keychain-bound / uncharacterized agents (claude, gemini, antigravity, opencode, grok) are flagged non-remotable with an honest reason instead of a mis-drive. Flags:--agents <csv>,--devices <csv>,--all,--interactive,--json. Source:apps/cli/src/lib/fleet/remote-login.ts,apps/cli/src/lib/fleet/auth-sync.ts(FLEET_LOGIN_FLOWS),apps/cli/src/commands/ssh.ts,apps/cli/src/lib/open-url.ts.agents fleet pingstops crying wolf on healthy accounts. The auth matrix painted a fully-logged-in fleet as half-broken:codex/grok(which have no in-repo live-probe endpoint) rolled up as an alarming yellow0/N, and the--verboseper-account list paintedexpiredred — lumped with a realrevoked— even thoughexpiredis soft and self-refreshes on the CLI's next launch (kimi/droid). Both renderers now share one truthful color model (verdictColor/authCellColor): red is reserved forrevoked(the only "re-login now");unverifiedreads as neutral gray "signed in (unverifiable)";expired/rate_limited/errorare soft yellow; and the cell numerator counts signed-in accounts (live + present) so a logged-in codex fleet reads1/1, not0/1. Separately,fleet ping --verbosenow actually emits the per-account breakdown: the root program's global--verbosewas shadowing the subcommand flag, so the breakdown was silently unreachable — the action now reads the effective value from the merged globals. Source:apps/cli/src/lib/auth-health.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/lib/auth-health.test.ts.Wire allowlist support for ForgeCode and Hermes (RUSH-1748, RUSH-1749). ForgeCode now receives permission groups as
~/.forge/permissions.yamloperation-family policies (read,write,command,url) for built-in tools; this file is active only when.forge.tomlhasrestricted = true, and MCP tools bypass it. Hermes now receives command allow rules in~/.hermes/config.yamlcommand_allowlistand deny globs inapprovals.deny, preserving sibling YAML keys such asmcp_serversandhooks. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts,apps/cli/src/lib/resources/permissions.ts,apps/cli/src/lib/staleness/detectors/permissions.ts,apps/cli/docs/{00-concepts,02-resource-sync}.md.agents run --lease/--boxfail fast outside a git repo instead of billing a dead box. crabbox syncs the working directory to the leased box withgit ls-files, so from a non-git directory the run died atbuild sync file list: exit status 128— but only after provisioning (and billing) the box.agents runnow checks the working directory is a git repo before provisioning and exits with an actionable message (… is not a git repository. Run from inside a git repo, or initialize one: (cd <dir> && git init)). Source:apps/cli/src/commands/exec.ts.Fix
agents run --leasesetup-copy (andagents ssh <slug>) to actually reach the box. Both used a rawssh crabbox@ip:2222, which failspublickey— crabbox provisions a per-lease identity key. They now tunnel through crabbox's own ssh invocation (crabbox ssh --id <slug> --reclaim), so the git-tracked~/.agentsconfig really lands on a leased box (agents repo refreshthen materializes it) andagents ssh <slug>connects. Verified end-to-end on a real Hetzner box. Source:apps/cli/src/lib/crabbox/{setup-copy,cli}.ts,apps/cli/src/commands/ssh.ts.Fix the Linear
--labeltrigger filter matching nothing.routinesandmonitorsjobs triggered onlinear:Issuewith--label <name>never fired: the matcher readdata.labels.nodes[].name(the GraphQL-query connection shape), but Linear webhook bodies flatten list relations —data.labelsis a flat array of label objects. The.nodesread always yielded[], so every label filter silently failed to match. It now reads the flat array. The prior unit test fixtured the same wrong shape, so the suite was green while the integration was dead; the fixture now uses the real webhook shape and a regression test locks it. Source:apps/cli/src/lib/triggers/webhook.ts.agents menubarnow works from the Bun single-file binary. When the CLI runs as the compiled Bun executable,import.meta.urlpoints inside the virtual/$bunfs/bundle, so the menu-bar helper couldn't find the shippedMenubarHelper.appon disk (bundle source: missing (cannot enable)) or read its ownpackage.json(current version: unknown, and a perpetual "stale" warning).enablerefused with "no menu-bar helper bundle ships with this install." Version and bundle resolution now fall back to the real on-disk install, located by following theagentslauncher symlink, soenable/disable/statusbehave the same whether the CLI runs under Node or the Bun binary. Source:apps/cli/src/lib/version.ts,apps/cli/src/lib/menubar/install-menubar.ts.Add OpenClaw workflow sync by projecting agents-cli workflows into Lobster
.lobsterfiles under.openclaw/workflows/.Fix the PTY sidecar (
agents pty, interactiveagents teams,agents fleet login) on the macOS standalone binary. Since the macOS release became abun --compilestandalone (#315), the sidecar was spawned AS that binary (process.execPath pty _server) — but a Bun standalone cannotrequire()a native addon, so node-pty'spty.nodefailed to load (Cannot require module ../build/Debug/pty.node) and every PTY-backed command died with "PTY server failed to start within 5 seconds."getServerSpawnArgsnow detects the standalone case and runs the sidecar via a realnodeexecuting thedist/index.jsthat ships beside the binary (where the prebuiltpty.nodeloads from disk), falling back to the binary only when no node / no dist is found. Verified end-to-end against a real compiled Mach-O standalone. Source:apps/cli/src/lib/pty-client.ts.agents sessionsnow indexes routine-run transcripts from durable run history; useagents sessions --routine --alloragents sessions <run-id>to inspect a routine run with the existing summary view.agents routines add,run, andrunsnow support--json. Previously onlylist/statusemitted JSON, so an agent creating a routine or triggering a run had to scrape human strings for the job name / run id.addemits{ ok, added, job },runemits{ ok, job, runId, logDir }, andrunsemits an array of run records — all on stdout, with the scheduler-start banner suppressed so it never pollutes the JSON stream. Source:apps/cli/src/commands/routines.ts. (RUSH-1833)agents run <agent>no longer hangs when launched headless without a prompt. A run with no prompt and no explicit--interactiveresolves to interactive intent — but in a non-TTY shell (a headless agent, a pipe, CI) there is no terminal to host the REPL, so it attached a TUI to dead stdin and hung forever. It now fails fast with the headless alternatives (agents run <agent> "<task>"oragents run <agent> --headlessto read the prompt from stdin). An explicit--interactiveis still honored. Source:apps/cli/src/commands/exec.ts,apps/cli/src/lib/exec.ts(inferredInteractiveWithoutTty). (RUSH-1829)Menu bar routines now include latest run
exitCodeandfailureReasonfromagents routines list --json, show failed routine reasons inline, label healthy-but-overdue routines asoverdueinstead ofexit 0, and open the concise logs summary instead of a raw Terminal dump.Show Droid teammate activity in
agents teams collectby normalizing stream-json tools, file edits, and final messages.
Fix: route remaining specialized direct SSH spawns through the shared hardened SSH baseline.
Agent feed dispatch now keeps local and remote answer paths separate (RUSH-1472).
agents feedonly applies stall suppression, default-on-no-answer policy, and dispatch controls to blocks owned by the local machine, so remote feed rows cannot enqueue answers into the wrong local mailbox. Per-blocktimeoutMinutesis honored for approval defaults and decision parking, policy/default answers are tested against the real mailbox spool, block-specificallowedOperatorsrestrict high-consequence answers, and urgent notification text is emoji-free. Source:apps/cli/src/commands/feed.ts,apps/cli/src/lib/{ask-classifier,feed,feed-policy,notify}.ts.agents feedranks blocked agents by cost of delay and surfaces runaway/needy control cards (RUSH-1478). Open blocks are sorted by idle time, downstream blast radius, dollar burn rate, and classifier irreducibility, while silent high-burn/relaunch-loop agents and chronic askers render once as control cards withag feed --pause/ag feed --killactions. Source:apps/cli/src/lib/feed-ranking.ts,apps/cli/src/commands/feed.ts,apps/cli/src/lib/feed.ts.Make live session follows compact by default:
agents sessions tailnow prints low-noise message/tool/result lines unless--jsonis passed, andagents logs -f <id>keeps raw transcript streaming behind--full.Fix
agents repo refreshfor stale plugin skill shadows. Full refresh now forces a materialization pass, copies trusted plugin-bundled skills into legacy top-level agent-home skill dirs when those names already matter to the agent, and prunes orphaned top-level skill dirs whose source no longer exists.agents run <profile> --leasenow provisions the profile's host runtime and temporary profile config on the leased box without copying base-runtime OAuth credentials when the profile authenticates with its own API key, including OpenCode and Antigravity-hosted profiles.agents ptynow starts its sidecar correctly from the standalone CLI binary and includes the spawned command plus recent sidecar log lines when startup fails.Fixed
agents secrets openclaw-keychain migrateso OpenClaw Keychain writes no longer expose secret values in process argv, and migration now fails closed when matching plaintext credentials disagree.agents share setupandagents setup sharenow read Cloudflare provisioning credentials from thecloudflaresecrets bundle and persist the Worker write token asWRITE_TOKENin thesharebundle, matching the setup/onboarding contract while keeping endpoint config inagents.yamlundershare:.Fix
agents routines listandagents routines viewso a project-layer routine with the same name no longer hides the user-layerdevicesallowlist written byagents routines devices --set.Keep piped CLI output human-readable unless
--jsonis passed.Add
--jsontoagents monitors viewandagents monitors test, and send monitor errors to stderr so JSON stdout stays parseable.Add
--jsonoutput toagents routines add,agents routines run, andagents routines runsso scripts can capture routine and run ids without scraping human text.Fix hook directory sync status so bundled hook directories such as
hooks/testscopy correctly and no longer appear permanently drifted after sync.Neutralize residual OSS scrub breadcrumbs by replacing browser/session test fixture hostnames with
remote-hostand removing legacy private product path references from cloud proxy comments.Add
agents setup mine/agents mine— white-label the CLI. Mint your own personally-named binary (e.g.jack) that runs every agents verb under your name, with the built-in commands you disable hidden and a per-brand resource profile that curates skills/plugins/MCP/etc.agents setup mineis the wizard;agents mine init/list/toggle/removemanage brands. Free and Apache-2.0. Source:apps/cli/src/commands/mine.ts,apps/cli/src/lib/brand.ts.One-time "star us on GitHub" nudge after your first successful run. After a user's first successful
agents runoragents teams, agents-cli prints a single plain inline line pointing at the repo. Shown at most once ever (claimed with an atomic O_EXCL sentinel so concurrentagents teamsprocesses can't double-print), and skipped for non-TTY, CI,--json/--quiet, orAGENTS_NO_NUDGE=1. Theagents teamscall site only nudges on a clean drain (no failed teammates). Source:apps/cli/src/lib/star-nudge.ts,apps/cli/src/commands/exec.ts,apps/cli/src/commands/teams.ts,apps/cli/src/lib/teams/supervisor.ts.
1.20.72
Stop
agents doctorfrom reporting phantom drift andagents prunefrom deleting source-managed resources. Three reconciler false positives are fixed: the instruction file (CLAUDE.md/AGENTS.md) is now compared against the composed active-preset output the rules writer actually emits — not the raw whole-reporules/AGENTS.md— so a correctly-synced home no longer shows as permanent drift; plugin-bundled commands installed as<plugin>-<command>command-skills (e.g.swarm-plan,code-review) are no longer flagged as orphans/extras thatprune cleanupwould delete; and command-as-skill wrappers (theagents_commandmarker) are no longer miscounted as skills and surfaced as deletable skill orphans. Source:apps/cli/src/lib/staleness/,apps/cli/src/lib/commands.ts,apps/cli/src/lib/skills.ts.Capture your whole fleet into
agents.yaml, then rebuild it anywhere withagents apply(#1305). Newagents fleet capture(aliasagents devices capture) snapshots the live environment into the portablefleet:block — the device roster (names only), the source's agents asdefaults, secrets-bundle names, and routine names. It commits zero Tailscale IPs or usernames:agents applyreconstructs a fresh machine's roster by resolving each device name live from Tailscale (ensureDevicesRegistered), sogit clone+agents applyreplicates the fleet with nothing sensitive in the repo.applynow also passes declaredsync:scopes through toagents sync <scope>(previously a baresync) and surfaces declared secrets-bundle names to recreate on each device (values stay keychain-local, never pushed). Browser profiles are intentionally not duplicated intofleet:— they already sync via the centralbrowser:block. Source:apps/cli/src/commands/fleet-capture.ts,apps/cli/src/lib/fleet/capture.ts,apps/cli/src/lib/devices/sync.ts,apps/cli/src/lib/fleet/{types,manifest,apply}.ts.agents fleet statusno longer hangs on a stale~/.ssh/config. The fleet probes (version / doctor /fleet ping) now dial each device at its registry Tailscale address (dnsName/IP) instead of the bare host name — so a hand-writtenHost <name>block carrying a drifted LAN IP can no longer shadow the correct entry and make a reachable box look dead. It also fails fast: a device the stats probe already found unreachable is skipped straight to an unreachable row instead of eating a 15s+30s version+doctor timeout. Source:apps/cli/src/commands/ssh.ts.No more macOS keychain password prompt from an interactive command. Writing a non-
agents-cli.keychain item (e.g. a refreshed Claude OAuth token duringagents view, or anagents secrets add) via/usr/bin/security add-generic-password -wpiped the value over stdin — butreadpassphrase(3)reads the controlling terminal when one exists, so in an interactive shellsecurityprompted the user ("password data for new item:") and hung to the timeout, ignoring the piped value. The write now runsdetached(a new session with no controlling terminal) so the piped stdin is always used. Verified under a pty. Source:apps/cli/src/lib/secrets/index.ts.agents logs <id> --jsonnow reports the true final status of a host task. For a run that finished remotely between dispatch and the one-shot--jsonread, the payload emitted a stalestatus: "running"with noexitCode— even though the completed log was already present — becausehostTaskLogJsondiscarded the reconciled recordreconcileTaskreturns (it heals a new object rather than mutating in place). It now emits the reconciled task, so a polling agent seescompleted/failed+exitCode+finishedAt. Source:apps/cli/src/lib/hosts/logs.ts.Fix the macOS menu-bar auto-heal so upgrades actually restart the helper.
agentshas an on-startup self-heal that re-copiesMenubarHelper.appwhen the CLI version changes, but on modern macOSlaunchctl bootstrapfails when the job is already bootstrapped, and the deprecatedlaunchctl load -wfallback pluskickstart -kdid not recover a job that launchd had stopped respawning after aWindowServer event port death. The helper would stay updated on disk but invisible in the menu bar.enableMenubarServicenow boots the old job out, bootstraps the fresh plist, and kickstarts it — the same sequence that reliably restores the icon by hand. Source:apps/cli/src/lib/menubar/install-menubar.ts,apps/cli/src/lib/menubar/install-menubar.test.ts.agents repo pullno longer wedges on per-machine pin drift. The committeddevices/<machineId>/agents.yaml(each box's agent version pins) is rewritten whenever a pin changes, leaving the working tree perpetually dirty — soagents repo pull, which refuses a dirty tree, kept failing until the file was hand-committed.pullReponow durably commits just that one path (explicit pathspec) before pulling, viacommitOwnDeviceMeta. Genuine uncommitted edits to any other file still (correctly) block the pull. No-op for the system/extra repos that don't own the path. Source:apps/cli/src/lib/git.ts.agents secrets unlocknow stays unlocked across an agents-cli upgrade (and, with--durable, across sleep + reboot). The macOS secrets broker held an unlock only in RAM, so it evaporated every time the daemon restarted (upgrade) or the machine slept — forcing a Touch ID re-tap and breaking headless reads with "not unlocked in the secrets agent". An unlock now also persists a device-local, non-biometry keychain session item that the broker rehydrates on start and that reads fall back to silently. Split default: it survives upgrade/restart automatically; pass--durable(or setsecrets.agent.durable: true) to also survive sleep/reboot — otherwise a bundle re-locks on sleep as before.lock/ rotate / delete clear it. On Linux and Windowsunlockis now a friendly no-op (secrets already resolve durably from the OS store with no prompt), so the command behaves the same on all three platforms. Source:apps/cli/src/lib/secrets/session-store.ts(new),apps/cli/src/lib/secrets/agent.ts,apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/lib/secrets/index.ts,apps/cli/src/commands/secrets.ts,apps/cli/src/lib/types.ts.agents sharecover capture finds Playwright'schrome-headless-shellpackages.scanCaches()only knew the classicchrome-mac/Chromium.appandGoogle Chrome for Testinglayouts, so on machines whose Playwright cache holds only the newerchromium_headless_shell-*packages (a rawchrome-headless-shellbinary, not an.appbundle) — and no system Chrome/Brave/Edge — the OG cover capture silently returned null and shared plans published without a preview card. The scan now matcheschrome-headless-shell-mac-arm64,chrome-headless-shell-mac-x64, andchrome-headless-shell-linux64layouts alongside the existing ones. Source:apps/cli/src/lib/share/capture.ts,apps/cli/src/lib/share/capture.test.ts.New
agents uninstall— cleanly reverse adoption and restore your original setup. Installing agents-cli adopts your agent config: it moves~/.<agent>aside and replaces it with a symlink into the version homes, adopts the launcher onPATH, and adds the shim dir to your shell rc — but until now nothing put any of that back, so removing the CLI stranded your original config under~/.agents/.history/backups/and left~/.claudea dangling symlink.agents uninstallis the reverse ofagents setup: it restores every adopted~/.<agent>(from the timestamped backup, or the version home for imported installs), restores owned home files, releases adopted launchers, strips the shim dir from every shell rc, then disposes of~/.agents— moved aside to~/.agents.removed-<ts>(recoverable) by default, or hard-deleted with--purge. A config agents-cli never adopted is never touched (ownership is decided structurally bygetConfigSymlinkVersion, the same checkremoveVersionuses);--dry-runprints the full plan without changing anything; and if any restore step errors,--purgeself-downgrades to the recoverable move-aside so a swallowed error can never take your only copy. Works on macOS, Linux, and Windows (junctions and cross-volume~/.agentshandled). Source:apps/cli/src/lib/uninstall.ts,apps/cli/src/commands/uninstall.ts.
1.20.70
Fix
agents setup computer/agents computer setuprefusing to install a valid downloaded helper. The signature check readcodesign -dvfrom stdout, but that command writes its details to stderr on success — so the Team-ID check saw an empty string, found noTeamIdentifier, and rejected every validly-signed, notarized helper with "signed by unexpected Team (none)". It now reads both streams viaspawnSync. Verified end-to-end against the real publishedv1.20.69release asset (download → sha256 → extract → codesign + Team2HTP252L87+spctlnotarization → install). Source:apps/cli/src/lib/computer/download.ts.The bundled macOS menu-bar helper is now a true universal binary on Xcode-less release hosts.
menubar/scripts/build.sh releaseusedswift build --arch arm64 --arch x86_64(needs Xcode's xcbuild) and, on a Command-Line-Tools-only host, silently fell back to a single-arch build — shipping an arm64-onlyMenubarHelper.appin the tarball that could not run on Intel Macs. It now builds each slice via--tripleandlipos them into one universal binary, matching the computer helper. Source:apps/cli/menubar/scripts/build.sh.
1.20.69
Choose a safe account with
agents run <agent>@. A trailing@opens a per-run picker showing each installed version's account identity, login state, plan, and available session/weekly/monthly capacity. Logged-out, rate-limited, and out-of-credit accounts remain visible but disabled; signed-in accounts without quota data remain selectable and saylimits unavailable. Source:apps/cli/src/commands/run-account-picker.ts,apps/cli/src/commands/exec.ts,apps/cli/src/lib/rotate.ts.The macOS
agents computerhelper now ships as a signed + notarized release asset, downloaded on demand. A freshnpm i -g @phnx-labs/agents-clino longer needs to build the Swift helper from source:agents computer setup/agents setup computerfetchComputerHelper.app.zipfrom the matchingv<version>GitHub release, verify it against the published.sha256, and re-check the code signature (Developer ID Team2HTP252L87) and notarization (spctl --assess) before it is ever copied to /Applications — mirroring the Windows helper's distribution. The download cache is never a trusted resolver source; a cached bundle is only ever read back through the verifying downloader. The helper is version-stamped at build time and the release pipeline publishes the asset automatically. Source:apps/cli/src/lib/computer/download.ts,apps/cli/src/lib/computer-rpc.ts,apps/cli/src/commands/computer.ts,native/computer-mac/scripts/build.sh,apps/cli/scripts/publish-computer-helper-mac.sh,apps/cli/scripts/release.sh.Live fleet auth health (
agents fleet ping) +agents viewchip (#1285). Newagents fleet pingcompletes a real authenticated request for every agent account across the fleet — the ground truth the local "signed in" flag can't give (it can't tell a revoked-but-unexpired token from a good one). Claude/Kimi/Droid are network-verified; Codex/Grok are best-effort.agents viewnow shows a live-status chip per version, read from the shared cache the ping writes. The probe hits the usage endpoint (no model tokens, no session created). Source:apps/cli/src/lib/auth-health.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/commands/view.ts.agents fork— branch a session into a new independent copy. Copies a Claude session transcript to a fresh session id (rewriting only thesessionIdfield so the per-message uuid chain stays intact) beside the original, then registers it so it resumes independently from the same cwd and version — the original is left untouched.--namelabels the fork. Source:apps/cli/src/lib/session/fork.ts,apps/cli/src/commands/fork.ts.agents setupis now a capability hub with guidedbrowser/computer/sharesubcommands. Bareagents setupstill clones the system repo and imports unmanaged agents, but on a TTY it now also offers to set up the optional capabilities a fresh machine needs. Each is also runnable on its own and is idempotent (re-run to change settings):agents setup browserdetects an installed Chromium-family browser and creates/points thedefaultprofile;agents setup shareprovisions or joins a Cloudflare share endpoint (reusingagents share setup/join);agents setup computerinstalls the macOS helper and walks you through the Accessibility + Screen-Recording grants — opening the exact System Settings panes and polling until trust lands. The existingagents share setup/agents computer setupremain for scripted use. Source:apps/cli/src/commands/setup.ts,setup-browser.ts,setup-computer.ts,setup-share.ts,apps/cli/src/lib/browser/chrome.ts,apps/cli/src/commands/share.ts.
1.20.68
MCP resource handler now syncs project-level agent configs alongside user-level configs (RUSH-671).
McpHandler.syncpreviously wrote resolved MCP servers only to the version-home (user-level) config path. It now also writes project-layer MCP servers to each agent CLI's project-level config path (e.g.,.mcp.jsonfor Claude,.codex/config.tomlfor Codex) so agent CLIs can discover project-scoped MCPs natively. User-level sync is unchanged. Source:apps/cli/src/lib/resources/mcp.ts,apps/cli/src/lib/agents.ts(getProjectMcpConfigPathexported),apps/cli/src/lib/resources/mcp.test.ts.Production MCP sync path also writes project-level configs.
installMcpServersnow merges project-layer servers into the agent's project-level config file, using a sharedwriteMcpConfigserializer with overwrite/merge modes. OpenClaw serialization is corrected to nest undermcp.servers, matching the existing reader; Grok/OpenClaw user-level configs are written directly with merge mode so multiple servers don't clobber each other; andinstallMcpServersonly reportsappliedfor agents it actually wrote a config for. Source:apps/cli/src/lib/mcp.ts,apps/cli/src/lib/mcp.test.ts.Accurate account tier and legible usage limits in
agents view. Each row's plan tier is now derived fromorganizationType(Max/Pro/Team/Enterprise) instead of a billingType guess that mislabelled every Max account as "Pro", and the redundant tier badge next to the email is dropped for personal plans (multi-seat orgs keep their org name, which is real identity). The compactS:/W:usage bars now show the exact percentage and a compact reset hint (S: ███░░ 58% (3d)), a signed-in account whose usage can't be fetched readsusage unavailableinstead of a blank gauge, and a newagents view --refresh(-r) forces a live usage refresh past the cache. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/usage.ts,apps/cli/src/commands/view.ts.Fleet health status and drift gate.
agents fleet statusnow renders a fleet-wide warnings rollup plus a device matrix for reachability, resource headroom, sync drift, CLI readiness, and agents-cli version skew;--jsonemits the same report for scripts and--strictexits non-zero when any warning is present.agents check --devicesnow fans the existing drift gate across registered devices and exits non-zero when any device is drifted or unreachable. Source:apps/cli/src/commands/ssh.ts,apps/cli/src/commands/check.ts,apps/cli/src/lib/devices/fleet.ts,apps/cli/src/lib/devices/health-report.ts.Login state per agent —
agents doctorshows signed-in/logged-out, andagents runwarns before launching into a logged-out account.agents doctor's Agent CLIs section now renders a✓ signed in <account>/✗ logged outbadge per installed agent (also surfaced in--jsonundersignIn), and an interactiveagents run <agent>now prints a one-line stderr warning —⚠ <agent> looks logged out — log in with: <cmd>. Launching anyway...— when the account looks logged out, so you find out before the TUI opens instead of after typing a prompt and getting/loginback. It is advisory (file-basedgetAccountInfo, no Keychain ACL prompt) and never blocks: skipped for--json/--quiet, when a rotation already picked a signed-in account, for--host/--lease, and via--no-auth-check/AGENTS_NO_AUTH_CHECK=1.agents view's logged-out line now names the exact login command too. One shared badge/hint renderer keeps all three surfaces consistent across claude, codex, kimi, grok, opencode, and gemini. Source:apps/cli/src/lib/signin-badge.ts(+signin-badge.test.ts),apps/cli/src/commands/exec.ts,apps/cli/src/commands/doctor.ts,apps/cli/src/commands/view.ts.agents repos addadopts an existing checkout instead of dead-ending. When the target~/.agents-<alias>/already holds a git repo whose origin matches the requested source,repos addnow registers it in place (no re-clone) rather than erroringDirectory already exists. A repo with a different origin is left untouched unless you pass--adopt. This removes the trap that forced a second, inconsistent install method when a repo had been cloned by hand. Remote matching is transport-agnostic (SSH and HTTPS forms of the same repo compare equal). Source:apps/cli/src/commands/repo.ts,apps/cli/src/lib/git.ts.--jsonforrepos listandplugins list. Both list commands now emit machine-readable JSON with--json, matching therepos view --json/plugins marketplaces --jsonthat already existed — so an agent can enumerate registered repos (with per-repo sync/drift state) and installed plugins (with per-agent-version sync targets) without scraping the human table. Source:apps/cli/src/commands/repo.ts,apps/cli/src/commands/plugins.ts.teams add --remote-cwdnow fails loud instead of silently doing nothing. The flag rides the shared--hostoption family butteams addtreats--host/--deviceas placement and never reads it, so passing it used to be a silent no-op that misled you into thinking it set the teammate's repo path. It is now rejected with guidance (place with--device, set the code with the team's--repo, one team per repo). The shared--remote-cwdhelp also warns that a local~expands on your machine, not the remote host — pass a single-quoted'$HOME/…'path or a valid remote absolute path. Teams docs + skill lead with this. Source:apps/cli/src/commands/teams.ts(remoteCwdOnAddError),apps/cli/src/lib/hosts/option.ts,apps/cli/docs/teams.md,skills/teams/SKILL.md.No more CLIXML blobs from Windows hosts. A remote
agents …invocation routed to a Windows box (--host win-mini,agents doctor --devices,agents fleet status) no longer comes back wrapped in a raw#< CLIXML <Objs …>envelope. PowerShell 5.1 serializes its progress stream ("Preparing modules for first use.") to CLIXML when stderr is a captured pipe rather than a console; the Windows command builder now silences that stream ($ProgressPreference = 'SilentlyContinue') so failures read as plain text for humans and the JSON parsers that consume the output. Source:apps/cli/src/lib/hosts/remote-cmd.ts.
1.20.67
Interactive session browser —
agents sessions --activeand a bareagents sessionsnow open a live, filterable picker on a TTY (RUSH-1802). One canonical filter driven by single keys, re-pulled across the fleet as you toggle:ssearch,rrunning-only,cteams,aagent (cycles),ddevice (cycles),pthis-repo↔all-dirs,wtime window; filters stack (AND together) and the active set shows in the header, with a live preview of the highlighted row and⏎to resume/attach via the existing dispatch. Every hotkey mirrors a flag, so the view is reproducible as a command —ycopies (and--print-cmdprints) the exactag sessions …line the filters map to, bridging the human picker and the agent/script flag surface. The interactive front-end is TTY-only:--json, a pipe, or the new--no-interactivekeep the existing static listing verbatim, so scripts and headless agents are unchanged. Adds-pas the short form of--project,--print-cmd,--preview(agents sessions <id> --previewprints the compact digest without the pager), and--no-interactive. Built on a new async-refetchdynamicPickervariant that reuses the existing render/pagination/preview machinery, the fleet SSH fan-out, and the resume/focus path. Source:apps/cli/src/lib/picker.ts(dynamicPicker),apps/cli/src/commands/sessions-browser.ts(+sessions-browser.test.ts),apps/cli/src/commands/sessions.ts.kimi/grok headless
--mode plannow auto-downgrades toautoinstead of crashing or stalling (RUSH-1810). kimi's headless-prefuses to combine with--plan(it hard-failed at spawn) and grok's--permission-mode plansilently stalls a headless run at its ExitPlanMode gate. Both now model this honestly with acapabilities.headlessPlan: falseflag: a headless plan request degrades toauto(kimi-pauto-runs; grok mapsauto→edit) with a one-line stderr warning, mirroring the graceful plan→edit degrade cursor/antigravity already get. Interactive plan is unchanged, and claude/codex/droid/opencode keep read-only plan headless. The same downgrade coversagents run,agents teams addteammates, and routine jobs. Source:apps/cli/src/lib/exec.ts(resolveHeadlessMode),apps/cli/src/lib/runner.ts,apps/cli/src/lib/agents.ts,apps/cli/src/lib/types.ts.
1.20.66
Fix (
agents monitors, RUSH-1782 follow-up):--watch-deviceno longer silently watches the local machine on a bad name. An unregistered or mistyped--watch-devicename is now rejected ataddtime (same registry gate as--device/--devices), and the device source evaluator returns an explicitdevice not registeredobservation instead of falling back to local stats if a watched device is removed later — closing a "monitors the wrong box, silently" gap. The rate-limit firehose trip now also writes a fire record (ok:false, error:'rate limited'), soagents monitors runsreflects the auto-pause thatview'slast firedalready showed. Addssources/device.test.ts. Source:apps/cli/src/lib/monitors/sources/device.ts,apps/cli/src/commands/monitors.ts,apps/cli/src/lib/monitors/engine.ts.agents monitors— durable event-triggered watchers (RUSH-1782). A monitor watches a SOURCE, detects a CONDITION change, and fires an ACTION — a routine whose trigger is a watched source instead of a clock, reusing the routines daemon, dispatch (executeJobDetached), device model, and notify path. Sources:--watch/--poll(a shell command's stdout),--poll-http(a URL's status+body),--watch-file,--watch-device(fleet reachability + load headroom, the first scheduler consumer ofdevices/health.ts), plus--ws/--on(push sources, accepted; delivery wired in a follow-up). Conditions:--on-change(default; first observation is a silent baseline),--match <regex>(fires once per distinct matched token),--every, with--dedupe-key. Actions:--run <agent> --prompt(the event is injected as{event}),--routine,--notify,--webhook-out. Pin-to-one placement:--device <name>names the single OWNER machine (exactly-once, v1 — no distributed lock);--devicesis the advanced allowlist;--run-onoffloads the action over SSH. The one genuinely new primitive is a native state-diff store (~/.agents/.history/monitors/<name>/state.json) that replaces the hand-rolled markdown memory files ad-hoc watchers needed.agents monitors test <name>is a dry-run that evaluates the source once and prints the emitted event + would-fire decision without acting. ArateLimit: {max, per}firehose guard auto-pauses a runaway monitor. The daemon hosts aMonitorEnginebeside the cron scheduler, reloading on SIGHUP. Source:apps/cli/src/lib/monitors/*(config.ts,state.ts,engine.ts,dispatch.ts,sources/*),apps/cli/src/lib/daemon.ts,apps/cli/src/lib/state.ts,apps/cli/src/commands/monitors.ts,apps/cli/src/lib/hosts/passthrough.ts,apps/cli/docs/10-monitors.md.NEW:
agents share <file>— publish any HTML to a shareable link on your own Cloudflare R2, for ~$0 (RUSH-1791). A one-command "publish" for agent-generated artifacts (plans, viz, reports).agents share setupprovisions an R2 bucket + a ~30-line Worker on your Cloudflare (read from yourcloudflare.comsecrets bundle), enables the free*.workers.devsubdomain, and — if your token owns the zone — maps a custom domain likeshare.agents-cli.sh;agents share plan.html [--slug x] [--expire 30d]then does an authedPUTand prints the link. R2 has zero egress and a 10 GB free tier, so this is effectively free even at scale. The Worker is the ingress: uploads are bearer-gated through it (its R2 binding does the write, so the client needs no S3 keys) and reads are public — the link outlives the agent, since the page is stored in R2, not streamed. Fleet/central mode: the owner provisions one endpoint and every fleet/cloud/ephemeral agent publishes through it via the shared write token (asharesecrets bundle) + syncedshare:config inagents.yaml(agents share joinuses an existing endpoint with no provisioning). Expiry is per-object (x-share-expires-atmetadata → the Worker 410s + lazily deletes past that instant). Source:apps/cli/src/commands/share.ts,apps/cli/src/lib/share/{worker-template,provision,publish,config}.ts(+Meta.shareinapps/cli/src/lib/types.ts).agents sharenow auto-generates an OG cover so links unfurl (RUSH-1809). Publishing an HTML page screenshots its own hero at 1200×630 and attaches it asog:image+twitter:card, so ashare.agents-cli.sh/<slug>link previews as a rich card in Slack, iMessage, Twitter/X, and Discord. The capture is client-side (no central render service, ~$0): it reuses the CLI's browser detector (findFirstInstalledBrowser) and falls back to a managed Chromium in the Playwright/Puppeteer caches, skipping poor headless hosts; if nothing headless-capable is present the cover is skipped and the plain link still publishes. Pass--no-coverto opt out. Default slugs are now Notion-style<project>-<feature>-<hash>(the repo name scopes the link; a random tail keeps it unguessable). New:apps/cli/src/lib/share/{capture,og}.ts; wired throughpublish.tsand theagents sharecommand.SSH host-key pinning for credential-copy and
agents ssh(Security, RUSH-1767).--copy-credsnow refuses to ship credentials to a--hostwhose SSH host key is not pinned, andagents sshpins a host key (viassh-keyscaninto a managed~/.agents/.cache/devices/known_hosts) on first connect, resolving an ssh-config alias to its real HostName so the pin target and the strict-check target line up. Scope: this hardens the--copy-credsgate and theagents sshpin path specifically; other SSH call sites still use OpenSSH default~/.ssh/known_hosts(wiring them onto the managed store is follow-up). Source: apps/cli/src/lib/devices/known-hosts.ts, apps/cli/src/lib/ssh-exec.ts, apps/cli/src/commands/exec.ts.
1.20.65
agents serve --control— the authenticated anchor for the iOS/iPadOS cockpit (RUSH-1731). The read-onlyagents servegains an opt-in control mode: a bearer-gated HTTP surface that addsPOST /api/run(dispatch a headlessagents run, local or--host <device>, returning a server-minted session id so the run is immediately addressable) andPOST /api/session/:id/message(steer a running agent viaagents message), on top of the existingGET /api/state+ SSE/events— which are reused verbatim, not duplicated. It adds no execution machinery: both mutations re-invoke the same CLI paths (inheriting host offload, secrets, and detached dispatch), so a run outlives the request. Every request is verified against a token whose SHA-256 hash only is stored on disk (<cache>/serve/control-tokens.json, 0600) — the raw token is shown once at first--controlboot and never persisted;--bind <addr>allows reaching it from a paired phone over the tailnet (keep it on the tailnet, never public Funnel). First step of the "Fleet Cockpit" — iOS is a control plane, not a compute worker. Source:apps/cli/src/lib/serve/control.ts,apps/cli/src/lib/serve/token.ts,apps/cli/src/lib/serve/server.ts(extractedhandleServeGet),apps/cli/src/commands/serve.ts,+ control.test.ts/token.test.ts.Live NDJSON event stream for the iOS cockpit —
GET /api/session/:id/stream(RUSH-1732). The authenticated control server (agents serve --control) can now stream a run's events to the phone as Server-Sent Events. A control-mode run is launched with--jsonand its harness output captured to a per-session NDJSON file (<cache>/serve/streams/<id>.ndjson); the stream route offset-tails that file — the same resumable patternhosts/progress.tsuses — normalizing each line to{type, raw}(message / tool_use / tool_result / result / error) and emitting one SSE frame per event. Each frame'sid:is the exact byte offset past its line, so a phone that drops mid-run reconnects with?offset=<bytes>or the standardLast-Event-IDheader and loses or duplicates nothing; the stream closes on the terminalresult/errorevent. Scope: streams anchor-local runs; streaming a--host-offloaded run reusespullRemoteLogDeltaand is a follow-up. Source:apps/cli/src/lib/serve/stream.ts,apps/cli/src/lib/serve/control.ts(startSessionStream,defaultRunnercapture,spawnDetachedstdio),+ stream.test.ts/control.test.ts.Devices gain a
controlrole +agents devices pair-iosfor the iOS cockpit (RUSH-1733). ADeviceProfilenow carries an optionalrole: 'worker' | 'control'(absent =worker). A control device is a cockpit that drives the fleet but never runs agents itself (an iPhone/iPad running the companion app): it appears in the fleet but is skipped from theagents sessions --activeSSH fan-out (remote-list.tsnow bails onisControlDevice(d)regardless of platform, so a control node is never dialed and never burns a ConnectTimeout). The team scheduler is unaffected — it only places onto a user-declared device pool. Newagents devices pair-ios [name](run on the anchor) mints a bearer token foragents serve --control(hash-only on disk, shown once), marks a matching registered devicerole=controlso the fleet stops dialing it, and prints how to point the app at the anchor over the tailnet. Source:apps/cli/src/lib/devices/registry.ts(DeviceRole,deviceRole,isControlDevice,DeviceInput.role),apps/cli/src/lib/session/remote-list.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/lib/devices/registry.test.ts.Project-scoped MCP servers are untrusted by default (RUSH-1776). A cloned repo's
<repo>/.agents/mcp/*.yamldefines an arbitrary command spawned under the agent's authority, so merely using a hostile repo no longer auto-registers or runs it. Project-scoped MCPs now enter the register/spawn path only after an explicit per-project opt-in (agents mcp trust, revoke withagents mcp untrust), recorded in a user-owned store (~/.agents/mcp-trust.yaml) that a cloned repo can't write to. The gate lives at the register/spawn choke point (getMcpServersByName→installMcpServersand workflow assembly) and in the sync path, and it also closes the name-collision case where an untrusted project entry could shadow a same-named user entry.agents mcp listnow flags an untrusted project server and shows the exact command+args that would run. User- and system-scoped MCPs (~/.agents/mcp/*) remain trusted and unchanged. Source:apps/cli/src/lib/mcp.ts(isProjectMcpTrusted,trustProjectMcp,untrustProjectMcp,listMcpServerConfigs,getMcpServersByName),apps/cli/src/lib/versions.ts,apps/cli/src/commands/mcp.ts.agents viewsurfaces Claude org identity per account — same-email installs in different orgs now read distinctly. Two Claude installs signed into the same email (a personal Max plan and a Team seat) used to render identically.getAccountInfonow readsorganizationType/organizationNamefrom each version home's.claude.jsonoauthAccount(file-only, no keychain access, so no macOS ACL prompts);agents viewappends an org badge inside the existing account column —[email protected] (ModSquad · Team),[email protected] (Max)— mapping known tiers (Max/Pro/Team/Enterprise/Free) and title-casing unknown future ones, showing the org name only for team/enterprise seats (a personal org's name is auto-generated boilerplate). Theagents useversion picker gets the same badge, andview --jsonemits the raworganizationType/organizationNamefields. Companion fix:agents view --prunenow keys duplicate detection onaccountKey(account + org) instead of email alone, so a Max + Team install sharing one email is no longer proposed for deletion. Source:apps/cli/src/lib/agents.ts(formatClaudeOrgLabel,accountOrgBadge),apps/cli/src/commands/view.ts,apps/cli/src/commands/versions.ts.agents viewcompact usage bars now show every blocking window — Droid gains its monthly bar (M:). Droid meters usage on three windows (5-hour, weekly, monthly), but the compact row hard-filtered to session + week, so an account throttled by an exhausted month window could read as rate-limited with no bar explaining why. The compact filter now renders every blocking window (all except Claude's non-blocking per-modelsonnet_week), matching the exact setderiveUsageStatusFromSnapshotalready uses for the rate-limited badge. Claude, Codex, and Kimi rows are byte-identical (their fetchers emit no month window), and row alignment is unaffected. Source:apps/cli/src/lib/usage.ts(formatUsageSummary).agents apply/ag apply— one-command fleet profile sync. A new declarative command reconciles every registered device to a profile declared in thefleet:block of any-ffile (defaultagents.yaml): ensure agents installed, sync config, and propagate login so a machine that is signed in once seeds the fleet — killing the "6 hosts × ~8 harnesses = ~48 OAuth flows" slog.--plan/--dry-runrenders a device×dimension matrix (agents-cli · agents · config · login) without changing anything;-y/--yesskips the confirm;--device <name>scopes to one device;--only agents,config,loginlimits dimensions;--no-loginskips login propagation. Login propagation captures portable credential files on the source (claude, codex, gemini, grok, kimi, opencode, droid, antigravity) and streams them to each target over the existing encrypted SSH channel (sshExecstdin, never shell-interpolated); an internal--recv-authreceiver validates + materializes them at 0600 and rejects path traversal. Honest boundary: macOS keychain-bound tokens (claude, antigravity) can't be read from the ACL-locked keychain — those are surfaced as a one-time manual login, never faked.fleet:is additive to theMetaschema; projectagents:version-pins are untouched. Source:apps/cli/src/commands/apply.ts,apps/cli/src/lib/fleet/{types,manifest,apply,auth-sync}.ts(+ tests),apps/cli/src/lib/hosts/passthrough.ts(apply owns--device),apps/cli/src/lib/types.ts(Meta.fleet).Hosts become first-class run/task execution options. (1)
agents cloud run --host <name>dispatches onto your own machines through a newhostcloud provider — tasks visible in bothagents cloud psandagents hosts ps(one sidecar store, two views); status reconciles from the remote.exitwith a per-target reachability memo, never guessing failure. (2)agents run --hostgains a forwarding contract (RUN_OPTION_FORWARDING):--effort --env --add-dir --timeout --strategy/--balanced/--fallback, the--loopfamily,--json --verbose --yes --acpand--passthrough now forward to the remote;--secrets*, bare--resume,--resume-checkpointreject loud before dispatch (all previously silently dropped) — locked by a commander-introspection test. (3) Devices join the host pool via adevicesHostProvider:agents hosts listshows them, capability routing reaches them,agents hosts add <device> --capenrolls from the device profile. (4) Routines placement:agents routines add --run-on <host> [--run-cwd <dir>]executes the job body on a machine (auto-pinsdevices:to the adding machine against duplicate fleet fires; daemon finalizes from the remote exit). Auto-dispatch projects can pinprovider: 'host'+host:. Also fixes--no-auto-secretsbeing a local no-op (commander stores it asautoSecrets). Source:apps/cli/src/lib/hosts/{run-target,remote-cmd,dispatch,registry,types}.ts,apps/cli/src/lib/hosts/providers/devices.ts,apps/cli/src/lib/cloud/{host,types,registry}.ts,apps/cli/src/lib/{runner,routines,auto-dispatch,auto-dispatch-provider}.ts,apps/cli/src/commands/{exec,cloud,routines,hosts}.ts.Fix:
agents run <agent>@<version> --host <host>now forwards the version pin and most run flags to the remote host. Previously the--hostbranch stripped@versionand ignored--strategy,--effort,--add-dir,--json,--verbose,--timeout,--yes, and--acp, so the remote host applied its own defaults. The local CLI now parsesagent@versionverbatim, normalizes--strategy/--balanced, makes--add-dirpaths remote-portable, and forwards all of these flags to the remoteagents runinvocation.--add-dirportability uses the same~/$HOMEre-rooting that--cwdalready uses, so a Linux remote resolves home paths against its own/home/<user>. Source:apps/cli/src/lib/hosts/dispatch.ts,apps/cli/src/commands/exec.ts,apps/cli/src/lib/hosts/dispatch.test.ts.Fix: routine edits no longer rewrite the whole YAML file, so
~/.agentsstays clean andagents repo pullcan sync routines across the fleet.writeJobpreviously re-emitted the entire document viayaml.stringifyon every mutation (pause/resume,routines devices --set, add), restyling untouched scalars — unquotingschedule, re-wrapping the foldedpromptblock — which left the git-backed user repo perpetually dirty. That made cross-deviceagents repo pullrefuse ("uncommitted changes"), so adevices:pin set on one machine never reached the others andDevices: allroutines kept firing on every box. A newserializeJobedits only the changed keys via the YAML Document API, preserving byte-for-byte formatting of untouched nodes; new/unparseable/non-mapping files fall back to canonical stringify. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/__tests__/routines.serialize.test.ts.agents secretsgains a server-independent recovery path (RUSH-1414). Secrets recovery no longer hard-depends onapi.prix.dev:secrets export --to-file <path>writes an encrypted offline bundle (AES-256-GCM via the existingencryptForFallback, mode0600) andsecrets import --from-file <path>restores it — both gated onAGENTS_SECRETS_PASSPHRASE, never auto-provisioned.secrets import --from-ssh --host <peer>pulls a bundle straight from a fleet peer over the existing encrypted SSH channel, mirroring theexport --hostpush mechanics. Source:apps/cli/src/commands/secrets.ts(exportBundleToFile,importBundleFromFile,--from-ssh),apps/cli/src/commands/secrets.test.ts.agents sessions <id> --jsonnow carriessession.todos— checklist progress from the state engine (RUSH-1503). The single-session JSON already surfacedsession.plan(the lastExitPlanMode); it now also carriestodos— the most-recent checklist write as{ items: [{ content, status, activeForm }], done, total, activeForm }, computed from the unfiltered transcript so it's stable regardless of any--includefilter. This lets the Factory extension read the CLI's computed checklist instead of re-parsing raw JSONL itself. The state engine's todo extraction now also covers Codexupdate_plan(plan: [{ step, status }]), not just ClaudeTodoWrite, so--active --json(ActiveSession.todos) and the Factory Floor show live plan progress for Codex sessions too.TodoItem/TodoProgressmoved fromlib/session/state.tstolib/session/types.ts(re-exported fromstate.ts) soSessionMetacan carrytodoswithout an import cycle. Source:apps/cli/src/lib/session/state.ts(extractTodoProgress,inferActivity),apps/cli/src/lib/session/types.ts(SessionMeta.todos),apps/cli/src/commands/sessions.ts(json branch),apps/cli/src/lib/session/{state,render}.test.ts.Plugin installs strip symlinks that escape the install root and gate OpenCode exec surfaces (Security, RUSH-1755, RUSH-1756). Installing a plugin ran a recursive
fs.cpSyncthat preserved symlinks verbatim, so a plugin carrying a symlink pointing outside its source root could redirect a follow-up managed-marker write through that link and clobber an arbitrary file on disk. Every per-agent install path — Claude/Codex marketplace, Gemini, Goose, and now Hermes — audits the copied tree and removes any symlink whose resolved target escapes both the destination and source roots, while preserving internal (in-tree) symlinks. Separately, OpenCode plugins with executable surfaces (hooks/bin/scripts/.mcp.json/settings) are no longer auto-enabled on sync; they require explicit--allow-exec-surfacesconsent like the other agents. Source:apps/cli/src/lib/plugins.ts(stripEscapingSymlinks,installHermesPlugin,syncPluginToVersion),apps/cli/src/lib/plugin-marketplace.ts(copyPluginToMarketplace).agents servenow rejects non-loopbackHostheaders (Security, RUSH-1766). The read-only viewer binds127.0.0.1, but binding alone didn't stop DNS-rebinding: a remote page could point a hostname at127.0.0.1and drive the victim's browser toGET /api/state, exfiltrating uncommittedgit diff HEADof every worktree plus routine/cloud config. The server now serves only requests whoseHostheader is loopback (localhost/127.0.0.1/[::1], any port); a missingHost(raw non-browser client) is still allowed, and the authenticated--controlserver is unaffected (it gates on a bearer token and is intended to be reachable off-box). Source:apps/cli/src/lib/serve/server.ts(isAllowedServeHost).Secrets
dailyhold is now reliable, configurable, and diagnosable. Three changes to the secrets-agent so adaily-policy bundle actually stays silent after its first Touch ID: (1) reliability — the auto-cache warms an already-running broker synchronously (gated on a real liveness ping, not a lingering socket file) instead of firing a detached worker that lost the race under load, so a short-lived reader (agents secrets export, a release loop) no longer exits before the cache populates and re-prompts on every read; a dead/stale-socket broker still costs the foreground read nothing (it drops to the detached path). (2) Configurable hold cap — a newsecrets.agent.holdMskey inagents.yamlcaps how long an unlocked/auto-cached bundle is held before the next re-prompt (default 7 days; e.g.86400000for 24h), clamped to[1m, 30d]and applied consistently across the value read-path, thesecrets listmetadata cache, andunlock. (3) Diagnostic —agents secrets statusnow shows the hold window, a version-skew warning (a broker on an older build gets torn down onagents-cli-update, wiping held bundles — the top "why diddailyre-prompt" cause), and clear held-vs-prompts-once guidance. Source:apps/cli/src/lib/secrets/agent.ts(secretsHoldMs/clampHoldMs,agentReachableSync, load-truthfulrunAgentLoadFromStdin),apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/commands/secrets.ts(formatHoldWindow, status diagnostic),apps/cli/src/lib/types.ts(Meta.secrets.agent.holdMs).Secrets headless-read guard — background processes never raise an unwatched Touch ID prompt (#1212). A background/headless read (scheduled routine, teammate, detached release script, the daemon sync loop,
agents run --headless,agents secrets exportin a pipe) now resolves broker-only and fails with an actionable "runagents secrets unlock <b>first" message instead of popping a Touch ID sheet on the interactive user's screen. Interactive terminal reads still prompt; file-backed bundles and non-macOS platforms are unaffected. Gated byisHeadlessSecretsContext()(macOS-keychain-only; false off-darwin) across everyreadAndResolveBundleEnvcall site. Source:apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/commands/{exec,secrets,browser,ssh}.ts,apps/cli/src/lib/{session/sync/config,cloud/antigravity,browser/chrome,crabbox/cli,secrets/mcp}.ts.Fix: workflow routines that orchestrate subagents no longer silently no-op. A
WORKFLOW.mdtools:list becomes Claude's--toolsallowlist, which restricts the available built-ins. A workflow that ships asubagents/dir — whose filesagents run <workflow>copies into the shared agents dir specifically so theTasktool can dispatch them — but whosetools:omitsTaskhad its one dispatch path stripped: the orchestrator ran with no way to reach its own subagents and degenerated to a one-line no-op ("I'll wait for the completion notification") before the process exited. This bit every subagent-orchestrating workflow run headlessly (e.g. thedoc-gaps/blog-engine/iterate-until-goodroutines, which showed "failed" on schedule).agents run <workflow>now keepsTaskin the restricted tool set whenever the run installs ≥1 dispatchable subagent, so atools:list that forgetsTaskcan't strip an orchestrator's ability to orchestrate. Source:apps/cli/src/lib/workflows.ts(ensureSubagentDispatchTool),apps/cli/src/commands/exec.ts,apps/cli/src/lib/workflows.test.ts.
1.20.64 — 2026-07-15
teamsskill documents the fleet-comms surface (RUSH-1739). The Monitoring section now points teammates-of-teams atagents feed(what agents need from you),agents mailboxes/--watch/--graph/--between(what agents say to each other), and theagents message/agents teams messagereply path — so an operator running a team can see and answer the whole conversation. Source:skills/teams/SKILL.md.agents feedreskin to shared fleet-comms visual language (RUSH-1738). Presentation only: amberthey need youmasthead, sharedGLYPHask/answered markers (▲ / ✓), and↳ ag message <id> "…"reply hints — same product face asagents mailboxes. Grouping, fan-out, policy, and JSON contracts are unchanged. Source:apps/cli/src/commands/feed.ts.agents mailboxesgrows into the fleet-comms surface (RUSH-1737). The overview now opens with the sharedfleet commsmasthead (N live · M boxes, total messages, count still awaiting delivery, last activity) plus a 24-hour hourly-volume sparkline. New views and filters, all mirrored in--json:--watch/-fstreams cross-box messages live (HH:MM:SS from ─→ toLabel text, NDJSON with--json, clean Ctrl-C via an AbortController,--sincebackfills the window first, and mail addressed to the watching agent's ownAGENTS_MAILBOX_DIRbox renders as▲ youso an orchestrator sees its replies);--between <a> <b>reads one relationship as a chronological thread in both directions under ana ⇄ b · N messages · spanheader;--graphrenders who-talks-to-whomfrom └─▶ to ···· countadjacency, busiest first;--from/--to/--sincefilter the overview recency log, the watch stream, and the graph. The<id>detail view,--limit, and themailboxalias are unchanged. Built on the RUSH-1736 comms engine (lib/comms-render.ts,watchMessages). Source:apps/cli/src/commands/mailboxes.ts,apps/cli/src/commands/mailboxes.test.ts,apps/cli/docs/06-observability.md.- Routines can run a plain shell
command:— no LLM agent required. A routine (JobConfig) now acceptscommand: <shell>as a third execution mode alongsideagent:andworkflow:(exactly one is required). A command routine runs the shell string directly via/bin/sh -c(cmd /con Windows) in the real environment (no sandbox overlay — it cannpm i -g/git pull), honoringtimeoutwith the same SIGTERM→SIGKILL kill the agent path uses, and writes the identical run record (meta.json+stdout.log, status from exit code) soagents routines list/runs, overdue tracking, and device scoping are unchanged.agents routines addgains--command. This exists because deterministic housekeeping routines (e.g. a built-in update checker) shouldn't depend on a logged-in agent, burn tokens, or gamble on account rotation — a real failure mode where the rotation dispatched an update-check to a logged-out agent version and the run died on "Not logged in." Source:apps/cli/src/lib/routines.ts(JobConfig.command,validateJob),apps/cli/src/lib/runner.ts(executeCommandJob{Foreground,Detached}),apps/cli/src/lib/daemon.ts,apps/cli/src/commands/routines.ts. - Shared fleet-comms rendering and mailbox streaming (RUSH-1736). Adds the common masthead, glyph, sparkline, aggregation, hourly-volume, and route-graph helpers used by
agents mailboxesandagents feed, plus an abortable spool watcher that emits each new box/message pair once without replaying history unless backfill is requested. Source:apps/cli/src/lib/comms-render.ts,apps/cli/src/lib/mailbox.ts. - Fix: Claude usage bars now render on Linux — so
agents view claude --host <linux-box>shows them too.agents … --host Xruns the whole command on the remote box over SSH, soagents viewexecutes on Linux there. Claude usage needs a live OAuth-token fetch, butloadClaudeOauthread the token only from the OS keychain — which on macOS falls through to/usr/bin/securityand reads Claude Code's real login-keychain entry, while on Linux it routed to agents-cli's own secret store and never found the token (Claude Code on a headless Linux box writes its OAuth to the plaintext<home>/.claude/.credentials.jsoninstead). The token load now falls back to that file when the keychain has no item — the same keychain-then-.credentials.jsonorderreadClaudeCredentialsBlob(cloud/rush.ts) already uses — so the live usage fetch succeeds and the bars render. Account + plan were unaffected because those come from the plaintext.claude.json. Codex (session logs), Kimi (kimi-code.json), and Droid (auth.v2.file) were already file-based and unaffected. Source:apps/cli/src/lib/usage.ts(loadClaudeOauth,parseClaudeOauthPayload),apps/cli/src/lib/__tests__/usage.test.ts. agents mailboxes— a read-only window onto the agent mailbox spool. The mailbox spool (~/.agents/.history/mailbox/<id>/{inbox,processing,consumed}) is the transport underagents message/agents feed/agents teams message, but until now it had no inspection surface —agents mailboxesfailed withunknown command. The new command lists every box with pending/total counts, last activity, and a live-session label when the owning agent is running, then renders a recency-ordered log of the messages that flowed between agents — including already-consumed(delivered) mail — so an operator can see agent-to-agent chatter after the fact, not just what a running agent is currently blocked on.agents mailboxes <id>shows one box in full across all three buckets;--jsonfor machine output,-n/--limitbounds the overview log;mailboxis an alias. AddslistBoxes()(enumerate boxes, consistent with the GC's validity contract) andreadBox()(read all buckets, tagged by state, non-destructive — unlikepeek, includesconsumed/) to the mailbox lib. Source:apps/cli/src/commands/mailboxes.ts,apps/cli/src/lib/mailbox.ts,apps/cli/src/lib/mailbox.test.ts,apps/cli/src/lib/startup/command-registry.ts,apps/cli/src/index.ts.agents usagenow reports live usage for Droid and Kimi, matchingagents view. Both agents already render live usage bars inagents view— Droid viaGET https://api.factory.ai/api/billing/limits(decrypted from~/.factory/auth.v2.file; 5-hour/weekly/monthly rolling windows), Kimi via its/usagesAPI — but the standaloneagents usagecommand still marked them "does not publish usage data" because its supported-agent set had drifted from the live sources.agents usage droid/agents usage kiminow show the same live windows with reset times, and the observability docs reflect that Droid exposes live usage. Source:apps/cli/src/commands/usage.ts,apps/cli/docs/06-observability.md.agents devices listnow shows live resource headroom — which box has room right now. The list used to show only name / platform / address / reachability. It now probes every reachable device in parallel (one SSH round-trip each:uptime+vm_stat//proc/meminfo+nproc/hw.ncpu), bounded by a per-probe timeout so a slow or wedged node degrades to—instead of hanging the table, and the local machine is measured directly (no self-SSH). Each row gains normalized load (load1 / cores), memory pressure %, and an idle / light / busy / loaded headroom badge (colored by the worse of load and memory); a trailing fleet-capacity summary aggregates total cores and free/total RAM (164 cores · 421G free / 518G RAM (81% free) across 10 reachable devices).--fulladds per-device core count and free/total memory;--no-statsrestores the instant registry-only view;--jsonstays registry-only and fast (the path the Factory extension polls). This is the utilization signal the teammate scheduler doesn't yet consume. Source:apps/cli/src/lib/devices/health.ts,apps/cli/src/commands/ssh.ts,apps/cli/src/lib/devices/health.test.ts.- Portable session export / import over the SSH fleet (RUSH-1710, RUSH-1711, RUSH-1712).
agents sessions exportbundles selected sessions into a portable, self-describing archive andagents sessions importrestores one — the user-driven successor to background R2/CRDT sync for the durable-archive / hand-off case (no daemon, no bucket). A bundle is NDJSON (a header line + one line per transcript file) so it pipes over SSH with no external archiver, stays greppable, and carries a per-file AES-256-GCM envelope under--encrypt; secrets are redacted by default (--no-redactto keep them). Selection reuses theagents sessionsflags (--since,-n/--limit,--all,-a/--agent); dir-shaped sessions (Kimi) carry all their files. Import places each session at the cross-machine mirror keyed by its origin machine, so it shows up inagents sessionstagged with that machine and never overwrites your own local sessions; dedup is byte-exact (--overwriteto replace conflicts,--dry-runto preview). Multi-device transfer rides the existing SSH transport —agents sessions import --from-host <h>(andexport --host <h>) run the export on the peer and stream the bundle back, equivalent toagents ssh <h> 'agents sessions export --stdout' | agents sessions import -; no R2, no daemon. Source:apps/cli/src/lib/session/bundle.ts,apps/cli/src/lib/session/remote-bundle.ts,apps/cli/src/commands/sessions-export.ts,apps/cli/src/commands/sessions-import.ts. - SSH-first recall is now the documented default; R2/CRDT background sync is demoted to an opt-in backup (RUSH-1714).
agents sessions --host <box>reads any online peer's sessions live (no sync, always current) and covers almost all cross-machine recall; export/import handles the offline / hand-off case. Background sync (agents sessions sync) stays an opt-in beta, off by default — a passive mirror for when you want offline machines' sessions to appear automatically, not the primary mechanism. Documented inapps/cli/docs/05-sessions.md. - Session sync now round-trips directory-shaped sessions (Kimi, Grok) instead of silently dropping the conversation (RUSH-1466). A session used to be assumed to be one transcript file, so for agents that store a session as a directory — Kimi's
session_<id>/state.json+agents/<name>/wire.jsonl+ per-tooltasks/*.json, Grok's<uuid>/events.jsonl— only a single file survived and the actual conversation was never synced.SyncAgentSpecgainsdirShaped/exts/fileFilter/mergeableExts;listLocalTranscriptsnow returns every file of a session (LocalTranscript.files[]), each stored under its own R2 sub-key and mirrored at its own relative path. Per-file reconciliation splits by kind: append-only logs (wire.jsonl) take the CRDT G-Set union; mutable blobs (state.json, task sidecars) take last-writer-wins by(lastTs, hash), where a blob'slastTsis derived from its file mtime (blobs carry no event timestamp, so without this LWW degraded to an arbitrary highest-hash-wins that could keep a stale copy). The manifest entry shape is backward-compatible (ManifestEntry | ManifestEntry[]) so older CLIs read file-shaped entries byte-identically. Source:apps/cli/src/lib/session/sync/agents.ts,apps/cli/src/lib/session/sync/sync.ts(deriveLastTs,resolveMirrorWrite),apps/cli/src/lib/session/sync/manifest.ts, and their*.test.ts.
1.20.63 — 2026-07-15
Built-in routines: the daemon now fires routines shipped in the system repo. Routine discovery (
listJobs/readJob) unions a new system layer —~/.agents/.system/routines/*.yml(shipped viagh:phnx-labs/.agents-system, which every install pulls atagents setup) — under the existing project and user layers. Ordering is project > user > system with first-seen-wins, so a routine shipped as a built-in fires for every install, while a user routine of the same name overrides it and a user copy withenabled: falsedisables it. The daemon (which loads with nocwd) sees user + system routines;writeJobstill only ever writes to the user layer, so built-ins are never mutated in place. This is what lets a routine likecheck-updatesship to all users centrally. Source:apps/cli/src/lib/state.ts(getSystemRoutinesDir),apps/cli/src/lib/routines.ts,apps/cli/src/lib/routines.test.ts.--host/--devicework across virtually all first-class subcommand groups (RUSH-1691). Previously only a handful of groups accepted the flags;agents repos list --host yosemite-s0(and the same with--device) died with commander's rawunknown option. Remote routing now coversrepos/repo, status/inspect groups, config/resource groups (plugins,skills,hooks,sync, …),teams,routines, and more via the centralmaybeRunOnHostallowlist. Commands with their own richer host handling (run,sessions,feed,computer,secrets,logs) still fall through to their actions. Groups with no remote semantics reject the flag with a clear message instead ofunknown option. Self-host targets strip the routing flags before the local command parses so fall-through never trips an unregistered option. Source:apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/commands/repo.ts,apps/cli/docs/{00-concepts,hosts}.md.Subagent integrations are now a declarative capability registry — one table entry per agent instead of copy-pasted
else if (agent === …)arms across six files (RUSH-1698). Wiring subagents for an agent used to mean editing the same near-identical per-agent branches insubagents.ts(install / list / remove-from-agent / orphan-diff / soft-delete), the staleness writer, and the staleness detector — roughly O(agents × operations), and the top source of merge conflicts when new-agent PRs landed in parallel. All of it now iterates a singleSUBAGENT_TARGETStable (apps/cli/src/lib/subagents-registry.ts) keyed by agent, each entry declaring the target dir, on-disk layout (flat-file/dir-file/dir-copy), transform, and ownership marker; the install/list/detect/orphan/remove engine is generic with zero per-agent branches. Genuinely-bespoke agents keep a handler in the same table (Kimi: two files per subagent + a managed parent index). Adding a standard integration is now one registry entry plus thesubagentscapability gate — the writer, detector, andsubagents.tsneed no new arm, and a test pinsObject.keys(SUBAGENT_TARGETS)tocapableAgents('subagents')so the flag and the shape can never drift. This also closed real latent gaps the old hand-written chains had left inconsistent:droid(synced to.factory/droids/but absent from everysubagents.tsfunction, so its subagents could not be listed, pruned, or removed), andcopilot/codex(present in some operations, missing from others) are now uniformly install/list/remove-capable. Behavior for every already-supported agent is unchanged (verified: full subagent + versions suites green, byte-identical trash/list semantics per layout). A documented integration tier list (apps/cli/docs/subagents.md) scopes future "wire X" tickets by importance instead of treating every agent equally. Source:apps/cli/src/lib/subagents-registry.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts,apps/cli/src/lib/subagents-registry.test.ts,apps/cli/docs/subagents.md.Signed webhook ingress for routines via Tailscale Funnel (RUSH-1456, RUSH-1459, RUSH-1460, RUSH-1461). Routine triggers now understand both GitHub and Linear event sources, including Linear action/team/label filters.
agents webhook serve --secrets-bundle <name>exposes signed localhost endpoints at/hooks/githuband/hooks/linearwith raw-body HMAC verification, Linear timestamp checks, duplicate delivery suppression, and rate limiting.agents funnel status/upwraps the allowed Tailscale Funnel ports through the existing SSH/device path so a webhook receiver can be exposed without hand-written SSH commands. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/triggers/webhook.ts,apps/cli/src/commands/routines.ts,apps/cli/src/commands/webhook.ts,apps/cli/src/commands/funnel.ts,apps/cli/src/lib/funnel.ts.Cross-machine session sync verified end-to-end + documented (RUSH-1464). The R2/CRDT session-sync beta (Claude + Codex) is now signed off across the full matrix: a machine push with a read-write R2 token uploads with zero errors (the read-only-token 403 from #412 is resolved), a second machine pulls and folds those sessions into its own list, CRDT G-Set union converges byte-identically regardless of sync order, and a machine that fell behind catches up automatically when it returns (a grown session's manifest hash no longer matches the puller's recorded signature, forcing a re-fetch + re-merge). Ships the previously-missing docs: a "Cross-machine sync (R2 + CRDT)" section in
apps/cli/docs/05-sessions.mdcovering the single-writer prefix layout, manifest + mirror model, CRDT convergence, client-side AES-256-GCM encryption, the opt-in beta gate, and ther2.backupscredential bundle (including the read+write scope requirement). Verification only — no runtime change. Source:apps/cli/docs/05-sessions.md.Guided session-sync provisioning in
agents setupandagents sessions sync --setup(RUSH-1468). Joining a machine to the cross-machine session-sync fabric no longer requires hand-running fouragents secrets add r2.backups …commands. A new interactive step mints ther2.backupsbundle (R2 account/bucket/access-key/secret + a generatedR2_SYNC_ENC_KEY), probes read+write connectivity with a throwaway object, and opts the machine into thesession-syncbeta on success. The first machine mints and prints the shared encryption key; every other machine pastes it so the whole fabric shares one key (an existing key is reused, never overwritten — overwriting would orphan peers' encrypted transcripts).agents setupoffers it opt-in (default No, never blocks setup);agents sessions sync --setupruns it explicitly and can re-show the shared key. Source:apps/cli/src/lib/session/sync/provision.ts,apps/cli/src/lib/session/sync/provision.test.ts,apps/cli/src/commands/sync-provision.ts,apps/cli/src/commands/setup.ts,apps/cli/src/commands/sessions-sync.ts.agents fleetalias + fleet-wide rollout (RUSH-1632).fleetis an alias fordevices. New subcommandsupdate [version]andrun <cmd…>roll out across every online device with a per-device result table. Source:apps/cli/src/commands/ssh.ts,apps/cli/src/lib/devices/fleet.ts.agents hosts stop <id>(aliaskill) terminates a detached host run from the origin machine (RUSH-1360). Sends SIGTERM to the remote process group, writes exit143only when a live group was signaled (or no.exitexisted), and keeps the remote log foragents hosts logs <id>. Source:apps/cli/src/lib/hosts/dispatch.ts,apps/cli/src/commands/hosts.ts.Fix: mailbox GC actually archives expired messages on live boxes (RUSH-1611). The live-box branch of
gcMailboxonly incrementedmessagesDroppedExpiredwithout moving the file toconsumed/, soagents feed --dispatchcould report drops while leaving expired messages ininbox//processing/. GC now reusessweepExpired(the same path as drain/peek). Source:apps/cli/src/lib/mailbox-gc.ts.Fix: Antigravity sign-in detection on Linux when the OAuth grant lives in Secret Service (RUSH-1329).
agyuses the Go keyring library, which prefers libsecret (gnome-keyring) over the file fallback whenever a Secret Service daemon is running — so~/.gemini/antigravity-cli/antigravity-oauth-tokenmay be absent even when the user is signed in. After the file check,getAccountInfonow probessecret-tool lookup service gemini username antigravity(exit 0 = present; stdout discarded), mirroring the macOSsecurity find-generic-passwordprobe from #506. Missingsecret-tool, locked collections, and timeouts all read as signed out. Opt out withAGENTS_NO_KEYCHAIN_PROBE=1. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/agents.test.ts.Client-side (zero-knowledge) encryption of session transcripts before R2 upload (RUSH-1463). Transcripts carry secrets, tokens, and absolute file paths; R2's server-side encryption uses Cloudflare's key, so anyone with bucket-read access (or Cloudflare) could read them as plaintext NDJSON.
agents sessions syncnow seals each transcript BODY client-side with AES-256-GCM before upload and decrypts on pull, so R2 only ever stores ciphertext. The 32-byte key is a newR2_SYNC_ENC_KEYin ther2.backupsbundle — shared across the sync fabric (every machine derives the identical key) and deliberately separate from the R2 access key so rotating the token never orphans encrypted objects. CRDT identity stays over plaintext (the manifest hash is cleartext; pull decrypts before the G-Set union), so cross-machine merge is unaffected. Pull transparently reads legacy plaintext objects (migration-safe); a push with no key configured still uploads but emits a loud per-cycle warning. A newR2_ENDPOINToverride points sync at any S3-compatible store (MinIO/other providers), which is also how the flow is verified end-to-end without live R2. Source:apps/cli/src/lib/session/sync/transcript-crypto.ts,apps/cli/src/lib/session/sync/transcript-crypto.test.ts,apps/cli/src/lib/session/sync/sync.ts,apps/cli/src/lib/session/sync/config.ts,apps/cli/src/commands/sessions-sync.ts,apps/cli/src/lib/daemon.ts.Extend session sync to Droid, Grok, Kimi, and OpenCode (RUSH-1467).
agents sessions syncnow includes these four agents in its upload/download matrix.SyncAgentSpecgains an optionalextfield so agents with non-.jsonltranscript files (e.g., Kimistate.json) are walked correctly. Droid.jsonlrollouts, Grokevents.jsonlstreams, and Kimistate.jsonmetadata files round-trip through the R2 mirror; OpenCode is slotted inSYNC_AGENTSbut remains a placeholder because its sessions live in a SQLite DB and still require an SQLite-to-JSONL export step. Source:apps/cli/src/lib/session/sync/agents.ts,apps/cli/src/lib/session/sync/agents.test.ts,apps/cli/src/commands/sessions-sync.ts.agents reposis canonical (repoalias); push/pull echo the resolved target; push no longer no-ops when clean-but-ahead; pull rebases on diverge (RUSH-1454). Help now printsUsage: agents repos …. Push/pull reportuser (~/.agents → origin/main): …instead of the bare alias.commitAndPushstillgit pushes when the tree is clean but local is ahead of origin (previously returned success without pushing).pullRepousesgit pull --rebaseso divergent branches reconcile instead of failing with a raw git error. Source:apps/cli/src/commands/repo.ts,apps/cli/src/lib/git.ts,apps/cli/src/lib/startup/command-registry.ts.agents run --host <name> --copy-credsprovisions runtime credentials on a persistent host (RUSH-1608). Reuses the--leasecredential path (resolveClaudeCredentialsBlob+ the~/.claude/.credentials.jsonbootstrap) but makes copying tokens to a persistent host strictly opt-in per run. The user picks runtimes, sees a consent prompt naming accounts and the Claude OAuth token, and the files are shredded after the run. Source:apps/cli/src/commands/exec.ts,apps/cli/src/lib/hosts/dispatch.ts,apps/cli/src/lib/hosts/credentials.ts,apps/cli/src/lib/hosts/credentials.test.ts.
1.20.62 — 2026-07-14
Browser downloads land in a known per-profile dir; profile data is consolidated. A browser profile is now one self-contained tree under
~/.agents/.cache/browser/<profile>/:chrome-data/,downloads/, andsessions/<task>/(screenshots, PDFs, recordings). Previously downloads had no configured destination — the CLI only set the download dir when the agent explicitly ranbrowser download --path, so absent that call a download fell to Chromium's own default (for an attached user browser like comet, wherever that browser was last configured), which is how downloads escaped into random locations. The service now sets the profile'sdownloads/dir browser-global at connect time (both fresh launch and every attach path), so downloads always land somewhere agents-cli controls;browser download --pathbecomes an optional override (omit it to use the profile default) and reports the resolved path. Screenshots/PDFs/recordings moved from the old GLOBALbrowser/sessions/<task>/root to the per-profilebrowser/<profile>/sessions/<task>/, with a one-shot migration that folds existing captures into the owning profile (attributed via each profile'stasks.json; unattributable captures go to a_legacybucket). Newagents browser sessions [--profile <name>] [--open latest|<file>] [--json]lists a profile's captures + downloads, aliased asagents sessions --browser. Source:apps/cli/src/lib/browser/{profiles,service,ipc,sessions-list}.ts,apps/cli/src/lib/migrate.ts,apps/cli/src/commands/{browser,sessions}.ts.Wire Goose commands support (RUSH-1572).
agentsnow syncs slash commands to Goose as recipe YAML files under~/.config/goose/commands/<name>.yaml, each registered in~/.config/goose/config.yamlunder aslash_commands: [{ command, recipe_path }]array (Goose has no native slash-command file format — a slash command IS a recipe). The command recipes live in a dir distinct from the workflow recipes dir (~/.config/goose/recipes/) so the workflow detector never treats a command recipe as a workflow. Registration is a read-modify-write that preserves every otherconfig.yamlkey (mcp_servers,extensions, …) and otherslash_commandsentries, and removal soft-deletes the recipe + unregisters the entry. Flip Goose'scommandscapability and addgoosebranches to install/list/match/remove, the staleness commands writer, and doctor-diff, backed by a newgoose-commands.tsmodule + amarkdownToGooseRecipeconverter. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/goose-commands.ts,apps/cli/src/lib/commands.ts,apps/cli/src/lib/convert.ts,apps/cli/src/lib/staleness/writers/commands.ts,apps/cli/src/lib/doctor-diff.ts.Fix: Hermes plugin sync no longer disables a plugin the user explicitly enabled. The plugin install path (RUSH-1688) unconditionally forced
plugins.enabledto the exec-surface trust verdict on every sync. An ordinary un-flagged background re-sync computesenable=falsefor a plugin with hooks/tools, so it stripped that plugin from the~/.hermes/config.yamlallowlist — clobbering a plugin the user deliberately enabled with--allow-exec-surfaces. The install path now enables only when trusted and never down-toggles (matching the marketplace flow's add-if-trusted semantics); removal still unregisters explicitly. Source:apps/cli/src/lib/plugins.ts.Wire Goose subagents support (RUSH-1573).
agentsnow syncs subagents to Goose as recipe YAML files under~/.config/goose/agents/<name>.yaml— Goose has no dedicated subagent format, so a named subagent IS a recipe (goose auto-discovers~/.config/goose/agents/and delegates to them by name in autonomous mode).transformSubagentForGooseemits the same recipe schema agents-cli already uses for Goose workflow recipes (version/title/description/instructions/prompt, plus optionalsettings.goose_model). Flip Goose'ssubagentscapability and wire the install/remove/list/orphan/version-remove branches plus the staleness subagents writer and detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts.Wire ForgeCode commands and subagents support (RUSH-1689, RUSH-1690).
agentsnow syncs slash commands to ForgeCode as Markdown files under~/.forge/commands/<name>.md(previously ForgeCode hadcommands: falseand received commands only as skills), and named subagents as Markdown-with-frontmatter definitions under~/.forge/agents/<name>.md(samecolor-less shape as Droid/Copilot/Cursor, sotransformSubagentForForgealiasestransformSubagentForDroid). Flip ForgeCode'scommands/subagentscapabilities, setcommandsDir, add the subagent transform plus install/remove/list/orphan/version-remove branches, and register the subagents writer + detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts.Wire allowlist (permissions) support for OpenClaw (RUSH-1570). OpenClaw gates at TOOL granularity only, so permission sync maps just blanket (whole-tool) rules into
~/.openclaw/openclaw.jsontools.alsoAllow(allow) /tools.deny(deny):bash → exec,read → read,write/edit → write,webfetch → web_fetch,websearch → web_search. Sub-command/path/domain rules (Bash(git:*),Write(secrets/**),WebFetch(domain:x)) have no tool-level equivalent and are skipped — coarse-mapping a specific deny to a whole tool would wrongly gate every use of that tool. The absolutetools.allowlist is never touched, and all other keys (mcp,exec,agents, …) are preserved on read-modify-write. Flip OpenClaw'sallowlist: true, addconvertToOpenClawFormat+ theopenclawbranch inapplyPermissionsToVersion, register the config path and staleness detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts,apps/cli/src/lib/resources/permissions.ts,apps/cli/src/lib/staleness/detectors/permissions.ts.Wire subagents support for Cursor CLI (RUSH-1388). cursor-agent loads custom subagents as Markdown with YAML frontmatter under
~/.cursor/agents/*.md(project-scoped.cursor/agents/also supported natively), same shape as Claude/Droid/Copilot minus thecolorfield, gated at>= 2026.1.22(cursor-agent's CalVer build tag for Cursor 2.4). Flip Cursor'ssubagents, addtransformSubagentForCursor(alias oftransformSubagentForDroid), and wire the install/remove, list, orphan-detection, writer, and detector paths. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts.Wire hooks support for Hermes (RUSH-1687).
agentsnow registers central hooks into Hermes Agent's~/.hermes/config.yamlunder ahooks:block (YAML, ≥ 0.11.0). The registrar read-modify-writes that shared config so sibling keys likemcp_serverssurvive, maps canonical events to Hermes' snake_case lifecycle names (SessionStart→on_session_start,SessionEnd→on_session_end,PreToolUse→pre_tool_call,PostToolUse→post_tool_call,SubagentStop→subagent_stop,UserPromptSubmit→pre_llm_call,Stop→on_session_finalize), and clamps each hook's timeout to 300s (default 60s). Managed entries are re-synced idempotently while user-authored hooks are preserved. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/hooks.ts,apps/cli/src/lib/staleness/writers/hooks.ts.
1.20.61 — 2026-07-14
- Detect OpenCode sign-in state so
agents viewstops mislabeling a logged-in install as "not signed in."getAccountInfohad noopencodecase, so it fell through tosignedIn: falseand every row printed "(not signed in — run opencode to log in)" even with a live login. It now reads OpenCode'sauth.json($XDG_DATA_HOME/opencode/auth.json, defaulting to~/.local/share/opencode/auth.jsonon every platform —xdg-basedirdoes not special-case macOS), validates each provider entry against itsoauth/api/wellknowncredential shape, and reports the account as signed in with the non-secret provider ids surfaced as the account label (e.g.id:muse-spark). Credential secrets (access/refresh/key/token) are only inspected for presence — never read into any display or JSON output. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/agents.test.ts. - Wire Antigravity workflows support (RUSH-1580).
agentsnow syncs workflows to Antigravity as markdown files with the requireddescriptionfrontmatter plus anagents_workflowownership marker, invocable as/<name>slash commands. Antigravity workflows are the one non-version-isolated target:agyscans a single shared, HOME-global~/.gemini/config/global_workflows/at startup (a real home directory, never symlinked per version — verified via strace ofagy), so the writer and detector both resolve that shared dir for every installed version instead of a per-version home. Gated at>= 1.0.6. The ownership marker prevents overwriting or removing user-authored workflows of the same name. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/workflows.ts,apps/cli/src/lib/staleness/detectors/workflows.ts.
1.20.60 — 2026-07-14
- Fix Goose skill sync status for its native central-storage path.
agents skills list goose@<version>now reports skills under~/.agents/skills/as installed instead of falsely requiring a per-version.config/goose/skills/copy that Goose never reads. Source:apps/cli/src/lib/skills.ts. - Correct the documented
autoand ACPskipsemantics. The README and bundledrunskill now distinguish Kimi's interactive--autofrom its already-auto-approved headless-ppath, document Droid's native--auto high, and explain that ACPskipprefersallow_alwaysbut falls back to the first permission option offered by the server. Documentation only; runtime behavior is unchanged. Source:README.md,skills/run/SKILL.md. - Wire Antigravity subagents and Kimi workflow sync (RUSH-1548, RUSH-1581). Antigravity now receives subagents as custom-agent Markdown under
~/.gemini/config/agents/<name>/agent.mdwith the>= 1.0.16version gate enforced during sync. Kimi receives workflows as managedtype: flowskills under.kimi-code/skills/<name>/SKILL.md, using the canonical slug as the flow name and anagents_workflowmarker so native user-owned flows are not overwritten or removed. Antigravity workflows are wired separately in RUSH-1580 (they target a shared HOME-global dir, not a version home). Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/workflows.ts. - Release retries rebuild the exact merged, CI-tested release tree even after
mainadvances. A registry/auth failure after the release PR merged previously stranded that version because the catch-up guard required the release merge to remain currentmain. The local release script now validates the original PR head and full green matrix, verifies the SHA-pinned keychain helper, rebuilds the unpinned menu-bar helper from historical source in a detached temporary worktree, rejects mismatched remote tags, and tags/publishes that exact merge without including later commits. Source:apps/cli/scripts/release.sh. - Wire Gemini plugins/subagents and Goose workflows/allowlists. Gemini now syncs plugin bundles as Gemini extensions (
.gemini/extensions/<name>/gemini-extension.json) from CLI 0.8.0+ and subagents as.gemini/agents/*.mdfrom CLI 0.36.0+. Goose now syncs workflows as recipe/subrecipe YAML under.config/goose/recipes/and permission groups into.config/goose/permission.yaml. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/plugins.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/workflows.ts,apps/cli/src/lib/permissions.ts. (RUSH-1568, RUSH-1569, RUSH-1574, RUSH-1582) - Wire Gemini permissions/allowlist support (RUSH-1567). Gemini permission groups now sync Bash allow/deny rules into
.gemini/settings.jsonastools.core/tools.excludeentries with per-commandShellTool(...)patterns; non-Bash canonical permissions remain unsupported by Gemini's native tool grammar and are skipped. Flipallowlist: true, register the permission writer/detector through the capability table, and replace the dormant legacytools.allowedserializer. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts,apps/cli/src/lib/resources/permissions.ts,apps/cli/src/lib/staleness/detectors/permissions.ts. - Fix:
agents run <agent>@<version> --host <host>now forwards the version pin and most run flags to the remote host. Previously the--hostbranch stripped@versionand ignored--strategy,--effort,--add-dir,--json,--verbose,--timeout,--yes, and--acp, so the remote host applied its own defaults. The local CLI now parsesagent@versionverbatim, normalizes--strategy/--balanced, makes--add-dirpaths remote-portable, and forwards all of these flags to the remoteagents runinvocation.--add-dirportability uses the same~/$HOMEre-rooting that--cwdalready uses, so a Linux remote resolves home paths against its own/home/<user>. Source:apps/cli/src/lib/hosts/dispatch.ts,apps/cli/src/commands/exec.ts,apps/cli/src/lib/hosts/dispatch.test.ts. - Fix: routine edits no longer rewrite the whole YAML file, so
~/.agentsstays clean andagents repo pullcan sync routines across the fleet.writeJobpreviously re-emitted the entire document viayaml.stringifyon every mutation (pause/resume,routines devices --set, add), restyling untouched scalars — unquotingschedule, re-wrapping the foldedpromptblock — which left the git-backed user repo perpetually dirty. That made cross-deviceagents repo pullrefuse ("uncommitted changes"), so adevices:pin set on one machine never reached the others andDevices: allroutines kept firing on every box. A newserializeJobedits only the changed keys via the YAML Document API, preserving byte-for-byte formatting of untouched nodes; new/unparseable/non-mapping files fall back to canonical stringify. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/__tests__/routines.serialize.test.ts. agents sessions --active --jsonnow carries live plan progress (RUSH-1380). The state engine parses the latestTodoWriteoff the transcript tail intoActiveSession.todos({ items: [{ content, status, activeForm? }], done, total, activeForm }), and the live preview verb readsPlan N/M: <current step>instead of a bare "TodoWrite". This lets consumers (the Factory Floor) show an N/M pill + checklist for every session — including remote / device-dispatched agents that have no local tool-call stream. Source:apps/cli/src/lib/session/state.ts(extractTodoProgress,inferActivity),apps/cli/src/lib/session/active.ts(ActiveSession.todos,applyState),apps/cli/src/lib/session/parse.ts(summarizeToolUse).
1.20.59 — 2026-07-13
- Fix: remote secrets now choose the Windows PowerShell wrapper from the original
--hostname, not the resolveduser@ipSSH target. Inline enrolled Windows hosts resolve to address-based SSH targets, but the OS registry is keyed by the host name;agents secrets view/list/exec --host <windows>,agents run --secrets bundle@<windows>, and remote secrets unlock/export paths now pass that original name into command construction so Windows hosts no longer fall back tobash -lc. Source:apps/cli/src/lib/secrets/remote.ts,apps/cli/src/commands/secrets.ts,apps/cli/src/commands/exec.ts. (RUSH-1431) - Wire Droid skills support (RUSH-1397). Droid loads skills from
.factory/skills/(since 0.26.0). Flipskills: { since: '0.26.0' }, register generic skills writer/detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/__tests__/capabilities.test.ts. - Wire skills support for Goose CLI (RUSH-1394). Goose reads skills directly from
~/.agents/skills/via the Summon extension (block-goose-cli >= 1.25.0). Flipskills: { since: '1.25.0' }, register generic skills writer/detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/staleness/writers/commands.ts. - Wire Droid allowlist support (RUSH-1396). Droid stores allow/deny in
.factory/settings.json(commandAllowlist/commandDenylist). Flipallowlist: truesince 0.57.5, addconvertToDroidFormat, wire writer/detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts. - Wire Codex permissions/allowlist support (RUSH-1566). Codex stores allow/deny in
.codex/config.toml(approval_policy,sandbox_mode). Flipallowlist: truesince 0.128.0, addconvertToCodexFormat, wire writer/detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts. - OpenCode permissions write to the loaded config path (RUSH-1623). Global config is
~/.config/opencode/opencode.jsonc(not~/.opencode/); project config isopencode.jsoncat the project root. Source:apps/cli/src/lib/permissions.ts,apps/cli/src/lib/agents.ts. - Catch-up publishes now require the exact CI-tested release tree and its full green matrix. A package version already present on
mainno longer counts as release validation by itself:release.shresolves the mergedrelease/v<version>PR, requires its merge commit to be currentmain, fetches the PR head that ran CI, requires that head tree to equal currentmain, and rechecks every expected CI context before publishing. This closes the path that let 1.20.58 reach npm before its tag-triggered Windows matrix exposed failures. Source:apps/cli/scripts/release.sh. - Windows release validation now matches portable path behavior. Home-relative project paths normalize native separators to
/before they are stored as~/…, and the systemd-manifest and remote-shell assertions now compare the escaped/quoted forms that the runtime deliberately emits. This restores the Windows Node 22/24 release matrix without weakening command escaping. Source:apps/cli/src/lib/project-root.ts,apps/cli/src/lib/{project-root,daemon}.test.ts,apps/cli/src/lib/hosts/dispatch.test.ts. - Wire subagents support for Kiro CLI. Kiro custom agents are JSON files under
~/.kiro/agents/*.json(introduced in kiro-cli v1.23.0). Flip Kiro'ssubagents: { since: '1.23.0' }, addtransformSubagentForKiro, and wire the subagents writer, detector, install/remove, and orphan-detection paths. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts. (RUSH-1393)
1.20.58 — 2026-07-13
Self-updating agent CLIs are represented as one live installation.
agents viewno longer invents version-home rows for single-binary installers such as Droid, Grok, Cursor, Kiro, Goose, and Hermes; it reports the version returned by the installed binary and folds away stale per-version directories.agents add <agent>@<version>now installs or keeps that agent's current release instead of rejecting an unsupported pinned install. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/versions.ts,apps/cli/src/commands/{versions,view}.ts. (RUSH-1321)Stopped teammate resumes are transactional from launch through persistence. If a local or remote resume fails, the existing teammate record, directory, runtime metadata, stdout mirror, and log cursor are restored; any replacement wrapper and its descendants are terminated as one process group. A successful resume whose log was truncated restarts parsing at byte zero, and a secondary restore-write failure retains the original launch error as its cause. Source:
apps/cli/src/lib/teams/agents.ts,apps/cli/src/lib/hosts/dispatch.ts. (#1104, #1108)Wire allowlist support for Cursor CLI. Cursor agent CLI stores allow/deny in
~/.cursor/cli-config.json(permissions.allow/denywith Shell/Read/Write/WebFetch/Mcp). Flipallowlist: true, addconvertToCursorFormat(Bash→Shell), and write viaapplyPermissionsToVersion+ detector. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts. (RUSH-1387)GitHub Copilot CLI subagents now sync (RUSH-1390). Installed subagents flatten into GitHub Copilot custom-agent profiles at
~/.copilot/agents/<name>.agent.md(the Droid custom-droid format), gated to Copilot CLI ≥ 0.0.353.agents subagents list/viewnow surfaces synced Copilot agents andagents subagents removesoft-deletes their.agent.mdfiles to trash — both previously skippedcopilotentirely. Source:apps/cli/src/lib/subagents.ts(listSubagentsForAgent,removeSubagentFromVersion,transformSubagentForCopilot),apps/cli/src/lib/staleness/writers/subagents.ts,apps/cli/src/lib/staleness/detectors/subagents.ts,apps/cli/src/lib/agents.ts.Menu-bar Quick Dispatch preserves typed drafts when focus is stolen (RUSH-1592). If another app activates while the
Cmd-Shift-Ocapture panel is open, the panel can hide without destroying the note; the next summon restores the draft text plus selected screenshots, action, and agents. Return submits and clears the draft; Escape clears without dispatching. Source:apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift,apps/cli/docs/menubar.md.Menu-bar ticket agents now carry every selected screenshot into the Linear issue (RUSH-1668).
Cmd-Shift-Oalready passed selected file paths to the ticket agent, but the prompt only asked it to inspect them, so agents could create text-only issues and stop. The brief now identifies every selected path as user-provided ticket material, requires each file to be uploaded, supplies the existinglinear update <id> --proof <path>path as a reliable default, and leaves description/comment/other placement to the agent's judgment. Source:apps/cli/menubar/Sources/MenubarHelper/{AgentsCLI,IssueSelfTest}.swift,apps/cli/docs/menubar.md.agents sessions --active --jsonnow carries session attachment metadata for Factory previews (RUSH-1524). Claude and Droid prompt image/document blocks that reference local files are preserved as{ path, name, mediaType, sizeBytes }, and the active-session state dedupes them intoattachmentsso consumers can render screenshot thumbnails and open the original files instead of only seeing an attachment count. Source:apps/cli/src/lib/session/parse.ts,apps/cli/src/lib/session/state.ts,apps/cli/src/lib/session/active.ts.Retired the standalone
com.phnx-labs.agents-secrets-agentlaunchd service — the always-on daemon is now the sole broker host (#416, step 2).ensureAgentRunning()no longer installs a separate launchd service: it retires any leftover plist via the newretireLegacySecretsAgentService()and relies on the daemon (Path 0), with a one-off detached broker as the only fallback. The upgrade migration (scripts/postinstall.js→healLongRunningProcesses) nowlaunchctl bootouts the legacy service first, then (re)starts the daemon so it takes over the broker socket, instead of kickstarting the old service onto new code.agents secrets startis now a thin alias that brings the daemon up (and waits for the broker to answer);agents secrets stoplocks all bundles and retires any leftover legacy service while leaving the always-on daemon running;agents secrets statusreports broker reachability (daemon-hosted vs standalone) rather than "service installed". The stale broker teardown (version-skew self-heal) retires the legacy service instead of kickstarting it. Source:apps/cli/src/lib/secrets/agent.ts(retireLegacySecretsAgentService,ensureAgentRunning,teardownStaleBroker,uninstallSecretsAgentService; removedinstallSecretsAgentService/kickstartSecretsAgentService/generateServicePlist),apps/cli/scripts/postinstall.js(healLongRunningProcesses),apps/cli/src/commands/secrets.ts(start/stop/status).Clarify the native escape hatch behind
--mode skip. The README and bundledrunskill now discourageskip, list its exact direct-exec per-harness flag mappings and ACPallow_alwaysbehavior, replace an older recommendation of unsafefullfor ordinary writes, and distinguish Codexauto(sandboxededit, which can still prompt) from Codexskip(--dangerously-bypass-approvals-and-sandbox, equivalent to unsandboxed--yolo). Documentation only; runtime behavior is unchanged. Source:README.md,skills/run/SKILL.md.Wire allowlist support for Kiro CLI. Kiro 2.8.0+ permission groups now sync into
~/.kiro/settings/permissions.yamlas v3 capability rules for shell, filesystem, and web access; existing user-authored rules are preserved and duplicate generated rules are removed. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts,apps/cli/src/lib/staleness/detectors/permissions.ts. (RUSH-1392)Fix: remote
agents secrets view <bundle>@host --revealno longer leaves a 60-second SSH control master behind. The interactive-ttreveal path now opts out of default SSH multiplexing (multiplex: false), matching the transport guidance for one-shot commands that must not keep aControlPersistsocket open after a Touch ID/passphrase reveal. Source:apps/cli/src/lib/secrets/remote.ts(remoteSecretsRaw).agents run --host <host> --cwd <dir>now sets the working directory ON the host (and a new--projectshorthand jumps to a project by name). Previously--cwdwas silently dropped for--hostruns — only the separate--remote-cwdflag worked — soagents run claude --host s1 --cwd ~/src/foolanded in the remote login-shell's default directory with no warning.--cwdis now forwarded as the host working directory, and a home-anchored path (~/…,$HOME/…, or a local-home absolute the shell already expanded like/Users/me/…) is re-rooted at the remote$HOMEso it resolves correctly across machines with different home paths (/Users/me→/home/me).--remote-cwdremains as the explicit override. New-P, --project <slug>[@worktree]resolves a bare project name against your projects root (e.g.~/src/github.com/<user>) — auto-inferred from the repo you launch inside and cached inagents.yaml, or set/shown withagents defaults project-root [path];--project foo@fixtargets thefixgit worktree. Works for local and--hostruns. Verified end-to-end:agents run claude --host yosemite-s1 --project agents-cliruns the remote agent withpwd=/home/muqsit/src/github.com/muqsitnawaz/agents-cli. Source:apps/cli/src/lib/project-root.ts(new),apps/cli/src/lib/hosts/dispatch.ts(remoteCdPrefix),apps/cli/src/commands/exec.ts(--project/--cwdhost wiring),apps/cli/src/commands/defaults.ts(project-root).Fix daemon crash-looping when its pinned Node version is pruned (fleet-wide). The routine daemon's launchd/systemd manifest hardcoded
~/.nvm/versions/node/v24.0.0/binon PATH, and launched the CLI entry bare when it was an extension-less shim or abin/agents → dist/index.jssymlink (an extension check on the link name missed it). The moment that exact nvm patch was upgraded away, the shim's#!/usr/bin/env nodeshebang fell through to an ancient system node (Node 18 →SyntaxError: node:util has no export 'styleText'from@inquirer/core), and the service crash-looped at import — observed at 100k+ restarts on Linux workers, silently killing all scheduled routines.getDaemonLaunchnow detects Node-script entries by resolving symlinks and sniffing the shebang (not just the.js/.cjs/.mjsextension), so it pins them toprocess.execPath; and the generated PATH now leads withpath.dirname(process.execPath)— the Node that installed the service — instead of a hardcoded nvm version, so both the shim and child routine processes always resolve a working runtime. Source:apps/cli/src/lib/daemon.ts(getDaemonLaunch,isNodeScriptEntry,daemonNodeBinDir,generateSystemdUnit,generateLaunchdPlist).Fix global npm upgrades restarting the routines daemon through
scripts/postinstall.js. The postinstall process is itselfprocess.argv[1], so its daemon self-heal could stampnode scripts/postinstall.js daemon _runinto launchd. Daemon startup now accepts an explicit CLI entry and postinstall passes the resolved signed native binary (or JavaScript entrypoint), with the same value threaded through launchd, systemd, and detached startup. Source:apps/cli/scripts/postinstall.js,apps/cli/src/lib/daemon.ts.Fix a standalone secrets service stealing the daemon-hosted broker socket during postinstall. The standalone and hosted brokers now bind through one race-safe owner arbitration path: an existing reachable broker wins without its socket being unlinked, a persistent losing service stays quiescent instead of triggering launchd restart churn, takes over if the owner stops, and releases its standby PID on service shutdown; only an unreachable stale socket is reclaimed. This covers the release ordering where postinstall restarts the daemon first and then kickstarts an installed standalone service. Source:
apps/cli/src/lib/secrets/agent.ts(bindBrokerSocket,runSecretsAgent,startHostedBroker).
1.20.57 — 2026-07-13
agents teams resume/agents teams message— resume a stopped teammate with a follow-up message. A teammate that ended its turn with more to do (PR open awaiting review, headless turn cap, a redirect after the fact) could not be reached:agents messageresolves only live sessions, so a completed/stopped/failed teammate had no path back short of finishing the work by hand or spawning a fresh, context-less teammate.teams resume <team> <teammate> <message>re-enters the teammate's own session with the message as the next user turn, re-launching through the same backend (local process or remote host) in its original worktree and flipping it back torunningsoteams statustracks it live.teams messageis the same command with automatic routing by reconciled status: a running teammate is steered via its mailbox (delivered at its next tool call, no re-launch); a stopped one is resumed; a pending one is refused with a pointer toteams start. Works for every harness — the resume delegates toagents run --resume, inheriting native resume for Claude/Codex and the universal/continuereplay for the rest (OpenCode, Grok, Kimi, …); the resume target is the teammate's captured underlying session id (remoteSessionId ?? agentId), and a non-Claude teammate that died before emitting a session id is refused with a clear error rather than resumed into a fresh run. This also makes good onteams stop's long-standing "can be restarted later" promise, which no code implemented. Source:apps/cli/src/commands/teams.ts(message/resumesubcommands,decideTeamMessageRoute),apps/cli/src/lib/teams/agents.ts(AgentManager.resumeTeammate, resume-awarebuildRunArgv/buildCommand/launchProcess/launchRemoteProcess).- The always-on daemon now hosts the secrets broker (socket-first) — one supervised backbone instead of a separate service (#416, step 1).
runDaemon()binds the broker via the newstartHostedBroker()before the scheduler and the heavy browser/session-sync services, soagents secretsresolves within ms of daemon start. It serves the same socket + wire protocol as the standalone broker (noPROTOCOL_VERSIONbump —agentGetSync/agentPing/agentAutoLoadSyncare unchanged), but is daemon-safe: no pid-guard, noprocess.exit/signal handlers/self-heal-exit (which would take the daemon down), TTL-eviction only.ensureAgentRunning()gains a Path 0 that prefers the daemon and falls back to the standalonecom.phnx-labs.agents-secrets-agentlaunchd service, and the daemon only hosts when no broker is already reachable, so a live standalone broker is never orphaned. Retiring the standalone service (a gatedlaunchctl bootoutmigration) and child-spawning the heavy services are the follow-on (#417). Source:apps/cli/src/lib/secrets/agent.ts(startHostedBroker,ensureAgentRunningPath 0,agentPingexported),apps/cli/src/lib/daemon.ts(runDaemonbroker host + shutdown). - Clarified
agents secrets listPOLICY column labels. The column previously mixed policy names, runtime state, and implementation jargon (daily · 7d left,always ask,never · NO ACL). It now uses a consistentpolicy · stateform:daily,daily · held 7d,always · prompt, andnever · no prompt. Source:apps/cli/src/commands/secrets.ts(renderPolicyCol).
1.20.56 — 2026-07-13
- Fix native routine schedulers rejecting the published CLI as a Bun virtual path. Bun's standalone runtime reports the embedded
/$bunfs/root/agentsentry as existing atprocess.argv[1], while the real physical executable lives atprocess.execPath. Daemon resolution now substitutes that physical executable before generating launchd/systemd manifests or detached launches; the existing virtual-path guard still rejects any virtual path that reaches supervision. Source:apps/cli/src/lib/daemon.ts. - Fix:
agents teams,agents message, andagents profiles checkwork again on the signed standalone binary (regression from #315). Whenagentsresolves to the bun-compiled Mach-O (shipped since 1.20.53), three self-spawn sites relaunched the CLI as[process.execPath, process.argv[1], …]— but under a bun standalone executableprocess.argv[1]is the virtual entry/$bunfs/root/agents, so the child died withunknown command '/$bunfs/root/agents'(or/bin/sh: /$bunfs/root/agents: No such file or directory). Every teammate spawned by a compiled-binary install failed in 0s. New sharedgetAgentsInvocation(subArgs)(apps/cli/src/lib/daemon.ts) resolves the real on-disk binary — mapping the/$bunfs/root/…virtual path toprocess.execPath, running a.jsentry under node, and a native binary directly — andteams/agents.ts,commands/message.ts, andcommands/profiles.tsroute through it. Verified end-to-end: a teammate spawned by the freshly-compiled binary runs tocompletedwith no$bunfserror. Source:apps/cli/src/lib/daemon.ts(getAgentsInvocation),apps/cli/src/lib/teams/agents.ts,apps/cli/src/commands/{message,profiles}.ts.
1.20.55 — 2026-07-13
- Routine scheduler health is now observable and self-healing.
agents routines statusdistinguishesrunning,wedged, andstopped, and reports the daemon binary plus heartbeat age. Routine listing/status opportunistically finalize orphaned runs; PID reuse checks and a 24-hour wall-clock limit prevent stalerunningrecords; daemon startup rejects bun virtual paths and warns about worktree binaries that can disappear. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/lib/runner.ts,apps/cli/src/commands/routines.ts. - Built-in Open-Claude and OpenCode profiles.
agents profilesnow shipsopen-claudeandclaude-sparkfor Claude Code through OpenRouter, plusopencode,opencode-spark, andopencode-qwenpresets for the OpenCode harness. Source:apps/cli/src/lib/profiles-presets.ts,apps/cli/docs/profiles.md. - Fix: exiting a user split inside an
agents runtmux session reliably closes just that split (de-flakes CI #965). The guardedpane-diedhook's else-branch was a barekill-pane, which relies on the hook context supplying an implicit "current pane" — nondeterministic on a loaded detached server, so the dead split intermittently survived as a husk (the same failure the flakysession.test.tspane-died tests reproduced in CI). An intermediate externaltmux -S <socket>self-client still raced the server under Linux load. The else-branch now runsrun-shell -C "kill-pane -t #{hook_pane}", which format-expands the event pane and executes the targeted command inside tmux's own server queue. Interactive tmux-backed runs now require tmux 3.2+, the release that introducedrun-shell -C.AGENT_HOOK_SCHEMAbumps to 4; the daemon reconcile retrofits live sessions automatically and only stamps the marker after tmux accepts the hook, so a transient failure stays retryable. Source:apps/cli/src/lib/tmux/session.ts(agentPaneDiedHook,AGENT_HOOK_SCHEMA),apps/cli/src/lib/tmux/binary.ts,apps/cli/src/lib/exec.ts. agents devices syncpins the login user on Windows too.os.userInfo().usernamereturnsCOMPUTER\user/DOMAIN\useron Windows, which failed the safe-charset guard, so Windows boxes synced with no pinned user and--host <device>fell back to the wrong local account.sanitizeLoginUsernow strips the domain prefix to the bare ssh account before the guard. Also folds the duplicateuser@hostsplitter (parseTargetinssh.ts) into the canonicalsplitUserHostso there is one parser. Source:apps/cli/src/lib/devices/sync.ts,apps/cli/src/commands/ssh.ts.- Menu bar ACTIVE section now shows every local session, not just extension-registered terminals. The dropdown's session source was
live-terminals.json, which only carries terminals the Factory extension registers — a machine with 25 live sessions (tmux, ghostty, headless) renderedACTIVE · 1 running. The helper now feeds triage + ACTIVE fromagents sessions --active --local --json(the session engine's authoritative view, issue #741 contract) on the same warm-cache pattern as routines (30s TTL, refreshed off the click path; the cheap file still covers cold start and the 10s badge poll). Blocked sessions outside the extension's view now surface in NEEDS YOU too. Idle rows cap at 3 per repo group — the group header carries the true counts — so a big idle fleet can't wall the menu. Source:apps/cli/menubar/Sources/MenubarHelper/{StatusItemController,LocalState,AgentsCLI,Models}.swift. - Daemon service manifests pin JavaScript installs to the current Node runtime. launchd and systemd now invoke
process.execPath <entry> daemon _run, matching the detached launcher, instead of executing the JS entrypoint through#!/usr/bin/env node. Linux user services therefore stop falling back to an obsolete system Node (observed as Node 18 failing onnode:util.styleText) when the CLI was installed under Node 22/24. Nativeagentslaunchers remain direct executables. Source:apps/cli/src/lib/daemon.ts. - Routines support a
devices:allowlist so multiple machines each fire the same job independently. Routine YAMLs sync fleet-wide via the user repo, so without a restriction an enabled routine fires on every device running the scheduler. Adevices: [yosemite-s0, mac-mini]allowlist makes each listed machine run the job independently on schedule; omitting the field (or--clear) leaves the job unrestricted. A single-entry listdevices: [yosemite-s0]replaces the legacy singulardevice:pin — v12 migration converts any existingdevice: XYAML automatically todevices: [X]. All automatic paths (cron scheduler, webhook triggers, overdue/catchup, daemon nags, detached runner fires, one-shot--at) skip devices outside the allowlist; attempting to run a job on an ineligible host errors with the allowed device names and a ready-to-paste--hosthint.routines add --devices yosemite-s0,mac-minisets the list at creation (validated against the registered fleet);routines devices <name>opens a preselected multi-select picker;--set <csv>and--clearupdate it non-interactively and are mutually exclusive.routines listgains a Devices column;--jsongainsdevicesarray andrunsHere.--host <device>(alias:--device) routes anyroutinessubcommand to a remote machine over SSH. Source:apps/cli/src/lib/routines.ts,apps/cli/src/lib/scheduler.ts,apps/cli/src/lib/overdue.ts,apps/cli/src/lib/triggers/webhook.ts,apps/cli/src/lib/runner.ts,apps/cli/src/lib/hosts/passthrough.ts,apps/cli/src/lib/migrate.ts,apps/cli/src/commands/routines.ts. - Routines now default to
--mode autoinstead ofplan(RUSH-1595). A routine created without an explicitmodenow runs under the smart classifier (auto) rather than read-onlyplan, so unattended jobs can create PRs, write files, and run tests end-to-end without every user opting in —automaps to--permission-mode auto(claude), workspace-write + network (codex),--auto high(droid), and kimi's default headless run (which had no read-only mode and previously errored atplan). Opt down tomode: planfor read-only monitoring/reporting.JOB_DEFAULTS.mode, theagents routines add --modeflag default, and the file-add default all move toauto;writeJobnow omitsmodewhen it equalsauto. Source:apps/cli/src/lib/routines.ts,apps/cli/src/commands/routines.ts. agents publish— a self-hosted, zero-infrastructure skill registry that round-trips withagents search/agents install. Publish walks a git repo'sskills/directory, records a sha256 of everySKILL.md, and writes a flatskills-index.json(SkillIndexDocumentshape) at the repo root, then commits + pushes it and prints theraw.githubusercontent.comURL plus the exactagents registry add skill <name> <url>command to share. No hosted aggregator: the index is just a file in your GitHub repo, consumed directly by the existingfetchSkillIndex/searchSkillRegistriespath. Targets your~/.agentsrepo by default or an extra repo via--repo <alias>(--dry-runpreviews without pushing). Each index entry carriessha256, threaded throughSkillEntry/normalizeSkillEntry, andagents installnow verifies the freshly clonedSKILL.mdagainst it — a mismatch aborts with a clear error rather than trusting a tampered artifact. This is the self-hosted/git-index slice of #336; global no-URL discovery (a hosted aggregator) remains future work. Source:apps/cli/src/commands/packages.ts(publishsubcommand + install-time verify),apps/cli/src/lib/registry.ts(buildSkillIndex,verifySkillIntegrity,sha256OfFile,parseOwnerRepoFromRemote,SkillIndexEntry.sha256),apps/cli/src/lib/types.ts(SkillEntry.sha256). (#336)
1.20.54 — 2026-07-13
- Unified fleet target resolution for
agents ssh+sessions --host.agents sshnow accepts the full target grammar the fan-out already used — a registeredname, auser@device(same device, login user overridden, still dialed via its Tailscale route rather than raw LAN DNS), and an ad-hocuser@host/hostliteral — instead of only an exact device name (agents ssh muqsit@mac-minino longer errors "Unknown device"). A bare unregistered alias still reports "Unknown device".sessions --host user@devicenow resolves the host part through the registry too, so it stops silently diverging onto the non-Tailscale route. NewresolveDeviceTarget;resolveSshTargetshares one host-part matcher. Source:apps/cli/src/lib/devices/resolve-target.ts,apps/cli/src/commands/ssh.ts. agents sessions --hostsearches the peer's whole index, not its login cwd. A remote listing runs in the peer's SSH-login home dir and was silently cwd-scoped, sosessions --host <box>read as empty (No sessions found for /home/<user>) even when the box's index was full.--hostnow defaults to whole-index (--all) scope; an explicit path query /--project/--since/--agentfilter still narrows on top. It also runs the peer once, for itself (AGENTS_SESSIONS_LOCAL=1), so it no longer re-sweeps the fleet and prints a spurious<this-machine>: unreachable. Source:apps/cli/src/lib/session/remote.ts,apps/cli/src/commands/sessions.ts.agents devices syncpins each device's login user. Tailscale status carries a node's OS + address but not the account you ssh in as, so sync now materializes the local operator's username onto newly-synced devices (never clobbering a user you pinned). This makes--host <device>dial the same account no matter which machine launches the fan-out, instead of leaning on ssh's implicit local-username default. Source:apps/cli/src/lib/devices/sync.ts.
1.20.53 — 2026-07-13
agents add <agent>@latestresolves to a concrete version before installing (no install race).latest(likeoldest) is now resolved vianpm viewup front and installed as a pinned spec directly intoversions/<agent>/<version>/. Previouslylatestinstalled into a shared, well-knownversions/<agent>/latest/scratch dir and was renamed to the real version only after npm finished — so a concurrentagents viewreconcile (reconcileStaleLatestForAgent) or a secondlatestinstall could rename that dir out from under npm mid-extraction, corrupting the install withENOENTon the seededpackage.json. A concrete dir per version has no shared name to race on. Source:apps/cli/src/lib/versions.ts.Native memory sync preserves unmanaged Markdown files (RUSH-1621). Sync tracks managed fact names in
.agents-cli-memory.jsonand only deletes those; user-authored*.mdunder the agent memory dir survive. Source:apps/cli/src/lib/memory.ts.Feed high-consequence authz uses the canonical operator registry (RUSH-1618).
recordAnswerno longer looks foroperators.yamlunder the feed root; it resolves operators from~/.agents/vialoadOperators(). Source:apps/cli/src/lib/feed.ts.Menu bar groups worktree sessions under the real repo name (RUSH-1635). Paths under
.agents/worktrees/<slug>use the enclosing repository directory as the grouping key instead of the worktree slug. Source:apps/cli/menubar/.../LocalState.swift.PR outcome keys include repository identity (RUSH-1630). Full GitHub pull URLs normalize to
owner/repo#Nso two repos' PR #10 no longer collide underpr:#10. Source:apps/cli/src/lib/feed-outcome.ts.Urgent OpenClaw notifications use
--targetand--message(RUSH-1620).openclaw message sendrequires a destination and the--messageflag (not--text); without--targetthe send was invalid. Source:apps/cli/src/lib/notify.ts.High-consequence answers require env-proven operator identity (RUSH-1619).
agents message --as <id>alone is not verification;AGENTS_OPERATOR_IDmust match the claimed id (and the id must be inoperators.yaml). Source:apps/cli/src/lib/operator.ts,apps/cli/src/commands/message.ts.Hermes and ForgeCode are first-class install targets (RUSH-559).
AgentIdnow includeshermesandforge, with resource capability metadata for skills, rules, and MCP. MCP sync writes Hermesmcp_serversYAML in~/.hermes/config.yamland ForgeCodemcpServersJSON in~/.forge/.mcp.json, so registering these targets no longer requires their CLIs to be installed first. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/mcp.ts,apps/cli/src/lib/resources/mcp.ts.Cloud tasks can now report a resumable
idlestatus (RUSH-601). Provider-side stopped states such as Rushidle/paused/needs_review, Codexpaused/needs_review, and Antigravityidle/pausednormalize to canonicalidle; stream output,agents sessions, andagents cloud list/statusrender idle as an idle state instead of falling through to queued or unknown-status output. Source:apps/cli/src/lib/cloud/types.ts,apps/cli/src/lib/cloud/stream.ts,apps/cli/src/lib/session/active.ts,apps/cli/src/commands/cloud.ts.OpenCode plugin install only writes loader-visible direct
.ts/.jsfiles (RUSH-1617). Drop nested and.mjs/.cjsinstalls that OpenCode never scans; multi-module plugins flatten into~/.config/opencode/plugins/. Source:apps/cli/src/lib/plugins.ts.Routine credit-failover scans only the current attempt's log (RUSH-1616). Each failover spawn writes
stdout.attempt-N.logand rate-limit detection uses that file alone; prior attempts still append intostdout.logfor the continuous trail. Source:apps/cli/src/lib/runner.ts.Cursor hook sync drops stale managed entries when matcher/event change (RUSH-1615). GC keys managed hooks by
event|command|matcherinstead of command path alone, so a matcher or event edit no longer leaves dead entries. Source:apps/cli/src/lib/hooks.ts.Stop advertising Goose SubagentStart/SubagentStop hooks (RUSH-1613). Goose does not emit those events; drop them from
GOOSE_EVENT_MAPso sync no longer installs dead entries. Source:apps/cli/src/lib/hooks.ts.Mailbox delivery receipts are monotonic (RUSH-1614).
recordMessageReceiptno longer lets a latequeuedwrite overwrite an already-recordedconsumed/continuedwhen enqueue races the drain. Source:apps/cli/src/lib/feed.ts.Per-session rate-limit detection + feed badge (RUSH-1523). The session state engine flags rate/usage-limit text in the transcript (
detectRateLimited);ActiveSession.rateLimitedflows through remote fan-out into Factory'sFloorAgent.rateLimited, which renders a rate limited pill on the feed card. Source:apps/cli/src/lib/session/state.ts,apps/factory/.../floorAdapter.ts,FeedItem.tsx.Kiro launches with
--v3so standalone hooks actually fire (RUSH-1612). Agents-cli writes Kiro hooks as v3 standalone files under~/.kiro/hooks/*.json, but those only load on the v3 engine.AGENT_COMMANDS.kiro.basenow includes--v3soagents run kiroopts into the engine that reads them. Source:apps/cli/src/lib/exec.ts.Ask classifier + stall suppression for the agent feed (RUSH-1477). Every open block is classified as Decision / Approval / Clarification / Stall / Fyi. Workflow-stalls ("should I…?", "what's next?", "looks good?") are auto-answered and removed so they never render as cards; Decisions and Approvals still surface.
agents feedreports a digest (N stalls auto-resolved by policy);--allshows suppressed items;--jsonstamps each block with itsaskclassification. Agent-taggedblockClass: decisionis never auto-suppressed. Source:apps/cli/src/lib/ask-classifier.ts,apps/cli/src/commands/feed.ts.Parked-agent answer router: PTY-select / resume / mailbox by runtime (RUSH-1474).
agents messageno longer always enqueues to the mailbox. When the target is parked on an open feed question, delivery routes by runtime: tmux/iterm/pty rails get keystrokes that select the matching option label (or free-text via Other); headless parked runs resume viaagents run --resume <id> -- <answer>; running agents still use the mailbox. Wrong-state delivery is refused with a clear error instead of silently rotting in the spool. Source:apps/cli/src/lib/answer-router.ts,apps/cli/src/commands/message.ts.Wire subagents support for OpenCode. OpenCode loads agent markdown from
~/.config/opencode/agents/with frontmattermode: subagent. Flipsubagents: true, addtransformSubagentForOpenCode, wire writer/detector/list/diff/remove. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts. (RUSH-1386)agents feedgroups by outcome (ticket/PR/worktree), not by agent (RUSH-1479). The default human view collapses open blocks under the deliverable they serve — Linear ticket first, then PR, then worktree/epic, then a shared Unassigned bucket — so a 1,100-agent fleet reads as dozens of initiatives (RUSH-1125 · 4 agents · 1 needs you). Each block is attributed to exactly one outcome; live session meta fills missing ticket/PR/worktree at list time;--flatrestores the per-agent list;--jsonstamps each block with itsoutcomeref. Factory Floor's Group control gains an Outcome axis (the new default). Source:apps/cli/src/lib/feed-outcome.ts,apps/cli/src/commands/feed.ts,apps/factory/ui/settings/components/mission-control/floorModel.ts.Menu bar dropdown redesigned around triage: attention floats up, context groups down. The dropdown used to stack ~11 flat sections at equal weight, so a session waiting on you sat as loud as setup noise. Now: a ⚠ NEEDS YOU strip on top, sorted by wait-time across all projects (most-stalled first), each row carrying the actual question the session is waiting on plus how long it's waited (
Claude · agents-cli — Claude needs your permission to use Bash · 2h 25m); live work grouped by repo below (ACTIVE · <repo>headers, rich rows show the session's own title inline); ROUTINES expanded into a glanceable section (next few upcoming + any failing routine inline,All routines…for the rest); RECENT TICKETS and RECENT stay dedicated sections; Setup + Auto-nudge collapse into one System row (submenu keeps the doctor items and the auto-nudge toggle). A density toggle in the footer cycles Auto → Rich → Compact — compact folds rows to one-liners and tucks Recent behind a submenu; Auto (default) is rich while something needs you, compact on a calm machine (menubarDensityin UserDefaults,MENUBAR_DENSITYenv override for dump probes). The question text + wait-time come from the attention sentinel: the Notification hook now writes the notification message as the sentinel content (phnx-labs/.agents-system#74), and the helper reads content + mtime (LocalState.attentionMarks); an empty sentinel still renders as "awaiting input".Sessiongainedtitle/question/attentionSinceMs; terminal rows group by working-dir name and carry the live-terminal label as the title. Source:apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift,apps/cli/menubar/Sources/MenubarHelper/LocalState.swift.Wire allowlist support for OpenCode. OpenCode stores per-tool allow/ask/deny rules in
opencode.json/opencode.jsoncunderpermission(bash patterns etc.; present since ~1.1.1). Flipallowlist: { since: '1.1.1' }so the existingconvertToOpenCodeFormat/applyPermissionsToVersion/ detector path actually runs. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/permissions.ts. (RUSH-1385)Wire subagents support for Grok CLI. Grok discovers agent definitions as Claude-compatible
.mdfiles under~/.grok/agents/(docs: user-guide/16-subagents.md). Flipsubagents: true, reuse the Claude flatten transform for install/writer paths, and register a detector plus list/diff/remove. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/writers/subagents.ts,apps/cli/src/lib/staleness/detectors/subagents.ts. (RUSH-1384)Wire subagents support for Kimi CLI. Kimi Code loads custom agents as YAML under
~/.kimi-code/agents/*.yamlwith a sibling*.system.mdreferenced viasystem_prompt_path(Kimi has no inlinesystem_promptfield) and a managed parent_agents-cli.yamlthat declaresagent.subagentsfor--agent-file. Flipsubagents: true, addtransformSubagentForKimi/writeKimiSubagentFiles, list/diff/remove paths, and wire the subagents writer + detector (underscore-prefixed parent excluded from the installed name list). Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/writers/subagents.ts,apps/cli/src/lib/staleness/detectors/subagents.ts. (RUSH-1383)Grok + Antigravity now register dir-form subrule hooks. The hooks writer excluded
grokfromregisterHooksToSettings, so subrule-bundled guards (absolute paths outside the central hooks copy set) never reached~/.grok/hooks/hooks.json. Grok is now in the gate. Antigravity entries carrymatcherso guards are tool-scoped. Source:apps/cli/src/lib/staleness/writers/hooks.ts,apps/cli/src/lib/hooks.ts. (RUSH-1353)Menu-bar Quick Dispatch can now pick agents and fan out autonomous fixes from the screenshot panel.
Cmd-Shift-Ostill supports filing one Linear ticket, but the panel now has a File Ticket / Fix mode control plus a roster picker sourced from the menu-bar agent list. File Ticket runs the selected ticket agent; Fix dispatches every selected agent withagents run <agent> --mode auto --name quick-<agent>-<timestamp>, carrying the typed note and selected screenshots into a repo-discovery prompt so those runs surface in normal session/tray views instead of hidden background work.AGENTS_QUICK_DISPATCH_ROSTER=claude,codexfilters visible agents andAGENTS_QUICK_DISPATCH_AGENTS=claude,codexpreselects them. Source:apps/cli/menubar/Sources/MenubarHelper/{LocalState,PromptPanel,AgentsCLI,IssueSelfTest}.swift,apps/cli/docs/menubar.md. (RUSH-1416)Menu-bar Quick Dispatch keeps immediate typing in the capture field. The
Cmd-Shift-Oquick-capture panel now uses an activating borderless window, orders it front, and waits briefly for the field editor to become ready before returning from summon, so notes typed immediately after summon no longer lose their leading characters to the previously focused app. Source:apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift. (RUSH-1591)Internal: consolidated the drifted provider status-normalizers and git-root helpers (#753). The three copies of
mapStatusincloud/{rush,codex,antigravity}.ts— which had drifted (different vocabularies and defaults) — collapse into one exportednormalizeProviderStatus(provider, wireStatus)incloud/types.ts, with provider-specific defaults still explicit (rush defaultrunning, codex defaultrunning, antigravityundefined-safe defaultcompleted); factory's structurally-differentmapResultStatusis left in place.getGitRootmoves tolib/git.tsandcommands/worktree.tsnow calls it instead of a privategitRootcopy; the two divergentisGitRepovariants (synchronous root-only ingit.tsvs async worktree-correct inteams/worktree.ts) are documented and deliberately not merged. Source:apps/cli/src/lib/cloud/{types,rush,codex,antigravity}.ts,apps/cli/src/lib/git.ts,apps/cli/src/lib/teams/worktree.ts,apps/cli/src/commands/worktree.ts. (#753)macOS installs now run a Developer-ID-signed + notarized
agentsbinary — the primary fix for EDR (CrowdStrike Falcon) blocking the CLI. The resolvedagentson macOS was a node-shebang JS file in a user-writable path; unsigned and spawned by editor/Electron children, it matched Falcon's post-exploitation profile and the sessions feature silently died on EDR-enabled Macs (mitigation 1 of #315; the behavioral hardening, mitigations 2-4, shipped earlier). Releases now build a standalone arm64 Mach-O withbun build --compile(scripts/build-bin.sh), sign it with Developer ID + hardened runtime + the JIT entitlement bun's JavaScriptCore needs under the hardened runtime (scripts/sign-cli-binary.sh,scripts/bun-jit-entitlements.plist), notarize it withnotarytool, and ship it in the npm tarball atdist/bin/agents.postinstallpoints the alias shims and the~/.local/bin/agents/aglinks at the signed binary — with a run-probe that falls back loudly to the JS entrypoint if the binary is missing, wrong-arch, or blocked — and repoints links an earlier install left at the JS shim. Aprepackgate (scripts/verify-cli-binary.sh) refuses to pack unless the binary matches its sign-run sha pin, embeds the release version, and (on macOS) passescodesign --verifywith a Developer ID authority. Linux-driven releases build + sign the binary on the mac sign host viascripts/remote-sign-mac.sh. Intel Macs and non-mac platforms keep the JS entrypoint. (#315)Codex multi-file apply_patch now surfaces every path.
parseCodexused.match()on the patch body so only the first*** Update/Add/Delete File:path became a tool_use; files 2+ were invisible to artifact discovery. NowapplyPatchTargetPathsusesmatchAlland emits one Edit event per file. Source:apps/cli/src/lib/session/parse.ts. (RUSH-1410)memoryis a first-class top-level resource (distinct fromrules).agents memory list|add|remove|view|syncmanages portable knowledge facts under~/.agents/memory/(project > user > system;MEMORY.mdindex + one<slug>.mdper fact). The legacyagents memory→rulestombstone is gone. Capable agents (claude, codex, openclaw, grok) get facts fanned into version homes onsyncResourcesToVersion/agents memory sync. Plugins can ship amemory/dir (surfaced inpluginResourceGroups). Note: internalResourceSelection.memorystill means the composed rules file — rename torulesis a follow-up. Source:apps/cli/src/lib/memory.ts,apps/cli/src/commands/memory.ts,apps/cli/src/lib/resources/memory.ts,apps/cli/src/lib/versions.ts,apps/cli/src/lib/plugins.ts. (RUSH-1330)agents sessions --active --jsonnow carriestokPerSec, andagents sessions --roots --jsonemits the session-scan directories — one CLI contract the Factory extension consumes instead of re-implementing (issue #741). Every active row gainedtokPerSec: live output-token throughput over a rolling 60s window from the transcript tail (Claude assistantoutput_tokens; Codextoken_countoutput + reasoning; Gemini output + thoughts), absent when the session is idle or its format reports no usage. The newagents sessions --roots --jsonprints, per on-disk agent, the exact directories the CLI scans for transcripts (every version home + backup mirror), so an external watcher (the Factory Floor'sfs.watch) tracks the same paths the CLI does instead of hardcoding~/.claude|.codex|.gemini— add an on-disk agent to discovery and every consumer watches it automatically. The throughput math and the roots list are now the single source of truth; the extension used to keep parallel copies. Source:apps/cli/src/lib/session/throughput.ts(computeTokPerSec),apps/cli/src/lib/session/active.ts(ActiveSession.tokPerSec,computeLiveSignals),apps/cli/src/lib/session/tail.ts(readSessionTailWithRaw),apps/cli/src/lib/session/discover.ts(getSessionRoots),apps/cli/src/commands/sessions.ts(--roots).Fix:
agents sessions --activeno longer attaches stale Codex transcripts to live processes. Codex session lookup now sorts by indexedlast_activityand refuses to borrow a transcript outside an explicit 24-hour freshness bound, so a desktop app service or unrelated long-lived process cannot light up a months-old session asrunning. Source:apps/cli/src/lib/session/active.ts,apps/cli/src/lib/session/db.ts. (RUSH-1489)Startup shim self-heal stays silent by default, with
--verbosediagnostics on stderr. The unified shim/shadow/PATH repair path no longer pollutes command stdout, includingagents sessions --active --json;agents --verbose <cmd>now prints a concise startup self-heal summary to stderr for debugging. Source:apps/cli/src/index.ts,apps/cli/src/lib/shim-heal.ts. (RUSH-1533)Wire subagents support for Codex CLI. Codex custom agents are standalone TOML under
~/.codex/agents/*.toml(required:name,description,developer_instructions; multi-agent plumbing since 0.117.0). Flipsubagents: { since: '0.117.0' }, addtransformSubagentForCodex, and wire the subagents writer + install/remove paths. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/subagents.ts,apps/cli/src/lib/staleness/writers/subagents.ts. (RUSH-1382)Routines use the same version/account selection and credit failover as
agents run. Scheduled jobs used to spawn a bareclaude/codexname under a sandbox HOME, which could surface asagents: no version of claude configuredeven when installs existed, and never walked past a credit-exhausted default. The runner now resolves a healthy install via the configured run strategy (defaultbalanced), pins the absolute binary, injects per-version config dirs + the daemon'sCLAUDE_CODE_OAUTH_TOKENinto sandboxed spawns, and on foregroundagents routines runre-dispatches to the next healthy same-agent account when a mid-run rate/usage limit is detected (daemon detached fires use the pre-flight pick only). Diagnostic lines log the pick, skipped accounts, and each failover hop. Source:apps/cli/src/lib/runner.ts(resolveRoutineLaunch,pinJobBinary,buildRoutineSpawnEnv),apps/cli/src/lib/sandbox.ts(OAuth allowlist),apps/cli/src/commands/routines.ts(help). (RUSH-1016)Wire plugin support for Goose. Goose loads Open Plugins from
$HOME/.agents/plugins/<name>/(same layout as agents-cli). Flipplugins: trueand copy each selected plugin into the version home under.agents/plugins/. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/plugins.ts(installGoosePlugin). (RUSH-1339)Wire plugin support for Cursor CLI. Cursor agent plugins use
.cursor-plugin/plugin.json(re-enabled 2026-05). Flipplugins: true, setpluginManifestDir: '.cursor-plugin', and reuse the centralized marketplace mirror path under~/.cursor/plugins/. Source:apps/cli/src/lib/agents.ts. (RUSH-1338)Wire plugin support for OpenCode. OpenCode loads JS/TS plugin modules from
$HOME/.config/opencode/plugins/(not Claude marketplace layout). Flipplugins: true, install modules from a plugin'sopencode/orplugins/dir (or root) into the version home, and track install/remove viaisPluginSynced/removePluginFromVersion. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/plugins.ts(installOpenCodePlugin,openCodePluginsDir). (RUSH-1336)Wire hooks support for Cursor CLI. Cursor agent CLI (
cursor-agent) gained lifecycle hooks on 2026-01-16 (~/.cursor/hooks.json,{ "version": 1, "hooks": {…} }). Flip the capability, map canonical events to Cursor camelCase (SessionStart→sessionStart,UserPromptSubmit→beforeSubmitPrompt,Stop→stop, …), and merge managed entries intohooks.jsonwhile preserving user-authored commands outside managed prefixes. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/hooks.ts(registerHooksForCursor,CURSOR_EVENT_MAP),apps/cli/src/lib/staleness/writers/hooks.ts. (RUSH-1326)Wire hooks support for Goose. Goose (
block-goose-cli≥ 1.34.0) auto-discovers Open Plugins hooks at$HOME/.agents/plugins/<name>/hooks/hooks.json. FlipsupportsHooksand gatehooks: { since: '1.34.0' }, write a managed plugin (agents-cli-hooks) under the version home with Claude-shaped event groups, and leave user plugins alone. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/hooks.ts(registerHooksForGoose,GOOSE_EVENT_MAP),apps/cli/src/lib/staleness/writers/hooks.ts. (RUSH-1325)Wire hooks support for Kiro CLI. Kiro CLI v3 stores standalone hooks under
~/.kiro/hooks/*.json({ "version": "v1", "hooks": [...] }with command/agent actions); PreToolUse/PostToolUse firing was fixed in kiro-cli 0.10. FlipsupportsHooksand gatehooks: { since: '0.10.0' }, map canonical events to Kiro PascalCase triggers, and write a single managedagents-cli-hooks.jsonon every sync (user-authored sibling files untouched). Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/hooks.ts(registerHooksForKiro,KIRO_EVENT_MAP),apps/cli/src/lib/staleness/writers/hooks.ts. (RUSH-1324)Wire hooks support for GitHub Copilot CLI. Copilot GA (
@github/copilot≥ 1.x) ships a real hooks system (~/.copilot/hooks/*.json, schema{ "version": 1, "hooks": {…} }), but agents-cli still declaredhooks: falseso nothing ever installed. Flip the capability, map canonical events to Copilot camelCase (SessionStart→sessionStart,PreToolUse→preToolUse,Stop→agentStop, …), and write a single managed file (agents-cli-hooks.json) on every sync so GC is a rewrite and user-authored sibling JSON files are never touched. Source:apps/cli/src/lib/agents.ts,apps/cli/src/lib/hooks.ts(registerHooksForCopilot,COPILOT_EVENT_MAP),apps/cli/src/lib/staleness/writers/hooks.ts. (RUSH-1323)agents run --leasenow shows live progress instead of a 30-90s blank terminal. After the runtime picker + consent, the lease used to go completely silent while the box provisioned (crabboxWarmupwas a blockingspawnSync, so nothing could animate), then dump crabbox's raw sync/bootstrap log interleaved with the agent's output. Now a spinner animates each previously-silent phase —Leasing a hetzner box… (Ns)→✔ Box <slug> ready (<ip>) · Ns→Setting up box — <latest step>→✔ Box provisioned — <agent> output:— and the agent's own output then prints verbatim. The box bootstrap echoes a marker (LEASE_AGENT_MARKER) right before the agent run;createLeaseOutputRoutersplits the crabbox stream on it so setup noise feeds the spinner while the agent output streams clean, and a setup failure dumps the captured setup log so errors are never swallowed.crabboxWarmupis now async so the event loop stays free to animate. The spinner is a purpose-builtcreateSpinner(NOTora): ora hooksstream.writeand re-renders on every external write while spinning, which ballooned to multi-GB output when a lease streamed past a live spinner —createSpinnerwrites exactly one short line per fixed tick and nowhere else (a unit test asserts 100kupdate()calls produce zero writes), and on a non-TTY prints each phase label once and stays silent on update (no piped/CI flood). Verified live on Hetzner: animated warmup counter, cleanLOGIN_OK, box auto-destroyed, 43KB total output. Source:apps/cli/src/lib/crabbox/progress.ts(createSpinner,createLeaseOutputRouter,LEASE_AGENT_MARKER),apps/cli/src/lib/crabbox/{lease,cli}.ts,apps/cli/src/commands/exec.ts.Fix: Grok hooks now respect their
matcherand no longer run twice per event. The Grok hook registrar (registerHooksForGrok) dropped the manifestmatcherwhen writing~/.grok/hooks/, so a matcher-scoped hook fired on every tool call — the plan-presentation hook (meant forExitPlanModeonly) hard-blocked whole Grok sessions (exit 2 = explicit deny). It also double-registered every hook into BOTHhooks.jsonAND a per-event file (pretooluse.json, …); Grok merges all of~/.grok/hooks/*.json, so each hook ran twice. Now the registrar emitsmatcheronly for the events Grok accepts it on (PreToolUse,PostToolUse,Notification— lifecycle events reject it), groups hooks one-per-distinct-matcher like the Claude writer, translates tool names Grok doesn't auto-alias (ExitPlanMode→ExitPlanMode|exit_plan_mode, since Grok's plan tool isexit_plan_mode), and writes a singlehooks.json, pruning the stale per-event files an older build left behind so already-synced installs stop double-running. Source:apps/cli/src/lib/hooks.ts(registerHooksForGrok,isManagedGrokHookFile,GROK_MATCHER_EVENTS,GROK_MATCHER_ALIASES).Agent feed replies now have delivery confirmation and atomic first-answer-wins closure (RUSH-1476).
agents messageties a local reply to the agent's current open feed block; if any surface already answered that block, the second attempt is rejected with the surface that won. The feed block records queued → consumed → continued receipts:queuedwhenagents messageenqueues,consumedwhen the mailbox drain archives the message, andcontinuedonce the agent continues past the block. A human answer typed directly in the terminal is reconciled via theUserPromptSubmithook, which records a terminal answer and removes the visible block within one feed poll cycle. Source:apps/cli/src/lib/feed.ts(recordAnswer,recordMessageReceipt,recordContinued, answered-marker lifecycle),apps/cli/src/lib/mailbox.ts(blockIdonMailboxMessage, consumed-receipt surfacing),apps/cli/src/commands/message.ts(open-block lookup +--surface),apps/cli/src/commands/feed.ts(receipt rendering).24/7 multi-operator controls for agent feed (RUSH-1480). Blocks can now carry
blockClass(approval/decision),consequence(normal/high),allowedOperators,timeoutMinutes,safeDefault, andcostOfDelay; the feed-publish hook captures these from the agent'sAskUserQuestiontool_input. High-consequence answers require a verified operator id known to~/.agents/operators.yaml(agents message --as <operator>).agents feed --dispatchevaluates default-on-no-answer policy loaded from~/.agents/feed-policy.yaml: approval blocks resolve to their safe default after the timeout, decision blocks are hard-parked. Urgent blocks (costOfDelayat or above the phone threshold) are paged once via the OpenClaw Telegram gateway (openclaw message send --channel telegram --account default). Source:apps/cli/src/lib/operator.ts,apps/cli/src/lib/feed-policy.ts,apps/cli/src/lib/notify.ts,apps/cli/src/lib/feed.ts(block metadata + authz + parked/defaulted/notified timestamps),apps/cli/src/commands/message.ts(--as),apps/cli/src/commands/feed.ts(--dispatch, metadata rendering).Mailbox TTL + liveness/GC so messages never rot in dead-agent inboxes (RUSH-1475).
enqueueaccepts an optionalttlSeconds; expired messages are archived toconsumed/withdropped: expiredinstead of being returned bydrain/peek.agents feed --dispatchruns a liveness sweep againstgetActiveSessions(): boxes whose owning agent is no longer alive are treated as dead, their pending messages are archived withdropped: dead, and any feed block tied to that mailbox is removed so the operator never answers a ghost. Oldconsumed/entries are pruned after 24 hours by default. Source:apps/cli/src/lib/mailbox.ts(expiresAt,isExpired,sweepExpired),apps/cli/src/lib/mailbox-gc.ts,apps/cli/src/commands/feed.ts(--dispatchliveness sweep).Keychain service names are no longer silently enumerable — items are stored under opaque HMAC-hashed names (macOS). The helper's
listnever decrypts and never prompts (that's what keepsagents secrets listsnappy), which meant the service names themselves —agents-cli.secrets.<bundle>.<KEY>,agents-cli.bundles.<name>,agents-cli.<provider>.token— were readable metadata: any same-user process could silently inventory your bundles, keys, and providers before ever popping Touch ID. Everyagents-cli.*item now lives under an opaque name (agents-cli.h.<ns>.mfor bundle metadata,agents-cli.h.<ns>.k.<kh>for values,agents-cli.h.o.<ih>otherwise) keyed by a per-machine random HMAC key (agents-cli.hmackey, no-ACL so silent operations stay silent). The structured shape keeps a bundle's items under one hashed prefix, sosecrets exec/run --secretsstill resolve metadata + all values behind a single Touch ID. A one-time re-key migrates existing items — automatically on the first interactive keychain use, or via the newagents secrets rekey(--statusto inspect, exit 4 on a cancelled Touch ID; re-running resumes). It is crash-safe end to end: values are batch-read once, hashed copies are written and verified before ANY original is deleted, activation is all-or-nothing, and an interrupted delete phase is finished silently by the next run. A--prefix-restricted (partial) run resolves each value item's bundle tier directly from the keychain — scoping anever-policy bundle's value items without its metadata item keeps the silent no-ACL tier intact. The signed Swift helper is untouched (it already treats service names as opaque), so no re-notarization or sha re-pin. Caveat: an older agents-cli on the same machine writes/reads cleartext names and won't see re-keyed items — keep all installs current. Source:apps/cli/src/lib/secrets/index.ts(hashedServiceName,rekeyServiceNames),apps/cli/src/lib/secrets/bundles.ts,apps/cli/src/commands/secrets-migrate.ts,apps/cli/docs/secrets.md. (GitHub #316, Finding 1)
1.20.52 — 2026-07-12
- Re-pinned the keychain helper to a freshly notarized build carrying the new iCloud verbs. The #904 session changed
keychain-helper.swift(legacy iCloud/synchronizable item verbs) and re-pinned the sha, but the notarized binary it pinned lived only in that session's since-removed worktree — the release prepack gate (verify-keychain-helper.sh) then failed on every machine (SHA256 mismatch) because no reachablebin/Agents CLI.appmatched the pin. Rebuilt the helper from current source on the release machine (universal, signed, notarized: GatekeeperNotarized Developer ID, submission accepted) and pinned that binary. Same class of fix as #835. - Fix: version-skewed CLI invocations no longer wipe the secrets-agent's hot cache — the recurring Touch ID storm on version-churning machines is closed.
ensureAgentRunningtore down a reachable broker whenever the broker's running version differed from the caller's on-disk version — unconditionally, vialaunchctl kickstart -k, wiping every unlocked bundle. On a machine where installed versions churn (dev builds stamp a fresh0.0.0-dev.<sha>per install; an npm copy and a dev copy invoke in turn), that meant constant wipes, and the next read of each held bundle popped a fresh Touch ID prompt — the exact storm #435 fixed on the server side, reintroduced client-side. The client now accepts a protocol-compatible, version-skewed broker while it holds real unlocks (shouldTeardownVersionSkewedBroker); the broker's own sweep still adopts new code at the next quiet moment (store empty), so upgrades land without ever costing a re-prompt. Source:apps/cli/src/lib/secrets/agent.ts(ensureAgentRunning,shouldTeardownVersionSkewedBroker). - Fix: mutating a bundle now evicts the broker-held copy —
rotateno longer serves the old secret for up to 7 days. Onlysecrets policyinvalidated the secrets-agent after a write;add,rotate,remove,rename,delete, andimportupdated the keychain while a broker-held snapshot kept serving the pre-write values for the rest of the ~7d hold — a rotated credential silently kept injecting the OLD value into every run.writeBundleanddeleteBundlenow evict the bundle from the broker after every mutating write (agentEvictSync, a synchronous socket client mirroring the read fast-path), so the next read re-resolves fresh from the keychain (one prompt) and re-caches. The usage-telemetry stamp (stampLastUsed, fired on every broker HIT) opts out — evicting there would make the cache destroy itself on first use — and the eviction honorsAGENTS_SECRETS_NO_AGENTplus the test-backend override so suites never evict a user's real unlocks (shouldEvictAfterBundleWrite). Thepolicycommand's bespoke eviction is superseded by the chokepoint. Source:apps/cli/src/lib/secrets/bundles.ts(writeBundle,deleteBundle,shouldEvictAfterBundleWrite),apps/cli/src/lib/secrets/agent.ts(agentEvictSync),apps/cli/src/commands/secrets.ts. agents secrets importgains a unified--from <source>and recovers bundles stranded in the iCloud Keychain. One axis for every source: a .env path (-reads stdin),1password:<vault>(the boolean--from-1password --vault <name>pair still works as a hidden deprecated alias), and the newicloud. Bundles created in the pre-biometry era were synced via iCloud Keychain; the device-local cutover pinned every query tokSecAttrSynchronizable: false, which orphaned those items — visible in Keychain Access under iCloud, invisible tosecrets listandmigrate-acl.agents secrets import --from iclouddiscovers them (bundle metadata and bare per-key secret items whose metadata never synced), offers an interactive multi-select (or takes an explicit bundle name for non-interactive use), re-imports them as normal device-local biometry-gated bundles, and with--purgedeletes only the iCloud copies whose value provably lives locally — imported this run or already present in the local bundle; an unreadable item, a key the modern store refuses by policy (reserved/loader env names the pre-cutover store accepted — reported asreserved, not importableinstead of aborting the bundle), and the metadata item of a partially-recovered bundle all survive.secrets view <name>on a missing bundle now points at the recovery command when an iCloud copy of that name exists. Three new keychain-helper verbs (list-synced,get-batch-synced,delete-synced) match synchronizable items exclusively, so the live device-local store is untouchable from the recovery path. Source:apps/cli/src/lib/secrets/icloud-import.ts,apps/cli/src/lib/secrets/keychain-helper.swift,apps/cli/src/commands/secrets.ts(parseImportSource).agents feedsurfaces every top-level agent block that is waiting on the user across the fleet.AskUserQuestionand waitingNotificationhooks publish atomic open-block records with the full question/options or notification message, mailbox/session identity, host, and runtime; answer/resume/stop hooks remove resolved blocks, and Task subagents are gated out so internal questions do not flood the operator view. A bareagents feedmerges local blocks with every registered online device in parallel,--host/--devicescopes the fleet,--localskips SSH, and--jsonreturns the same merged view. Runtime-managed hooks install from the CLI-writable user layer without dirtying the auto-pulled system repository. Source:apps/cli/src/commands/feed.ts,apps/cli/src/lib/feed.ts,apps/cli/src/lib/remote-agents-json.ts, runtime labelling inapps/cli/src/lib/exec.tsandapps/cli/src/lib/teams/agents.ts. (RUSH-1473)- Browser-profile credentials: account identity,
secrets get <bundle> <KEY>, andbrowser type --secretfor leak-free login.agents browser profiles loginsnow shows, per profile, the account signed into each live service (plaintext username from ChromiumLogin Data— never decrypts the encrypted password) and whether login creds are declared in the profile's secrets bundle (columnsSERVICE | ACCOUNT | CREDS);profiles showgains aLogins:block.agents secrets get <bundle> <KEY>prints one resolved value from a bundle (arg-count overload of the existing rawget <item>; ungated like it, and thesecrets.getaudit event fires inside the resolver).agents browser type <ref> --secret <bundle>/<KEY>resolves a credential in-process and types it into the page — the value never crosses stdout or the agent transcript — so an agent can drive a login by composingprofiles logins→browser start <loginUrl>→refs→type --secret→screenshot, handling 2FA/selectors itself (no fragile CLI auto-login engine; Google/X block automation anyway). A profile's--secretsbundle is the credential store, keyed by the<PREFIX>_USERNAME/<PREFIX>_PASSWORDconvention (per-service prefixes inAUTH_SIGNATURES);profiles create --secretsnow warns if the bundle doesn't exist yet. Cookie-persistence-first remains the headline (thebrowserskill's credential guidance was corrected — the bundle only injected env vars into the browser process before, inert for web login). Source:apps/cli/src/lib/browser/login-detection.ts,apps/cli/src/lib/browser/secret-ref.ts,apps/cli/src/commands/browser.ts,apps/cli/src/commands/secrets.ts. agents logs audit/agents logs stats/agents logs rotate— user-facing audit trail viewer. The append-only local event log (~/.agents/events.jsonl) is now a first-class audit surface.agents logs auditqueries events with filters (--module,--command,--event,--agent,--caller,--level,--since,--limit,--json) and--followfor live tailing;agents logs statsshows aggregate breakdowns by level, event type, module, and user;agents logs rotateprunes old numbered archives (--days, default 7). Events carrylevel(audit/warn/info/debug) and an environment-derivedcaller(claude-code, Factory agent kind, terminal, or script). Sensitive flag values, secret-shaped payload fields, token-like strings, and raw prompts are redacted before append. Security-relevant operations (secrets, teams lifecycle, cloud dispatch) auto-classify asaudit. At 10 MB the active file rotates losslessly throughevents.1.jsonl.gz,events.2.jsonl.gz, and so on;query()reads every archive transparently. New instrumentation incloud.ts,factory.ts,teams.ts,secrets.ts,mcp.ts, androtate.ts. Source:apps/cli/src/lib/events.ts,apps/cli/src/commands/logs.ts, instrumentation call sites. (RUSH-460)agents run claude --leasenow runs the box logged-in — the Claude OAuth token ships alongside the config. The lease copied~/.claude.json(config/account-metadata) but never the OAuth token, so Claude booted "Not logged in" on every leased box. The token lives in the macOS Keychain (hash-suffixed service for an agents-cli managed home, bare for a default install) and on Linux at~/.claude/.credentials.json.resolveClaudeCredentialsBlob()now reads the raw wrapped Keychain payload silently (/usr/bin/security … -w— Claude's item trusts it, no Touch ID): bare service first, then enumerate installed version homes, preferring the account whose email matches the copied config; off-darwin it reuses the existing.credentials.jsonfile branch.buildCredentialScriptwrites that blob to~/.claude/.credentials.json(0600) via the same quoted-heredoc that carries every other cred (the box's~/.claudeis a symlink into the versioned home, so it lands exactly where the shim'sCLAUDE_CONFIG_DIRreads it); it is shredded after the run regardless of--keep-box. Resolved in the command layer after the existing per-run consent prompt, whose text now names the token explicitly. Scope is Claude-only — Codex/Grok already ship their token in the copied auth file. Verified live on Hetzner: a leased box ranagents run claudeand returned a real model reply (LOGIN_OK, exit 0), and the token file was absent (shredded) afterward. Source:apps/cli/src/lib/crabbox/runtimes.ts(resolveClaudeCredentialsBlob,buildCredentialScript),apps/cli/src/lib/crabbox/lease.ts,apps/cli/src/commands/exec.ts.agents browser profiles set-default <name>picks the profile a bareagents browser startuses — so agents stop opening a logged-out Chrome. With no--profile,startused to auto-detect the first installed Chromium-family browser (Chrome first on macOS) and save it asdefault, ignoring a profile you'd actually logged into. Nowstartresolves in order: (1) your configured default, (2) an existingdefaultprofile, (3) auto-detect. The configured default ALSO re-points an explicit--profile default, so an agent that hardcodesdefaultstill lands on your chosen profile. The setting is device-local — stored in~/.agents/devices/<machine>/agents.yaml, never synced to other machines (the target profile may hold machine-local logins).profiles list/showmark it;set-default --unsetreverts to auto-detect; a missing target warns and falls back rather than hard-failing. Source:apps/cli/src/lib/browser/profiles.ts(ensureDefaultBrowserProfile,getConfiguredDefaultProfileName),apps/cli/src/lib/state.ts(writeMetaUnlocked,overlayMachineLocal),apps/cli/src/lib/types.ts(Meta.defaultBrowserProfile),apps/cli/src/commands/browser.ts.agents browsernow warns when a task opens a login-gated site on a logged-out profile — grounded in real session state. Newapps/cli/src/lib/browser/login-detection.tsreads a profile's Chromium cookie store (presence only — never decrypts the Keychain-encrypted values, and filters expiry in SQL so Chromium's >2^53 microsecond timestamps never tripnode:sqlite's integer range) to tell which login-gated services (LinkedIn, Google, X, GitHub, Reddit) have a live session.agents browser start --url <login-gated>prints a stderr hint likeprofile "default" has no linkedin.com session. logged in elsewhere: comet-local. try: --profile comet-localwhen the chosen profile is logged out; it never blocks or slows start.agents browser profiles loginsshows a profile-by-service table. Source:apps/cli/src/lib/browser/login-detection.ts,apps/cli/src/commands/browser.ts.- Fix: a finished session that signed off with a trailing "?" no longer reads as
input_requiredforever (RUSH-1522). The session state engine's prose-question heuristic (last assistant message ends with a question) now decays after 30 minutes without a session write: an unanswered prose question older than that classifies asidle, notwaiting_input— soagents sessions --activeand the Factory Floor's NEEDS YOU lane stop surfacing long-finished sessions as needing input. The structural signals are exempt and never decay: a genuinely pendingExitPlanMode(plan review) orAskUserQuestionstill classifies aswaiting_inputat any age. Source:apps/cli/src/lib/session/state.ts(inferActivity,PROSE_QUESTION_FRESH_MS). - The post-upgrade "What's new" summary shows the release notes again. The summary parser only recognized the old changelog format (standalone
**Heading**lines with sub-bullets); every release since the changelog moved to single-line- **Title.** prose…entries rendered as a bare version header with zero bullets, so upgrades looked like they shipped nothing. The parser now extracts the bold heading from both formats (prose still dropped — full notes stay in the changelog). Verified against the real changelog: the 1.20.49 → 1.20.50 range renders all four 1.20.50 entry titles. Source:apps/cli/src/lib/whats-new.ts. - Fix: daemon no longer crash-loops when started from the bare
browserorcomputershim. Daemon launch resolution now maps installed sibling shims to theagentslauncher and compiled shims toindex.jsbefore generating launchd/systemd commands, and fails clearly if that invariant is broken. Headless auto-start reads the long-lived Claude token only from an already-unlocked secrets-agent snapshot, so it cannot hang on a biometric prompt nobody can answer; an interactive start can still prompt normally. Source:apps/cli/src/lib/daemon.ts(getAgentsBinPath,readDaemonClaudeOAuthToken),apps/cli/src/lib/secrets/bundles.ts(agentOnly). (RUSH-1527)
1.20.51 — 2026-07-10
Fix:
agents run --leasebootstraps a fresh crabbox image and no longer leaks the box after the run. Three failures compounded on a stock Hetzner lease (Ubuntu 24.04, no node preinstalled): (1) the bootstrap'snpm install -g @phnx-labs/agents-cliran with no node/npm on the box and swallowed the failure with|| true, so every run died deep in the script withagents: command not found(exit 127) and no hint why; (2) even with the CLI installed, a fresh install refusesagents runwith "agents-cli is not set up" untilagents setuphas run; (3) teardown calledcrabbox stop --id <slug>, but crabbox'sstoptakes a positional target (unlikestatus/run/ssh) and died withflag provided but not defined: -id— silently, becausecrabboxStopis best-effort — so every one-shot lease box was kept, billed, and left carrying the run's working data until someone noticed (Box … keptinstead of destroyed). The bootstrap now: exports~/.local/binonto PATH, installs node user-level from the officiallatest-v22.xtarball when missing (arch-aware, satisfiesengines.node >=22.5.0, no sudo needed), points the npm prefix at~/.local, fails loud with exit 96 and a diagnostic when the CLI still isn't runnable, and runsagents setupbehind the same[ ! -d ~/.agents/.system ]first-run guard the hosts bootstrap uses;crabboxStoppasses the slug positionally. Verified live on a fresh Hetzner cpx62 by the run's own progression across builds: the pre-fix lease exited 127 (agents: command not found) withBox … kept; after the node/npm fix it reachedagents-cli is not set up; after the setup fix it reached the agent's login check (Not logged in); and every post-fix run ends withBox <slug> destroyed.instead of leaking. Source:apps/cli/src/lib/crabbox/lease.ts(ENSURE_AGENTS_CLI,buildBootstrapScript),apps/cli/src/lib/crabbox/cli.ts(crabboxStop). Known follow-up: leasing a Claude runtime from a Mac whose Claude Code credential lives in the login Keychain (the default install, and any agents-cli managed home — service name is hash-suffixed) still lands "Not logged in" on the box, because the picker copies~/.claude.json(config/state) but not the OAuth token, and extracting the token from the Keychain needs an interactive ACL approval; tracked separately.agents repo pull user <git-url>now git-backs a plain~/.agentsinstead of silently skipping it — fixing config sync on Windows/fresh machines. Setup only ever git-clones the system repo (~/.agents/.system/); the user repo is created as a bare directory (state.ts ensureAgentsDir), so~/.agentsis git-backed only where it was cloned by hand as a dotfiles step. On a box where that never happened (a fresh install, or Windows),agents repo pulljust printeduser: not a git repo, skippingand the machine silently fell out of config sync — norules/, noagents syncof shared resources. Now, passing your config remote once —agents repo pull user [email protected]:you/.agents.git— adopts the existing directory in place: it clones your remote and moves the.gitin without deleting anything, materializes the tracked resources it was missing, and backs up any locally-modified tracked file (e.g. a machine-specificagents.yaml) to a sibling~/.agents.pre-adopt-backup/before overwriting it. Untracked runtime state (.cache/,.history/,.system/— all gitignored) is never touched. Every subsequentagents repo pull/agents syncis plain (the remote is noworigin). No new command; the URL is only needed the first time. SSH transport is preserved (agit@…URL clones over SSH, not a rewritten https that would hang on a private-repo credential prompt), and git never prompts (GIT_TERMINAL_PROMPT=0). Source:apps/cli/src/lib/git.ts(adoptRepo),apps/cli/src/commands/repo.ts.agents runnow warns when a headless run leaves committed-but-unpushed work, instead of stranding it silently. A headlessagents runin a writable mode (edit/skip/auto) could end with the agent having committed on a branch but never pushed it — the run's exit path did no git work, so those commits sat invisible in a worktree until someone audited the box (exactly how a batch dispatch loop can quietly lose a verified fix). After a non-interactive, writable run the CLI now inspects the cwd for commits on the current branch that haven't reached any remote (git log HEAD --not --remotes, correct even when no upstream is set — work already on anorigin/*ref is not flagged) and prints a loud stderr warning naming the branch, the unpushed commits, and the exactgit push/gh pr createcommands. Advisory only: it never pushes, never mutates the repo, and never throws (a 5s git timeout plus full error-swallowing guarantee it can't delay or break the run's exit). The check is wired into every headless exit path — single run,--loop,--acp,--resume-checkpoint, and the crash/catch path — and gated byshouldWarnUnpushed(mode, interactive)so it stays silent for interactive runs (the human sees their shell) and read-onlyplanmode. Source:apps/cli/src/lib/warn-unpushed.ts,apps/cli/src/commands/exec.ts. (#868)Codex mode flags now match what the mode names promise — only
--mode skipis yolo.--mode editused to append--dangerously-bypass-approvals-and-sandbox(Codex's--yolo) alongside--sandbox workspace-write, and the bypass flag wins — so "edit" silently ran Codex with no sandbox and no approvals, verified against codex 0.142.5's own session banner (sandbox: danger-full-access). And--mode planmapped toworkspace-write(writable!) because the template predated Codex'sread-onlysandbox. Now:plan→--sandbox read-only,edit→--sandbox workspace-write -c sandbox_workspace_write.network_access=true(sandboxed writes, network on so git/gh/installs keep working, no approval bypass),skip→--dangerously-bypass-approvals-and-sandbox(unchanged — skip IS the gnarly mode, equivalent tocodex --yolo). Same fix in routine jobs (runner.ts) and in headlesscodex exec resume, which used to get the bypass for ANY non-plan resume — it now maps plan/edit through-c sandbox_mode=…and reserves the bypass for skip; interactivecodex resumenow carries the mode's sandbox flags instead of none. Verified live per mode against codex 0.142.5 session banners: skip =approval: never / sandbox: danger-full-access, edit =sandbox: workspace-write (network access enabled), plan =sandbox: read-only. Source:apps/cli/src/lib/exec.ts(AGENT_COMMANDS.codex, resume block),apps/cli/src/lib/runner.ts(buildJobCommand).--add-diris now forwarded to Codex (it was silently dropped).agents teamspasses--add-dir ~/.agentsso Codex teammates can runagents teams add, butbuildExecCommandemitted--add-dirfor Claude only — the grant never reached Codex, masked until now by edit mode's accidental sandbox bypass. Codex takes--add-dirnatively (widens the workspace-write sandbox); it is now forwarded for fresh runs and skipped on resume (codex exec resumerejects it). Source:apps/cli/src/lib/exec.ts.Fix: the documented
agents run <agent> [prompt] -- <native flags>passthrough works again. commander ≥13 rejects excess operands by default, so any post---token (e.g.agents run codex -- --yolo) died withtoo many argumentsbefore the run started. The run command now allows excess operands, re-derives the--boundary from argv (a post---token can never be mis-parsed as the prompt —agents run codex -- --yololaunches the TUI with--yolo, it doesn't headless-run the "prompt"--yolo), and still errors, with a hint to quote the prompt, on excess operands NOT behind--. Verified live:agents run codex "…" -- --yoloforwards--yoloand codex reportssandbox: danger-full-access. Source:apps/cli/src/commands/exec.ts.Fix: grok launch shims resolve the binary from the versioned home before the global
~/.grok/downloads, so a pinned grok that installed into the versioned home no longer dies with "grok@not installed." Grok ships a native binary (not an npm package), and it lands in the versioned home's.grok/downloadswhenever the installer runs withGROK_HOMEset — via the shim, a correctagents add grok, or a grok self-update from within the shim. Both generated shims (the dispatcher ingenerateShimScriptand thegrok@<version>versioned alias ingenerateVersionedAliasScript) checked only$HOME/.grok/downloads, which was often empty, so they fell through to the "not installed" error even though the binary existed in the versioned home.getBinaryPathalready checked the versioned home first, soagents viewand the shims disagreed. Both shim blocks now check$VERSION_DIR/home/.grok/downloadsfirst and fall back to the global$HOME/.grok/downloadsfor pre-fix installs, then the existing adopted-launcher/PATH last resort. BumpsSHIM_SCHEMA_VERSION25→26 andVERSIONED_ALIAS_SCHEMA_VERSION12→13 so existing on-disk grok shims regenerate. Supersedes the pre-monorepo #830. Source:apps/cli/src/lib/shims.ts(generateShimScriptgrok dispatcher block,generateVersionedAliasScriptbinaryResolution).Fix:
browser stop --host <windows>tree-kills the remote browser — relaunches never wedge on a staleSingletonLock. The kill script usedStop-Processon the CDP port owner only; orphaned Chromium child processes survived, kept the profile'sSingletonLockheld, and the nextbrowser start --hostagainst the same profile exited immediately as a second instance. The script now usestaskkill /PID <owner> /T /Fto take down the whole process tree. Source:apps/cli/src/lib/browser/drivers/ssh.ts(buildWindowsKillScript). (GitHub #561)Fix:
agents browser start --host <windows>actually serves CDP now — the remote browser launches in the user's interactive session instead of session 0. The Windows launch used WMIWin32_Process.Create(chosen so the browser outlives the ssh session), but a WMI-created process lands in session 0, where Edge binds the debugging port yet its DevTools server never initializes — every/json/versionprobe hung forever andDevToolsActivePortwas never written, sobrowser start --hostfailed with a connection error on every attempt. The launch is now a one-shot scheduled task registered and started by the logged-on user: it survives ssh disconnect the same way, runs in the interactive session where DevTools comes up normally, and is unregistered immediately after start. The launch args also gained the same automation-modal suppressors the local launcher has (--no-first-run --no-default-browser-check --hide-crash-restore-bubble --disable-session-crashed-bubble) — without them a relaunch against a previously hard-killed profile triggers session-restore churn that closes the CDP page target mid-command. Verified live against win-mini (Edg/150). Source:apps/cli/src/lib/browser/drivers/ssh.ts(buildWindowsLaunchScript). (GitHub #561)Fix: remote CDP no longer dies on large payloads — screenshots of content-rich pages over
browser --hostwork. The CDP client rode the platform (undici) WebSocket, which enforces a non-configurable max decompressed message size; aPage.captureScreenshotresponse for a content-rich page blew past it and the socket closed with 1006 ("Max decompressed message size exceeded") while the command was pending, surfacing as "CDP connection closed". The websocket transport now uses thewsclient (no permessage-deflate offer by default, explicit 256MBmaxPayload); the local pipe transport is unchanged. Source:apps/cli/src/lib/browser/cdp.ts. (GitHub #561)agents sessions <id> --jsonnow exposes the ExitPlanMode plan markdown as a top-level field, and the shape changed from a bare event array to{ session, events }. The session-state engine already detected plan-review (awaitingReason: 'plan_review') off a trailingExitPlanModetool call, but the plan markdown itself was dropped on the floor — forcing every consumer that wanted it (the Factory NEEDS-YOU panel viaparsePlanFromClaudeJsonl, external dashboards) to re-open the raw JSONL and scan for the same tool call. That "extension re-implements the session engine" gap now closes at the source: the state engine surfacesstate.planalongsideawaitingReason, the Claude scanner captures the plan text at scan time and persists it tosessions.db(schema v11, additive, rescan-on-migrate), it's exposed asplanonSessionMetain everyagents sessions --jsonrow, andagents sessions <id> --jsonnow emits{ session: SessionMeta, events: SessionEvent[] }so the plan is one top-leveloutput.session.planread instead of a needle-in-haystack scan. Verified live against a real Claude session with an ExitPlanMode event:agents sessions 74464df7 --jsonprints the plan markdown at.session.plan. Source:apps/cli/src/lib/session/{state,discover,db,render,types}.ts,apps/cli/src/lib/session/active.ts,apps/cli/src/commands/sessions.ts. (issue #743 / RUSH-1505)agents computerWindows parity: scoped screenshots,get-text --max-chars,status/reload --host, and honest--background/--require-frontmosthandling (#548). Four params the CLI already sent were silently ignored by the Windows daemon. (1) Screenshots are now pid-scoped like macOS —screenshot --listenumerates the target pid's top-level windows (window_idis the Win32 HWND, the same idraise --window-idtakes), the default capture crops to the pid's largest on-screen window,--window-idshoots one window, and--displaycaptures the display the app is on; previously every capture was the whole virtual desktop. Verified live on win-mini: window capture 2097x984/28KB vs full display 2560x1440/380KB. (2)get-text --max-charsis honored (default stays 20k, ceiling 200k like macOS) —--max-chars 100now returns exactly 100 chars. (3)status --host <device>andreload --host <device>— status reports the recorded tunnel plus a live daemon probe (previously it misreported macOS-local install state for a remote Windows daemon); reload restarts the daemon's scheduled task (the way to pick up a freshly pushed exe) and confirms it answers through the tunnel. (4)--require-frontmostis enforced on Windows —SendInputlands in the focused window, sotype-text/keynow reportfrontmost(feeding the existing CLI warning) and the flag hard-fails withnot_frontmostwhen the target isn't foreground;--backgroundis rejected withaction_unsupportedinstead of silently no-oping (macOS postToPid delivery has no Win32 analogue — element-mode clicks via UIA patterns are the focus-safe path). Source:native/computer-win/{Screenshot,Automation}.cs,apps/cli/src/commands/computer.ts,apps/cli/docs/computer.md.The
neverprompt-policy is now live — the signed keychain helper was rebuilt, re-notarized, and re-pinned.agents secrets create --policy never --i-understandstores bundle values with no biometry ACL (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, device-local, non-synchronizable) so headless automation can read them with zero Touch ID prompts. The Swiftset-no-aclpath shipped in #682 but the pinned helper binary predated it, so the policy failed against the shipped helper; the helper is rebuilt from current source, notarized (Apple submissiona2373c91-7fc2-4894-a801-b37c111597aa, status Accepted, stapled, GatekeeperNotarized Developer ID), andscripts/Agents CLI.app.sha256re-pinned to the new binary. (GitHub #421)Consolidated ~30 copy-pasted terminal-formatting helpers into one shared
apps/cli/src/lib/format.ts, fixing three user-visible drifts at the source.die,truncate,relTime,humanDuration,visibleWidth,padRight/padVisible,isJsonMode,readStdinSync, andtermLinkhad drifted into per-command copies with different behavior; every consumer now imports the single canonical version. Three normalizations are user-visible: (1) the truncation ellipsis is now the single glyph…everywhere —agents cloudtask lists,agents sessionsoverflow, and session prompt/tool summaries previously showed ASCII...or a bare.; (2)agents cloudrelative timestamps switch from the long "5 minutes ago" form to the compact "5m ago" form already used byagents teams; (3) theagents teamspicker duration cell gains a space ("2h5m" → "2h 5m") to match the sessions/browser pickers. It also fixes a latent bug:agents repo's divergence-table column alignment used avisibleWidthregex missing its\x1bescape, so ANSI-colored cells were mis-measured and columns could misalign — the canonicalvisibleWidthstrips the full SGR sequence.lib/events.ts'struncate(a distinct nullable, exported helper that truncates persisted event payloads) and the domain-specificstatusColorcopies (different status vocabularies with conflicting color assignments) are deliberately left in place. Internal refactor plus the noted string normalizations; no other behavior change. Source:apps/cli/src/lib/format.tsand consumers acrossapps/cli/src/commands/andapps/cli/src/lib/. (GitHub #753 / RUSH-1515)agents computer setup --hostnow works from a plainnpm i -ginstall — the Windows helper exe downloads on demand from GitHub releases. The ~157MBcomputer-helper-win.exenever shipped in the npm tarball, so setup died with "Windows helper exe not built. Run: bash scripts/build-win.sh" for anyone without a repo checkout. Onv*tags thecomputer-helper-win.ymlworkflow now builds the self-contained exe, smoke-tests it, and uploads it plus a.sha256as GitHub release assets;setup --hostresolves a local build first, then downloads the asset for the exact running CLI version, verifies its sha256 against the published checksum, and caches it under~/.agents/.cache/computer/win-helper/v<version>/. A tag with no asset is a hard error naming that tag — never a silent fallback to a different release. (GitHub #547) Source:apps/cli/src/lib/ssh-tunnel.ts(ensureWinHelperExe,downloadWinHelperExe),.github/workflows/computer-helper-win.yml(release-exe).registerMcpHTTP transport now routes through the capability table instead of an inline agent-id allowlist. MCP-over-HTTP support and MCP-header support were gated by hardcodedagentId !== 'claude' && agentId !== 'codex' && agentId !== 'gemini'/agentId !== 'claude'checks inapps/cli/src/lib/agents.ts, bypassing thecapabilities.ts/supports()table that is the single source of truth for "which agent supports what." A newly-added agent would silently get the wrong HTTP-MCP behavior with no compile-time signal. Two new capabilities land on theAgentConfigmatrix —mcpHttp(Claude/Codex/Gemini today) andmcpHeaders(Claude only) — and both inline allowlists are replaced withsupports(agentId, cap)calls. Pure refactor:capableAgents('mcpHttp')is['claude','codex','gemini']andcapableAgents('mcpHeaders')is['claude'], matching the pre-change behavior exactly. Source:apps/cli/src/lib/{agents,capabilities,types}.ts,apps/cli/src/lib/{agents,capabilities}.test.ts,apps/cli/src/lib/__tests__/capabilities.test.ts. (issue #742 / RUSH-1504)Hook
matches:predicates are now enforced at fire time — the documented gating was inert. A hook manifest entry could declarematches:predicates (prompt_contains/prompt_matches/tool_name/tool_args_match/cwd_includes/project_has/git_dirty) to gate when it fires, and the docs described the gate ("all predicates AND together; an empty block always fires"), butshouldFire()(the evaluator insrc/lib/hooks/match.ts) had zero runtime callers: the agent execs the registered command directly and nothing evaluatedmatches:, so any hook with amatches:block fired unconditionally. A hook that declaresmatches:(with or withoutcache:) is now registered as a generated wrapper shim that evaluates the predicates against the event JSON on stdin before running the script — a non-matching event exits 0 without running the hook body (logged ascache:"skip"), a matching event runs it. Matches-only hooks (nocache:) get a gate-only pass-through shim; cached hooks apply the gate before the cache. The shim gate is a faithful port ofshouldFire()(same AND semantics, same ReDoS guard) and is pinned to it by a 20-case conformance test so the two can't drift. Gating is fail-open: a garbled predicate runs the hook rather than silently disabling a safety hook (e.g.git-guard). No installed or bundled hook currently declaresmatches:, so this changes no existing hook's behavior — it activates a documented feature for authors who add one. Verified end-to-end by generating a shim from amatches: { tool_name: Bash, tool_args_match: "rm -rf" }manifest and firing it: aReadevent and aBash+lsevent were skipped, aBash+rm -rfevent ran the body. Source:apps/cli/src/lib/hooks/cache.ts(renderShimgate + pass-through tail),apps/cli/src/lib/hooks.ts(resolveHookCommand),apps/cli/docs/hooks.md. (RUSH-1506)Browser-over-SSH no longer hangs on an unreachable remote host — it fails fast (~10s). The raw-
sshspawns in the browser SSH driver (ensureRemoteBrowser,runSSHCommand) passed only-o BatchMode=yes, with noConnectTimeout, so a dropped SYN to a down host stalled on the OS default TCP timeout (~127s) instead of erroring. Both call sites now compose the shared hardened baselineSSH_OPTSfromssh-exec.ts(BatchMode+ConnectTimeout=10+ServerAlivekeepalive) rather than re-listing options — the same baselinesshExecand the-Ltunnel already use. The options now also precede the target (matchingsshExec); on macOS/BSDgetoptan option placed after the target is swallowed into the remote command instead of applied. Verified against TEST-NET203.0.113.1(guaranteed unreachable):-o BatchMode=yesalone was still hanging at a 30s cap (en route to ~127s), while theSSH_OPTSset failed in10.04swithconnect to host 203.0.113.1 port 22: Connection timed out. Source:apps/cli/src/lib/browser/drivers/ssh.ts. (RUSH-1508)Fix: OpenCode sessions now load on Windows. Reading OpenCode sessions shelled out to the
sqlite3CLI at three call sites —parseOpenCode(transcript parse) plus the session scan and active-account lookup in discovery — and that binary is absent on Windows, so OpenCode sessions silently never appeared inagents sessionsthere. All three now read through the same runtime-aware node/bunDatabasewrapper the Antigravity parser already uses (bun:sqlite/node:sqlite, no native addon, no CLI), and the OpenCode transcript query binds the session id as a parameter instead of interpolating it. No behavior change on macOS/Linux. Source:apps/cli/src/lib/session/parse.ts(parseOpenCode),apps/cli/src/lib/session/discover.ts(scanOpenCodeIncremental,getOpenCodeAccount). (RUSH-1513)agents sessions --active --jsonnow carries the agent's actual decision, not a truncated status line. A session waiting on you used to collapse everything to a one-linepreview— anAskUserQuestionbecame the generic"Asked you a question"(throwing away the options that are already in the tool input), and a trailing thinking block masked the real turn as"thinking…". The state engine now emits a structuredquestionobject ({ text, reason, options: [{ label, description, key }] }) for every waiting path —AskUserQuestion(with each option's 1-based select key), plan review, permission (Approve=1/ Deny=esc), and a trailing prose question — plus a short assistanttailfor context, andpreviewno longer degrades to"thinking…"when a real turn exists. Every consumer (the Factory NEEDS-YOU panel, teams, cloud) now gets the real "what does it want from me" instead of re-deriving it from prose. Verified live: the blocked session in the screenshot now reportsawaitingReason: questionwith the real question text. Source:apps/cli/src/lib/session/state.ts(structuredQuestionFromAsk,inferActivity),apps/cli/src/lib/session/active.ts. (RUSH-453)
1.20.50 — 2026-07-08
- Distributed agent teams: teammates can now run on different machines across your fleet, not just the box running
teams start. A single team can place the backend teammate on a Linux box and the UI teammate on a Mac while one orchestrator still drives the DAG, polls status, and cleans up. One vocabulary, all optional (omit it and teams stay 100% local as before):teams create --devices a,b,c(alias--hosts) declares a pool the team may auto-schedule onto,--repo <url|path>(defaults to the local checkout'sorigin) says how each device gets the code, andteams add --device X(alias--host) pins one teammate to a host — which needs no pool, so "send just one teammate elsewhere" is zero-setup. Placement resolves top-down at launch: explicit--devicepin → single-device pool (whole team there) → multi-device pool (least-loaded auto-schedule) → local. Remote teammates dispatch over SSH via the existingagents devices/host machinery (a third teammate backend beside local and cloud), are monitored by offset-tailing the remote log +.exitsentinel, and get the repo auto-provisioned per device (reuse an existing checkout, else clone into~/.agents/repos/<team>) with an optional per-teammate git worktree on the host.teams status/teams logsshow each teammate's host and stream its output back with the local mirror capped (~512KB rolling tail) so a 10+-teammate fleet can't blow up the orchestrator. POSIX hosts only in v1 (Windows rejected with a clear message). Source:apps/cli/src/lib/teams/{scheduler,remoteWorktree,agents,api,supervisor,registry}.ts,apps/cli/src/lib/hosts/{progress,passthrough}.ts,apps/cli/src/commands/teams.ts,apps/cli/docs/teams.md. - NEW:
agents doctor --devicesshows a cross-device agent-readiness matrix.agents doctorcould already run on one remote machine via--host, but checking the whole fleet meant running the command once per box.--devicesfans outagents teams doctor --jsonto every registered device (plus the local machine), renders a device × agent matrix, and emits a stable JSON contract with--json.--device <name>or--host <name>scopes the same matrix to a single machine. The remote probe now bootstrapsPATHwith the canonical shim directories before running, so login shells that haven't sourced interactive rc files no longer report false "not installed" negatives. Source:apps/cli/src/commands/doctor.ts,apps/cli/src/lib/teams/agents.ts,apps/cli/src/lib/hosts/{passthrough,remote-cmd}.ts. agents run codex/agents teamsnow honor your configured Codex model instead of silently defaulting togpt-5.3-codex. Codex runs under a per-versionCODEX_HOME, and yourmodelpreference (~/.codex/config.toml) lives only in the version-home that was active when you set it. A dispatch pinned to a different version read a home with no top-levelmodel, so Codex fell back to its built-in default — which a ChatGPT-tier account isn't entitled to use, so the run died with400: The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT accountbefore doing any work, even thoughag viewreported Codex "signed in". When no explicit--modelis passed, the model is now defaulted (for Codex) to the top-levelmodelin your active~/.codex/config.tomland forwarded via--model; it's read-only (no file writes), so fanning out many parallel runs to one version-home can't race. Verified live on a box where Codex was 100% unusable: the request model changedgpt-5.3-codex→gpt-5.5and [email protected] returned successfully. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/shims.ts(readCodexConfiguredModel).- Fix:
agents add claude@<version>now produces a runnable install — it no longer ships a half-built binary that dies with "claude native binary not installed."installVersionrunsnpm install --ignore-scripts(the right posture for the dependency tree — never run arbitrary transitive postinstalls), but that also skipped the agent package's OWN postinstall, which for@anthropic-ai/claude-codeis a required step: the package ships a ~500-byte stub atbin/claude.exeplus per-arch native binaries as optional deps, and itspostinstall(node install.cjs) is what copies the correct ~231 MB native binary over the stub. Skipped, every launch died withError: claude native binary not installed. The existing launch-health self-heal (#764, and its Windows/daemon extension) couldn't save it on two counts: the stub reports its breakage politely rather than with a rawENOENT, so the probe's missing-binary signature didn't match and the gutted install read as healthy; and the repair path (ensureAgentRunnable→ clean reinstall) re-ran the same--ignore-scriptsinstall, so it never copied the binary either.installVersionnow runs the first-party package's declaredpostinstallafter the npm install (scoped to that one package — never the dependency tree, never claude-code'sexit 1prepareguard), best-effort, before the integrity gate. BecauseinstallVersionis the single choke point foragents add, config refresh, run-time heal, and the daemon's proactive heal, this also revives the repair path for the whole class.isMissingBinarySignaturewas additionally widened to recognize the stub's polite phrases (native binary not installed,postinstall did not run,optional dependency was not downloaded) so the self-heal catches this failure mode if a postinstall ever silently no-ops. Verified end-to-end on linux-arm64:installVersion('claude','2.1.186')into a clean HOME runs the postinstall automatically, lands the 231,782,112-byte binary (not the stub), andclaude.exe --versionreturns2.1.186 (Claude Code)— with no manualinstall.cjsstep. Source:apps/cli/src/lib/versions.ts(installVersion,isMissingBinarySignature).
1.20.49 — 2026-07-08
agents run --mode planno longer hard-fails on agents without a read-only mode (antigravity, cursor, kiro, …). Those agents have no plan flag, so an explicit or default--mode planused to abort withdoes not support 'plan' mode— breaking multi-agent scripts that pass a uniform plan flag, and diverging fromagents teams add(default modeedit).resolveModenow degrades unsupportedplanto the agent's safest native mode (capabilities.modes[0], typicallyedit), matching the existingauto→editdegrade. The CLI prints a yellow warning when the user explicitly asked for plan (gray for the implicit default) so the elevation is never silent.skipstill hard-fails when unsupported. Source:apps/cli/src/lib/exec.ts,apps/cli/src/commands/exec.ts.agents cloud cancelnow actually cancels paused runs.RushProvider.cancel()issuedDELETE /api/v1/cloud-runs/{id}, which the backend doesn't implement — it 404s — soagents cloud cancel(and the Factory Floor's cancel affordance) silently failed on any run that wasn't actively running:queued,needs_review, andinput_requiredruns stayed stuck (e.g. a 14-day-old input-required run lingering in the Floor's "NEEDS YOU" bucket forever). Switched to the cancel action endpointPOST /api/v1/cloud-runs/{id}/cancel, which the backend implements and which cancels paused runs too. Verified live againstapi.prix.dev(the POST returned{"ok":true,"status":"cancelled"}and the stuck run transitionedneeds_review→cancelled). Source:apps/cli/src/lib/cloud/rush.ts.
1.20.48 — 2026-07-07
Menu-bar helper: a RECENT TICKETS section shows the issues you filed via the quick-issue bar, each clickable to open in Linear. The completion notification is transient, so the tickets the
Cmd-Shift-Obar creates now also persist to a small local ledger (~/.agents/.history/menubar/recent-tickets.json, newest-first, deduped by id, capped at 10) that the menu-bar dropdown surfaces below RECENT sessions — click a row to open the ticket. The dispatch records the id + note + Linear URL on a successful create; the section renders nothing when the ledger is empty. Source:apps/cli/menubar/Sources/MenubarHelper/{RecentTickets,StatusItemController,AgentsCLI,IssueSelfTest}.swift.Menu-bar helper: the quick-issue completion notification now deep-links to the created ticket, and the helper self-heals onto the install you actually run. Two fixes from dogfooding the
Cmd-Shift-Obar. (1) Clickable notification — the "Created RUSH-####" banner carried no click target, so there was no way to open the ticket. The ticket agent now also prints the issue'sURL:line, the helper parses it, and clicking the notification (or its Open button) opens the ticket in Linear (via anNSUserNotificationCenterDelegate; the banner is also force-presented so it can't be silently swallowed when the accessory app is frontmost). (2) Dual-install self-heal — the helper bakes the node interpreter + CLI entry into its launchd plist so a GUI process can findagentswithout a login PATH, but the staleness check only re-baked on a version change. With two installs present (e.g. an nvm copy and a bun copy), the plist kept pointing at whichever copy first wrote it, so the menu data and the quick-issue dispatch ran on a stale install even afteragents upgrade. The startup self-heal now also re-points when the plist's bakedAGENTS_ENTRY/AGENTS_NODEno longer match the install currently runningagents(a null active entry — a dev/tsx run — never churns the plist). Source:apps/cli/menubar/Sources/MenubarHelper/{PromptPanel,AgentsCLI,IssueSelfTest}.swift,apps/cli/src/lib/menubar/install-menubar.ts.The npm release can now be driven from a Linux box by offloading the Mac-only helper signing to a remote sign host. The tarball bundles two signed macOS
.apphelpers a Linux runner can't build —bin/Agents CLI.app(the keychain helper:swiftcuniversal → codesign with entitlements + embedded provisioning profile →notarytool→ staple) andbin/MenubarHelper.app(the menu-bar status item:swift build→ codesign, no notarization) — which is the only reason publishing was macOS-pinned. Newscripts/remote-sign-mac.sh(invoked automatically byrelease.shwhen it runs on a non-macOS host and the signed apps are absent, or on any host withFORCE_REMOTE_SIGN=1) rsyncs the build inputs to${SIGN_HOST:-mac-mini}, runs both Mac build scripts there under the appliance's headless signing creds (unlocksrush-signing.keychain-db, injects Apple notary creds via theapple.comsecrets bundle), then pulls the signedbin/*.appback and re-verifies the keychain sha locally. Thebuildscript now copies the helpers intodist/on a presence gate ([ -d 'bin/…' ]) instead of[ "$(uname)" = 'Darwin' ], so a Linux box that pulled the pre-signed bundles packages them, andprepack's sha gate usesshasumorsha256sum(whichever is present) so it works on Linux too. Override the sign host withSIGN_HOSTand its checkout withSIGN_HOST_REPO. Source:apps/cli/scripts/remote-sign-mac.sh,apps/cli/scripts/release.sh,apps/cli/scripts/verify-keychain-helper.sh,apps/cli/package.json.The shim self-heal now repairs shims that point at a removed install and prunes orphaned command shims. A dispatch shim bakes its
AGENTS_BIN(the agents-cli entrypoint it execs) at generation time, so when that install moves or is deleted — a dev build under~/.local/agents-cli-dev, an old npm-global under/opt/homebrew, a rotated version dir — the shim keeps pointing at the dead path. Agent shims survive it via their runtime self-recovery block, but the previous self-heal only compared the schema marker, so a schema-current shim aimed at a removed install read as healthy and was never repaired. Two additions to theshimsself-heal check (daemon + interactive startup): (1) drift repair — an agent shim whose bakedAGENTS_BINnames a different, now-missing install is force-regenerated to the current install (shimPointsAtLiveInstall); a shim pointing at another install that still exists is left alone, so two live installs sharing the shims dir can't ping-pong. (2) orphan prune — legacy standalone command shims (browser/secrets/sessions/teams/pty) that a removed install left in the shims dir, which the current source never regenerates and which either die withexit 127or shadow the real package bin on PATH, are removed when their baked install is gone (pruneOrphanedCommandShim); useragents aliasshims and any shim whose install still exists are spared. Verified end-to-end against a real machine carrying a deleted dev build + a removed Homebrew install: the agent shims repoint to the live install and five dead command shims are pruned. Source:apps/cli/src/lib/shims.ts(shimPointsAtLiveInstall,pruneOrphanedCommandShim,listShimFileNames),apps/cli/src/lib/self-heal/checks/shims.ts.
1.20.47 — 2026-07-07
- Quick-issue bar (
Cmd-Shift-O):Cmd-Vnow pastes into the note field, and double-clicking a screenshot thumbnail opens it in Preview. Two fixes from dogfooding the new bar. (1) The panel is a borderless.accessorywindow with no main menu, so the standard clipboard key-equivalents (Cmd-V/C/X/A) were never dispatched to the field editor — paste silently did nothing.PromptPanel.performKeyEquivalentnow routes them through the responder chain so the text field handles them. (2) Thumbnails are small, so there was no way to confirm which screenshot you were attaching: single click still toggles selection, double click opens the full image in the default viewer (Preview). The single-click toggle is deferred by the double-click interval so a double-click previews without also flipping the selection, and the bar suppresses its own click-outside dismissal while Preview takes focus (so summoning Preview never closes the bar or drops your typed note; it re-arms when the bar regains focus). Source:apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift. - Fix: the headless file-store fallback no longer silently shadows the OS keyring; NEW
agents secrets import-keyringmigrates stranded secrets into it. On headless Linux/Windows the encrypted-file store is sticky — once any item is on disk,preflight()routed every op to the file store and never consulted GNOME Keyring / Windows Credential Manager again, so a secret written earlier into the native store (e.g. while a desktop keyring was unlocked) read back empty with no hint. This stranded real Linear CLI credentials in a locked keyring while other bundles lived in the file store, silently breaking the SessionStart hook. Two fixes: (1)get/hasnow read through to the native store on a file-store miss (the fast path and the non-fallback keychain-first path are untouched — the file store is still checked first), emitting a one-time stderr notice pointing atimport-keyring; once a locked/1312error is seen the store is marked unreachable so it stops re-probing a known-dead store. (2) NEWagents secrets import-keyring— the Linux/Windows analogue of the macOSmigrate-acl/orphan sweep — enumeratesagents-cliitems in the native store and copies them into the encrypted file store (the durable, passwordless headless backend). Dry-run by default;--commitwrites; existing file-store items are never overwritten; Windows enumeration is floored to theagents-cli.namespace since Credential Manager targets have no service scoping. macOS is unaffected (it has no file fallback and keepsmigrate-acl). Source:apps/cli/src/lib/secrets/{fallback,linux,windows,index}.ts,apps/cli/src/commands/{secrets-import,secrets}.ts,apps/cli/docs/secrets.md. - Launch-health self-heal now covers Windows, and the daemon repairs a gutted install proactively — before your next
agents run. #764 gaveagents runan install/run-time self-heal (probe<binary> --version; clean-reinstall in place, else fall back to another installed version that launches), but it skipped the probe on Windows —verifyInstalledBinaryLaunchesreturned healthy onwin32unconditionally, because probing the extensionless.bin/<cli>wrapper would ENOENT even on a healthy install. So the exact Windows failure the self-heal was built for went unhealed: a vendor auto-update renames the nativeclaude.exetoclaude.exe.old.<epochMs>and never lands the replacement, leaving the shim chain intact but pointing at a missing file, and every launch dies with'…claude.exe' is not recognized. The probe now runs on Windows against the real launch target — the npm.cmdwrapperagents runactually execs (getBinaryPath + '.cmd', resolved viacmd.exe), which chains to the native.exe— so a gutted install trips the existing missing-binary signature (is not recognized) and is repaired by the sameensureAgentRunnablemachinery; a missing.cmd(a non-npm/global agent likedroid.exe) is still treated as healthy so a good install is never destroyed. Separately, the daemon now runs a proactive launch-health pass (healBrokenDefaultLaunches) ~90s after startup and every ~6h: it probes each agent's default version and, if it won't launch, repairs it in the background — so a gutted install is fixed before the nextagents runhits the ENOENT, not at spawn time (the run-timeensureAgentRunnableonly fires once a run is already starting). Verified end-to-end on a real Windows host: renamingclaude.exeto.oldmakes the.cmdprobe emitis not recognized; restoring it returns2.1.191 (Claude Code). Source:apps/cli/src/lib/versions.ts(verifyInstalledBinaryLaunches,healBrokenDefaultLaunches),apps/cli/src/lib/daemon.ts.
1.20.46 — 2026-07-07
- NEW:
Cmd-Shift-Oopens a Spotlight-style quick-issue bar in the menu-bar helper — type a sentence, attach recent screenshots, and an agent files the Linear ticket for you. The menu-bar helper already turned a screenshot into a<host>:<path>token withCmd-Shift-V(clip capture), but there was no path from "I see a bug" to "a triaged ticket exists." The new chord summons a borderless panel (a thin capture surface, not another form): you type a one-line note, optionally toggle one or more recent screenshots (from the system screencapture folder, CleanShot's export path, or the clip history) as a thumbnail strip (the newest is pre-selected when it's fresh), and hit Return. It then dispatches a headless agent (agents run claude --mode auto, isolated behind oneAgentsCLI.dispatchTicketAgentcall so a cloud pod is a later swap) that reads the screenshots, runsagents sessionsto identify which repo/project this concerns, does a brief investigation for real context, and files the ticket via~/.agents/skills/linear/scripts/linear createwith an honest priority + arepo:<name>label — no preview step, the panel closes immediately and a notification reports the createdRUSH-####. Focus is handled for a no-Dock.accessoryapp (NSApp.activate→makeKeyAndOrderFront→makeFirstResponder, with a borderlessNSPaneloverridingcanBecomeKey; click-outside dismissal is armed only after the summon settles so the activation race can't self-dismiss the panel). TheCmd-Shift-Vclip hotkey is unchanged — the Carbon hotkey manager now demultiplexes both chords byEventHotKeyID.idthrough one installed handler. Self-test:MENUBAR_ISSUE_TEST=1 MenubarHelperexercises screenshot selection, ticket-id parsing, and the meta-prompt contract;MENUBAR_PROMPT_PREVIEW=1renders the panel without the global hotkey for QA. Source:apps/cli/menubar/Sources/MenubarHelper/{PromptPanel,Hotkey,AgentsCLI,main,IssueSelfTest,Clip}.swift. - NEW: a unified self-heal subsystem — the shim/PATH "repair" notice no longer nags on every terminal, and the daemon now heals shim drift in the background. agents-cli had accumulated ~37 separate repair routines scattered across the daemon, every CLI startup, and a handful of commands, each hand-rolling its own detect+fix on its own trigger. The most visible symptom: the interactive shim bootstrap (
maybeBootstrapShimIntegration) regenerated shims, adopted shadowing launchers, and offered to add the shims dir to PATH in the foreground on every invocation, suppressed only by aprocess.ppid-keyed temp sentinel — so a new terminal re-ran the whole detect-and-nag, and the underlying condition was never permanently fixed. This lands a singleHealCheckregistry (lib/self-heal/) with one runner (runSelfHeal) driven by two front doors — the daemon (on its existing ~30s-after-start + ~6hsafe-mode cycle) and the interactive startup — sharing the same checks:shims(regenerate stale shims/aliases),shadowing(adopt symlink launchers; report real-binary shadows),path(add the shims dir to PATH once), andresources(the existingheal()engine, wrapped unchanged). The daemon's heal cycle now runs all four insafemode (low-risk fixes silently; risky ones reported), replacing the resource-onlyheal()call — and drops the desktop toast for background heals (the log is the record). The interactive startup now heals silently and prints at most a persistent, once-per-condition notice (lib/shim-heal.ts, keyed to a signature of the actionable state under~/.agents/.cache/state/shim-notice.json) for what a machine genuinely can't fix for you — a real native binary shadowing the shim — instead of re-nagging every shell. What changes is where the repairs run (background/silent) and how often you hear about them (once, not every terminal). Source:apps/cli/src/lib/self-heal/(new),apps/cli/src/lib/shim-heal.ts(new),apps/cli/src/lib/daemon.ts,apps/cli/src/index.ts,apps/cli/src/lib/shims.ts(isShimCurrentexported).
1.20.45 — 2026-07-07
- NEW:
agents run <agent> --host <name>without a prompt forwards your TTY over SSH and runs the agent interactively on the remote host. Previously--hostruns required a prompt and were always headless (agents run <agent> "<task>" --host <name>). Now, omitting the prompt takes the interactive path: when local stdin is a TTY, the local CLI SSHes with-tt, runsagents run <agent>on the host, and lets the remote machine'sagentsstart its normal tmux wrapper. The tmux session lives on the remote box, so detaching (Ctrl-b d) ends the SSH connection but keeps the agent running; you can reattach from the host or resume by session id. Session ids for Claude are still minted up front soagents sessionscan surface and resolve the remote run.--no-followis rejected for interactive host runs (it is meaningless for an attached TTY), and--mode,--model,--name, passthrough args after--, and--raw/--no-tmuxare forwarded to the remote invocation. Source:apps/cli/src/commands/exec.ts,apps/cli/src/lib/hosts/dispatch.ts,apps/cli/src/lib/hosts/session-index.ts,apps/cli/docs/hosts.md. agents secrets export --hostnow works against Windows targets, and a newagents secrets unlock --hostunlocks a bundle on a remote machine. The export push was POSIX-only (bash -lc,--from /dev/stdin,create … || true,IFS= read), so a Windows remote died with'true' is not recognized … cannot find the path specified. Two changes fix it:agents secrets importnow accepts--from -(read the.envfrom stdin, replacing the POSIX-only/dev/stdin), and the push is platform-aware —bash -lcon POSIX,powershell -EncodedCommandon Windows, with the target's OS taken from the device registry. Because the npmagents.ps1shim does not forward ssh-piped stdin to the underlying node process (a raw--from -read hangs), the Windows keychain push bridges the piped.envthrough PowerShell into a temp file and imports--from <file>(deleted afterwards). File-backend export to a Windows target is refused cleanly rather than emitting broken PowerShell. Verified end-to-end:agents secrets export linear.app --host win-miniimported all 13 keys. Separately,agents secrets unlock --host <machine> <bundle>runs the unlock ON the remote overssh -tt, so a file-backed bundle's passphrase prompt surfaces on your terminal — the "unlock the Mac from the road with its password" path; keychain/biometry bundles are GUI-only (a local Touch-ID/passcode sheet can't cross SSH) and can't be remote-unlocked.unlock's--hostis single-valued so it never swallows the positional bundle name. Source:apps/cli/src/commands/secrets.ts,apps/cli/src/lib/hosts/remote-cmd.ts.- A session now has ONE name, not two.
--nameseeds the session label instead of a parallel column. Shippingagents run --name(1.20.43) as a separate immutablenamecolumn created two look-alike fields — an unshown, frozennameand the shown, searchablelabel— that both resolvedagents sessions <ref>and forced tie-break bookkeeping nobody could keep straight. They unify into one field.--nameis now the universal way to seed thelabelat launch — the same field an agent-generated title (Claude's/rename) later refines andagents sessionsdisplays and searches — and it works consistently across interactive, headless,--host, and teams teammate runs (a teammate's friendly name now seeds its session label; before, teammate sessions had no name at all). Priority is a plain fallback chain resolved at scan time, no stored winner: an agent-generated title wins, else the--nameseed, else the listing falls back totopic. So a Claude run's--nameshows until Claude titles it (your seed, then refined); a non-Claude run keeps its--nameas the label (it has no auto-title). The seeded name is now fuzzy-searchable in FTS (the oldnamecolumn was not).agents hosts logs <name>is unchanged — it resolves against the host-task sidecar, not the session column. Schema v10 folds any existingnameintolabel(where the label was empty), mirrors it into the FTS row, then drops thenamecolumn; the run-name sidecars re-seed every scan (seedLabelsFromNames), so no rescan is needed. Reworks the 1.20.43--namedesign (partly reverts its separate-column approach). Source:apps/cli/src/lib/session/{db,discover,run-names,types}.ts,apps/cli/src/lib/hosts/session-index.ts,apps/cli/src/lib/teams/agents.ts,apps/cli/src/commands/exec.ts,apps/cli/docs/{05-sessions,hosts}.md. - NEW:
agents teams add/startwarns when a version-pinned teammate is on a throttled or signed-out account. The 1.20.43balanced-default fix keeps bare teammates off rate-limited accounts (they route through bareagents run, which rotates), but a version-pinned ([email protected]) or profile teammate spawnsagents run <agent>@<version>/agents run <profile>, and a pin/profile deliberately bypasses rotation — so it would launch straight onto a maxed account and 429 on the first request, with no mid-run failover either (that only arms when a non-pinned strategy actually rotated).agents teams add(at add time) andagents teams start(per staged teammate, deduped byagent@version) now pre-check a version-pinned teammate's account and print an advisory when it's rate-limited, out of credits, or not signed in — reusing the router's exact eligibility gate (checkRunAccountReadiness→hasUsageAvailable, the same session-inclusive signal theagents viewbadge uses), so the warning can never disagree with what the spawn would actually do. It warns, never blocks (mirroring the existing "may not be signed in" advisory);--forcesilences it. Scoped to version-pinned teammates on purpose: bare teammates are already handled by rotation, and a profile injects its own auth (a different account than the version home carries) that isn't locally checkable — so no unreliable profile warning is emitted. Source:apps/cli/src/lib/rotate.ts(readinessFromCandidate,checkRunAccountReadiness,rotate.test.ts),apps/cli/src/commands/teams.ts.
1.20.44 — 2026-07-07
- Every
logscommand is concise by default; the token-heavy raw dump is now opt-in behind--full. Agents that spin up agents on other machines or add teammates were pulling whole transcripts just to glance at status —agents logs <session>printed the full markdown transcript, andagents hosts logs/agents teams logs/agents routines logseachcat'd their entire captured stdout, because each subsystem had hand-rolled its own "cat the log" verb over its own storage. All four now default to a bounded, concise view, with-m/--fullfor the raw log:agents logs <session>renders the same summary digest asagents sessions <id>(a real session shrank 92% — 29.9 KB → 2.6 KB);agents routines logs <name>shows a status header + the extracted report (a real run shrank 99.5% — 386 KB → 1.8 KB), falling back to a bounded stdout tail when no report was extracted;agents teams logs <teammate>renders the teammate's session summary (its agentId is the session id), with-n <lines>/--fullfor raw stdout;agents hosts logs <id>shows a bounded tail of the captured stdout (tailLines, with a "… N earlier lines hidden — pass --full" note) instead of the whole log.renderSessionLognow takes a mode and defaults to'summary';agents sessions <id>was already summary-by-default and is unchanged. Regression-tested:tailLinestruncation/elision math (hosts/logs.test.ts) andformatRunDurationhuman-time formatting (routines-logs.test.ts). Source:apps/cli/src/commands/{logs,sessions,hosts,teams,routines}.ts,apps/cli/src/lib/hosts/logs.ts. Scoped follow-up (not in this PR): host-task and sandboxed-routine runs write their real transcript on the remote / in an overlay HOME, sologscan't yet resolve them to the fullrenderSummary— making those runs discoverable is a separate change; until then the bounded tail / extracted report is the safe concise default. - The daemon now self-heals the
pane-diedhook on already-runningagents runsessions. The v1.20.42 fix that stops exiting a split from kicking you out of tmux is installed once, at session creation — so sessions already alive under the long-lived shared tmux server keep the old, unconditionaldetach-clienthook until they exit or the server is recycled. On a machine that's never "between sessions," that meant hand-repairing live sessions. The daemon now runsreconcileSessionHooks()~20s after startup and every ~5 min: it walks the managedag-sessions on the shared socket and retrofits the#{hook_pane}-guarded hook onto any whose hook predates the current schema. It is strictly non-destructive —set-hookonly, never akill-paneordetach-client— so it is safe to run against sessions you're attached to; a per-session@ag_hook_schemamarker makes steady-state a no-op. The hook string is now built in one place (agentPaneDiedHook) shared by the spawn-wrap and the reconcile so they can't drift. Source:apps/cli/src/lib/tmux/session.ts,apps/cli/src/lib/daemon.ts,apps/cli/src/lib/exec.ts. - NEW:
agents runself-heals a gutted install instead of crashing withENOENT. The recurring failure: an npm agent whose native binary ships as an optional per-arch dependency (codex →@openai/codex-<platform>) can have that tarball extract partially — the platform package'spackage.jsonlands, itsvendor/<triple>/…/codexbinary does not (an interrupted or concurrently-racedagents addinto the same version dir). The CLI's wrapperrequire.resolves the platform package, finds thepackage.json, and sails straight past its own "missing optional dependency" guard into aspawn(binaryPath)that dies with a rawENOENT.agents runnow probes the version it's about to launch and, if the binary can't run, repairs it in place (a clean reinstall — the partialnode_modulesis wiped first, because npm treats the present-but-gutted platform package as already installed and would otherwise skip re-fetching it), then falls back to another installed version that launches (re-pinning it as the default so the shim path heals too), then to installinglatest— only erroring if nothing can be made runnable.installVersiongained a{ clean }option for the wipe-then-reinstall. Source:apps/cli/src/lib/versions.ts(ensureAgentRunnable),apps/cli/src/commands/exec.ts. - Fix: a broken agent install no longer launches into a silent
[detached]— the real crash is surfaced. When an interactiveag run <agent>wrapped the agent in tmux and the agent died the instant it spawned (e.g. a gutted install crashing withspawn … ENOENT, a bad flag, a startup crash), thepane-diedhook detached the client before you could read anything — you got a bare[detached (from session …)]with zero indication of why.runInTmuxnow recaps the dead pane's last output (read from scrollback viacapture-pane -S -200, since the pane's visible screen is just the "Pane is dead" banner) plus the exit code to stderr, and points you at--no-tmux. Fast failures (dead before attach) always recap; a post-attach nonzero exit recaps too (a clean exit or a manual detach stays quiet). Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/tmux/session.test.ts. - NEW:
--no-tmux/--disable-tmuxonagents run. The interactive tmux wrapper (which gives%paneaddressing + re-attach) already had an opt-out, but it was hidden behind the opaquely-named--raw.--no-tmux(and its alias--disable-tmux) spawn the agent directly with full stdio inherited — the fastest way to see an agent's real startup output when a launch is failing. Same effect as--rawandAGENTS_NO_TMUX=1. Source:apps/cli/src/commands/exec.ts. - Fix:
agents add <agent>@<version>no longer records a gutted install as healthy (root cause of the ENOENT crash + a broken default pin). npm packages that ship their native binary via an optional per-arch dependency (e.g. codex →@openai/codex-<platform>) can land the JS wrapper atnode_modules/.bin/<cli>while the real platform binary is missing (interrupted install, omitted optional dep,--ignore-scripts).getBinaryPath()only checked the wrapper, so the broken version read as installed, got pinned as the default, and got picked to run — then died with ENOENT.installVersionnow probes<binary> --version(under the version's isolated HOME) after install and fails the install if the binary can't launch, so a broken version is never silently pinned. The check is deliberately narrow — only the missing-binary signature (ENOENT/"no such file"/"command not found") fails it; a plain nonzero exit or a timeout is treated as healthy, so a well-behaved agent that dislikes--versionis never false-failed. Source:apps/cli/src/lib/versions.ts,apps/cli/src/lib/versions-integrity.test.ts. - Security fix: the routines daemon log no longer leaks GitHub / AWS / npm tokens.
daemon.tscarried its own privateredactSecrets(used by everylog()write tologs.jsonl) that predated and diverged from the canonicalredact.ts— it caughtsk-,eyJ…,Bearer …, and a narrowNAME=valuelist, but notghp_(GitHub PAT),AKIA…(AWS access key), ornpm_(npm token), so any of those appearing in a daemon message (a git push URL, a bundle-env dump, an error string) was written to the log in the clear. The private copy is deleted;log()now routes through the canonicalredactSecretsinredact.ts, which covers all of those classes with a stronger quote-awareNAME=valuepattern. The one pattern the daemon copy had and the canonical lacked —Bearer <token>— is added toredact.ts, so the shared redactor (also used by session-transcript export insession/render.ts) is now a strict superset. Newredact.test.tspins every token class as a regression guard. Source:apps/cli/src/lib/daemon.ts,apps/cli/src/lib/redact.ts,apps/cli/src/lib/redact.test.ts. - Fix:
agents teams doctortells the truth, a version fallback never spawns an unspawnable literal, and shims survive a vanished dispatcher (completing this release's self-heal series). Three gaps remained after theagents runself-heal above. (1)agents teams doctorlied — it reportedinstalled: truewhenever a shim file existed, never checking the real binary, so a stub or gutted-native install (the exact codex/kimi failure) showed "ready" and thenENOENT'd at spawn.checkCliAvailablenow verifies the resolved default version is actually installed, and doctor additionally launch-probes each installed agent (verifyInstalledBinaryLaunches) and flips a gutted-native one to not-installed with a repair hint. (2) A version fallback spawned an unspawnable literal — when a specific version was requested (agents run [email protected], the path every version-pinned teammate takes) and no versioned shim existed on disk, the launch left the bare<agent>@<version>name asargv[0], which is not on PATH, so it died withspawn [email protected] ENOENT; it now resolves the version's real binary (getBinaryPath) instead, falling back to the literal only when no binary exists at all. (3) A shim couldn't survive its dispatcher vanishing — when the bakedAGENTS_BIN(often a dev build under~/.local/agents-cli-dev) was removed, moved, or went stale, the shim exited 127 and bricked every managed launch; it now self-recovers to whateveragentsresolves to on PATH before erroring (SHIM_SCHEMA_VERSION→ 25). Also drops a stale, npm-unrecoverablecodex 0.116.0pin from the repo's ownagents.yamlso codex resolves to the machine default instead of self-healing on every run. Source:apps/cli/src/lib/{exec,shims}.ts,apps/cli/src/lib/teams/agents.ts,apps/cli/src/commands/teams.ts,agents.yaml.
1.20.43 — 2026-07-07
- NEW:
agents run --name <slug>— a durable, human/agent-friendly handle for any run. An agent that dispatches another agent had no cheap status handle: the host-task id was never even printed (the--no-followtip showed a literal<id>placeholder), and only Claude's session id is known up front (pre-minted--session-id) — every other agent's id is discovered later by scanning transcripts, so callers fell back toagents logs, which dumps the raw, token-heavy transcript.--nameis chosen at launch, agent-agnostic, and stored on the structures that already back these views: a first-classnamecolumn onsessions.db(schema v9, additive, no rescan) parallel tolabel—agents sessions <ref>resolves against both name and label; the HostTask sidecar (forwarded to the remote run, soagents hosts psgains a NAME column andagents hosts logs <name>resolves by name); and a run-name sidecar (~/.agents/.cache/run-names/) that joins a local run's name onto the index by id every scan viasyncNames— the same idempotent pattern as/renamelabel sync. Thenamecolumn is deliberately left out of the upsertON CONFLICT … SETclause, so a discovery rescan can never null an existing name (regression-tested indb.names.test.ts). Omitting--nameis a strict no-op:namestays unset and every id-based path is unchanged. The--no-followdispatch tip now prints the real handle and steers to the compactagents sessionsdigest over the raw log. Source:apps/cli/src/commands/exec.ts,apps/cli/src/lib/session/{db,run-names,discover}.ts,apps/cli/src/lib/hosts/{dispatch,tasks}.ts. - New terminals (and teammates) no longer launch into a rate-limited account;
balancedis now the default run strategy. Two coupled fixes. (1) A bareagents run <agent>— every new agent terminal the extension spawns, and every non-version-pinnedagents teams add/startteammate, since both route through bareagents run— used to default to theavailablestrategy, which prefers the pinned default version when it looks healthy. But "healthy" was judged by the router'sgetRoutingUsedPercent, which excluded the 5-hour session window and looked at weekly usage only. So a session-maxed account with weekly headroom (e.g. session 100% / week 60%) was deemed eligible and kept getting launched — whileagents viewshowed it "rate-limited" (its badge,deriveUsageStatusFromSnapshot, counts the session window). The router and the badge disagreed. NowhasUsageAvailableshares the badge's exact signal: an account maxed on any blocking window (session or weekly) is ineligible and skipped by bothavailableandbalanced— you never spin up an agent on an account that can't serve the next request. Capacity weighting still ranks eligible accounts by weekly headroom, so a brief session spike doesn't distort long-run routing. (2) The default strategy is nowbalanced(wasavailable): a bare run spreads load across all healthy accounts by remaining headroom instead of sticking to the pinned default. Override per-workspace withrun.<agent>.strategyinagents.yaml, or per-invocation with--strategy/-b. Source:apps/cli/src/lib/rotate.ts,apps/cli/src/lib/usage.ts,apps/cli/src/commands/exec.ts. - [browser] Logins survive browser restarts: sandboxed profiles keep memory-only session cookies, without restoring tabs. Sites that issue login cookies with
expires=-1(idealista, many banking/classifieds sites) logged the profile out on every browser restart, because Chromium purges memory-only session cookies at startup unless the session-restore preference is set — a constraint that had already leaked into agent designs as "sessions can't survive restarts". Every launch now pinssession.restore_on_startup: 1("continue where you left off") in the profile'sDefault/Preferences, which is the switch Chromium's cookie purge actually keys off — and pairs it with--no-startup-windowso the visible side of restore never happens: no window exists at startup for restore to fill, no ghost tabs from the last task reopen, and the task flow creates its own tab over CDP exactly as before. Verified live on Windows/Comet: a memory-only cookie planted pre-restart was still present after a full stop/start, with OS-level window enumeration confirming a single window and zero restored tabs. The Preferences patch runs pre-spawn (browser down, so Chromium can't overwrite it on exit), stamps the profile name only on first launch, skips malformed files untouched, and is a no-op when already set. Electron profiles keep the old name-only seeding — they manage their own storage and need their startup window (the CDP driver binds to it). Bareagents browser start(no--url) recreates the old startup-window affordance by opening a blank page target when none exists, unregistered on the task like the startup window always was. Server-side session TTLs still apply — this removes the restart logout, not the site's own expiry. Source:apps/cli/src/lib/browser/chrome.ts(ensureProfilePreferences, launch args),apps/cli/src/lib/browser/service.ts. - Security fix:
agents sessions --host <target>no longer accepts a leading-dash target (SSH argv-flag smuggling).session/remote.tscarried its own copy ofassertValidSshTargetthat omitted thehost.startsWith('-')guard every other SSH path enforces, so a bare flag like-lor-F/path— which passes the character allowlist — was handed straight tosshas an argument (-oProxyCommand=…-class injection) before any connection. The duplicate validator (and itsSSH_TARGET_RE) is deleted;runRemoteSessionsnow routes through the canonicalassertValidSshTargetinssh-exec.ts, whose dash guard is already regression-tested (ssh-exec.test.ts). Source:apps/cli/src/lib/session/remote.ts.
1.20.42 — 2026-07-07
- Fix: exiting a split pane inside an interactive
ag runsession kicked you out of tmux entirely. When you split the window of an interactive agent session (ag run claude) with Ctrl-b"/%and thenexited your split, the whole tmux client detached and dumped you back to the parent shell — even though the agent was still running in the other pane. Cause:runInTmuxinstalled a session-widepane-diedhook (detach-client) meant to fire only when the AGENT pane exits (so the attach returns and the exit status is read), but with no#{hook_pane}guard it fired for any pane's death. The hook is now scoped to the agent pane; a user split that exits is closed in place (kill-pane, no lingering dead husk) and the agent keeps running full-window. Source:apps/cli/src/lib/exec.ts,apps/cli/src/lib/tmux/session.test.ts. - Every secret-value read is now audited, not just the ones that flowed through the resolver.
agents events --module secrets(or--event secrets.get) is meant to show "every secret accessed or revealed", but several paths read plaintext values without going throughreadAndResolveBundleEnv(the only place that emittedsecrets.get), so they were invisible:secrets push(which reads the whole bundle to upload it — the most sensitive silent read),secrets view --reveal, the rawsecrets get <item>,secrets set <item>(a raw write, nosecrets.set), and the initiating side ofsecrets exec --host/run --secrets bundle@host(only the remote host logged it). Each now emits with asourcetelling you HOW it was read —keychain,agent(served from the unlocked broker),reveal,raw-item,sync-push, orremote(with the targethost) — alongside the bundle, caller, keyCount, and OS-user/host/transport. The resolved value is never written to the log, only names and counts. Allsecrets.*events are now taggedmodule: 'secrets'so--module secretsactually surfaces the value reads (previously it matched only the coarse command events). Note: the event log has a 7-day retention, so export what you need for long-term records. Source:src/lib/secrets/bundles.ts,src/lib/secrets/sync.ts,src/lib/secrets/remote.ts,src/commands/secrets.ts,docs/06-observability.md. - Fix:
sessions --activeshowed the SAME preview + topic for every co-located session. Multiple Claude sessions in one cwd (e.g. several editor tabs, or two worktree siblings) all rendered identical activity — they looked like duplicate cards.findClaudeSessionFilefell back to the newest.jsonlin the cwd whenever a session's<id>.jsonlwasn't found, so every distinct session collapsed onto ONE file's preview/topic. The stale-id trigger: an editor caches the launch uuid inlive-terminals.json, but Claude rotates its transcript uuid on resume/compact, so the cached id no longer matches any file. Now the terminal path resolves each tab's EXACT id from the pid registry (mirroring the headless path), the newest-file fallback is gated to the no-id case (pickSessionFile), and an unresolvable file reads asidlerather thanrunning. Source:apps/cli/src/lib/session/active.ts. - Fix: one malformed Kimi session blanked the WHOLE
agents sessionslisting. A Kimistate.jsonwith neithercreatedAtnorupdatedAtmadereadKimiMetareturn anundefinedtimestamp, which bindsNULLinto thetimestamp TEXT NOT NULLcolumn and aborts the entire batch index — so a single bad session took down the listing for every session, not just itself. Two layers:readKimiMetanow coerces the timestamp to never-null, falling back to thestate.jsonmtime (matching how the listing already ranks Kimi vialast_activity, like every other parser); andupsertSessionsBatchwraps each row in a per-row guard so a future constraint-violating row skips itself (ledger deliberately not stamped, so the next scan re-tries it) instead of rolling back the whole batch. Source:apps/cli/src/lib/session/discover.ts,apps/cli/src/lib/session/db.ts.
1.20.41 — 2026-07-07
- NEW:
agents sessions focus [id]— one command to get back to a session, however it's reachable. It attaches a live session in place (tmuxswitch-client/attach-session, a remote tmux overssh -tt, or a Ghostty tab — joining the live process without forking); where there's no live terminal to attach, it opens a new tab and resumes the session — locally, or on the remote peer over SSH (runOnPeer, so the peer resolves the version-pinned binary). No id opens the rich live-session picker (this-machine first). Reuses the live-session detection and the terminal launch engine (openSurfaces), and foldsgo's attach paths in. Source:src/commands/focus.ts,src/commands/go.ts. --deviceis now a first-class alias of--hoston every host-routable command (sessions,run, …), registered centrally onaddHostOptionso a local fall-through no longer errors. Source:src/lib/hosts/.agents computersteers Electron/webview targets over CDP instead of reporting a fake success when the native-automation path can't reach them (#716).- Secrets: the "remember" policy hold now lasts 7 days and survives screen-lock, instead of re-prompting after every lock/sleep; stale copies are evicted when a policy is tightened. Source:
src/lib/secrets/. - Fixes: shim
machine_id()normalizes to matchnormalizeHost(), and shim resolution honors the per-device default pin (not just the centralagents.yaml). agents sessions gois retired as a deprecated alias foragents sessions focus --attach-only.gowas already a strict subset offocus— its only unique behavior was "attach the live terminal or refuse, never fork/resume." That behavior is now a first-class--attach-onlyflag onfocus(focus.ts:selectFallback()picksrefuseFallbackunder--attach-only, else the resume-in-a-new-tab fallback).gonow prints a one-line deprecation notice and delegates tofocusAction(id, { attachOnly: true }); the shared reach engine (jumpTo/gatherLiveTargets/pickLiveTarget/refuseFallback) still lives ingo.tsand is imported byfocus.ts. Source:src/commands/go.ts,src/commands/focus.ts.agents sessions --json --host <h>now emits a clean JSON array of recent (non-active) sessions instead of the legacy per-host raw banner stream, so a UI can fetch a remote device's recent sessions when it has no live agents.serializeSessionsJson()is shared by the local and remote--jsonpaths;runRemoteSessionsJson()reuses the existinggatherRemoteListSSH fan-out. The non-JSON banner path and--activeare unchanged (#711).
1.20.36 — 2026-07-07
[windows] agents sessions --active detects sessions on Windows, and shim launches carry cwd + session identity everywhere
The active listing found nothing on Windows: the headless scan shelled out to
ps -Aand per-pidlsof— both POSIX-only, both failing silently into "No active agent sessions" with a dozen liveclaude.exeprocesses running. The process table now comes from one CIM query on win32 (powershell.exe Get-CimInstance Win32_Process;wmicis removed on current Windows 11) parsed into the same pid/ppid/comm rows, agent-kind matching strips the.exeimage suffix case-insensitively (POSIX comms stay exact-match — macOS's Claude desktop app process is namedClaudeand must not be listed), and the ancestry walk recognizes Windows terminal hosts (Code.exe,Cursor.exe,VSCodium.exe,Windsurf.exe,WindowsTerminal.exe). Where no cwd can be recovered (nolsofon Windows), same-kind child agent processes — Claude runs subagents and its bundled ripgrep as childclaudeprocesses — fold onto their root candidate (foldSubordinateAgents) instead of printing one row per fork; on POSIX those children collapsed via shared-cwd session dedupe, which now accumulates pre-folded pid counts instead of resetting them. Verified live: 6 rootclaude.exeprocesses render as 6 rows (previously zero). Source:src/lib/session/active.ts.Those Windows rows grouped under
unknownwith no topic because onlyag runrecorded a pid → session/cwd registry entry. The transparent shim delegate (execShimPassthrough) — the path everyclaude/codextyped into a terminal actually takes — now writes the sameby-pidregistry entry at spawn: agent, launch cwd, and the exact session id when the caller passed--session-id(extractSessionIdArg; whole-arg match only, a uuid inside a prompt never counts). On the win32.cmdshell path the recorded pid is the cmd.exe intermediary rather than the agent binary, so the active scan resolves entries by walking a candidate's ancestors (readAncestorSessionEntry), accepting only a matching agent kind — a claude session shelling out to codex can't hand codex its identity — and the fork-fold keeps a descendant with a wrapper entry below its fold target as its own row (a claude launched from inside another claude session is a real second session, not a fork). Net effect: Windows shim launches list with their project directory, exact session id, and topic instead ofunknown. (POSIX bash shimsexecthe binary directly without the delegate, so they are unchanged and keep relying on lsof-recovered cwd + newest-jsonl.) Source:src/lib/exec.ts,src/lib/session/pid-registry.ts,src/lib/session/active.ts. [windows]browser profiles createno longer hands out a port an already-running browser is listening onfindFreeProfilePortprobed candidate ports by shelling out tolsof, which doesn't exist on Windows — the ENOENT was swallowed by the "assume free" catch, so every port in 9222–9399 scanned as free. The first profile created without--endpointwas assignedcdp://127.0.0.1:9222, and if the user's own browser was running with--remote-debugging-port=9222(a common Comet/Chrome setup), the new profile silently attached to that browser instead of launching its own sandboxed instance — tabs then opened in whatever profile the user had on screen. The scan now routes throughisPortInUse(chrome.ts, newly exported), the same platform-aware probe the launcher already used:lsofon POSIX,netstat -anoon Windows. Regression-tested with a real bound socket, no mocks, so the probe is exercised per-platform in CI (src/lib/browser/chrome.test.ts). Source:src/lib/browser/profiles.ts,src/lib/browser/chrome.ts.
[windows] Background spawns no longer flash console windows while agents run
- Every background chain root — the scheduler daemon, the auto-pull worker, the PTY sidecar server, the routine runner's job spawns, detached ssh tunnels — was spawned
detached: truewithoutwindowsHide. On Windowsdetachedmaps toDETACHED_PROCESS, under which CreateProcess ignoresCREATE_NO_WINDOWand the child runs console-less, so every console-subsystem descendant (powershell.exe for a Credential Manager read, git, node, a.cmdshim's cmd.exe wrapper) allocated its own visible console window — the "PowerShell windows popping up and closing while I type" bug. The worst repeat offender: the console-less daemon resolves secrets bundles throughpowershell.exeon every session-sync cycle (90s). The newbackgroundSpawnOptions()(src/lib/platform/process.ts) is the single place that decides the pattern: POSIX keepsdetached: true(own process group, group-kill still works); Windows switches towindowsHide: truewith no detach — the child owns a hidden console that all descendants inherit (nothing down the tree can flash) and that no launcher console-close event can reach, preserving the #556 daemon-teardown fix. Verified with a live Win32 probe (GetConsoleWindow+IsWindowVisible): the old pattern yieldsVISIBLE=True, the new pattern allocates no console window at all. Leaf spawns of console tools reachable from a console-less parent (powershell insecrets/windows.ts+platform/winpath.ts, tasklist/netstat/taskkill in the browser runtime probes,tailscale status, ssh, ffmpeg) now passwindowsHide: trueas defense in depth for callers this release can't re-parent (e.g. an already-running daemon). Source:src/lib/platform/process.ts,src/lib/daemon.ts,src/lib/auto-pull.ts,src/lib/pty-client.ts,src/lib/runner.ts,src/lib/ssh-tunnel.ts, plus the leaf call sites. - Fallout the hidden console surfaced (caught by the real-advapi32 round-trip test): the Credential Manager driver's
setread the secret from stdin as text via[Console]::In.ReadToEnd(), decoded with the console codepage — correct only when the caller's console happened to be UTF-8 (Windows Terminal). Under a fresh hidden console (OEM cp437), or any console-less caller like the daemon, non-ASCII secrets corrupted on write (café ☕stored ascaf├⌐ Γÿò). The static PS script now reads stdin as raw bytes ([Console]::OpenStandardInput()→MemoryStream) and pins[Console]::OutputEncodingto UTF-8, so the round-trip is codepage-independent in every calling context. Source:src/lib/secrets/windows.ts. - Correction for the fd-redirected roots (daemon, runner, PTY server):
windowsHideis inert whenever a stdio slot is redirected to an fd — libuv skipsCREATE_NO_WINDOWif any stdio fd is inherited, and log-file redirection counts. A non-detached daemon therefore shared its launcher's console and died on the launcher's console-close event the momentagents daemon startreturned (the #556 failure, reproduced live: childalive-after-launcher-exit=falseunder{detached:false, windowsHide:true}with fd stdio,trueunder{detached:true, …}).backgroundSpawnOptions({ fdStdio: true })now keepsDETACHED_PROCESSfor these roots — the child runs console-less and windowless, and its console-tool spawns stay invisible via the leafwindowsHidefixes above. Fully-piped/'ignore'roots (auto-pull, detached ssh tunnels) keep the hidden-console pattern, which the Win32 probe validated. Regression-tested: a hidden-console launcher spawns an fd-redirected child and exits; the child must survive (src/lib/platform/process.test.ts).
[teams] agents teams pr-watch <team> — autonomous PR lifecycle: CI-fix waves + review-comment routing (Closes #338)
- A team's teammates open PRs;
pr-watchwatches them and reacts without a human in the loop. Each poll it resolves the PRs the team opened (from each teammate'spr_url, else thegh pr createdetected in the session it ran), snapshots CI + review comments via theghCLI (gh pr checks --json,gh api …/pulls/{n}/comments), and decides follow-ups: RED CI spawns a fix teammate--afterthe one that failed, with the failing-run logs (gh run view --log-failed) injected so it pushes a follow-up commit to the same PR branch; a new review comment routes abugfixteammate (the existingTaskType)--afterthe source, with the comment body injected. Both slot into the team DAG the supervisor already drains — the loop callsstartReadyeach pass so staged fixers launch when their source completes — and every reaction is visible inagents teams status. Dedupe is by check-run id / comment id, persisted topr-watch-<team>.json, so the same failure or comment never spawns twice across restarts. The decision logic (decidePrActions) is a pure function over injected snapshot data (unit-tested insrc/lib/teams/pr-watch.test.ts, no network); theghcollectors and thehandleSpawn-backed reactor sit on top. Deferred (documented follow-up): the event-driven path from #331's webhook receiver —pollPrSnapshotis the seam where acheck_run/pull_request_review_commentpayload plugs in, producing the samePrSnapshotthe pure decider already consumes. Source:src/lib/teams/pr-watch.ts,src/commands/teams.ts.
[hosts] --host/--device now resolves registered devices and ad-hoc user@host — one concept, one flag
- Offloading a run no longer needs a machine enrolled in two registries.
agents run --host <name>(and the new--device <name>alias, plusteams add --host, and every other--hostconsumer via the shared resolver) now resolves in order: theagents hostsregistry (unchanged), then theagents devicesregistry, then an ad-hocuser@host. A machine registered once withagents devices syncis reachable immediately — previously it erroredUnknown hostunless you also ranagents hosts add. The fall-through lives in one place (resolveHost), so it's not a per-command band-aid. A bare unknown name still returns null so capability-tag routing (--host gpu) is unaffected; only a name containing@is treated as an ad-hoc target (validated byassertValidSshTarget). A device that authenticates by password can't offload over the BatchMode ssh path, so it throws a typed, actionableDeviceOffloadUnsupportedError(switch to key auth or enroll as a host) instead of dispatching a run that would hang. Source:src/lib/hosts/registry.ts,src/commands/exec.ts,src/lib/hosts/option.ts.
[secrets] recover credentials orphaned under a stale keychain access group (RUSH-1413)
- Secrets written before the access-group pin (#279, first shipped v1.20.27) were filed by macOS under the implicit default group — the literal wildcard
2HTP252L87.*, not the concrete2HTP252L87.com.phnx-labs.agents-keychainthat every query now pins (keychain-helper.swiftdpBase). Those items are intact and the wildcard entitlement authorizes reading them, but the pinned queries never ask for that group, sohas/get/listreported them missing and whole bundles vanished fromsecrets list(their metadata was orphaned too). On one machine this stranded 43 items including the ssh private key, the release signing key, and identity secrets. The helper now recovers them on three levels: (1)readItem/hasadd an un-pinned data-protection fallback pass after the pinned miss, so an orphan reads and reports present instead of missing; (2)get/get-batchre-home an orphan inline the first time it's read — reusing the read's Touch ID, add-before-delete, deleting the exact orphan bykSecValuePersistentRef— mirroring the existing file-basedmigrateInline; (3) a newmigrate-orphanshelper verb bulk re-homes every orphan behind a single Touch ID.listis now un-pinned so orphaned bundle metadata reappears, andset/deleteclear across all groups so a rotate/delete can't leave a shadow copy. Newlist-orphansverb enumerates orphans prompt-free. Source:src/lib/secrets/keychain-helper.swift,src/lib/secrets/index.ts. agents secrets migrate-aclnow sweeps orphaned-access-group items in addition to legacy-ACL stragglers: the dry-run lists both classes,--commitre-homes the orphans in one batched Touch ID (add-before-delete needs no pre-write backup), and any listed orphan the helper can't reach (e.g. under a different signing team) is surfaced, never dropped silently. Because every published helper shares team2HTP252L87and the same wildcard entitlement, one run recovers every affected user losslessly. Source:src/commands/secrets-migrate.ts,src/lib/secrets/index.ts(listOrphanedKeychainItems,migrateOrphanedKeychainItems,parseOrphanMigrationOutput). The signed helper must be rebuilt + re-signed + notarized and its sha re-pinned (scripts/build-keychain-helper.sh,scripts/Agents CLI.app.sha256) at release, per the standard keychain-helper release step.
CI: audit-event tests are green on Windows; the release re-gates on the windows-latest matrix legs (RUSH-1412)
- The cross-platform matrix (
ci.yml, runs only onrelease/**+v*) had bothbuild (windows-latest, …)legs red:tests/events-audit.test.tsandtests/teams-events.test.tsspawn the CLI with a redirectedHOMEand then read the audit trail under it, but the events writer rooted its log dir at a bareos.homedir()(src/lib/events.ts:24). On Windowsos.homedir()resolves fromUSERPROFILEand ignores aHOMEoverride, so everycommand.start/command.endrecord was silently written to the real profile instead of the test's temp home — the events array came back empty and the log fileENOENT'd (macOS/Ubuntu were green becauseos.homedir()honors$HOMEon POSIX). The writer now roots its log dir throughstate.getLogsDir(), the single canonical home anchor (process.env.HOME ?? os.homedir()), which honors an explicitHOMEon every platform and still resolves toUSERPROFILEin production on Windows (whereHOMEis unset), so real users are unaffected. Oneevents-auditcase also reconstructed its log filename from a UTCtoISOString()while the writer names files from the local date, so itENOENT'd whenever a runner's local and UTC dates straddled midnight; it now globs the log dir like the other assertions.scripts/release.shrestores bothbuild (windows-latest, 22|24)entries toEXPECTED_CHECKS, so Windows is a release gate again. Source:src/lib/events.ts,tests/events-audit.test.ts,scripts/release.sh. - Three more
build (windows-latest, …)failures fixed. (1) Antigravity sessions were invisible to Windows users, not just tests.parseAntigravityread its conversation SQLite DBs by shelling out to thesqlite3CLI (src/lib/session/parse.ts:893), which is absent on Windows — soexecFileSync('sqlite3', …)threwspawnSync sqlite3 ENOENTand the parser silently returned[]for every real Antigravity session on Windows. It now reads thestep_payloadBLOBs through the runtime-agnosticsrc/lib/sqlite.tswrapper (node:sqlite / bun:sqlite, the same path production already uses for the session index), so it works on every OS with no CLI dependency;parse-antigravity.test.tsbuilds its fixture DB through the same wrapper instead of the CLI. (2)parse-droid.test.tsderived itstestdatadir fromnew URL(import.meta.url).pathname, which on Windows yields/C:/…— sopath.joinproduced a doubled-driveC:\C:\…thatENOENT'd; it now usesfileURLToPath(import.meta.url). (3)git.test.ts'ssyncRepoGit"pull-only" case failed because the Windows runner'score.autocrlf=trueconverted the freshly-clonedREADME.mdto CRLF duringgit clone— beforeconfigIdentity()could setautocrlf=falseon the clone — sostatus.isClean()saw a phantom modification andsyncRepoGitrefused with "Working tree has uncommitted changes." The test seed now commits a.gitattributes(* -text), which wins overautocrlfat checkout time so every clone lands byte-identical LF content. (parseOpenCodeshells out tosqlite3the same way and has the same latent Windows gap, but its test mocksexecFileSyncso CI never caught it and its argv-injection regression test pins the CLI call — left untouched to avoid scope creep.) Source:src/lib/session/parse.ts,src/lib/session/__tests__/parse-antigravity.test.ts,src/lib/session/__tests__/parse-droid.test.ts,src/lib/git.test.ts.
agents message <target> <text>: deliver a message to an already-running agent mid-flight [RUSH-1415]
- One verb now reaches a live agent while it works, not just a cloud task.
agents message <id> <text>resolves the target to exactly one destination and routes it: a cloud task id takes the existing provider follow-up path (wasagents cloud message); a live local/teams/loop agent gets the text enqueued into a per-agent file-spool mailbox that aPreToolUsehook drains and injects at the agent's next tool call.resolveMessageTarget()is the anti-misroute gate — exact id wins over prefix, results de-dupe by canonical mailbox id, and a target matching zero or more-than-one live agent (or an empty string) is never guessed: the command errors with the candidate list.--from <who>records a sender label;--host <h>routes the whole command over SSH (viaREMOTE_PASSTHROUGH) to the box that owns the agent, andmessageregisters as a lazy SQLite-backed command likecloud/sessions/teams. Source:src/commands/message.ts,src/lib/mailbox-target.ts,src/lib/hosts/passthrough.ts,src/lib/startup/command-registry.ts. - The mailbox itself is a crash-safe file-spool under
~/.agents/.history/mailbox/<id>/{inbox,processing,consumed}/. Enqueue is atomic (temp-write +rename); drain is claim-first (inbox → processing → consumed) so an interrupted drain is recovered on the next call (at-least-once delivery; consumers dedup bymsgId). Every message stamps atofield and a monotonic FIFOmsgId; a message that lands in the wrong box or fails to parse is archived and dropped, never delivered or looped. A mailboxId must be a single separator-free path segment ([A-Za-z0-9._-], not./..) — validated at the id→path boundary and the write-timetostamp so a traversal-bearing id fails loud instead of silently misrouting. At spawn,buildExecEnvpoints each agent at its own box viaAGENTS_MAILBOX_DIR(keyed by session id); a loop overrides it to the run-level box so every iteration shares one inbox, and printsagents message <runId>at start since the runId is otherwise undiscoverable. Source:src/lib/mailbox.ts,src/lib/state.ts,src/lib/exec.ts,src/lib/loop.ts.
Watchdog core: stall detection + nudge decision for a stalled agent (#612) [RUSH-1415]
- Ports the pure, fs/vscode-free watchdog core so agents-cli can decide when a running agent has stalled and what to say to un-stall it:
classifyTerminal+isLikelyTrulyBlocked(blocked / waiting / completion-hint signals plus a promise-without-toolcall detector),renderWatchdogPrompt/composePromptWithPlaybook/WATCHDOG_SYSTEM_PROMPT, and a tolerantparseWatchdogResponse.summarizeWatchdogTailextracts the last user/assistant turn across Claude/Codex/Gemini transcript shapes and filters synthetic<system-reminder>-style tags. The session-tail reader seeks backward from EOF for the last N JSONL lines and resolves a transcript fromsessionId + agentby reusinggetAgentSessionDirs()rather than hardcoding paths — including the recursivewalkForFileswalk that reaches Codex's deepsessions/YYYY/MM/DD/rollout-…jsonllayout and Gemini's tmp layout, driven per-agent byWATCHDOG_SESSION_LAYOUT. Source:src/lib/watchdog/watchdog.ts,src/lib/watchdog/watchdogTail.ts,src/lib/watchdog/read.ts,src/lib/watchdog/index.ts.
Terminal injection: type into an already-running agent's exact terminal (#611, #616) [RUSH-1415]
injectIntoTerminalextends the Terminal Engine to type into a running surface, not just open new ones — the primitive a native watchdog needs to nudge a stalled agent with "continue" delivered into the precise terminal it lives in. It mirrors the engine's shape: pure per-backend spec builders produce aLaunchSpecrun through the samerunSpectransport, so injection inherits local/remote (--hostover SSH) execution for free. Backends: tmuxsend-keys -t <pane>(socket-addressed), itermtell session id "<uuid>" to write text(noactivate, so it addresses the exact split without stealing focus), vscodium (VSCodium/Cursor/VS Code) over the editor CLI's--open-urlinto the extension's/injectverb, and pty via the agents-pty sidecar (local-only). Ink-TUI Enter semantics: text and Enter are two separate writes by default (a fusedtext\ris swallowed by Claude's Ink TUI), andcombinedopts into the single fused write for plain shells. Source:src/lib/terminal/inject.ts,src/lib/terminal/index.ts.resolveInjectTargetis the single resolver the watchdog calls:sessionId →a preciseInjectTargetor an honest{ addressable: false, reason }, with precedence tmux > iterm > vscodium > pty and a deliberate safe skip for Ghostty (no addressable split API).deriveProvenancenow captures$ITERM_SESSION_IDand, absent tmux, exposes anitermreply rail carrying the iTerm2 session UUID — tmux still wins whenever present because a pane is reachable inside any host app.agents sessions inject <id> <text>is the CLI face: it resolves an active session to its provenance reply rail and routes to the matching backend, with--pane/--ptyto target a backend directly,--combinedto toggle the Ink-safe two-write default,--no-enterto send without submitting, and--hostto inject over SSH. Source:src/lib/terminal/resolve.ts,src/lib/session/provenance.ts,src/lib/session/inject.ts,src/commands/sessions-inject.ts.
Watchdog consumer + agents watchdog: run one stall-detection tick end to end (#619, #622) [RUSH-1415]
runWatchdogTickties the pure pieces together into one pass overgetActiveSessions():classifyTerminal()finds stalls,readWatchdogTail()reads the transcript,isLikelyTrulyBlocked()gates on the promise-without-toolcall heuristic (deterministic v1) or an optional--smartLLM decider,resolveInjectTargetForSession()is the absolute safety gate, andinjectIntoTerminal()deliversContinue.into the EXACT split. A nudge fires ONLY onaddressable:true; anaddressable:falsestall is flagged to a tray-readable state file and skipped — never a guessed target. Per-session policy isoff|keep|handsoff(handsoff detects and flags but never injects); cooldown and un-addressable flags persist under~/.agents/.cache/state/watchdog/. Theagents watchdogcommand runs it without the menu-bar: bare = one dry tick (reports would-nudge/skip + why),--nudgeinjects for real,--watchis a daemon loop (--interval, default 30s),--jsonis machine-readable, and--stall/--cooldown/--dormantoverride thresholds.runner.test.tsdrives real synthetic sessions through the pure logic (nothing mocked) with dry-run injection. Source:src/lib/watchdog/runner.ts,src/commands/watchdog.ts,src/lib/startup/command-registry.ts.- The macOS menu-bar helper now auto-nudges from its native tick:
StatusItemController.tick()reads the enable sentinel and runs one watchdog tick (nudge=enabled, detect-only when off), a checkable Auto-nudge menu row toggles it viaagents watchdog enable|disableand showsN stalled · M nudged, andAgentsCLIgainswatchdogStatus()/watchdogTick(nudge:)/watchdogSetEnabled()mirroring thedoctorOverview()shell-and-decode pattern.refreshWatchdog()is throttled to a 30s floor (siblings: doctor 60s, routines 20s) so it doesn't spawn two node subprocesses on every 10s tick — still well under the 5-minute stall threshold. Source:packages/menubar-helper/Sources/MenubarHelper/StatusItemController.swift,packages/menubar-helper/Sources/MenubarHelper/AgentsCLI.swift,src/commands/watchdog.ts.
VSCodium / Cursor / VS Code terminal backend (#608, #620)
- A new
vscodium-agentterminal backend opens each resumed session as an agent-terminal tab in a running VSCodium / Cursor / VS Code window — via theswarm-extextension's/spawnURI verb — instead of scripting a GUI terminal app. It builds<cli> --open-url '<scheme>://swarmify.swarm-ext/spawn?…'(default VSCodium:codium/vscodium://); the editor CLI forwards the URL over its IPC socket, so it needs no OS scheme handler, works on Linux, and flows over--host(SSH) like the other backends — with nozsh -ilcwrap since the target is already an interactive login shell. The{command, cwd, split}payload is base64url-encoded into a single query param because VS Code percent-decodesuri.queryonce before the handler parses it (a bareecho a && touch bwas otherwise truncated at the&). Wired intosessions resumeas--vscodium; auto-detect is intentionally omitted (TERM_PROGRAM=vscodecan't disambiguate the three products). Because VSCodium agent terminals open as individual full-width editor tabs, this backend defaults packing to one tab per session (--tabsstill forces tabs elsewhere). Source:src/lib/terminal/backends/vscodium-agent.ts,src/lib/terminal/index.ts,src/commands/sessions-resume.ts.
agents sync <repo>: git-sync a single DotAgent repo (#535)
- Giving a DotAgent repo name alone —
agents sync system/agents sync user/agents sync <alias>— now git-syncs just that one repo instead of running the umbrella reconcile. The newsyncRepoGitrefuses on a dirty working tree (commit or discard first), otherwisegit fetch origin+git pull --rebase origin <branch>against the repo's own HEAD branch (falling back tomain), reinstalls the git hooks, and reports the resulting short commit. Theuserrepo and enabled extra-repo aliases alsogit pushlocal commits up;systemis a pull-only mirror of the npm-shipped upstream (push: false).projectand unknown names are rejected — the project.agents/lives inside the user's own repo and isn't independently synced. This repo-name form is matched before agent-spec parsing, since names likesystem/userwould otherwise failparseAgentSpec. Source:src/lib/git.ts,src/commands/sync.ts. - Bare
agents syncno longer eager-fetches secrets and sessions: the umbrella planner now defaults to config repos + reconcile only, with secret bundles and session transcripts made opt-in via--secrets/--sessions(pulling every secret bundle onto a machine was more blast radius than a bare sync should carry; transcripts stay queryable on demand viaagents sessions --host <machine>). Interactive bareagents sync(TTY, no flags) now drops into a two-checklist picker — which repos to sync FROM, which installed agents to sync INTO — then pull-only freshens the selected repos and reconciles a single merged selection into each agent, unioned across repos viamergeRepoScopedSelections/unionResourceSelections. Source:src/lib/sync-umbrella.ts,src/lib/versions.ts,src/commands/sync.ts.
Split agents.yaml into portable, per-device, and machine-local files (#538)
- The committed central
~/.agents/agents.yamlused to carry machine-specific fields and was held back with agit skip-worktreeband-aid so it wouldn't sync. It's now partitioned by sync-domain:agents:(version pins) moves to per-device~/.agents/devices/<machineId>/agents.yaml(committed and synced, but each machine only writes its own folder so pulls never conflict),versions:(per-version resource tracking) moves to gitignored, machine-local~/.agents/.history/version-resources.json, and centralagents.yamlis left portable.writeMetaUnlockedwrites the device and history files BEFORE stripping and rewriting central, so a crash mid-write never drops pins/versions before they persist;readMetaoverlays the machine-local files back on viaoverlayMachineLocal(device pins win and self-heal a pre-migration central). Source:src/lib/state.ts,src/lib/machine-id.ts. - Migration
migrateSplitDeviceLocalMeta(sentinel bumped tov11) performs the one-time split on raw YAML, merging into any existing device/history files (existing entries win) viaatomicWriteFileSync, and only rewrites central when it actually carries machine-local fields — a portable-onlyagents.yamlis left byte-untouched — while always clearing theskip-worktreebit so every machine's file syncs cleanly. The meta cache stamp is now a|-delimited string of all four source files' mtimes rather than a numeric sum that could round sub-unit device/history changes away and serve stale reads in long-lived processes.machineId()/normalizeHost()were extracted to a dependency-free leaf module so low-levelstate.tscan key per-device paths without an import cycle. Source:src/lib/migrate.ts,src/lib/machine-id.ts,src/lib/session/sync/config.ts,src/index.ts.
agents sessions: the interactive picker now shows origin machine, PR/ticket, and worktree columns
- Every discovered session carries the machine it originated on — the local box for live-home transcripts, or the origin host parsed from the cross-machine mirror layout (
backups/<agent>/<machine>/…) — recorded onSessionMeta.machinebydiscoverSessions. The picker row, previously stuck onshortId · agent · version · project · topic · when, now folds in a gray machine column (only when the pool spans >1 box, with the longest shared dash-delimited prefix stripped soyosemite-s0/yosemite-s1read ass0/s1), a bluePR#/ticket column (only when some row carries a ref), and a magentawt:<slug>worktree badge. Column flags are computed once over the whole pool viapickerColumnsForand shared by both the browse picker and the multi-select resume picker, and the topic width is now terminal-aware so the extra columns never wrap. - A dim
subtitlehint line renders between the header and the rows (newsubtitlefield onPickerConfig/SessionPickerConfig), rotating aTip:that surfaces the filter flags (-a/--agent,--project,--all,-H/--host,--since/--until), keyed off pool size so it stays fixed across re-renders. Fixed a wrap bug where the resume picker prepends a 6-cell> [x]gutter butformatPickerLabelreserved only the 2-cell single-select cursor, overflowing every row by 4 cells and halving the viewport; the gutter width (2 browse, 6 resume) now threads throughPickerColumnsand is reserved from the topic width. Source:src/commands/sessions.ts,src/commands/sessions-resume.ts,src/commands/sessions-picker.ts,src/lib/picker.ts,src/lib/session/discover.ts,src/lib/session/types.ts.
Reach Windows peers over --host (RUSH-1429)
- The SSH command layer gained a PowerShell dialect so
--hostoperations can target Windows remotes, where ssh lands incmd.exe/PowerShell andbash -lcdoes not exist.remoteShellFor(os)routeswindows → powershelland everything else (including unknown/absent) → posix, so linux/macOS never regress;buildWindowsAgentsCommandemitspowershell -NoProfile -EncodedCommand <base64-utf16le>, which survivescmd.exere-parsing with zero quoting hazards. The peer OS is resolved from the tailscale-synced device registry (fleet fan-out) or the enrolledHostEntry.os(explicit--host). This fixesagents sessions --host/--activeand remote secrets reads (browse + use-a-remote-bundle), which previously wrapped the remote invocation inbash -lcand got'bash' is not recognizedfrom a Windows peer. Thesecrets export/import --hostwrite path stays POSIX-only for now (documented follow-up). Source:src/lib/hosts/remote-cmd.ts,src/lib/hosts/remote-os.ts,src/lib/devices/registry.ts.
Windows portability + CI hardening
agents sessions … resumeno longer crashes on Windows withspawn EFTYPE:resumeSessionInPlacespawned the version-pinned launcher ([email protected]) withshell:false, but on Windows that shim is a.cmd/extensionless file, so spawn threw synchronously and the error was mis-reported as a discovery failure. It now spawns through the shell on Windows vianeedsWindowsShelland reports a synchronous launch failure truthfully. The generated hook-cache shim also hardcodedpython3for its hash/timer/mtime, but on Windowspython3is often the Microsoft Store execution-alias stub (prints to stderr, exits non-zero, 0 bytes) — silently emptyingmtimeso every call missed the cache and re-ran the hook; it now probes for a runnable interpreter (python3, thenpython) by executing-c 'import sys'. Source:src/commands/sessions.ts,src/lib/hooks/cache.ts.- Two new CI guards keep these Windows-only, separator-prone bugs from reaching a release: a path-filtered
test-windowsjob runs the suite onwindows-latestfor changes under hooks/platform/shims (the requiredtestgate runs onubuntu-latest, wherepath.sepis/, so a backslash-path bug is invisible), andtoPortableCommandis now pure/exported with injectable home + separator so a unit test can assert WindowsC:\…→~/…folding on any host. Separately, aprepare: npm run buildhook rebuilds the gitignoreddist/on every install/link (and beforenpm publish), so a dev-linked checkout can't silently run a staledist/behind a source fix. Source:.github/workflows/tests-windows.yml,package.json.
License: MIT → Apache-2.0 (#504)
- The project relicenses from MIT to Apache-2.0.
LICENSE,README, andpackage.jsoncarry the new license, and the human-facing docs (theAGENTS.mdbrand lines, theCONTRIBUTING.mdCLA clause,DESIGN.md) were aligned so the stated license is consistent everywhere.
Security hardening batch (#474–#478)
- Shell / option injection.
agents inspectno longer builds itsgitcall as a shell string: a crafted repo path could inject via$(…)or other shell syntax throughexecSync(\git -C ${…} ${args}`). It now uses argv-formexecFileSync('git', ['-C', root, …args]), so the path can never reach a shell (#474). Separately, MCP server management rejects a server name that starts with-or contains whitespace/control characters and places every user-controlled positional after--, closing an option-injection vector (#478). Source:src/commands/inspect.ts,src/lib/mcp.ts`. - Path-traversal containment. Plugin resolution rejects a plugin name that resolves to the plugins root itself, so a crafted name can't escape or target the directory root (#475). Hook-shim generation validates the shim name before constructing any path and asserts the resolved shim path stays inside the shims directory — rejecting separators, traversal components (
..), NUL bytes, and leading dashes (#477). Source:src/lib/plugins.ts,src/lib/hooks.ts,src/lib/hooks/cache.ts. - Supply-chain. Per-version agent installs now run
npm install --ignore-scripts, so a dependency's install/postinstall lifecycle script can't execute arbitrary code during anagentsversion install (#476). Source:src/lib/versions.ts.
1.20.35 — 2026-07-07
CI: build node-pty's native binary on macOS/Windows so the release matrix is green cross-platform
- The cross-platform matrix (
ci.yml, runs only onrelease/**+v*) installed deps withbun install --ignore-scripts, sopty.nodefrom@homebridge/node-pty-prebuilt-multiarchwas never fetched/built. That package ships prebuilt binaries only for Linux; macOS/Windows obtainpty.nodevia its own install script (prebuild-install download, else a node-gyp compile). With that script skipped the native module was absent, so the daemon-liveness integration test added in #568 — which spawns the real daemon (it loads node-pty) and asserts the browser IPC socket stays up — crashed on macOS/Windows while passing on Linux, and had been red on every release since. The matrix runs only on release branches, so it never surfaced on normal PRs (bun does not run that install script even without--ignore-scriptsin bun 1.3.x). CI now runs a dedicated step that invokes the package's own install script (npm run install), which prefers a prebuilt download and falls back to a node-gyp compile, so it self-heals across platforms and node ABIs. Production (npm install) already built the native module, so end users were unaffected. A second macOS/Windows-only failure in the same #568 daemon-liveness test was also fixed: the test rooted its fakeHOMEunderos.tmpdir(), which on macOS is the long/var/folders/…/T/…, pushing the daemon's AF_UNIX socket path to ~116 bytes — past macOS's 104-bytesun_pathlimit — sobind()failed withEADDRINUSE. The test now rootsHOMEat a short base on POSIX (Windows uses length-unlimited named pipes); real users with a normalHOMEwere never affected. Source:.github/workflows/ci.yml,src/lib/daemon.test.ts.
agents logs: a top-level, unified run-log viewer (#575)
- Viewing a dispatched run's output used to be nested and undiscoverable — only
agents hosts logs <id>andagents daemon logsexisted, andagents hostswasn't even in--help.agents logs [id]is now a discoverable top-level command that resolves a run across two substrates — host-dispatch task stdout (agents run --host) and the local session index — and shows or (-f) follows it.[id]/--sessionload directly (host task tried first, then session); with no id,--host/--agent/--versionfilter a merged candidate list (one match shows, several open a fuzzy picker, non-TTY prints the list). Additive:agents hosts logsandagents sessions tailare unchanged and share the same helpers. Source:src/commands/logs.ts,src/lib/hosts/logs.ts.
Host-follow log tailer: no self-corruption on localhost, byte-accurate offsets (#586, #589)
- Following a run dispatched to localhost tripled the on-disk log and triple-printed the output, because the local mirror file and the remote log were the same file and the tailer appended its own reads back into it; it now detects that aliasing by file identity (
dev:ino) and echoes only. Separately, the offset tracker advanced by a re-encoded string length, so a multibyte UTF-8 char split at a poll boundary drifted the offset and corrupted the stream on non-ASCII output; the tail is now byte-exact (rawBufferviasshExecRaw). Source:src/lib/hosts/progress.ts,src/lib/ssh-exec.ts.
agents upgrade: the "What's new" changelog is now a compact heading list (#562)
- The post-upgrade changelog dumped every heading and every verbose sub-bullet for each version in the range — a screenful across a multi-version jump. It now prints one bullet per feature/fix heading and links to the full CHANGELOG for the details. The parser was extracted to a pure, unit-tested
renderWhatsNewso it can be exercised without the CLI's import-time side effects. Source:src/lib/whats-new.ts,src/index.ts.
agents sessions --active: a per-pid registry de-collapses co-located agents (#546)
- On a host with no terminal extension (bare SSH/tmux — e.g. any Linux box),
--activecould only map a discovered agent process to a session by guessing the newest.jsonlin its cwd, so several agents in the same repo collapsed onto one session row (observed live: a single id listed 28 times), and/restorecouldn't tell them apart.agents runnow records each launch to~/.agents/.cache/terminals/by-pid/<pid>.json({agent, cwd, tmuxPane, sessionId, startedAtMs}) — the headless equivalent of the terminal extension'slive-terminals.json— so--activeand/restoreattribute each co-located agent correctly. Source:src/lib/session/pid-registry.ts,src/lib/session/active.ts,src/lib/exec.ts.
1.20.34 — 2026-07-07
Test suite runs remotely on a crabbox VM (#525, #540)
scripts/release.sh's test gate now runsbun install && bun run build && bun run teston a leased crabbox VM viascripts/sandbox.shinstead of freezing the local machine, matching CI's Build→Test order (crabbox's sync honors.gitignore, so the gitignoreddist/is built on the box). A newbun run test:remoteoffloads the suite the same way for local dev. Publishing still happens locally — only the signed macOS keychain helper can be produced and notarized here, and crabbox boxes are Linux. Source:scripts/sandbox.sh,scripts/release.sh,package.json.scripts/sandbox.shbox acquisition is now robust: secrets load viaagents secrets export --plaintext(the bare form now hard-errors), a missing.crabbox.yamlno longer aborts the script underset -e, and the agents-cli/claude install is gated to PR mode so test-mode runs match GitHub CI. Box selection gates oncrabbox status … ready=true— skipping failed-bootstrap duds (which still reportstatus=running) and warming a fresh box if none are ready — keyed on the stableprofilelabel rather than an ephemeral slug. A dedicatedagents-clicrabbox profile (.crabbox.yaml) isolates this repo's warm pool. Source:scripts/sandbox.sh,.crabbox.yaml.
1.20.31 — 2026-07-07
agents sessions <id>: a catch-up digest for switching between many agents (#502)
- Opening a single session now leads with its auto-inferred title (user
/rename> Claudeai-title> first-prompt topic) and PR / worktree / ticket badges, then a Changes section that groups touched files by directory and tags each as created / modified / deleted (with a+N ~N -Nsummary) instead of the old flat "Modified" list, a Tools histogram (per-tool call counts), and a Tests verdict parsed from the lastvitest/jest/pytest/go test/cargo test/tscrun. The same signals are folded into the interactive picker preview. agents sessions --activenow collapses the many subagent/fork PIDs of one session into a single row with a×Ncount instead of printing dozens of identical lines. Source:src/lib/session/digest.ts,src/lib/session/render.ts,src/lib/session/active.ts,src/commands/sessions.ts,src/commands/sessions-picker.ts.
1.20.30 — 2026-07-07
agents sessions live state engine: waiting / PR / worktree / ticket detection + reliable preview (#494)
agents sessions --activeinfers real activity from each transcript's tail — working / waiting / idle — rather than the old mtime-only running/idle guess, using structural signals (ClaudeExitPlanMode/AskUserQuestion) plus a question + mtime heuristic for Codex. It detects and badges a PR opened during the session (gh pr create+ the resulting pull URL), a git worktree (.agents/worktrees/<slug>/), and a Linear/Jira ticket (from the prompt or branch), and shows the latest turn as the preview instead of the first prompt.--waitingfilters--activeto only sessions blocked on your input and exits non-zero (a scriptable gate);--treegroups the listing by directory, dropping the id/version columns while keeping the short-id handle.- The preview line is now width-correct: measurement is ANSI- and wide-char-aware and reads
$COLUMNSfirst, so it no longer wraps or drifts under tmux or over--hostSSH (the remote is handed the caller's width). Session index schema v7 persists the PR / worktree / ticket signals so historical listings carry them too. Source:src/lib/session/state.ts,src/lib/session/tail.ts,src/lib/session/width.ts,src/lib/session/{discover,db,active}.ts,src/commands/sessions.ts.
agents sessions --host <machine>: query a remote machine's sessions live over SSH
agents sessions "<query>" --host <alias|user@host>runs the same session query on a remote machine's own index over SSH and streams the result back — repeat--host(or pass several) to fan out across machines. SSH access is the only auth; there's no daemon or shared store. Targets are validated against a strict allowlist (SSH_TARGET_RE) to block flag-smuggling, and the forwarded invocation is double-quoted (shellQuote) so a query like$(whoami)survives as a literal string on both shell layers. Source:src/lib/session/remote.ts,src/commands/sessions.ts,docs/05-sessions.md.
Fix: migrations + menu-bar self-heal were silently disabled on Homebrew-node installs
- The "is this a dev build?" check walked
dirname(dirname(argv[1]))looking for a.git, without resolving the bin symlink. On a Homebrew-node setupagentsis/opt/homebrew/bin/agents, so it walked up to/opt/homebrew— which is itself a git repo — and false-positived as a dev build. Dev builds auto-setAGENTS_SKIP_MIGRATION=1, which gates both one-shot migrations and the menu-bar upgrade self-heal. Net effect: every Homebrew-node user ran with migrations and the menu-bar refresh permanently off. - Detection now
realpaths the entrypoint (so a symlinked bin resolves into the real package dir) and requires the.git's repo root to actually be the@phnx-labs/agents-clipackage — an unrelated ancestor repo no longer counts. Extracted tosrc/lib/startup/dev-build.tswith tests covering the Homebrew symlink layout, a real checkout, and unrelated-ancestor cases.
Secrets default policy is now daily (one Touch ID per ~24h), not always
- The default prompt policy for bundles without an explicit one flipped from
always(Touch ID on every read) todaily(one prompt, then held ~24h until screen-lock / sleep / logout). This is the fix for the prompt storm: a background reader like sessions-sync hammering a bundle now costs one Touch ID per ~24h instead of one per read. - Auto-cache is on by default. The secrets-agent is the mechanism that delivers the daily policy, so it self-caches a
dailybundle on first read with nosecrets.agent.auto: trueneeded. Opt out withsecrets.agent.auto: false. - Configurable, still flexible. Set the global default in
agents.yaml(secrets.policy: alwaysto restore prompt-every-time), or override per bundle withagents secrets policy <bundle> alwaysfor high-value keys (signing, SSH) you want to confirm on every read. - Explicit
alwaysnow persists under the legacytier: biometrytoken (older CLIs read it as their own always default). Bundles with no stored policy inherit the configured default — so an existing always-by-default bundle quietly becomesdailyon first read by the new CLI, which is the intended migration.
Menu bar: a macOS status item for agent activity (agents menubar)
- New no-Dock menu bar app showing live agent activity on the machine: a NEEDS YOU section (sessions awaiting input + failed/overdue routines), a per-agent roster (running / idle counts across installed agents), a + New session launcher, and a one-line routines summary. The icon badges red
!when something needs you, green with a count when sessions are running. - Reads state directly from disk —
live-terminals.json, teamsmeta.json, and the cloudtasks.db— so opening the menu never triggers the costly sessions transcript re-index. The CLI is shelled only for actions (start a session, run a routine). - Auto-enabled on macOS for every user as a launchd login service (
com.phnx-labs.agents-menubar); a fresh install brings the icon up with no manual step. Manage withagents menubar enable | disable | status. Opt out withagents menubar disable— sticky across upgrades. - Upgrade self-heal: the installed bundle is version-stamped, and the startup self-heal now re-installs the helper when a newer release ships a newer build (or the installed copy goes missing), instead of skipping whenever a service already existed. So
npm updateactually moves users onto the new helper binary + plist rather than leaving the old one running (#442).agents menubar statusshows installed vs current version and staleness. - Docs: Menu bar. macOS only.
agents repos view [name]: inspect one repo's contents without opening it
- New
agents repo view <name>(also reachable asagents repos view, now a first-class alias of therepocommand) prints a single repo's git state and per-kind resource counts —system,user,project, or an extra-repo alias. Omit the name for an interactive picker over the registered repos. It reuses theinspectrepo renderer, so output matchesagents inspect <repo>; supports--briefand--json. Source:src/commands/repo.ts,src/commands/inspect.ts.
agents doctor --fix + a daemon safety check: heal the gap between defined and installed
- Root cause behind "a plugin/command silently vanished": a DotAgents repo can DEFINE a resource that never makes it into an agent home, and nothing closes the gap. Two concrete failure modes — (1)
agents plugins update/synconly reconcile each agent's default version, so a non-default installed version keeps serving stale/invalid resources; (2) a plugin.json with a bare-nameskills/commandsfield makes Claude Code silently reject the entire plugin, and the sync path only warned. The detection (agents doctor's live-home diff) and the healing (syncResourcesToVersion) existed but were never wired together — and the sync fast-guard keyed off the staleness manifest, which is blind to home-side rot. agents doctor --fixturns the read-only diagnosis into a heal: installs missing resources, repairs Claude-invalid plugin manifests (strips the bareskills/commandsfield — Claude auto-discovers from the dirs), fast-forwards stale plugins from their.source, and reconciles drift — across every installed version, not just defaults. With no target it heals the whole install;agents doctor <agent> --fixscopes to one.- Daemon safety check: the routines daemon now runs the same heal in conservative
safemode (~every 6h + ~30s after start) — it fixes only unambiguous gaps (missing resources, invalid manifests, provably-unmodified stale plugins) and notifies rather than clobbers on hand-edited content or a plugin it can't prove is pristine. - Built on the live-home diff, not the staleness manifest, so it catches drift the sync fast-guard can't. Heal fills and fixes, never deletes (orphans stay
agents prune cleanup's job), excludes the project layer (the global home isn't reconciled against per-cwd project resources), and verifies after writing — it only claims resources that actually reconciled, so repeated runs converge instead of "fixing" the same item forever. .sourcenow records the plugin version at pull time, a baseline that lets the safe path tell an untouched mirror (fast-forward) from a user edit (leave alone).agents doctoroverview now covers every installed version, not just defaults. Sync status and orphans previously reported only each agent's default version — so a stale NON-default version (the exact rot--fixheals) was invisible in the readout. Each version is now listed with its default marked. The Agent CLIs list also stops nagging: it shows the agents you actually run (ready, or managed-but-broken) and collapses the rest of the supported catalog to a single+N more supported …hint instead of a column of red "not installed" lines for tools you never adopted.- Says exactly WHAT is out of sync — plugins first. A stale version in the overview now lists the specifics under it, prioritizing plugins and their bundled content:
plugin code — 0.6.1→0.7.0, missing skills: ship, learn. The plugin diff went from presence-only ("installed: yes") to content-aware — it compares the version's marketplace mirror against the central source and surfaces a stale mirror version, a Claude-invalid manifest, and the plugin's own skills/commands that never reached the mirror (the system-repo content that matters most).agents doctor <agent>@<version>shows the same detail per plugin row. - Fixed a false "drift" that could never be reconciled: a hook's
.md/.rstdoc sibling (e.g.git-guard.mdnext togit-guard.sh) was wrongly treated as the hook's runtime data file, so the installer's correct omission of docs showed as perpetual drift indoctor(and as an un-healable item under--fix). Docs are no longer counted as hook data; structured siblings (.yaml/.json/...) still are. - Corrected a false promise in the sync-status readout. Stale/cold versions used to say "will sync on next launch" / "first launch will populate" — but version homes are NOT reconciled on launch (the shim hot path only resolves a version and compiles project-scoped resources; v15/v16 moved version-home reconciliation to management commands). The readout now states the fact ("sources changed since last sync" / "never synced") and points at the real fix:
agents doctor <agent>@<version> --fixoragents sync <agent>@<version>.
Secrets prompt policy: human-readable always / daily, and secrets list now shows it
- Renamed the secrets-agent
tierto a prompt policy with plain-language names:biometry→always(ask every time),session→daily(ask once, then held ~24h until screen-lock / sleep / logout). The old namesessionwas misleading — it never meant "once per login session" — and collided with the half-dozen other "session" concepts in the CLI (agents sessions, sessions-sync, pty/browser sessions). Set it withagents secrets policy <bundle> [always|daily]. - Disclosure fixed.
agents secrets listnow has aPOLICYcolumn — previously there was no way to tell which bundles would Touch-ID-prompt you.dailybundles currently held by the agent showdaily · Nh left.agents secrets viewandcreatenow always state the policy (before, only the quiet tier was shown; the noisy default printed nothing). - Back-compat: the policy still persists under the legacy
tier/sessiontoken, so bundles stay readable across mixed CLI versions on synced machines.agents secrets tier,--tier, and thebiometry/sessionvalues keep working as aliases. - A third
neverpolicy (silent, no biometry ACL) is tracked for later in #421.
Self-healing: long-running processes reload onto new code after an upgrade
- Root cause behind a class of "stale behavior" bugs: a routines daemon or secrets-agent broker keeps running pre-upgrade code for days. An in-place
npm i -gswaps the files but not the running processes, so fixes (keychain read-memoization, the broker fast-path, etc.) silently never take effect — the daemon kept popping Touch ID from the keychain because it predated the fix. - Heal-on-upgrade:
postinstallnow bounces the routines daemon and kickstarts the persistent secrets-agent broker onto the just-installed code — the one moment we know the code changed. Best-effort, non-fatal, skipped in CI / withAGENTS_NO_HEAL=1. - Broker version-skew self-heal: the broker's
pingreports the version of the code it's running;ensureAgentRunning(the unlock / auto-cache path, never per-read) restarts a broker found running stale code, and a persistent broker self-exits on detecting an in-place upgrade so launchd relaunches it fresh. NewgetCliVersionFresh()re-readspackage.jsonto detect the swap. - No hot-path cost: all checks live on existing control-plane paths (postinstall, the broker sweep,
ensureAgentRunning), never on a per-secret-read. macOS only. Complements #412 (daemon session-sync memoization) by ensuring the daemon actually runs that code.
agents secrets start: persistent secrets-agent service (fixes the broker under heavy load)
- On a heavily-loaded machine (many concurrent agents, high load average) the on-demand broker — a full CLI cold-start — couldn't get scheduled enough CPU to finish booting and bind its socket, so
unlock/auto-cache silently failed and reads kept prompting. Newagents secrets startinstalls the broker as a launchd user service (RunAtLoad+KeepAlive,ProcessType: Interactivefor foreground scheduling priority): it starts once and stays up for the whole login session, so every read just connects — the cold start happens once (and launchd retries until it wins), never per read.agents secrets stopremoves it;agents secrets statusshows whether it's installed. unlockand the auto-cache worker now install/kickstart this service automatically viaensureAgentRunning, falling back to the old one-off detached spawn only if the service path is unavailable. So the persistent broker is set up on first use with no extra step.- macOS only. Security model unchanged: in-memory only, per-bundle TTL, wiped on screen-lock/sleep.
Fix: secrets-agent auto-cache now survives a slow broker cold-start under load
secrets.agent.auto(auto-cache on first read of asession-tier bundle) used a fire-and-forget inline loader that gave up connecting to the broker after 3s. But the broker it spawns is itself a full CLI cold-starting; under heavy load (many concurrent agents) that can exceed 3s, so the loader quit before the broker bound and the cache silently never populated — every read kept prompting. The auto-load now runs through a detachedsecrets _agent-loadworker that reuses the robustensureAgentRunningpath (spawn-then-ping, 20s budget) and loads synchronously, so it reliably populates even when the broker is slow to start. Manualagents secrets unlockwas always reliable and is unchanged. (secret values still travel over stdin, never argv.)
agents secrets unlock: a secrets-agent that ends Touch ID prompt spam (macOS)
- macOS pops a Touch ID prompt per bundle, per process — the biometry assertion is process-local and macOS refuses to cache
kSecAccessControl+biometry items, so running several agents at once (agents teams, parallelagents run --secrets) re-prompts once per process. Newagents secrets unlock <bundle>reads the bundle once (one prompt) and holds the resolved env in a local broker; every later resolution —agents run, teammates, browser profiles, the routines daemon — is served from memory over a user-only Unix socket (~/.agents/.cache/helpers/secrets-agent/,0700) with no prompt.agents secrets lockwipes it;agents secrets statusshows what's held and when it locks. The hold also ends on TTL expiry (default 24h,--ttl) and on screen-lock / sleep. - Opt-in by construction: if you never
unlock, resolution is byte-for-byte the existing keychain path — guarded behind a singleagentSocketExists()stat. The single integration point isreadAndResolveBundleEnv, so every consumer benefits without per-call-site changes. Broker-served reads are tagged"source":"agent"in the audit log. - Security trade-off (documented in
docs/secrets.md): while unlocked, a same-user process that can reach the socket reads the bundle silently — the same trust boundary the keychain already concedes (the ACL is user-presence, not code-identity), minus the visible prompt. Bounded by explicit per-bundle opt-in, TTL, screen-lock/sleep auto-lock, andlock. - Snapshot semantics:
unlockfreezes a bundle's dynamicexec:/env:/file:refs at unlock time; keychain and literal values are unaffected. - Release note: auto-lock on screen-lock/sleep adds a
watch-locksubcommand tokeychain-helper.swift. The signed helper must be rebuilt + re-notarized and its sha re-pinned (scripts/build-keychain-helper.sh,scripts/Agents CLI.app.sha256) for that path to ship; until then the agent degrades gracefully to TTL-only locking. Source:src/lib/secrets/agent.ts.
Per-bundle tiers + opt-in auto-cache for the secrets-agent
- Bundles now carry a tier (
agents secrets tier <bundle> [biometry|session], or--tieroncreate).biometry(default) is today's behavior — only an explicitunlockputs it in the agent.sessionmakes a bundle agent-eligible. - New
secrets.agent.auto: trueinagents.yaml(default off): the first real keychain read of asession-tier bundle auto-loads it into the broker in the background (no added latency, secret passed over stdin not argv), so the next concurrent run reads it silently — no manualunlock. Abiometry-tier bundle is never auto-held. - A
nonetier (items without the biometry ACL, fully silent, no agent) is intentionally not offered yet — it needs a separate signed-helper change and is the global downgrade the agent exists to avoid. - Default secrets-agent TTL is 24h.
Headless Linux: agents secrets works out of the box when the keyring is locked
- On a headless server the libsecret/GNOME-keyring collection is locked, so the encrypted-file fallback is the only option — but it previously hard-failed unless
AGENTS_SECRETS_PASSPHRASEwas set, leavingagents secretssilently unusable. Now, on a headless run with no passphrase set, a random machine-local passphrase is auto-provisioned once at~/.agents/.cache/secrets/.passphrase(mode 0600) so the encrypted-file store just works.AGENTS_SECRETS_PASSPHRASEstill takes precedence (off-disk key), an existing.passphraseis reused for stable interactive/headless behavior, and interactive TTY sessions are still prompted. Security model + resolution order documented indocs/secrets.md. (#371)
agents secrets get/set <item>: raw, cross-platform keychain access for hooks
- New
agents secrets get <item>/agents secrets set <item>read and write a single keychain item by bare name (outside the bundle namespace), so shell hooks and automation have one platform-agnostic credential primitive to call instead of hardcoding/usr/bin/security(macOS-only) orsecret-tool(Linux-only).getprints the value to stdout (newline-terminated for clean$(…)capture), sends diagnostics to stderr, and exits 1 with empty stdout when the item is missing — exactly what aSessionStarthook needs to probe-and-fallback quietly. Routing goes through the existing cross-platform keychain layer: macOS via/usr/bin/security, Linux viasecret-toolwith the encrypted-file fallback. setKeychainTokennow writes bare (non-agents-cli.) items on macOS without the biometry ACL, mirroring the existing no-prompt read path for such items. This is what lets a hook read e.g.linear-api-keysilently on every launch — routing it through the Touch ID helper would attach an ACL the/usr/bin/securityread can't satisfy without popping the legacy password sheet. The change is purely additive: every existing caller passes anagents-cli.-namespaced item and is unaffected (still biometry-gated via the signed helper).
agents inspect summary: expanded detail for hooks, plugins, and MCP
- The bare
agents inspect <agent>/agents inspect <repo>summary no longer collapses everything to a count table. Simple kinds (commands, skills, rules, subagents, workflows) keep a count line but now preview a few names; the rich kinds get their own expanded sections: hooks show their events +matches:predicates + cache (PreToolUse(Bash) · git_dirty · prompt~"deploy" (5m cache)), plugins show version + bundle contents (v2.1.0 skills:6 commands:5 hooks:2 mcp:1), and MCP show transport + url/command. Drill-down flags (--hooks,--plugins,--mcp) and--briefare unchanged;--jsongains the structured detail additively (existing keys retained). - Hook detail joins installed hooks to the manifest by script basename (installed hooks are named after their script file while the manifest keys on the logical name), and the repo Hooks section uses the grouped hook reader so a script + its data file collapse to one clean entry.
Plugin hooks were misreported — fixed
discoverPluginHooksread the top-level keys of a plugin'shooks/hooks.json, so the official{ description, hooks: { SessionStart: [...] } }format surfaced asdescription, hooksinstead of the real events. It now reads thehookswrapper when present (falling back to top-level keys for the flat format), soagents inspect --plugin <name>and the plugin row show the actual lifecycle events (e.g.SessionStart, PreToolUse, …).
agents doctor / agents prune: precise orphan-hooks detection
- Orphan-hook detection now flags hook scripts present in a version home that no
agents.yaml/hooks.yamlentry registers — i.e. scripts that sync to disk but are never wired to a lifecycle event, so they never fire. This replaces the source-diff heuristic, which compared only against the user hooks dir and so false-flagged valid system-sourced, registered hooks (e.g.03-linear-inject,04-capture) as orphans — meaningagents prune cleanupcould have deleted live hooks. Doctor's Orphans section andprune cleanup hooksnow share this single manifest-based definition.parseHookManifestgained a silent ({ warn: false }) option so the diagnostic doesn't emit shadow/override warnings.
Regression coverage: resource sync from extras repos
- Added end-to-end regression tests (
src/lib/__tests__/extras-sync.test.ts) locking in two behaviors for repos registered viaagents repo add(~/.agents-<alias>/): a top-levelcommands/<name>.mdis written into the agent's version home onagents sync, and plugins underplugins/<name>/are synthesized into a registeredagents-<alias>marketplace on launch. Both already work inmain; the tests exercise the real sync path (no mocking, isolated$HOME) so the extras-repo behavior can't silently regress (#313, #314).
Windows: agents is discoverable right after npm i -g
- On a global Windows install, postinstall now prepends npm's global-bin dir (where
agents.cmd/agents.ps1live) to the User PATH via the .NET environment API. Node's installer normally adds it, but winget / portable / nvm-windows setups often don't — and thennpm i -g @phnx-labs/agents-clisucceeds yetagentsis "not recognized". The shims dir (claude/codex/…) is still left toagents setup, which the user can now run becauseagentsresolves. - Postinstall also detects a
Restricted/AllSignedPowerShell execution policy (which blocks the generated.ps1launchers, so even an on-PATHagentsfails in PowerShell) and prints the one-line fix (Set-ExecutionPolicy -Scope CurrentUser RemoteSigned). The policy is a security setting, so it is never changed silently — only surfaced. - Refactor: the Windows User-PATH prepend logic moved from
shims.tsinto a newsrc/lib/platform/winpath.tsleaf module (prependToWindowsUserPath,getEffectiveExecutionPolicy,blocksLocalScripts,npmGlobalBinFromEntry);addShimsToWindowsUserPathnow delegates to it. Pure helpers are unit-tested.
Factory AI Droid (first-class support)
- Add
droidas a first-class supported agent (AgentId + full registry entry for Factory AI'sdroidCLI, config in~/.factory/). Installs via the official script (curl -fsSL https://app.factory.ai/cli | sh); the binary is resolved through the standard install-script path and isolated per version via the~/.factoryconfig symlink (Droid has no*_HOMEoverride). - Resource sync wired for the four resource types Droid supports natively: MCP (
~/.factory/mcp.json), rules (nativeAGENTS.md), subagents (custom droids flattened to~/.factory/droids/*.md, with the unsupportedcolorfrontmatter key stripped), and commands (~/.factory/commands/). Skills/plugins/workflows have no Droid equivalent and are disabled; hooks/permissions are deferred. agents run droidandagents teams add … droidwork end-to-end: headlessdroid execwith mode mapping (plan → read-only, edit →--auto low, auto →--auto high, skip →--skip-permissions-unsafe),-o stream-jsonoutput,-mmodel selection, and-rreasoning effort. Routine/daemon jobs (buildJobCommand) support Droid too.- Known limitation:
agents teamsrenders Droid events through the generic normalizer pending a verifieddroid exec -o stream-jsonevent schema; structured tool/file categorization will follow. Session reading and Factory cloud dispatch remain follow-ups.
agents upgrade now refreshes the macOS Keychain helper
- Upgrading runs
npm install -g … --ignore-scripts, so the postinstall that installs the signed Keychain helper never fired — a user upgrading away from a broken build (e.g. the entitlement-less 1.20.4 helper that failedSecItemAddwitherrSecMissingEntitlement -34018) kept the broken helper until the lazy staleness check ingetKeychainHelperPath()happened to repair it on their next secret operation.installResolvedPackagenow force-refreshes the helper (ensureKeychainHelperInstalled({ forceReinstall: true })) on darwin after the install, so both the explicitagents upgradeand the auto-update prompt land the fixed helper immediately. Best-effort and non-fatal: an upgrade never fails because the helper could not be reinstalled, andagents helper install --forceremains the manual path.
agents inspect <repo> summary now shows what's actually inside, not just counts
- The bare repo summary gained four enrichments so it reads as an inventory instead of a tally: (1) resource name previews — each kind lists its first few names with a
…(+N)tail; (2) manifest summary —agents.yamlis parsed for itsrun.<agent>.strategyand anyagents.<agent>version pins, shown undermanifestsinstead of just the filename; (3) git detail — last commit (sha, subject, relative time), ahead/behind upstream when non-zero, and the names of dirty files; (4) size + file counts — total repo size and a per-kind byte size.--jsoncarries all of it (git.lastCommit,git.ahead/behind,manifest,size, and per-kind{count, bytes, files, names});--briefstill skips resources and size. - Fixed a path-parse bug surfaced by the dirty-files list: the shared git helper trimmed leading whitespace, which clipped the first character off the first
git status --porcelainpath; status is now read untrimmed.
agents inspect . reads the project .agents/, and plugin drill-down shows bundled skills
agents inspect .(and any path to a repo root) now resolves to the project's nested.agents/tree when that tree is a populated DotAgents root, instead of the project root itself. Previously a top-levelagents.yamlversion-pin or an unrelated sourceskills/dir at the repo root was mistaken for a DotAgents root, soinspect .reported the wrong directory's resources (e.g.plugins 0while the real.agents/plugins/held a plugin). A bare.agents-named dir still resolves to itself, and standalone clones / extra repos that keep resources at the top level (using.agents/only for worktrees) are unaffected — their nested.agents/is not a DotAgents root, so the top level still wins.agents inspect <repo> --pluginsnow reads plugin bundles through the plugin discoverer: the list shows each plugin's manifest description, and drilling into one (--plugins <name>) reports its bundled skills, commands, subagents, hooks, MCP servers, and version. Previously plugins were treated as opaque directories with no description and no view into what they ship.
Single-typo agent names auto-correct everywhere, not just agents run
agents view cladueused to printUnknown agent 'cladue'even thoughagents run cladueauto-corrected.resolveAgentName— the canonical resolver behindview,usage,inspect,doctor,sync,models,skills,hooks,import,sessions --agent, and everyagent@versionspec (agents add claud@latest,agents use [email protected]) — now falls back to Damerau-Levenshtein distance-1 matching against canonical ids and multi-letter aliases:cladue->claude(transposition),kim->kimi,codx->codex,gemni->gemini.- Corrections apply only when unambiguous: every distance-1 candidate must agree on one agent.
kiri(one edit from bothkiroandkimi) and inputs under 3 characters still error.agents runkeeps its existing exact -> profile -> workflow -> fuzzy precedence, so a profile namedclaudstill beats the typo correction. - Fixes
kimibeing listed as a valid agent but missing from the alias map —agents view kimipreviously errored. Addedkimi/kimi-codeentries.
1.20.7 — 2026-07-07
agents inspect — DotAgents repo targets (#256)
agents inspectnow accepts a DotAgents repo as the target, not just an installed agent:user(/.agents/),/.agents/.system/),system(project(nearest.agents/from cwd), any extra-repo alias registered viaagents repo add, or a filesystem path. Paths accept either a repo containing a.agents/dir or a DotAgents root directly.- Repo summary shows the root (OSC-8 linked), git branch / dirty count / origin URL, manifest files (
agents.yaml,hooks.yaml), and per-kind resource counts. All existing drill-down flags (--commands,--skills,--plugins, ... with fuzzy queries and--json) work against the single repo root — what is physically in that repo, with no layered resolution or same-name overrides. - Resolution precedence: a directory that is itself a DotAgents root wins over its nested
.agents/, so extra repos that keep resources at the top level and use.agents/only for worktrees resolve to their real resources. - Unknown targets now error with both halves of the namespace: the known agent ids and the available repo targets (built-in layers plus registered aliases).
scripts/install.sh — bash 3.2 fix (#256)
set -uplus"${BUILD_ARGS[@]}"on an empty array aborted the dev install withBUILD_ARGS[@]: unbound variableunder macOS system bash; the expansion is now guarded with${BUILD_ARGS[@]+...}.
1.20.5 — 2026-07-07
agents inspect — per-agent+version detail view with drill-down (#217)
- New top-level command
agents inspect <agent>[@version]. Summary mode shows install path, config symlink target, shim path, versioned alias, run strategy, capability table (hooks/mcp/skills/commands/subagents/plugins/workflows/rules/allowlist), resource counts with project/user/system scope breakdown, and session total. Replaces the awkwardagents view <agent>@<version>deep-detail mode as a dedicated verb;viewitself is unchanged. - Drill-down flags for every resource kind —
--commands,--skills,--hooks,--mcp,--rules,--plugins,--workflows,--subagents. Bare flag lists every entry; passing a positional query fuzzy-searches that kind, ranking exact > substring > Damerau-Levenshtein. Zero matches exit 1 with the three closest names as suggestions. One drill-down at a time (validation error otherwise).--jsonworks with summary and every drill-down for scriptable consumption. - Resource names render as OSC-8 terminal hyperlinks to the marker file (
SKILL.md/WORKFLOW.md/AGENT.md) for clickable navigation in modern terminals (Ghostty, iTerm2, WezTerm) — no inline path noise. Plain text on terminals without OSC-8 support. - MCP detail intentionally suppresses path and env values to avoid leaking secrets — only the server name, scope, and version reach the output.
- Removes the deprecated
agents statusalias forview @default. Top-level help text updated; no consumers referenced it.
Headless Linux: encrypted-file fallback when libsecret collection is locked (#183)
- On server-class Linux (Ubuntu 24.04 over SSH on the reporter's box),
agents secrets create xfailed withsecret-tool: Cannot create an item in a locked collection. Diagnosis in the issue:gnome-keyring-daemonis running and D-Bus is reachable, but the defaultlogincollection is locked because no graphical login has fed the daemon the passphrase, andsecret-toolfromlibsecret-toolshas no--collectionflag so it can't target the unlockedsessioncollection. This madeagents secretseffectively macOS-only on any headless box. src/lib/secrets/linux.tsnow transparently falls back to a file-based AES-256-GCM encrypted store at~/.agents/.cache/secrets/<item>.enc(mode 0600, per-file random scrypt salt + 96-bit IV, GCM auth tag). The encryption key is scrypt-derived from a passphrase read fromAGENTS_SECRETS_PASSPHRASE(preferred) or a TTY prompt via/dev/ttywithstty -echofor non-echoing input. The fallback also activates whenlibsecret-toolsis not installed at all butAGENTS_SECRETS_PASSPHRASEis set, so a fresh install can store secrets without any apt-get step.- The decision is cached per process; on first activation we emit one stderr line:
[agents] secret-service collection locked, using file-based store at <dir>. TheKeychainBackendinterface insrc/lib/secrets/index.tsis unchanged —has/get/set/delete/listwork identically against either backend, sobundles.ts,sync.ts, and every consumer above it sees no API change. - Items written into the file store before the fallback was added remain accessible only via libsecret if/when the collection is later unlocked; this PR does not migrate stranded items in either direction — the user simply re-creates them on a freshly headless box.
1.20.4 — 2026-07-07
Plugin marketplace sync (skip outside-pointing symlinks)
copyPluginToMarketplaceusedfs.cpSync(plugin.root, dest, { recursive: true, dereference: false }), which faithfully preserved every symlink — including the ones plugin authors put at the top of their plugin source for prompt-side references (the rush plugin'sapp -> ../../../rush/app,web -> rush/web,widgets -> rush/widgets). Those targets resolve to the rush monorepo (~8.7 GB ofapp/including node_modules + .next builds, 782 MB ofweb/, plus 463 MB brand-assets). Every claude version got a full set of those symlinks in~/.claude/plugins/marketplaces/agents-cli/plugins/rush/. When the consumer (Claude Code, OpenClaw) discovers plugins, it walks the marketplace tree and follows those symlinks — producing multi-minute startup hangs.- The copy now walks the source tree and drops symlinks whose
realpathescapes the plugin root, leaving internal symlinks intact (cpSync rewrites internal targets to absolute paths into the source tree, which the consumer still resolves correctly). One informational line per plugin lists the skipped names so plugin authors notice. - Existing per-version marketplace directories still hold the bloat from prior syncs; clean up with
rmagainst~/.claude/plugins/marketplaces/agents-cli/plugins/*/{app,web,widgets,*-symlinks-that-escaped}then re-runagents pullor any plugin sync to re-copy with the filter.
1.20.3 — 2026-07-07
agents run startup latency (stale-while-revalidate the usage probe + memoize agents.yaml)
- The default
agents runstrategy isavailable, which callsgetUsageInfoForIdentityto skip rate-limited accounts. With a 2-minute cache, every cold invocation past that window made a blockingfetchtoapi.anthropic.com/api/oauth/usage(5 s timeout, plus an optional 15 s OAuth token refresh) beforespawn(claude)— soagents run clauderegularly stalled 5–8 s with nothing on screen after the rotation banner. - The cache is now stale-while-revalidate: fresh (<2 min) returns instantly with no network, stale-but-recent (<24 h) returns the cached snapshot instantly and refreshes in the background, and only a fully cold / >24 h cache blocks on the live fetch. The background refresh defers its first await past
setImmediateso the synchronous Keychain CLI call (security find-generic-password, invoked byloadClaudeOauth) cannot block the foreground caller — that's how an SWR returns "instantly" even while the refresh is technically still on its first sync step. readMeta()had ametaCachemodule global pluswriteMetaUnlockedcache-invalidation logic wired in years ago — but no read path ever consulted the cache. So every call did 2xfs.readFileSync+ 2xyaml.parseon system + useragents.yaml, and hot callers (getConfiguredRunStrategy,getGlobalDefault,getVersionResources,ensureVersionResourcePatterns) fire it multiple times peragents run. The read path now consults the cache, keyed on the combined mtime of both source files — out-of-band edits still invalidate on the next stat, and in-process writers already clear it.
1.20.2 — 2026-07-07
Grok and Antigravity Support & Documentation
- Grok CLI Integration: Added support for installing Grok via
agents add grok@<version>, which invokes the official xAI installer with the specified version. Grok MCP server configuration paths (viaconfig.toml) and memory file mapping are now correctly documented. - Antigravity (AGY) CLI Integration: Added support for the Google Antigravity CLI. Since the AGY installer doesn't support version-pinned installs currently,
agents add agyuses thelatestversion. Documented the canonical config path~/.gemini/antigravity-cli/and itsmcp_config.json. - Documentation: Updated
02-resource-sync.mdto reflect accurate MCP mappings and memory file symlinks for both Grok and Antigravity. - Profiles: Hardened presets with verified 2026 model IDs and added generic proxy configuration. Show custom profiles in agents view.
1.20.1 — 2026-07-07
Agents selector (auto-install missing versions + unified @all everywhere)
--agents [email protected]used to hard-error when 2.1.999 wasn't installed. Now the CLI prompts to install it inline and continues (auto-install with--yes). No more breaking flow to runagents addfirst.--agents claude@alland the bareallliteral now work across every callsite that takes--agents— previouslyagents install gh:...,mcp register,mcp remove, and inlinemcp addhad diverged from the canonical syntax and threw "Version all is not installed" despite the help text advertising it. Selector is unified end-to-end.
Prompt (fail loud on non-TTY + @all syntax in picker)
- Scripts that called
agents <resource> addwith no--agentsand no--yesused to silently auto-pick a default version. That hid scripted misuse behind unpredictable picks. The non-TTY path now throws with a clear pointer at the new syntax:--agents claude@all(every installed version of Claude),--agents all(every capable agent at all versions), or--agents [email protected](one specific version). --agentsparsing in<resource> addunderstands@alland the bareallliteral;promptAgentVersionSelection's picker surfaces version counts when there's more than one installed, mirroring what@allwould target.
Resources / install (gh: form sniffs every type, mcp add gh:, --names + @all unified across resource add)
agents install gh:<owner>/<repo>now sniffs every resource type in the source repo (commands, skills, hooks, MCP, permissions, profiles, subagents, workflows) instead of requiring one--typesper kind. Pass--types skills,workflowsto narrow.- New
agents mcp add gh:<owner>/<repo>form — install MCP servers directly from a git source, parallel to the other<resource> add gh:paths. <resource> addaccepts--namesand@alluniformly across commands, skills, hooks, MCP, permissions, profiles, rules, subagents, workflows — same flags, same semantics, regardless of resource kind.
Profiles (interactive create wizard, gateway + self-hosted presets)
- New
agents profiles createcommand — interactive wizard to assemble a profile from gateway or self-hosted presets (OpenRouter, OpenAI-compatible) without hand-writing YAML. --smoke-testexercises the resolved env block against the configured endpoint before writing the profile.
Feedback (in-CLI bug / idea / question routing)
- New
agents feedbackcommand — collects a short description + optional category (bug, idea, question) and routes to the project's tracker without leaving the terminal.
Routines (real exit codes for detached scheduled runs)
monitorRunningJobsused to hardcodestatus: 'failed'whenever it detected that a detached child had exited —executeJobDetachedfires-and-forgets, so the real exit code was unreachable. Every scheduler-driven routine ended up labeledfailed/exitCode: null, even when the agent completed cleanly.- Fix: when finalizing a vanished child, scan the tail of its stream-json
stdout.logfor Claude'stype: resultterminator (which carriesis_error). If found, setstatusandexitCodefrom it. Only fall back tofailedwhen no result marker exists (process was killed mid-run). - Routines list cell rendering hardened around 7-day retention boundaries.
- Codex/Gemini run finalization continues to fall back to
faileduntil their stream tail parsers are added.
Security
security(cli): eliminatedshell: truefrom manifest-driven installs — closes a command-injection vector ininstall/addpaths that took git URLs or shell-interpolated metadata.security(logs): prompts and tokens are redacted beforeevents.jsonlis written, and event retention is shortened from 30d to 7d. Reduces blast radius on accidental disclosure.security(exec): strip loader env vars (DYLD_*,LD_*,NODE_OPTIONS) from environments propagated to child agents — avoids passing host-process loader state into spawned binaries.security(browser): CDP origin allowlist replaces the previous wildcard — onlylocalhostand explicitly configured browser hosts can speak CDP into a session.security(ci): keychain helper SHA is verified at publish time, so a tampered helper binary cannot ride a release.
Copilot (fix user-scoped MCP path)
- Copilot's user-scoped MCP path now correctly resolves to
mcp-config.json(the path the IDE actually reads) instead of the legacy filename. Fixes user-level MCP registrations not appearing in Copilot sessions.
Docs
- Full docs site IA shipped: browser, cloud, computer, hooks, plugins, profiles, pty, secrets, subagents, teams, workflows.
- Brand identity block:
agents-cliis Phoenix Labs OSS, not part of the Rush brand — guards downstream agents against pulling Rush styling into this project.
Build / install
- Staged dev install tarball strips
prepackandpreparehooks so side-by-side dev installs don't accidentally re-run the full publish pipeline locally. test(jobs): un-break 3 stale assertions on main.
1.20.0 — 2026-07-07
Routines (overdue detection + catchup)
- Detect routines whose most recent scheduled fire was missed (laptop off, daemon crashed, reboot). The daemon logs them on startup and pops a native desktop notification (
osascripton macOS,notify-sendon Linux). agents routines listannotates overdue rows with(overdue)and prints a footer pointing at the catchup command.- New
agents routines catchupcommand: lists overdue routines and fires them in the background under the scheduler.--dry-runlists without triggering. JobScheduler.schedulenow sets croner'scatch: trueand forwardstimezonedefensively, so a synchronous throw in one job's callback can't kill the whole cron loop.
Landing page (agents-cli.sh)
- Expanded the homepage with seven new sections: rotate accounts (
--rotate), parallel teams (agents teams), browser automation, cross-agent session search, routines/cron, keychain secrets, and machine-to-machine sync (agents drive). - Rewrote meta description + lede to spell out the actual feature set (pin versions, swap models, rotate accounts, drive a browser, spawn parallel teams, schedule on cron) instead of just "same interface, on your machine."
Codex (commands-as-skills sync fix)
- Fix recurring "N commands new" prompt on
agents view codexfor Codex >= 0.117.0.getActuallySyncedResourcesnow detects converted command-skills via theagents_commandmarker in~/.codex/skills/<name>/SKILL.mdinstead of only scanning the empty legacyprompts/directory. - Summary and selection prompts are version-aware: the static
COMMANDS_CAPABLE_AGENTSgate is replaced bysupports(agent, 'commands', version)so the "X commands" line only appears for versions that can actually take them. - Generalize
shouldInstallCommandAsSkillbeyond Codex — any agent where commands are gated off and skills are on (e.g. Grok) now gets the same automatic slash-command → skill conversion at install/sync time.
Grok Build (first-class support)
- Add
grokas a first-class supported agent (AgentId + full registry entry using official~/.grok/README.mdpaths). - Implement proper binary resolution from
~/.grok/downloads/. - Add
GROK_HOMEisolation to generated shims for true versioned config (skills, hooks, plugins, agents/, MCP, memory, etc.). - Extend
installVersionto support Grok via its official installer script (curl ... -s <version>). - Update shims, exec templates, MCP path helpers, session helpers, unmanaged detection, and docs.
agents add grok@<ver>,agents use grok@<ver>, resource sync, and shims now work end-to-end for Grok Build.
Browser
agents browser start --recordconvenience flag for one-shot recording sessions.- Auto-discover per-site
SKILL.mdonbrowser startso skills appear under the active task without manual wiring. - Auto-pick a Chromium-family browser when
--profileis omitted; the limitation is surfaced in--helpand the auto-pick error. - No more stacktraces when the daemon is down or CDP is unreachable — error paths print a single human-readable line.
- Drop the Playwright
bundled-chromiumdevdependency.
Secrets / Keychain
agents secrets listandagents run --secrets <bundle>collapse to one Touch ID prompt per bundle instead of one per key. Previously every secret in a bundle would re-prompt for keychain unlock.
Sessions
- Extract
groupActiveSessionsinto a tested helper for--activewindow grouping. - Propagate
windowidfrom live-terminals into the active session record.
Copilot
- Emit
COPILOT_HOMEin the shim and exec env builder for versioned isolation. - Wire the Copilot session dir and
.jsonlextension into the sessions reader.
OpenClaw
- Carry OpenClaw user data forward on version switch.
Teams
- Warn loudly when
--afterteammates reference a name whose watch process never launched, instead of silently sitting in pending state.
Plugins
- Use
'directory'source discriminator (not'local') for marketplace registration so plugins reload correctly.
Dependencies
- Bump
@inquirer/prompts7.10.1 → 8.5.1,diff8.0.4 → 9.0.0,tsx4.22.2 → 4.22.3,actions/setup-node4.4.0 → 6.4.0.
1.18.6 — 2026-07-07
Claude
- Add auto permission mode support for Claude runs.
- Remove a dead automatic mode flag from the Claude command template.
Teams
- Fix the cycle-detection test to accept running or failed teammate status.
1.18.5 — 2026-07-07
Browser
- Breaking: action commands no longer accept a leading
<task>positional. Bind the task once per shell viaAGENTS_BROWSER_TASK, or pass--task <name>for a per-call override:
Env vars are per-process, so parallel agents in different shells never collide.export AGENTS_BROWSER_TASK=$(agents browser start --profile work) agents browser navigate --url https://example.com agents browser click 42 agents browser screenshot - Breaking: URL/text/expression/scroll arguments are now flag-only — positional forms removed:
navigate --url <url>(wasnavigate <url>)tab add --url <url>(wastab add <url>)type <ref> --text "..."(wastype <ref> "...")evaluate --expression "..."or--file <path>(wasevaluate "...")scroll --dx <n> --dy <n>(wasscroll <dx> <dy>— fixes negative-value parser collision)
screenshotprints a one-line auto-save tip on stderr when--outputis not passed, so agents see the directory without having to dirname() the path.
1.18.4 — 2026-07-07
Browser
agents browser startwrites the resolved task name to stdout as a single line (e.g.swift-crab-falcon-a3f92b1c), and routes the human commentary ("Started task ... with tab ...", "Tip: export AGENTS_BROWSER_TASK=...") to stderr. This makesT=$(agents browser start --profile X)Just Work — no--quietflag needed.- Auto-generated task names are now three English words plus an 8-char hex
suffix, e.g.
swift-crab-falcon-a3f92b1c. Memorable, distinct, 32 bits of entropy so parallel agents never collide. Daemon retries on the (vanishingly rare) name clash and rejects explicit--task <name>values that already exist. agents browser start --profile <name>now pre-validates the profile locally before touching the daemon. Missing profile prints the list of available profiles plus the create-command hint instead of a generic error.agents browser tab listis nowagents browser tabs(top-level), pairing cleanly withagents browser tab focus <id>. The oldtab listform is removed.agents browser --helpis reorganized by mental model — Session lifecycle, Drive the page, Capture evidence — instead of an alphabetical dump. Rare commands stay under a trailing Commands section.- BREAKING:
agents browser profiles primeandagents browser profiles launchare removed. Both were thin duplicates ofstart. For first-run onboarding, justagents browser start --profile <name>and complete the interactive screens in the browser; the user-data-dir persists across runs. The daemon'slaunch-profileIPC action is also gone. - Named endpoint presets per profile. One profile can now cover the local
and remote variants of the same app instead of forcing two parallel
profiles. YAML supports both the legacy
endpoints: [url]shape and the new map form:name: rush browser: custom electron: true endpoints: local: target: cdp://127.0.0.1:9223 binary: /Applications/Rush.app/Contents/MacOS/Rush mac-mini: target: ssh://mac-mini?port=9223 # no binary — daemon attaches only defaultEndpoint: localagents browser start --profile rush --endpoint mac-minipicks a specific preset;--endpointfalls back todefaultEndpointor the first preset. Pre-validated client-side so a typo doesn't waste an IPC round-trip. Per-endpointbinaryandtargetFilteroverride the profile-level fields.agents browser profiles showlists every preset, marks the default, and shows per-endpoint overrides. - The daemon's runtime identity is now
<profile>@<endpoint>so the same profile can run at multiple endpoints concurrently without colliding on pid/port files.agents browser statusandtasksshow the composite name, so you can tell at a glance which variant a task is using. agents browser screenshot --quality rawcaptures pixel-faithful PNG (no downscale) for archived QA evidence. Default stayscompressed(JPEG, capped near 100 KB) for chat-injected screenshots.- New
agents browser record start/agents browser record stoprecording verbs. Captures via CDPPage.startScreencast, pipes frames into ffmpeg (image2pipe → libvpx-vp9) and writes a webm undersessions/<task>/recordings/. Bounded three ways —--fps(default 5),--duration(hard cap, default 60s),--max-mb(default 25); whichever fires first auto-finalizes the file. Requires ffmpeg on PATH (brew install ffmpeg).
1.18.3 — 2026-07-07
Plugins (#22)
agents plugins syncnow installs plugins via Claude Code's native marketplace path —<versionHome>/.{claude,openclaw}/plugins/marketplaces/agents-cli/plugins/<name>/— instead of flattening contents into~/.claude/skills/<plugin>--<skill>/. Skills resolve as/plugin:skill(the documented form) instead of/plugin--skill. Plugins appear in Claude's/pluginsUI under Installed and respond to/plugin enable,/plugin disable.- A synthetic
agents-climarketplace is materialized per version:.claude-plugin/marketplace.jsonis synthesized from discovered plugins, an entry is added to<versionHome>/.claude/plugins/known_marketplaces.json, andsettings.json#enabledPlugins["<plugin>@agents-cli"]is flipped totrue. Removal is symmetric — last plugin out drops the marketplace dir and the known_marketplaces entry. - The sync now copies the whole plugin tree verbatim (single
fs.cpSync) instead of re-implementing per-feature merges intosettings.json. Every Claude plugin feature — skills, commands, subagents, hooks,.mcp.json,.lsp.json,monitors/monitors.json,bin/,settings.json— is preserved end-to-end.${CLAUDE_PLUGIN_ROOT}and${CLAUDE_PLUGIN_DATA}are left intact so Claude can expand them at runtime; only${user_config.*}(agents-cli-specific) is pre-expanded in copied text files. - Legacy dual-dash layout from prior versions is auto-migrated at sync time —
~/.claude/skills/<plugin>--*,~/.claude/commands/<plugin>--*.md,~/.claude/agents/<plugin>--*.md,plugin-bin/<plugin>/, and namespacedmcpServers["<plugin>--*"]entries are removed after the marketplace install succeeds. agents plugins view <name>surfaces every feature the plugin ships: Skills, Commands, Subagents, Hooks, MCP Servers, LSP Servers, Monitors, Bin, Scripts, Settings. Theagents view <agent>@<version>Plugins section gains MCP/LSP/Monitor/Bin/Settings counts. NewdiscoverPluginMcpServers,discoverPluginLspServers,discoverPluginMonitorshelpers parse.mcp.json,.lsp.json, andmonitors/monitors.json.
1.18.2 — 2026-07-07
Teams
- Dropped
~/.agents/teams/config.jsonentirely. It duplicated information agents-cli already has — agent commands, enabled flags, model defaults, provider endpoints — none of which the team runner was actually reading. Teams now discover agents vialistInstalledVersions()(the same sourceagents viewuses) and invoke them via the canonicalagents runsubcommand. One spawn path, one canonical exec module (src/lib/exec.ts). The deprecatedAGENT_COMMANDS,applyEditMode,applyFullMode,readConfig,writeConfig,setAgentEnabled,AgentConfig,SwarmConfig,ProviderConfig,ModelOverrides,ReadConfigResult, andEffortLevel(the persistence-module copy) exports are removed from@phnx-labs/agents-cli/teams. Migration deletes both~/.agents/teams/config.jsonand the legacy~/.agents/config.json. ~/.agents/teams/registry.jsonmoves to~/.agents/.history/teams/registry.json— it's per-machine runtime state (timestamps + absolute worktree paths) and shouldn't be synced across machines viaagents repo push.- New
agents run --quietflag suppresses the rotation banner andRunning: …preamble lines. Used by the team runner so stream-json events reach the parser without non-JSON preamble.
Dev builds
- The CLI auto-detects dev builds (version stamped
0.0.0-dev.<sha>byscripts/install.sh, or invoked from a working tree where<cli-dir>/../.git/exists) and defaultsAGENTS_NO_AUTOPULL=1,AGENTS_SKIP_MIGRATION=1, andAGENTS_CLI_DISABLE_AUTO_UPDATE=1. No more typing those three env vars on every iteration. Production installs (registry global, no.git/at package root) are unaffected.
1.18.1 — 2026-07-07
Fixes
scripts/build.shnow sets mode0o755on every file declared inpackage.json#binaftertscemits dist/. Newer npm versions preserve file mode from the published tarball and do NOT auto-chmod the bin target duringnpm install -g, so 1.18.0 shipped with mode-644 entrypoints. Users hitzsh: permission denied: agentsafter auto-update. Re-install to recover:npm install -g @phnx-labs/agents-cli@latest.- New
scripts/install.shbuilds the working tree as a side-by-side dev install at$HOME/.local/agents-cli-dev/, symlinked into$HOME/.local/bin/agents. The registry install is never touched —agents --versionshows0.0.0-dev.<sha>[-dirty]when the dev build is on PATH.
1.18.0 — 2026-07-07
Plugins
~/.agents/plugins/is now a first-class user-resource location, alongsideskills/,commands/,hooks/, etc. — git-tracked as source of truth. Previously,migrateRuntimeToCachemoved~/.agents/plugins/into~/.agents/.cache/plugins/on every CLI version bump, silently destroying user-authored plugins in the working tree. Fixed by (1) removing the destructive move, (2) restoring discovery to the user-root, (3) a one-shot reverse migration that moves any cached plugins back to the user-root without overwriting an existing user-root copy, and (4) decoupling the migration sentinel from the binary version so migrations only re-run on real schema bumps. (#20)agents view <agent>@<version>gains aPluginssection listing each plugin that supports the agent, with a(N skills, N commands, …)content summary and an OSC 8 hyperlink to the plugin source.
Hooks
getAvailableResourcesand the version-home sync now treat only executable files inhooks/as hooks. Docs (README.md) and data files (promptcuts.yaml) that live alongside hooks no longer get synced into version homes as hooks, and the orphan-pruner trusts the manifest's declared hook list rather than re-scanning every source dir.
1.17.6 — 2026-07-07
Workflows
- New
workflowsskill — author-and-run guide for workflow bundles (WORKFLOW.mdfrontmatter,subagents/directory for multi-agent pipelines, scopedskills/andplugins/, sharing viaagents repo pushor GitHub install). Calls out the--mode plandeadlock that bites workflows which need to post comments or edit files. agents workflows --helprewritten with a structure diagram, project > user > system resolution order, and an explicit note that workflows mutating state need--mode editor--mode fullto avoid a headless deadlock atExitPlanMode.- README gains a
Workflowssection between Teams and Browser covering the bundle layout, frontmatter, subagents/skills/plugins, and the--moderequirement.
1.17.4 — 2026-07-07
Browser
agents browser typenow detects rich-text editor frameworks (Lexical, ProseMirror, Slate, Draft.js, Quill, CKEditor5, Trix) by walking up to 5 ancestor levels from each textbox and tagging refs with[editor=<framework>]. Editor-tagged refs route through the WHATWGbeforeinputdispatch (InputEvent('beforeinput', { inputType: 'insertText', ... })) for Lexical/ProseMirror/Slate/Quill/CKEditor5/Draft andel.editor.insertString()for Trix.agents browser refs --jsonsurfaces the neweditorfield, andtype --clearprepends a select-all +deleteContentBackwarddispatch before inserting.- Plain-input reliability also improved:
typeTextnow issues a single CDPInput.insertTextinstead of per-characterdispatchKeyEvent, so framework-controlled inputs (React, Vue, Solid, MUI/Chakra/MantineTextField, masked-number fields, Canva-style pickers) actually receivebeforeinput/input/textInputevents.focusNodefalls back to the first focusable descendant whenDOM.focusthrows "Element is not focusable" — fixes wrapper-ref UIs like Slack composer, Linear comments, Notion blocks, and every MUI/Chakra/MantineTextField. (#12)
1.17.3 — 2026-07-07
Browser
agents browser profiles creategains--electron,--binary, and--target-filterfor driving Electron desktop apps (Canva, Slack, etc.) that expose multiple CDP page targets. The picker matches byurl:<substring>ortitle:<substring>(case-insensitive) and falls back to a skip-invisible heuristic when no filter is set; misses against an explicit filter throw with the full candidate list.BrowserService.evaluatenow usesawaitPromise: trueand surfacesexceptionDetailsso async script errors propagate as thrown errors. (#14)
Secrets
agents secrets listrework — drop the misleadingSENSITIVEcolumn and addSYNC(iCloud yes/no) plusCREATED/UPDATED/USEDrelative-age columns. Timestamps live inside the keychain bundle JSON, are stamped on write (created sticky, updated always advances), and on resolve via a 60s throttle. SetAGENTS_NO_USAGE_TRACK=1to disable the usage stamp.agents secrets viewshows the matching absolute ISO + relative age fields. (#18)
1.17.2 — 2026-07-07
Fixes
- Auto-update prompt no longer hangs in non-interactive environments (CI, k8s pods, cloud sandbox factories). The TTY check now requires both stdin and stdout to be terminals before prompting, and
AGENTS_CLI_DISABLE_AUTO_UPDATE=1forces the check off entirely for headless deploys. (#15)
1.17.1 — 2026-07-07
Agent management
agents import <agent>— adopt an existing global npm/homebrew install into agents-cli management without reinstalling. Supports--version,--from-path,--yes. The imported version is wired in as the global default with shim + versioned alias so it behaves the same as a freshlyagents add'd install.
1.17.0 — 2026-07-07
Workflows: a new first-class resource
agents workflows list / add / remove / view— WORKFLOW.md bundles (with optionalsubagents/,skills/,plugins/) install from GitHub or a local path and resolve through the same system → user → project layer model as every other resource.agents run <name>resolves a workflow or named subagent as an orchestrator: prepends WORKFLOW.md / AGENT.md body to the prompt, copiessubagents/*into~/.claude/agents/for Agent-tool discovery, and syncs workflow-scopedskills/andplugins/at run time.agents viewnow has a workflows section.
Browser
- Port-per-profile with auto-allocation and viewport enforcement — concurrent browser profiles no longer collide on CDP ports.
agents browser scrollplus newprofiles launch,profiles doctor,profiles prime, viewport position, and port diagnostics commands.agents browser profiles listnow shows a description column when any profile has one.isProcessRunningtreats EPERM as process-alive (fixes false-negative on sandboxed processes).
Cloud dispatch
--balancedstrategy and--upload-account-tokensflag on cloud dispatch.- Remote account API client;
--balancedskips the client manifest path.
Plugin system extension
- Plugins now ship with
commands/,agents/,bin/, MCP configs, settings, andinstall/updatehooks. Discovery and sync extended end-to-end.
Secrets
agents secrets import <bundle> --from-1password/export <bundle> --to-1passwordwith vault picker, skip-empty-fields on import, overwrite-only-with---forceon export. Wires the existing 1Password library into the CLI.
Sandbox
scripts/sandbox.sh --pr— author real PRs from a Crabbox-isolated box via a bare-mirror clone off main.sandbox.sh --linearand--post-filepost run output to Linear tickets.- Dynamic GitHub App token,
ghCLI installed, stale git credentials cleaned.
Sessions / SQLite concurrency
- Scan coordinator prevents concurrent session indexing.
- SQLite concurrency hardened with
BEGIN IMMEDIATEand ledger recheck on contention. - Session discovery uses
getHistoryDirfor version roots and backup paths.
Run / shims / hooks
- Versioned alias shims regenerate on startup if missing.
- Hooks prefer version-home scripts to prevent path breakage when the source dir moves.
- Linux: claude shim sources
CLAUDE_CODE_OAUTH_TOKENfrom the per-version.oauth_tokenfile when unset.
Resource UI
agents viewreplaces path columns with OSC 8 hyperlinks for commands, skills, and rules.- Flat version resource lists replaced with source-pattern selection.
CI / security
- Gitleaks secret-scanning workflow on every push (switched to the free CLI, no org license needed).
Postinstall
- Correct shims dir, expanded aliases, prints changelog on install.
Dev
- Test isolation via vitest
pool: 'forks'; mock state paths instead of hitting real~/.agents/. - Concurrent-writes benchmark for the session indexer.
- Dead code + phantom deps removed:
src/commands/fork.ts,@aws-sdk/client-s3,@modelcontextprotocol/sdk,semver.
1.16.0 — 2026-07-07
System-repo sweep: ~/.agents-system reduced to npm-shipped defaults only
- New migrators move every form of operational state out of ~/.agents-system into user-side buckets: sessions, teams (live + per-run), trash, repos (→ ~/.agents-
/ peer dirs), legacy swarm/, cache/, cloud/. - SQLite DBs merge row-level (INSERT OR IGNORE) into the user-side DB; filesystem dirs merge dir-by-dir with user-side winning on collision.
- Dead artifacts dropped automatically: bin/agents-keychain-*, empty shims/, .DS_Store-only versions/ skeletons.
- Unrecognized leftover dirs print a one-line stderr warning so future drift surfaces immediately.
- Migration diagnostics moved to stderr —
eval "$(agents secrets export …)"stops being polluted by log lines. - DB merge now skips FTS5 virtual + shadow tables (previously corrupted the session_text index). Indexer re-populates FTS on the next scan.
- Stale ~/.agents-system/agents.yaml is now dropped when a user copy exists.
~/.agents split into .history/ and .cache/ buckets
- Durable runtime state (sessions, versions, runs, teams/agents, trash, backups) moves to ~/.agents/.history/.
- Regenerable runtime state (shims, packages, cloud, logs, companion, helpers, browser runtime, fetch cache, dot-files) moves to ~/.agents/.cache/.
- Single-line gitignore for backing up ~/.agents/ — no more per-subdir cherry-picking.
Browser: profiles fold into agents.yaml + many new automation commands
- Profile YAMLs at ~/.agents/browser/profiles/*.yaml now live as a
browser:section in agents.yaml. Single user-facing file, single sync. - Single window per profile;
startrenamed toopen; new tab subcommands; session history with profile picker; viewport piped through to the launched browser. - New commands:
agents browser set viewport,set device,devices,console,errors,requests,responsebody,wait,download,waitdownload.
Hooks: hooks.yaml folded into agents.yaml hooks: section
- ~/.agents/hooks.yaml is migrated into agents.yaml on first run; the standalone file is removed.
- System repo ships the same shape — one config file, layered project > user > system.
Sessions & secrets
agents secrets exec <bundle> -- <command>injects a bundle's env vars into a one-shot subprocess (no shell-state leakage).agents sessionsnow groups active sessions by workspace and surfaces session topics in the picker.- Session discovery scans both version repos; migrator merges overlapping versions instead of leaving duplicates.
Renames
agents init→agents setup.permissions/sets/→permissions/presets/(resource directory + on-disk migration to match rules/presets convention).
Dev
- Crabbox remote-test profile (~$0.14/hr) +
scripts/sandbox.shdocumented in README and CLAUDE.md. Tests run remotely to avoid freezing the local machine.
1.15.0 — 2026-07-07
Secrets: Linux support via libsecret/GNOME Keyring
agents secretsnow works on Linux backed by libsecret/GNOME Keyring with the same UX as macOS Keychain. Headless workarounds documented.- New
agents password generatesubcommand. - Lifecycle events emitted for secrets and other subsystems; richer metadata (timing helpers) on the events system.
Browser
- HTTP and WebSocket endpoint support for remote browsers.
- Concurrent Electron profile forks no longer step on each other; cleanup hardened.
- Remote browser restart works; SSH port handling improved; page target created when none exists for Electron apps.
- Events emitted for navigation and screenshots.
First-run UX
- Improved new-user experience: clearer CLI help, better defaults, audit-log opt-out, better run-timing display.
Prune
agents prunelearnedtrash,sessions, andrunscleanup targets.
Fixes
- Command-injection hole in daemon + secrets closed.
- Layered permission resolution corrected; daemon tests isolated from real user state.
.tmp-bungitignore pattern fixed.codexinteractive mode no longer routes throughexecsubcommand.
Docs
- Security/privacy section in README, browser skill + automation guide, FAQ updated with audit-log transparency.
1.14.6 — 2026-07-07
Fix: OAuth token refresh now persists to Keychain
- Fixed bug where refreshed Claude OAuth tokens were used but never saved back to macOS Keychain
- Previously, agents-cli would refresh expired tokens on each run but discard them, eventually exhausting the refresh token
- Now refreshed
accessToken,refreshToken, andexpiresAtare written back to Keychain after successful refresh - Accounts will stay healthy across runs without requiring re-login
1.14.5 — 2026-07-07
Browser: custom binary and Electron app support
- Added
binaryfield to browser profiles for specifying custom executable paths (e.g., Electron apps like Rush) - Added
electronfield to browser profiles — when true, uses existing windows instead of creating new ones (Electron doesn't supportTarget.createTarget) - New
custombrowser type that requires a binary path - Works with both local and SSH-based browser connections
- Example profile for Rush:
agents browser profiles edit rush --browser custom --binary "/Applications/Rush.app/Contents/MacOS/Rush" --electron
1.12.0 — 2026-07-07
JSON output for sessions list
- Added
--jsonflag toagents sessions listandagents sessionsfor programmatic use - Output is a JSON array of session metadata (id, shortId, agent, version, account, project, cwd, filePath, topic, messageCount, tokenCount, timestamp)
- Enables the Companion VS Code extension's "Agents: Session Resume" and "Agents: Session Trace" pickers
OpenClaw workspace-aware sessions
- Fixed
agents sessions --agent openclawso synthetic OpenClaw rows now use the configured agent workspace from~/.openclaw/openclaw.json - When no per-agent workspace is available, OpenClaw session discovery now falls back to
~/.openclawinstead of leavingcwdempty or filling it with status text - Added a regression test covering managed OpenClaw homes symlinked through
~/.agents/versions/openclaw/...
1.11.1 — 2026-07-07
Session search and version labeling
agents sessions viewnow opens a live-search picker by default in interactive terminalsagents sessions --agent ...andagents sessions --project ...now open the same live-search picker before falling back to the table viewagents sessions view <query>now resolves prompt text, not just exact session IDs- Fixed
--projectsearch so it scans across directories instead of intersecting with the current working directory - Session topics now skip injected scaffolding and use the first human prompt
- Codex session rows now show the real CLI build from
cli_version(for example[email protected]) - Gemini, OpenCode, and OpenClaw session rows now resolve and display agent versions consistently in the shared
Agentcolumn - Claude usage lookup now falls back across scoped and legacy Keychain services when loading OAuth credentials
1.11.0 — 2026-07-07
PTY -- interactive terminal sessions for AI agents
- New
agents ptycommand suite for persistent, interactive PTY sessions - Sidecar server architecture -- lightweight daemon on
~/.agents/pty.sock, auto-starts on first use agents pty start-- spawn a session with configurable rows, cols, shell, and working directoryagents pty exec <id> <command>-- submit commands (non-blocking, sentinel-based completion detection)agents pty screen <id>-- render the terminal as clean text (no ANSI codes), powered by xterm-headlessagents pty write <id> <input>-- send keystrokes with escape sequence support (\n,\t,\e,\xHH)agents pty read <id>-- read raw PTY output with configurable timeoutagents pty signal <id> [INT|TERM|KILL]-- send signals to the PTY processagents pty list-- show active sessions with status, PID, age, and active commandagents pty server start|stop|status-- manage the sidecar server directly- Session idle cleanup (30 min) and server auto-exit (1 hour with no sessions)
--jsonoutput on all commands for scripting- Auto-fixes node-pty spawn-helper permissions on startup (bun install workaround)
1.10.0 — 2026-07-07
Drive -- sync agent sessions across machines
- New
agents drivecommand for syncing agent state between machines via rsync over SSH agents drive remote <user@host>-- set sync target (syncs to~/.agents/drive/on remote)agents drive pull/push-- additive rsync (no data loss, both sides accumulate)agents drive attach-- swap~/.claudesymlinks to the drive, so Claude reads/writes thereagents drive detach-- restore symlinks to the version homeagents drive status-- show remote, attached state, symlink targets, last sync times
1.9.1 — 2026-07-07
Better sessions
- Sessions list and picker show
Agent@Versioncombined column (e.g.,[email protected]) - Added
Topiccolumn showing first user message of each session - Account shows email instead of display name
1.9.0 — 2026-07-07
New agents, routines, and better sessions
Agents:
- Added support for 5 new agents: Copilot, Amp, Kiro, Goose, and Roo Code
- Agent type expanded to 11 agents total
Routines (renamed from cron):
agents cronis nowagents routines-- aligns with Claude Code Routines namingagents cronandagents jobsstill work as deprecated aliases~/.agents/cron/directory renamed to~/.agents/routines/
Sessions:
- Sessions list now shows
Agent@Versionin a combined column (e.g.,[email protected]) - Added
Topiccolumn showing the first message of each session - Account column now shows email instead of display name
- Session picker uses the same columns as the list view
Other:
- Account email preferred over display name across the CLI
- Rewritten help text for all top-level commands
1.6.12 — 2026-07-07
"memory" is now "rules"
The agents memory command has been renamed to agents rules. This better reflects what these files actually are -- instruction files like AGENTS.md, CLAUDE.md, and .cursorrules that tell your agents how to behave.
agents rules list-- see your instruction files across all agentsagents rules add-- install and sync rule files from a repo or local pathagents rules view-- view rule file content for any agentagents rules remove-- remove a rule file
If you run agents memory, you'll see a message pointing you to the new command.
The files themselves haven't changed -- AGENTS.md is still AGENTS.md. Only the CLI command name changed.
1.6.8 — 2026-07-07
Bug fix
- Skip commands and memory sync for agents that don't support file-based commands (openclaw)
- Added
commandscapability flag to agent configs agents use openclawandagents view openclawno longer show or sync slash commands or memory files- Fixed
hasNewResourcesto filter by agent capabilities (was triggering prompt even when no applicable resources existed)
1.6.5 — 2026-07-07
Bug fix
- Fixed memory file detection counting symlinks as separate files (CLAUDE.md/GEMINI.md -> AGENTS.md)
1.6.4 — 2026-07-07
Bug fixes
- Fixed Claude email not showing in
agents view(was reading from version home instead of real ~/.claude.json) - Fixed memory file updates not being detected in
agents use(now compares content, not just existence)
1.6.3 — 2026-07-07
Bug fix
- Fixed infinite "new resources available" loop in
agents view - Partial resource syncs no longer wipe out previously synced resources
1.5.82 — 2026-07-07
MCP & Permission improvements
- MCP configs now stored as YAML in
~/.agents/mcp/(was JSON) - Permissions now use groups from
~/.agents/permissions/groups/ - Resource selection shows proper counts: "Permissions (19 groups, 3132 rules)"
- When selecting "specific" permissions, shows individual groups with rule counts
- Added MCP support for cursor and opencode agents
- Removed
agentsfilter from MCP configs - selection tracked in agents.yaml - Added capability checks for MCPs (consistent with hooks/permissions)
1.5.81 — 2026-07-07
Cron jobs & unified execution
- Renamed
jobscommand tocron(jobsstill works with deprecation warning) - New
agents exec <agent> <prompt>for unified agent execution across all CLIs - Inline job creation:
agents cron add my-job --schedule "..." --agent claude --prompt "..." - One-shot jobs with
--at:agents cron add reminder --at "14:30" -a claude -p "..." - New
agents cron edit [name]opens job in$EDITOR - Timezone support:
--timezone America/Los_Angeles - Custom variables in prompts: define
variables:block, use{var_name}in prompt - Interactive pickers for all cron subcommands when name is omitted
- Smart filtering:
resumeshows only paused jobs,pauseshows only enabled jobs - Effort-based model mapping:
--effort fast|default|detailedmaps to agent-specific models
Resource command cleanup
- Added
viewcommand to commands, mcp, hooks, and permissions - Removed
pushcommands from all resources (commands, skills, mcp, memory, hooks) - Deprecated
permsalias forpermissions(shows warning but still works) - Deprecated
infoalias forskills view,showalias formemory view
1.5.68 — 2026-07-07
- Upgrade prompt now shows on ALL command flows (--version, --help, bare
agents)
1.5.67 — 2026-07-07
Unified view command
- New
agents viewcommand replaceslistandstatus agents view/agents view claudeshows installed versionsagents view [email protected]shows full resources (commands, skills, mcp, hooks, memory)- Old commands show deprecation warning but continue to work
1.5.48 — 2026-07-07
Simplified repo structure
- Flattened repo structure: removed
shared/prefix - Resources now live at top level:
commands/,skills/,hooks/,memory/,permissions/ - Removed agent-specific override directories (no more
claude/commands/, etc.) - Simplified discovery functions
1.5.29 — 2026-07-07
Version-aware resource installation
agents pullnow prompts for version selection per agent when multiple versions are installed- Resources (commands, skills, hooks, memory) are linked into version homes at pull time via
syncResourcesToVersion() - Simplified shims: HOME overlay + exec only (~80 lines, down from ~160). No more runtime sync logic.
- MCP registration uses direct binary path for version-managed agents (bypasses shim)
1.5.7 — 2026-07-07
- Remove trailing newlines from command output
1.5.5 — 2026-07-07
- Update prompt: Interactive menu before command runs (Upgrade now / Later)
1.5.4 — 2026-07-07
cli list: Shows spinner while checking installed CLIs
1.5.3 — 2026-07-07
skills view: Opens in pager (less) for scrolling, pressqto quit
1.5.2 — 2026-07-07
skills view: Truncate descriptions to fit on one line
1.5.1 — 2026-07-07
- Update check: Shows prompt when new version available
- What's new: Displays changelog after upgrade
skills view: Interactive skill selector (renamed frominfo)- Fixed
--versionshowing hardcoded 1.0.0 (now reads from package.json) - Silent npm/bun output during upgrade
1.5.0 — 2026-07-07
Pull command redesign
- Agent-specific sync:
agents pull claudesyncs only Claude resources - Agent aliases:
cc,cx,gx,cr,ocfor quick filtering - Overview display: Shows NEW vs EXISTING resources before installation
- Per-resource prompts: Choose overwrite/skip/cancel for each conflict
-yflag: Auto-confirm and skip conflicts-fflag: Auto-confirm and overwrite conflicts- Graceful cancellation: Ctrl+C shows "Cancelled" cleanly
1.4.0 — 2026-07-07
- Conflict detection for pull command
- Bulk conflict handling (overwrite all / skip all / cancel)
1.3.13 — 2026-07-07
- Enabled skills support for Cursor and OpenCode
- Fixed Cursor MCP config path (now uses mcp.json)
1.3.12 — 2026-07-07
- Fixed MCP detection for Codex (TOML config format)
- Fixed MCP detection for OpenCode (JSONC config format)
- Added smol-toml dependency for TOML parsing
1.3.11 — 2026-07-07
- Status command shows resource names instead of counts
- Better formatting for installed commands, skills, and MCPs
1.3.0 — 2026-07-07
- Added Agent Skills support (SKILL.md + rules/)
- Skills validation with metadata requirements
- Central skills directory at ~/.agents/skills/
1.2.0 — 2026-07-07
- Added hooks support for Claude and Gemini
- Hook discovery from hooks/ directory
- Project-scope hooks support
1.1.0 — 2026-07-07
- Added MCP server registration
- Support for stdio and http transports
- Per-agent MCP configuration
1.0.0 — 2026-07-07
- Initial release
- Pull/push commands for syncing agent configurations
- Slash command management
- Multi-agent support (Claude, Codex, Gemini, Cursor, OpenCode)