changelog

Every shipped version of agi-cli. Same file checked into the repo; this page is just rendered from it.

Changelog

1.22.122

  • agents monitors is removed (PHNX-4241). The command group, the monitor engine, and the daemon's monitors service are gone; agents monitors … now fails as an unknown command. Upgrading stops monitor polling and dispatch: the self-updating daemon relaunches onto code with no monitor service, and a stale monitors: key in ~/.agents/daemon/services.yaml is ignored. Monitor definitions in ~/.agents/monitors/ and fire history in ~/.agents/.history/monitors/ are left on disk untouched. Use agents routines for scheduled work, webhook receivers for push events, and agents projects prs automerge for merge-on-green. Source: cli/src/lib/daemon/daemon.ts, cli/src/lib/daemon-services.ts, cli/src/cli/command-registry.ts.

  • agents sessions stats ranks a plugin skill under the same name --zero lists it. A plugin skill invoked by its bare name (Skill image from the create plugin) was recorded as image, while the installed inventory calls it create:image, so the same skill showed as invoked in the ranked list and as never invoked in --zero. Usage is now recorded as <plugin>:<name> when the plugin is known, and a /name slash invocation that only resolves to a skill counts as a skill. Existing bare rows are renamed when the index opens; run agents sessions backfill resources once to reclassify older slash-typed skills (the coverage line prompts for it). Source: cli/src/lib/session/db.ts.

  • agents sync <repo> --delete removes what a repo deleted. A plugin, command or skill deleted from a DotAgents repo used to stay installed in agent homes, most visibly when a non-interactive sync skipped the resource selection. --delete trashes the ones whose recorded source is in the named repo and is gone from it, unregisters removed plugins from settings.json, and keeps everything from other repos or installed by hand (kept: N not from <repo>). It covers plugins, commands and skills (not hooks, MCP, subagents, rules or workflows), needs a repo, works without a TTY, and previews with --dry-run. A sync without --delete is unchanged. Source: cli/src/lib/sync-delete.ts, cli/src/commands/sync.ts.

  • agents sync no longer hangs without a terminal. When new resources were pending, a sync with no TTY opened the "Sync new resources?" prompt anyway and could wait forever (it never resolved under Bun). It now skips the selection, syncs nothing new, and says so: Skipped resource selection for <agent>@<v> (no terminal; rerun with --yes to sync new resources).

  • Session rows report what an agent made for you to look at. With @phnx-labs/sessions-cli 0.6.0, a row's artifacts include created images, video, PDFs, SVGs and CSVs (the harness scratchpad excluded) and the viewable files of folders the agent names in its replies, when they exist under this machine's home outside a git checkout. Each carries editedAtMs; the list is newest first, at most 60. AGI EXT 0.9.381+ shows them as folder thumbnails in the session sidebar. It costs about 0.6 ms more per transcript change on a 2,000-event session. Source: cli/package.json.

  • agents projects prs merge no longer refuses a PR whose mergeability GitHub never computed. On some repositories mergeable_state stays unknown for hours, so the merge (and AGI Menu's Merge button) was never possible. An uncomputed state now goes on to the merge pinned to the reviewed head commit, and GitHub's own merge check decides: it merges, or refuses with its reason. blocked, behind, dirty and draft are still refused before any write. Source: cli/src/lib/github/project-prs.ts.

1.22.121

  • agents sessions reads use the sessions binary this CLI already depends on. With $SESSIONS_BIN unset, a read runs the @phnx-labs/sessions-cli dependency instead of requiring a second global sessions install. The package exports map publishes only ./reader, so the lookup walks Node's module paths and reads the bin field. A non-empty $SESSIONS_BIN still pins the binary. An empty $SESSIONS_BIN keeps the PATH lookup. No standalone at all, an older binary, and a peer exit 127 still stay on the in-repo engine. Source: cli/src/lib/sessions-client.ts.

  • The transcript reader is imported from @phnx-labs/sessions-cli/reader, not kept in-repo (PHNX-4118). The pure parse→render→analyze pipeline (parse, render, state, tool-calls, timeline, prompt, insights, bash-command, trajectory + text/html/lineage/ compare, digest, tail, highlights, shell-programs, team-filter, linear, artifacts, stream-render, share-html, and the SessionEvent/SessionMeta/SessionStep types) now comes from the published @phnx-labs/[email protected] package's /reader subpath — the same code the standalone sessions bin runs — deleting ~13k LOC of a byte-for-byte in-repo copy. The reader is imported in-process (a normal node_modules import, never a subprocess), so the indexer warm-tick (db.ts), the eval loop (lib/traces/sync.ts), and live-state (active.ts) still parse without shelling out. No user-visible behavior change: sessions <id> --json/--markdown, trace, timeline, and insights render exactly as before. The CLI-owned half — the writer/indexer, lifecycle, live-identity, and remote/sync code — stays in cli/src/lib/session/. Source: cli/package.json, cli/src/lib/session/, cli/src/lib/traces/sync.ts.

  • Two processes can no longer both release the same feed answer claim. The release token was created empty and filled a moment later, so a peer that read it in that gap judged it stale, deleted it, and released the claim a second time. The token is now written to a temp file and linked into place with its owner already inside, and a token whose owner cannot be read is aged by its file time instead of being treated as abandoned. Source: cli/src/lib/feed/feed.ts (acquireReleaseToken, PHNX-4131).

  • agents ps: the live agent roster and the verbs that act on it (PHNX-4227, R1). agents ps lists the sessions running now on this machine and every reachable device (--json, --local, -D/--device, --status <state>), and opens the session picker filtered to running sessions on a TTY (r toggles the filter). agents ps stop|focus|detach|migrate <id> run the same code as the sessions verbs. agents sessions --active and every sessions verb keep working unchanged; callers move in the next release. Source: cli/src/commands/ps.ts, cli/src/commands/ps-roster.ts.

  • agents send --channel session --to <id> types into a running agent's terminal, what agents sessions inject <id> does (text, then Enter, as two writes). The id is a session id or prefix, the <shortid> of an ag-<agent>-<shortid> tmux name, or a %pane; --device <name> delivers on another box. --attach, --thread and --from are refused on this channel. Source: cli/src/lib/channels/providers/session.ts.

  • agents setup tools installs or upgrades the standalone CLIs to their pinned releases: sessions-cli 0.5.0, browser-cli 0.1.15, secrets-cli 0.1.8, computer-cli 0.1.5, term-cli 0.1.0. A newer install is left alone; exit 1 when a tool is still below its pin. Source: cli/src/lib/standalone-tools.ts.

  • Fix: sessions --active read empty after the daemon refreshed the live snapshot. The daemon published local rows without machine, which the running filter requires, so the roster (and a peer's answer to -D <device>) was empty whenever the daemon's snapshot was the newest. Rows are now stamped with this machine's id. Source: cli/src/lib/session/session-cache.ts.

  • agents send --device <box> now runs the send on that box, for any channel. It was refused before (send had no remote interpretation). It exists so --channel session can reach a pane on another device.

  • agents setup secrets installs secrets-cli 0.1.8, up from 0.1.5, the same version agents setup tools pins. Source: cli/src/lib/secrets-cli.ts.

  • agents projects todo add takes a description, assignee, due date and priority (PHNX-4244). --description, --assignee <name|email>, --due YYYY-MM-DD and --priority urgent|high|medium|low|none back AGI Menu's quick-add form; each beats the same field typed in the line (so does --project now). The issue still lands in the active cycle as Todo with no delegate, so any agent's queue picks it up. A title under 3 or over 120 characters, a due date in the past, and a description over 10,000 characters are refused before anything is created. Source: cli/src/lib/quick-todo.ts, cli/src/commands/projects-todo.ts.

  • agents setup browser installs browser-cli 0.1.15, up from 0.1.5. 0.1.15's browser profiles logins reads Arc, Comet and pinned profiles from their real stores and has a --json mode, which the fleet credentials-catalog hook uses to tell agents which profile is signed in where. The pin only applies to a fresh install; an existing browser is left alone. Source: cli/src/lib/setup-tool-install.ts.

  • Session rows carry the facts the sidebar needs to read an agent at a glance (PHNX-4218). A watch row now includes the latest reply's model, the newest 20 failed or policy-blocked calls, a 48-bucket activity histogram, the user's own turns, and this session's subagents (task, type, tools, result). subAgentCount counts this session's own subagent transcripts when it has any; an empty subagents/ directory no longer reports zero over the tool-call count. Pasted images are written once under ~/.agents/.cache/attachments/<session>/ so the editor can show them, including a paste larger than one timeline read. That record resumes from the next tick instead of freezing the row, and its image bytes are not stored in the timeline cache. An unfinished record that still fits in one read is left until its newline arrives, so a long turn that is still flushing is not dropped. Plans rendered by the artifacts CLI join the row by session id. The projection comes from @phnx-labs/[email protected] and is folded once in the daemon timeline pass. Older extensions ignore the new fields. Source: cli/src/lib/session/glance-files.ts, cli/src/lib/session/timeline-pass.ts, cli/src/lib/session/active.ts, cli/package.json.

  • Hooks no longer run twice after a sync. Hook registrations are now matched to the manifest by event, matcher and command, and any copy agents-cli wrote into another version home or account slot is treated as its own and dropped. Before, a Claude or Codex account slot kept the version-home registrations it carried forward next to its own, so every Stop, SessionStart and prompt hook ran twice in an account session, and hooks deleted from the source repos (or from a removed version) stayed registered. A hook whose script serves several events (feed-publish) also kept a direct registration beside its shim, which ran it twice on AskUserQuestion. The next sync removes the extra entries. Source: cli/src/lib/hooks/install.ts.

  • Installed host CLIs now follow their version pin. The daemon's update tick upgrades browser, secrets, computer and any other host CLI whose manifest pins an npm version (npm: "@phnx-labs/[email protected]") when the installed binary reports an older version. Before, the pin only applied to a first install, so a box kept whatever version it started with. The upgrade targets the npm prefix that owns the binary on PATH, is confirmed by re-running --version, never installs a missing tool or downgrades one, ignores project-layer manifests, and leaves the old version in place if the install fails. Source: cli/src/lib/cli-resources.ts (upgradeOutdatedClis), cli/src/lib/daemon/self-update-service.ts.

  • Act on a pull request from the CLI: ready, approve, comment (PHNX-4215). agents projects prs ready|review --approve|comment <project> --repo R --number N join prs merge, each with a --json result AGI Menu reads. review reads the live head, refuses one that moved since --sha, and records the approval against that SHA; comment takes --body or --body-file -. Approve and comment are single REST calls; ready is one GraphQL mutation because GitHub has no REST way to leave draft. Source: cli/src/lib/github/project-prs.ts.

  • AGI Menu preferences for pins, tabs, and milestone grouping (PHNX-3999). menubar.menu.pinnedProjects, menubar.menu.tabOrder, menubar.menu.hiddenTabs, and menubar.menu.groupTicketsByMilestone sync fleet-wide. In agents menubar snapshot --json, the boolean rides menuPreferences and the three lists ride a new menuListPreferences map, so menus that predate them keep decoding the snapshot. List keys take a JSON array or comma-separated items and are validated when written. Source: cli/src/lib/device-config.ts.

  • AGI Menu 1.7.0: each project opens on a Tickets / Pull requests switch (PHNX-4215). The pull-request board lists a project's open PRs with author, ages, and a description excerpt; filters for All, Ready, Drafts, Mine, and repo; scoping to the project's own folders in a shared monorepo; and a two-click merge pinned to the head you saw. It reads agents projects prs (1.22.119). The menubar floor moves to 1.7.0, so installed CLIs download it on their next helper check. Source: cli/src/lib/helper-versions.ts.

  • agents menubar snapshot --json names who is signed in, with a profile picture. The snapshot gains a top-level me object (name, email, github, avatarUrl, avatarSource) so AGI Menu can show the person instead of initials. When you are signed in with agents auth login, that session is the person: its name, email and picture are used, and your gh account adds its login (and fills a missing name or picture) only when its public profile email matches the session email. Without a Phoenix ID session, the gh account supplies everything except the email. me is null when neither is available. GitHub facts come from gh api user, recorded in ~/.agents/.cache/github-viewer.json (no email, only a SHA-256 of the public one). A snapshot spawns gh only when that record is stale: at most once a day, or once an hour after a failed read, which keeps the last good answer. Each spawn is capped at 5 seconds. agents auth login and agents auth whoami now save your Phoenix ID display name, and the session file is rewritten atomically and kept at mode 0600. Source: cli/src/lib/menubar/snapshot.ts, cli/src/lib/github/viewer.ts.

  • Slow monitors no longer restart the daemon every two minutes. The monitors service polled every due monitor one after another, so a set of slow polls (agents devices ps takes about 30 s) kept its tick past the 2-minute deadline and the daemon exited for a restart, which also dropped every agents feed watch stream. A tick now runs up to four polls at once and starts new ones only in its first minute; monitors it did not reach run first on the next tick. A timed-out command poll now kills the whole pipeline, not just the shell. Source: cli/src/lib/monitors/engine.ts, cli/src/lib/monitors/sources/command.ts, cli/src/lib/daemon/monitor-engine-service.ts.

  • Merge a blocked PR as an admin, or let it merge itself once checks pass. agents projects prs merge now refuses a PR that is not mergeable as-is (blocked by branch protection, behind, or a state GitHub has not computed yet) unless --admin is passed; --admin lets a repository admin merge past branch protection where GitHub allows it, and is meant only for a person's explicit confirm (AGI Menu's "Confirm admin merge"). The fleet's gh-merge-guard denies it to agents. Refusals now read plainly: "Required check test hasn't passed", "The head moved since you looked". New agents projects prs automerge turns GitHub auto-merge on (pinned to the head you reviewed) or off with --off. prs --json gains merge: { viewerIsAdmin, adminBypass, autoMergeAllowed, methods } per repository and autoMerge per PR. prs review --approve on your own PR answers without calling GitHub. Source: cli/src/lib/github/project-prs.ts.

  • See CI and recently merged PRs in agents projects prs. Every open PR row in --json now carries ciState (GitHub's rollup of the head commit) and failingChecks, and each repository adds recentlyMerged (PRs merged in the last 7 days, with CI on the merge commit), defaultBranch (the default branch head and its CI), ciError, and truncated, so AGI Menu can show build health without opening a browser. Everything is read over REST. A commit's CI is cached once all of its checks finished, for an hour when green and five minutes when red, so a re-run of a failed job shows up within five minutes. When a read fails, ciError says why instead of the menu showing "no checks", and truncated marks a merged list that hit its page cap. Shared monorepos scope merged PRs the same way as open ones. Source: cli/src/lib/github/project-prs.ts.

  • See why CI failed, re-run it, and spot a stalled release from agents projects prs. agents projects prs failure <project> --repo <r> --sha <commit> --json lists each failing check on a commit with the error lines of its GitHub Actions job log (timestamps, colour and runner cleanup removed, at most 12 lines), and agents projects prs rerun <project> --repo <r> --run-id <id> re-runs a workflow run's failed jobs. Each repository in prs --json now carries release: the latest version tag, when it was cut, how many merges landed since, and the npm version of the package it released, so AGI Menu can say "v1.22.121 tagged · npm still 1.22.120". Two new AGI Menu preferences, menubar.menu.prGroupOpen and menubar.menu.prGroupMerged (none, type or day), remember how the PR board is grouped. All GitHub reads are REST. Source: cli/src/lib/github/ci-failure.ts, cli/src/lib/github/release-drift.ts.

  • Capture a to-do into Linear from one line: agents projects todo. add "Renew npm token #AGI tomorrow !!" reads the project, due day and priority from the text and creates a Linear issue assigned to you in the active cycle; list shows your open quick to-dos and anything due today or overdue; done closes one; undo reopens a closed one or cancels a to-do created moments ago. All take --json; AGI Menu's Home to-do line runs them. Linear is the record: nothing is stored locally. Source: cli/src/lib/quick-todo.ts.

  • The daemon no longer crash-loops on self-heal. The self-heal pass compared every synced skill and plugin file in every version home synchronously on the daemon's only thread. On a box with many version homes that took over a minute, other services missed their deadlines, the daemon exited for a restart, and the restart ran self-heal again 30 seconds later, pinning a CPU core and dropping the feed stream every ~77 seconds. Self-heal now runs in its own child process (agents __self-heal-run), a restart no longer re-runs it inside its 6-hour interval, and identical files are compared as raw bytes instead of being decoded as text. Because a restart no longer triggers a pass, a daemon restart (including after agents upgrade) no longer heals 30 seconds after boot; the pass runs once 6 hours have passed since the previous one. Run agents sync to heal immediately. If you disabled self-heal as a stopgap, turn it back on with agents daemon services enable self-heal.

  • Session titles no longer show the injected credentials catalog. The transcript reader moves to @phnx-labs/sessions-cli 0.5.0, which treats the SessionStart ## Credentials you can reach block as injected context like ## Host & Fleet, so a harness that records hook output as a user turn (Codex) keeps the real first prompt as its topic. Source: cli/package.json.

1.22.120

  • An editor terminal now follows a claude restarted by hand (PHNX-4218). After /exit, running claude again in the same tab starts it without a launcher, so the SessionStart hook records its session with an empty agent kind, and the active scan skipped that record. The tab kept the old session. The scan now takes the kind from the process name when the record has none. Source: cli/src/lib/session/active.ts.

1.22.119

  • A setup-token (provider) Claude account no longer opens on Claude Code's first-run theme picker on a worker. A provider account has no slot, so it runs in the executable's shared version home, and nothing ever marked that home onboarded: an AGI EXT tab dispatched to a worker landed on "Choose the text style…" instead of the agent. A credentialed Claude spawn on a non-headed device now seeds hasCompletedOnboarding in the home it runs in, right after the worker login-trap preflight in spawnAgentLeased (explicit #account, default account, balanced pick, interactive tmux). Headless paths that spawn elsewhere (--loop, routines) never show onboarding in print mode; the Windows .cmd shim passthrough is not covered. With no email the seed writes only the flag, so the home's own login identity is untouched; it writes through the .claude/.claude.json -> ../.claude.json link instead of replacing it; and a document already onboarded is left byte-identical. Source: cli/src/lib/exec.ts, cli/src/lib/claude-account-token.ts.

  • A headless Cursor run in plan mode now runs in folders Cursor has not seen before, and prints its answer. Two bugs stopped agents run cursor "…" --mode plan, a Cursor teammate (agents teams add … cursor …), and a plan-mode Cursor routine from working unattended:

    • They passed --plan without --trust. In a directory Cursor had not seen, Cursor stopped on "Do you trust the contents of this directory?" and exited with no one to answer, so a teammate failed in 7 seconds with 0 tools. Every headless mode except skip (which passes -f) now passes --trust. --trust only accepts the configured working directory and does not bypass tool permissions.
    • In --plan mode Cursor delivers its answer through its createPlan tool, which -p text output never prints. A run would read the repo for 3 minutes, exit 0, and leave an empty log. A headless plan run now uses Cursor's read-only ask mode (--mode ask), which refuses file writes the same way and prints its answer.

    Interactive Cursor runs keep --plan and Cursor's own trust prompt. Source: cli/src/lib/harness/adapters/cursor.ts.

  • Grok subscription seats log in per worker through the device-code flow. grok now has the same per-device:device-auth worker path as codex: agents accounts add grok <name> --per-device registers the account with no API key, and agents accounts login grok#<name> --per-device on a worker runs grok login --device-auth into that account's slot. The box prints a URL and code that you approve in your own browser, so SuperGrok / X Premium+ seats run on workers without an XAI_API_KEY, which bills API credits rather than the subscription. accounts login --per-device is new and is the one login a worker may run for a dual-path harness (codex too); the plain re-login stays headed-only. Source: cli/src/lib/harness-auth-capabilities.ts, cli/src/lib/accounts/add.ts, cli/src/commands/accounts.ts.

  • The grok shim runs the release grok update installed. Grok's updater now writes ~/.grok/bin/grok-<version> and repoints ~/.grok/bin/grok, leaving downloads/ alone, but the shim only scanned downloads/, so it kept running the old binary after an update (on one worker, 1.0.4, which xAI rejects with 426 ... outdated). The shim, the grok@<version> alias, and getBinaryPath now follow grok's own bin/grok pointer first and fall back to the downloads/ scan for older layouts. The shim schema version is bumped so installed shims regenerate. Source: cli/src/lib/installations/shims.ts, cli/src/lib/installations/store.ts.

  • Editor terminals follow the current agent session after a restart (PHNX-4218). Active/watch rows adopt the live agent descendant's recorded session, transcript, terminal ID and launch ID while keeping the tab's shell PID and metadata. Only the nearest recorded agents qualify, so nested agents cannot take over the tab. The most recent recorded start wins at that depth, with PID as a stable tie-breaker; known tab kinds must match. Windows retains the published identity until process starts can be verified. Published terminal IDs also survive when no descendant supplies one. Rows expose a display-only accountLabel when the indexed account identifies one registered native slot; ambiguous identities stay unlabeled, and org-only keys require a matching email.

  • agents projects prs scopes a shared monorepo per project, and agents projects prs merge merges one PR (PHNX-4215). When several projects share one repository, each project now lists only the PRs whose changed files touch the paths it claims (scope: project) or touch no sharing project's paths (scope: repo-wide). It uses the same defaultPath/subpath claim that attributes a session's cwd, and changed files are cached per head SHA. Rows gain createdAt. The envelope gains viewer and a per-repo sharedWith, and the --number detail adds mergeable/mergeableState. agents projects prs merge <name> --repo … --number … --sha <head> merges with one REST call pinned to the reviewed head, so GitHub refuses if the branch moved since. Repo canonicalization moved from GraphQL gh repo view to a cached REST read, which drops a warm refresh from about 2s to under 1s. Source: cli/src/lib/github/project-prs.ts, cli/src/lib/projects.ts, cli/src/commands/projects.ts.

1.22.118

  • agents sessions <read> --device <name> routes a SINGLE-device READ through the standalone sessions --host when both sides support it (PHNX-4012). A read --device query (search / list / id lookup, and no explicit --host) that names exactly one device now resolves it to its SSH target and forwards sessions <read args> --host ssh://<target> to the local standalone — the local-orchestration collapse where sessions owns the remote hop, instead of the in-repo peer fan-out. --host is point-to-one, so a multi-device read (--device box mac-mini, a repeated --device, or a --device all/fleet sentinel) stays on the in-repo --device fan-out, which merges the peers. It takes the standalone path only when THIS box's sessions is ≥ 0.2.1. If the PEER lacks the standalone the remote bash -lc command-not-found surfaces as exit 127, on which the read falls through to the existing in-repo --device fan-out so it still succeeds — a capability gate keyed on that specific signal (same spirit as the 0.2.1 --host version gate), a migration bridge until the fleet is uniformly on 0.2.1. Below the host floor, or with no standalone installed, a --device read stays on the in-repo engine exactly as before. Lifecycle --device (resume / watch / inject / focus / …) is untouched, and an explicit --host still wins. Internal routing change with no user-visible behavior change on a healthy fleet. Source: cli/src/lib/sessions-client.ts, cli/src/index.ts.

  • agents sessions … --host <target> forwards to the standalone sessions CLI (PHNX-4012). A point-to-one remote read (agents sessions <query> --host box — SSH to ONE box, run sessions … --local, stream JSON back) now takes the standalone fast path when the installed sessions is ≥ 0.2.1, mirroring how the 0.2.0 filter/sort flags are version-gated. A box with sessions below the 0.2.1 host floor — or none installed — keeps the query on the in-repo engine exactly as before (which resolves --device against the fleet), so nothing mis-routes or crashes. This is an internal routing change: --host behaves the same for callers, only the engine that answers it changes. Source: cli/src/lib/sessions-client.ts, cli/src/index.ts.

  • agents browser is now a thin consumer of the standalone browser CLI (PHNX-4101). The duplicate in-repo browser engine — the CDP/BiDi/Arc drivers, the browser IPC service, the chrome-data/profile store and the network-capture pipeline — is deleted; every verb forwards verbatim to @phnx-labs/browser-cli (browser), which owns the engine and keeps every on-disk path the old subsystem used. agents-cli supplies only what the fleet CLI knows: --device resolved to an SSH target (bound once at start), remote-control consent, the acting session identity, and the capture/session history behind agents browser sessions. The daemon no longer hosts the browser IPC service or the browser-task reaper (browser-cli runs its own), and the retired browser alias shim no longer shadows the standalone binary on PATH. Install it with npm i -g @phnx-labs/browser-cli or agents setup browser. Source: cli/src/lib/browser-client.ts, cli/src/commands/browser.ts, cli/src/lib/browser/{context,record,sessions-list,paths}.ts, cli/src/lib/daemon/daemon.ts, cli/docs/browser.md.

  • A PreToolUse hook whose script is missing now denies the tool call instead of allowing it (PHNX-4106). Generated hook shims embed one source path inside a version home; when that file was gone (a pruned version, sync rewriting hooks/) the shim exited 127, which Claude Code, Codex and Droid read as "allow". On one laptop the primary-checkout guard was silently disabled for about 17 hours this way. A PreToolUse shim now exits 2 with the repair named (agents hooks sync) and logs cache: missing-source, after the matches: gate and only for a PreToolUse firing; other events and skipped fires stay fail-open. agents prune <harness>@<version> also re-points the shims at the surviving home immediately instead of waiting for the daemon. Source: cli/src/lib/hooks/cache.ts, cli/src/lib/installations/versions.ts.

  • Claude no longer gets every plugin skill twice. Sync flattened each plugin's skills/ into the top-level skills/ of every harness, but Claude Code already loads them from the plugin registration, namespaced (/sessions:continue), so the picker listed /continue and /sessions:continue side by side for 47 skills. Claude is flagged nativePluginSkills: sync stops flattening there, and the leftover copies are swept by the next full agents sync claude (or agents prune cleanup skills). Harnesses that only see plugin skills through the flat copy (Codex) are unchanged. Source: cli/src/lib/staleness/writers/sources.ts, cli/src/lib/agent-spec/agents.ts.

  • auth-sync credential pushes plan per peer off that peer's own first-hand reply, with no fleet-wide freshness gate (PHNX-4116). The daemon's auth-sync tick no longer skips every credential push when the newest fleet exchange goes stale — the old readLastSuccessfulExchangeMs marker and its "skipping credential push … need one within 900s" WARN are gone. Reserved-key presence on a peer is now verdict ∧ fingerprint: the peer's own per-account verdict row (accounts.rows) must be non-missing AND the credential fingerprint (workerCredential.mintedAt) the publisher last delivered to that peer must match the account's current one. The verdict half is first-hand — a key removed on a worker flips its next verdict to missing and the push resumes. The fingerprint half catches a re-mint: agents accounts login <harness>#<name> rotates the reserved key and bumps mintedAt while the worker's OLD token still authenticates (verdict stays live), so a publisher-side rotation cursor (reserved-sync-delivered.json, local-only, never synced) re-pushes the new key within one tick instead of reading present forever. A peer that has never replied, or whose reply carries no accounts.rows field at all (an older CLI — fail closed), is skipped this tick and logged at INFO, not WARN. The push stays idempotent, so a stale reply is safe. No user-visible surface change; a re-authed account now reaches every worker within a tick, and a briefly-quiet exchange no longer stalls provisioning. Source: cli/src/lib/daemon/auth-sync-service.ts, cli/src/lib/secrets-policy.ts.

  • Drop stale worker account slots and their credentials (PHNX-4116). On a worker, reconcileLocalWorkerSlots now removes any deviceAccounts.slots record whose account is no longer registered — the leftovers of an older account-id generation, each a full HOME carrying a live .claude/.oauth_token — deleting the 0600 credential while KEEPING the directory (its .claude/projects holds transcripts) and logging one line per drop. It fails closed: an empty or unreadable account registry drops nothing, so a transient read can never strip a healthy box's whole slot set. Source: cli/src/lib/secrets-policy.ts, cli/src/lib/accounts/slots.ts.

  • A routine skipped by an overlapping run reads blocked, not "wedged" (PHNX-4116). Since there is no longer a "wedged" daemon state, a routine whose prior run is still active now reports blocked: run <id> active since <t> — naming the live run and when it started — instead of the stale "wedged: a prior run is still active". Source: cli/src/commands/routines.ts.

  • agents daemon status no longer calls a worker's absent secrets broker down (PHNX-4116). On a box that reads file-backed secret stores (every headless worker), an unreachable broker is expected — secrets read one-shot with no broker — so the health line now reads info secrets agent not running (file-backed stores) instead of down secrets broker (unreachable). A keychain-backed box with no broker is still a real fault and still reads down. Source: cli/src/commands/daemon.ts.

  • A balanced pick or the account picker now launches the account it chose (PHNX-4116). Since accounts became registered slots (PHNX-3940), a balanced pick and the "no fresh usage" picker only set the executable version, so the run started in that version's home with whatever login it held. On yosemite-m0 the picker chose trp and Claude Code opened as another account, in its first-run wizard. Both paths now resolve the picked candidate through the same local launch resolver an explicit --account uses, so the spawn gets the slot's home and durable credential env. Source: cli/src/commands/exec.ts.

  • One run-readiness gate, computed on the box that runs (PHNX-4116). agents view --json now carries a per-agent runReady ({ ready, reason, accounts: [{ name, ready, reason }] }) computed from the router's own enumeration — native account slots AND version homes (collectRunCandidates → readinessFromCandidate), not the per-version list, which missed the slots a run actually picks from. --device auto reads that answer instead of re-deriving usage freshness on the dispatching box, so a worker holding a valid setup-token is schedulable on that fact: a synced-only usage row (its poller lives on the headed box that published it) is never refused for being old — its age weights the pick, it never decides eligibility — and a rate-limit window whose reset has already passed reads as available. A placement error now names the box's own reason, e.g. no ready harness account (all signed_out), instead of a bare no ready harness account. agents devices harnesses now derives its displayed usage percentage from the SAME live windows as the status verdict (liveUsageWindows), so a maxed-then-reset account no longer reads available next to a stale ~100%. The one-release fallback for an older remote CLI without runReady keeps the pre-fix throttle exclusion — a FRESH rate_limited/out_of_credits or a FRESH dead auth verdict still disqualifies a launchable version, while a STALE reading stays unverified — so a throttled worker cannot slip into --device auto during a rolling upgrade. Source: cli/src/commands/view.ts, cli/src/lib/hosts/ready.ts, cli/src/lib/accounting/rotate.ts, cli/src/lib/accounting/usage.ts, cli/src/lib/devices/harness-inventory.ts, cli/src/lib/smart-launch.ts.

  • A wedged daemon service now restarts the whole daemon instead of parking a dead service or reading wedged forever (PHNX-4116). A supervised daemon service that HANGS past its deadline (a stuck usage refresh, a frozen heartbeat) no longer parks and waits for an in-process backoff restart that could stall for days — the supervisor records the cause and exits the process (code 70), and systemd (Restart=always, RestartSec=30, StartLimitIntervalSec=0, KillMode=process) / launchd (KeepAlive + ThrottleInterval=30) restart the daemon within ~30s, so it recovers on its own like any well-behaved service. A service whose tick merely THROWS is recorded and keeps ticking. The parked service state and the wedged daemon state are gone: agents daemon status now reports running/stopped, the daemon's PID, uptime, heartbeat age, and the count of supervised restarts in the last 24h with the last cause. Source: cli/src/lib/daemon/supervisor.ts, cli/src/lib/daemon/daemon.ts.

  • Usage rows and daemon state travel over SSH; the shared user repo carries no daemon state (PHNX-4116). The 15-minute usage-sync tick used to commit every device's devices/<device>/daemon-state.json into the fleet-synced user repo and exchange it under a lock and a 45 s deadline. That grew the shared store to 1.1 GiB and 18,358 commits (100% of the last 2,000 were chore(devices): publish <device> daemon state), one worker's clone fell 10,398 commits behind with git fetch timed out on every tick, and a box holding a valid setup-token could not be scheduled because a repo was bloated. A headed (personal/desktop) daemon now dials every dialable peer in parallel with agents __usage-ingest --reply (20 s per peer; a timed-out peer is skipped for that tick), sending its own envelope on stdin; the peer merges the usage rows into its cache newest-wins, stores the envelope as that peer's file, and prints its own state back, which the headed box stores stamped receivedAt. Workers never initiate. The placement probe ingests the dispatcher's usage rows on the same round-trip, so the chosen worker holds current numbers at dispatch. agents repo sync user is a plain fetch-and-fast-forward of human-authored resources again; the 8-minute kickoff offset that only existed to dodge the repo lock is gone. __usage-ingest gains --reply; the legacy v1 payload from an older headed peer is still accepted. Source: cli/src/lib/accounting/usage-sync.ts, cli/src/lib/accounting/usage-ingest.ts, cli/src/lib/daemon/usage-sync-service.ts.

  • agents update claude#work works, and a refused update names the process holding the installation (PHNX-4116). The <agent>#<account> selector agents run already takes was rejected by update as "Unknown agent 'claude#work'"; it now resolves to the managed installation that account runs on and refuses only an account the registry does not know. "Account home is in use; retry after its sessions finish" is replaced by the actual holder, e.g. pid 2173999, up 01:10:22, pts/1: …/claude --resume 3ef30267…, with agents sessions stop <id> as the way out. Source: cli/src/commands/update.ts, cli/src/lib/installations/active-check.ts.

  • agents view / agents accounts list show FACTS per account per box, never a word that means "we did not look" (PHNX-4116). Each account row now states its evidence: the token on disk (sk-ant-oat01 (Sep 16) = the slot's .claude/.oauth_token scheme prefix + file date, api key (present), or no token), what happened when it was last used (last used ok 12m ago / last auth failure 401 Sep 20 14:02 / rate-limited until 15:00 / not used on this box yet), and a usage reading that names its origin when it came from another box (S 12% W 40% … (from zion)). The auth fact is recorded from real run outcomes into the auth-health cache with a new source: 'run' field — an auth failure at the routine runner's isAuthFailureFromLog sites, a success at statusline ingest and a clean headless agents run exit. agents view <harness> --json accounts now carry token and lastAuth for the emitting box. Source: cli/src/lib/auth-health.ts, cli/src/lib/account-catalog.ts, cli/src/commands/accounts.ts, cli/src/commands/view.ts.

  • A synced usage file no longer masquerades as a live auth verdict, and a worker no longer publishes a unverified probe row (PHNX-4116). verdictFromFreshUsage refuses a freshness.source === 'sync' snapshot — a fresh reading from another box's poller proves the shared token works somewhere, not that THIS box can authenticate. A worker's setup-token box, which cannot read the usage endpoint, now returns the honest no_evidence verdict (dropped from the published probe rows) instead of the overloaded unverified, so eight identical workers read one distinct picture instead of five. Source: cli/src/lib/auth-health.ts.

  • A --host <worker> routine activates on a token-backed worker again (PHNX-4116). Dropping the worker's no_evidence probe row left agents devices ping --local --json with no row for the agent, so host-placed routine readiness read the absent row as unconfigured and reported agent_auth_failed with a valid token on disk. devices ping --local --json now also emits launchable (the agents this box holds a launchable signed-in credential for, from the same collectRunCandidates the run router uses), and one shared decision (decideRoutineAuthReadiness) is used by both the local and host readiness paths: an absent row plus a launchable credential is ready, a box with no credential fails, and a fresh revoked blocks — so both paths reach the same answer for the same box. Source: cli/src/lib/routine-readiness.ts, cli/src/commands/ssh.ts.

  • --device auto no longer refuses a worker whose usage reading is merely old (PHNX-4116). Remote readiness re-derived a 40-minute usage-freshness refusal from agents view --json's per-version list, so one broken usage-sync made every worker read no ready harness account while --device <name> on the same box launched fine. The dispatcher now READS the running box's own runReady verdict instead of re-deriving it (see the readiness fragment); usage age weights the pick, it never decides eligibility. Source: cli/src/lib/hosts/ready.ts.

  • The daemon no longer freezes for minutes on session-index and mirror writes (PHNX-4154). session_text FTS rows now sit at the rowid of the sessions row they describe and are addressed by it (schema v51, a one-time rebuild: 24 s for a 669 MB index). Every delete and content read used to filter on the UNINDEXED session_id, a full scan of the FTS content per statement; the fleet session mirror runs one per mirrored row inside a daemon tick, so the daemon's event loop stalled for 10+ minutes per tick, every service logged tick exceeded deadline, and agents browser verbs on the same loop timed out with Arc Apple Event timed out. Source: cli/src/lib/session/db.ts.

  • Fix ENOTDIR crash on stale lock files. withFileLock and withFileLockAsync now self-heal when a prior crash left a regular file instead of the directory proper-lockfile expects, matching the caller's realpath option when computing the lock path. Source: cli/src/lib/fs-atomic.ts.

  • Fix Unknown account for unregistered native logins. ag run claude#email now discovers a native OAuth login from version-home .claude.json files when the account is not yet in accounts.native, and correctly routes it through the provider-backed harness guard. Source: cli/src/lib/account-registry.ts.

1.22.117

  • agents run --local, and --device <this machine> no longer SSHes to itself. A bare interactive run places itself like --device auto (PHNX-4083), and the documented way to stay put was --device <this machine> — which took the remote path and probed its own login shell over SSH, timing out on a loaded box. --local is the explicit local pin (the same door as --where local, which previously expanded to nothing and was then overridden by the default placement); any host flag naming this machine by short id, MagicDNS name, loopback, or the interactive sentinel now resolves to the same local run. The placement-failure hint names --local. Source: cli/src/commands/exec.ts (pinLocalWhenTargetIsSelf), cli/src/lib/placement.ts, cli/src/lib/hosts/remote-cmd.ts.
  • agents sessions filter/sort queries take the standalone fast path when it's new enough. --project/-p, --since, --until, --sort, and the harness shorthands (--claude/--codex/…) now route to the standalone sessions binary when the installed one is ≥ 0.2.0 (which implements them), and stay on the in-process engine otherwise — a version probe (sessions --version, cached, run only when such a flag is present) gates it, so an older binary never gets a flag it would mis-read as an FTS token. --json/--limit/--agent and a bare query/id already route unconditionally, unchanged. Behavior is unchanged; the fast path just covers more queries. Source: cli/src/lib/sessions-client.ts, cli/src/index.ts.

1.22.116

  • agents feed answer is bounded, exactly-once, and truthful about delivery (PHNX-3999). The AGI Menu abandons a reply at 30s; this path used to blow that budget and then report a fabricated receipt.

    • Bounded. The requested attention key names its session, so only that session is reconciled and the optional gh pr view enrichment runs only for a key whose generation is a PR review. Previously every active session was enriched — one 15s-bounded gh call each — before the key was compared. The whole operation now runs under one deadline, and a forwarded answer is raced against it directly rather than trusting the transport's own timer.
    • Exact tokens across the hop. A forwarded answer is POSIX-quoted with the canonical shellQuote and run through the canonical sshExecAsync, so a multiline answer or one carrying $, backticks, quotes or ; arrives byte-identical instead of being re-parsed by the remote shell.
    • Multiline free text. A multiline answer is delivered into a tmux rail as one bracketed paste (injectIntoTerminal's new paste option), so a TUI inserts it verbatim instead of submitting one line at a time. A rail that cannot carry the framing refuses before the claim is taken.
    • A claim is no longer a resolution. recordAnswer gains a two-phase pending mode: the claim locks the item but leaves the block open and writes no resolution tombstone, so an unconfirmed delivery can no longer make the request silently vanish from the feed. Only the AGENT's own acknowledgement resolves it — a consumed/continued receipt promotes the claim through confirmAnswerResolution, bound to the generation and attempt it was taken for, so a slow delivery cannot resolve the question the agent has since moved on to. A queued receipt never does.
    • Truthful, typed outcomes for the operator UI. --json now emits status (delivered / already_answered / unknown / failed), delivery (receipt / unconfirmed / failed), resolved, host, attempt, and the block's real MessageReceipt — never a synthesized one. queued is delivery, never resolution; dropped/expired read as failures rather than as the furthest-along success. A timeout, a truncated remote reply, or a partly-written keystroke sequence is unknown (the claim is kept, so a retry cannot double-send); only a provable non-delivery is failed.
    • agents feed answer <key> --check [--attempt <at>] is a new read-only reconciliation — the "check delivery" path for an unconfirmed answer. It never claims, routes or resends, and it is bound to the requested generation and attempt, so a stale card can never be resolved by the next question's receipt.
    • Stranded claims reconcile. A claim a kill left with no receipt is adopted and completed only on the mailbox rail, where the whole spool (inbox, processing and consumed) is scanned by block id so the answer is never enqueued twice, and only through a compare-and-swap so two retries cannot both adopt it.
  • AGI Menu: synchronized Headless-runs defaults, Home and device preferences. Five new agents config keys, validated at write time and synced fleet-wide like the existing menubar.menu.* set: menubar.menu.headlessAgent and menubar.menu.headlessFallbackAgent (each a registered agent id — a typo is refused, not stored), menubar.menu.headlessPlacement (auto | local | interactive | a device name), menubar.menu.sessionUpdates, and menubar.menu.deviceSort (name | role | load | memory | disk).

  • agents run --fallback <agent> now reaches the alternate harness when the primary's accounts are exhausted before the run starts. Previously that case printed "no healthy account" and exited 1, so a configured alternate never fired for the one situation it exists for; the mid-run handoff also no longer stops at three accounts when an explicit alternate is configured, so every healthy account of the primary is tried before another harness. A signed-out primary still fails loud with the login hint — logging in is the fix there, not a different harness.

  • agents menubar snapshot --json reports each device's role and specs. Every devices[] row gains role, autoEligible (whether --device auto may pick it) and stats — cores, total/free memory and disk, load, reachability, and when each was observed. Read from the existing fleet-stats cache, so no fleet probe; null for a device never measured, and every unobserved number is null rather than 0.

  • Session rows carry confirmedProject. The registered project whose root contains the session's working directory, or null when the association is not confirmed, on agents feed watch --json, agents sessions watch --json, and agents sessions --active --json. A directory no project definition names — even one that happens to be a git repository — is no longer filed under a project invented from its folder name. The existing project field is unchanged.

  • Session titles exclude shell and control scaffolding. A harness-generated title that is a <bash-input>/<command-name>/<system-reminder> wrapper, or a bare control command such as /clear or /compact, is rejected so the row shows the generated title or the user's own first prompt instead. A /rename title is still kept verbatim, and the original prompt stays on the row. Existing indexed sessions are re-derived on their next scan.

  • Session previews retain details across devices (PHNX-3999). An exact session ID and owning device use one bounded request, with a durable local copy for offline reading. Reopening unchanged sessions avoids network requests; activity changes invalidate the copy, and failed requests back off without hiding prior content. Concurrent requests coalesce, including failed manual refreshes. SQLite contention, lock waiting, and transport share a 10-second budget.

  • Large local transcripts show bounded recent details. Previews reuse the canonical transcript parsers and timeline projection, mark partial history, and report missing transcripts explicitly. Cached timeline data is accepted only for the exact file modification time and size that produced it.

  • agents setup secrets / agents clis install secrets now install Secrets CLI 0.1.5, which accepts the shared Phoenix --host address grammar (ssh://[user@]host, bare user@host/alias; other schemes fail loud) on secrets and secrets hosts pin (PHNX-4090) — matching the --host grammar computer-cli 0.1.5 already ships. Existing installs are unaffected; the canonical pin lives in cli/src/lib/secrets-cli.ts.

  • agents setup now installs the standalone term CLI as a first-class onboarding phase (PHNX-4092). The PTY engine agents fleet login and agents auth mint spawn on demand lives in @phnx-labs/term-cli (extracted PHNX-4091); a fleet device with it missing failed those flows loud. The onboarding hub gains a term phase and an agents setup term wizard (with --install-only / --terminal) that installs the pinned @phnx-labs/[email protected] — via the system clis/term.yaml manifest, else a pinned npm i -g — the same way browser/computer/secrets install. agents setup status and --tool term report its readiness: unlike the other tools term has no status --json health surface, so presence on PATH is the whole readiness signal (no probe, no configure step). Source: cli/src/commands/setup-term.ts, cli/src/commands/setup.ts, cli/src/lib/setup-tool-status.ts, cli/src/lib/setup-tool-install.ts.

  • browser status no longer goes blank when one Arc task is stale (PHNX-3999). A single Arc tab that had been moved to another window or Space — or closed — made agents browser status throw, and every profile vanished from the listing, including healthy ones with nothing to do with Arc. The refusal to adopt a moved tab is correct and is unchanged: an action still will not drive a tab its task no longer owns. What changed is that read-only status no longer inherits the blast radius. An unreadable task is reported with the tab count it owns on disk plus the reason its live tabs could not be read, an unreadable profile is listed with an unavailable reason instead of disappearing, and the other profiles are reported normally. tabs is absent rather than empty for such a task, so nothing claims a stale tab is live. Source: cli/src/lib/browser/service.ts.

  • browser status reads Arc once per pass instead of once per tab (PHNX-3999). A profile with 28 tasks paid one AppleScript round trip per owned tab, plus one liveness probe per saved Arc runtime while reconnecting, so a cold status spent dozens of serialized round trips before printing anything. It now takes a single snapshot and a single liveness probe for the entire pass, shared across every Arc profile, and never contacts Arc at all when no Arc profile is present. Actions are unchanged and still read Arc live per tab. Source: cli/src/lib/browser/service.ts.

  • grok signs in through its device-code flow. agents accounts add grok, agents accounts login grok#<name>, and the logged-out hint all run grok login --device-auth. Bare grok login defaults to --oauth, the loopback browser flow, which opens whatever browser profile the OS defaults to. The device-code screen prints the URL and code instead, so the human finishes the sign-in in the browser profile they choose — which is what makes it usable over an SSH shell on a worker. Source: cli/src/lib/harness-auth-capabilities.ts, cli/src/lib/signin-badge.ts.

  • A feed snapshot larger than the reader budget now reaches the reader whole (PHNX-3999). The daemon's shared feed socket wrote each envelope in one socket.write and dropped the reader the moment its unflushed bytes passed 4 MiB — so a healthy Menu or agents feed watch --json client attaching to a fleet whose reset exceeded that got 8 KiB of it, no newline, then EOF, and the CLI exited 0 as if the fleet were empty. Each connection now has one ordered writer that streams the snapshot before any live event in 64 KiB chunks paced by the socket's own 'drain'. A reader is dropped only when it stays more than 4 MiB of live events behind for 2 s, or leaves a chunk unaccepted for 30 s — a burst of peer resets to a healthy reader is never a drop. The client rejects any close it did not ask for, naming a mid-frame truncation, and a throwing consumer fails that same promise instead of escaping as an uncaught exception. Source: cli/src/lib/feed/hub-server.ts.

  • Browser setup installs Browser CLI 0.1.5 (PHNX-3999). New installations include the status fix that reports unavailable Arc tasks without hiding healthy profiles and reads visible Arc tabs once per status request. Existing browser installations are preserved; upgrade them explicitly with npm install -g @phnx-labs/[email protected]. (0.1.5 is a clean re-cut of the partially-published 0.1.4; same behavior.)

  • Removed agents devices login (a.k.a. "fleet login"). The remote device-code login orchestration could not work in the account-slot model — a box may hold several accounts per harness, and a per-box native login has no way to say which account it is signing in. It is gone entirely (not deprecated-and-hidden). Accounts are provisioned from durable worker credentials (agents accounts add/login <harness>#<name>); a token-less harness (kimi/antigravity) logs in per box — run the harness on that device and complete its native login. Logged-out hints for per-device harnesses now point at that path instead. Source: cli/src/commands/ssh.ts, cli/src/lib/fleet/auth-sync.ts, cli/src/lib/signin-badge.ts, cli/src/lib/accounts/add.ts, cli/src/lib/auth-mint.ts.

1.22.115

  • Artifact sharing moved out to the standalone artifacts CLI (breaking). agents artifacts, agents artifacts share (and its list/delete/open/ visibility/edit/revisions/update/setup/join/status/analytics subcommands), and agents artifacts setup are removed — artifact sharing now lives entirely in @phnx-labs/artifacts-cli (artifacts share …, artifacts auth login), mirroring the secrets (PHNX-3989) and computer (PHNX-4075) extractions. Install it with npm i -g @phnx-labs/artifacts-cli. agents sessions share <id> stays — it renders the session locally and now publishes through artifacts share, so it needs that CLI installed. agents traces and agents sessions export/backup are unaffected (their own Cloudflare/R2 paths). The ordinary CLI release no longer deploys the managed share Worker (--deploy-worker is gone); that deploy moved to artifacts-cli's own release. Source: removed cli/src/lib/share/ and cli/src/commands/{artifacts,artifacts-setup,share}.ts; the generic Cloudflare provisioning primitives agents traces/agents sessions backup borrowed now live in cli/src/lib/cloudflare/provision.ts.

  • Browser tasks and computer runs ride the feed stream, and N readers now cost one connection per peer (PHNX-3999). agents feed watch --json carries a third row kind alongside agents and attention: canonical tool rows for each browser task and computer run, with the driven device, the owning agent session, captures, and — for a still-bound browser task only — the commands that show a tab or close the task. Switching an All/Agents/Browser/Computer filter launches no commands: every row for every tab is already on the one stream. Computer runs are ledger history: they report live: false and carry no stop/close affordance, because the process that performed them has exited. Task URLs are redacted (userinfo stripped, credential-shaped query parameters replaced) before they leave the producing machine. Source: cli/src/lib/feed/tools.ts, cli/src/lib/feed/tool-activity.ts.

  • One shared fleet feed collector, owned by the daemon (PHNX-3999). watchFleetFeed opened a persistent ssh <peer> agents feed watch --json --local per dialable device per caller, so three readers on a thirteen-device fleet held thirty-nine ssh children carrying byte-identical NDJSON. The new feed-stream daemon service runs exactly one fan-out and serves every reader over a socket; a late reader is brought up to date from held per-scope state with no re-dial, and the fan-out is demand-gated so a box with no reader open holds no peer connections at all. agents feed watch --json --local is unchanged — it is the per-machine stream the collector itself subscribes to. Source: cli/src/lib/feed/hub.ts, cli/src/lib/daemon/feed-stream-service.ts.

  • The standalone browser and computer engines are read at their real source (PHNX-3999). The standalone computer engine always appends to its own <cache>/computer/actions/<day>.jsonl, which never reaches agents-cli's recorder — so a computer command run directly was invisible to every agents-cli surface. agents computer sessions and the feed now merge that ledger, deduped on invocationId (forwarding rewrites ts/pid, so a timestamp dedupe double-counts), and attribute a record the producer left unidentified to the machine whose ledger it came from instead of unknown. Live browser tasks are read from tasks.json, so a task that has produced no capture yet now appears — with its real tabs, and a show/close command that names the host to run it on. Source: cli/src/lib/computer/sessions-list.ts, cli/src/lib/feed/tool-activity.ts.

  • Tool setup readiness rides the feed (PHNX-3999). reset.setup and setup.snapshot carry each device's browser/computer/secrets readiness from the setup cache, so a Settings surface needs no per-tool polling and no probe of its own. Source: cli/src/lib/feed/watch.ts.

  • A long-offline peer is retired instead of dialed forever (PHNX-3999). The 60 s backoff cap bounded the delay but not the total work: a box off for a weekend was dialed every 60 s for two days. After ten consecutive failures the re-dial drops to 15 minutes, still waking immediately on a device-registry change. Source: cli/src/lib/session/remote/peer-stream.ts.

  • agents ssh --argv delivers exact argv tokens (PHNX-3999). The remote command was assembled by joining tokens with a space, so any token containing a space or a shell metacharacter was destroyed on the way to the peer — --title "two words" arrived as two arguments and a & b arrived as a backgrounded command. Pass --argv '["prog","two words","a & b"]' to deliver each element as exactly one token. The positional form is unchanged and still parsed by the remote shell, so the two are mutually exclusive. On Windows, where a process receives one string and splits it itself, a native target is launched through System.Diagnostics.Process with a CommandLineToArgvW-escaped argument string and a script or .cmd launcher is invoked with a splatted array — PowerShell 5.1's own native serializer drops an empty argument and eats embedded quotes. The Agents CLI itself is launched through its package's declared entry rather than the npm agents.ps1 shim, because that shim splats its own $args into native node.exe and lost the arguments one layer deeper — a quoted device name reached the parser unquoted. The fleet-provenance prefix is composed around the quoted argv rather than inside it; quoting it twice broke any actor value containing a space or a quote. Source: cli/src/lib/devices/connect.ts.

  • agents browser show <path> --device <host> views a capture held on another machine (PHNX-3999). The file is fetched into a private local path (0600 inside a 0700 dir), bounded as it streams so an oversized file is refused rather than transferred, and then opened with the normal local viewer — nothing is opened on the remote box. --device here names where the FILE is; it requires an absolute path and is refused together with a URL. Transport uses the device's canonical auth, so a password-auth box and an explicit identity file both work, and the read is platform-correct: POSIX gets cat, while Windows gets a .NET binary stream because cat there is a text reader that corrupts a capture. Source: cli/src/commands/browser.ts.

  • Setup from the menu uses a real terminal. agents setup browser|computer|secrets --terminal hands interactive onboarding to the existing terminal engine and reports launch errors. Installation and readiness remain separate; only explicit checks run health probes, and concurrent checks share one probe.

  • AGI Menu 1.5.0 adds tool sessions and Setup. The menu separates Agents, Browser and Computer activity, shows browser controls and recorded computer captures, and reports installation and readiness for Browser, Computer and Secrets CLI. It consumes the shared daemon feed without periodic tool subprocesses. The helper floor selects the signed and notarized menubar/v1.5.0 release.

  • Daemon startup preserves surviving local browsers (PHNX-3999). The startup reaper keeps a live local browser's runtime and task records when its previous daemon has exited. Dead process records and stale SSH tunnels are handled separately. Ownership changes only after an identity-checked reattach succeeds, preserving the original launch metadata. Reconnection requires an existing debugging port: --remote-debugging-pipe handles belong to the original service process and cannot transfer to a replacement daemon. Coordinate release of an active pipe-connected browser before restarting its daemon.

  • Browser setup selects standalone Browser CLI 0.1.3. The default package for agents setup browser --install-only now includes the browser preservation and task-restoration fixes. Existing installations and configured CLI manifests retain their current behavior.

  • BREAKING — deprecated command shims are deleted; every surface has one spelling. The hidden and warned aliases that kept "still works this release" paths alive are gone, not tombstoned: a script that still calls one now gets commander's unknown command / unknown option and exits non-zero. Removed → replacement: agents notify → agents send --to owner (or agents feed post --level important for a milestone that should reach the owner; a feed.broadcast sink that spawned agents notify "{message}" becomes channel: owner); agents doctor --fix → agents sync <agent>@all (doctor is diagnose-only); agents accounts connect|name|label|attach|detach|switch|set-default and the hidden agents accounts mint / agents auth mint → agents accounts add <harness> [name], agents accounts login <harness>#<name>, agents accounts default <harness> [name], agents accounts rename, and agents run <harness>#<name> (accounts select by name; installation bindings are gone); agents sessions go → agents sessions resume --attach-only; agents sessions attach and agents reconnect / agents sessions reconnect → agents sessions resume [id]; agents repo refresh → agents sync; agents mcp register → agents sync --mcp; agents memory sync → agents sync --memory; agents plugins sync <name> → agents sync --plugin <name>; agents skills info → agents skills view; agents run --smart → agents run --device auto; agents events --audit → agents events audit or --include ops; agents devices prefer|unprefer <name> → agents devices config <name> auto-launch.preferred on|--unset; agents devices set-interactive [name] → agents config set interactive.host <name> (or agents devices config <name> interactive.host <name>); agents devices configure|note|set <name> → agents devices config <name> <key> <value>. agents devices enable|disable <name> stay as first-class, visible sugar over auto-launch.enabled (no deprecation notice). Folding mcp register into agents sync also closed a gap in the replacement: sync's reconcile stage skipped every HTTP MCP server declared in agents.yaml (refresh.ts continued on transport: http) while the removed command registered it — it now registers HTTP servers through the same registerMcpToTargets path (codex mcp add <name> --url <url>), and the yaml-resource writer's Codex arm no longer silently no-ops on HTTP either (installMcpViaCodex). Source: cli/src/commands/send.ts, doctor.ts, accounts.ts, auth.ts, go.ts, sessions.ts, repo.ts, mcp.ts, memory.ts, plugins.ts, skills.ts, exec.ts, events.ts, ssh.ts, cli/src/lib/smart-launch.ts.

1.22.114

  • agents projects prs <project> — every open PR across a project's repos (PHNX-3999). Reads the whole open PR board (drafts included, no author filter) straight from GitHub over REST, paginated, for the project's own attached repos only — unlike projects status, whose openPrs only carries PRs the fleet's sessions opened. --repo <owner/repo> restricts to one attached repo; --repo … --number <n> lazily enriches exactly that PR with its head-SHA checks and reviewDecision. A per-repo fetch failure is reported and marks the result partial, never relabeled as zero open PRs. Source: cli/src/lib/github/project-prs.ts, cli/src/commands/projects.ts.

  • AGI Menu preferences are registered agents config keys (PHNX-3999). Thirteen user-scope menubar.menu.* keys (defaultProject, workingRowsShown, showPreviews, projectPriorityFilter, hideCompletedMilestones, bannerWhenNeedsYou, includeOtherDeviceRequests, groupBy [none/project/agent/device], thenBy, projectScope, projectSort, ticketSort, showPullRequests) that get/set/unset/list through the existing agents config surface and sync fleet-wide via the central agents.yaml. config list --json emits {key,value,hint} and omits unset keys, so the menu falls back to its own defaults. agents menubar snapshot --json also carries a menuPreferences map (effective value per key, defaultProject omitted when unset) so the menu reads them from the snapshot it already polls, not a second mechanism; on macOS a one-shot, sentinel-gated migration lifts legacy com.phnx-labs.agents-menubar UserDefaults into config without overriding an already-set value. Source: cli/src/lib/device-config.ts, cli/src/lib/config-keys.ts, cli/src/commands/config.ts, cli/src/lib/menubar/snapshot.ts, cli/src/lib/menubar/migrate-prefs.ts.

  • Devices carry a formFactor hardware fact (PHNX-3999). A new shared device-scope config key (laptop/desktop/server/unknown) set explicitly per device — never inferred from the OS platform — surfaced in agents menubar snapshot --json so the menu can draw a factual hardware icon. Source: cli/src/lib/device-config.ts, cli/src/lib/menubar/snapshot.ts.

  • A prose "needs-you" question is split into the ask and its context (PHNX-3999). The session state engine used to flatten a whole trailing report into one line (oneLine(content)), losing the actual question and its Markdown. extractProseQuestion now separates the trailing ask (StructuredQuestion.text / BlockQuestion.text) from the preceding report (context, Markdown + newlines preserved), so an operator surface can lead with the question and show the report as context. It keeps multiple/multiline asks together, peels a same-paragraph lead-in into context, and never treats a ? inside a code span, fenced block, or blockquote as the question. The freshness decay is unchanged. Source: cli/src/lib/session/state.ts, cli/src/lib/feed/feed.ts, cli/src/lib/feed/attention.ts.

  • Install AGI Menu 1.4.0 with Sessions device filters, in-place project details, synced preferences, native Quick Look, and question-first request cards.

  • Codex custom agents keep their backslashes. agents subagents wrote the Codex TOML body inside """ and escaped only the closing quotes, so a Windows path or a regex in an AGENT.md was mangled by TOML's backslash escapes. Backslashes are now doubled first. Fixed alongside four other CodeQL findings (terminal-escape scrubbing is a linear scan, prompt tag stripping is a fixpoint, R2 key decoding is single-pass, and share-page asset inlining treats any non-file: scheme as remote); none of those change behaviour on ordinary input. Source: cli/src/lib/subagents.ts, cli/src/lib/redact.ts, cli/src/lib/session/prompt.ts, cli/src/lib/session/sync/r2.ts, cli/src/lib/share/html.ts.

  • agents computer --device and agents secrets --device forward the standalone engines' own --host address (PHNX-4090). computer, secrets, and browser are extracted CLIs with no fleet registry of their own (PHNX-4075, PHNX-3989); they share one address grammar (ssh://[user@]host, vnc://host[:port], tcp://host:port, or a bare user@host/alias) via a copied lib/address.ts. A device's computer.host config (agents config set devices.<name>.computer.host <address>, next to defaultBrowserProfile) selects the transport — vnc:///tcp:// for a non-Windows desktop, or ssh:// still resolved against the fleet identity; with none set, --device falls back to the historical Windows-only ssh tunnel. agents computer's withDeviceFlag is gone, replaced by withHostFlag + resolveDeviceHost; agents secrets's passthrough gained rewriteDeviceToHost. An explicit --host on the command line always wins over --device; agents view --device <name> is unaffected — it still SSHes the whole command. Source: src/lib/address.ts, src/commands/computer.ts, src/commands/secrets-passthrough.ts, src/lib/device-config.ts, src/lib/config-keys.ts.

  • Removed agents pty; use the standalone term CLI instead (PHNX-4091). The PTY engine (agents pty, its unix-socket sidecar, and the sessions inject --pty target) moved to @phnx-labs/term-cli (npm i -g @phnx-labs/term-cli), published separately with the same verbs, flags, and JSON shapes. The OAuth device-code driver (agents fleet login, agents auth mint) now spawns the standalone term binary instead of an in-repo pty sidecar. Source: cli/src/lib/term-client.ts.

1.22.113

  • Remote sessions have one execution-owned row (PHNX-3939). Fleet session and feed streams reconcile launcher observations with the worker's state, retaining terminal/viewing metadata separately. Reconnects and stale history cannot replace a genuine worker row. Registry cleanup retains records whose PID namespace cannot be verified; nested agent runs keep launch lineage without claiming their parent's editor terminal.

  • Codex forks retain their child identity. Full and incremental scans keep the first native metadata record. Reindexing repairs cold files and removes a stale parent binding to child bytes even when the parent rollout is absent. Team descendants also keep their immediate parent lineage without inheriting its editor ownership.

  • Nested daemon starts preserve shared session state. Legacy migration and reboot recovery require the initial host namespace or kernel-verified live daemon ownership before touching singleton state. Foreign snapshots remain read-only, and invisible processes are never treated as dead. Automatic reuse of a private-container HOME across namespaces is unsupported, including after the prior namespace exits; writers fail safely with a diagnostic.

  • Repair session indexes whose schema version is current but whose identity columns are missing, preserving existing session metadata (PHNX-3940).

1.22.112

  • Runs expand portable home paths on the executing device (PHNX-3940). A remote resume terminal could open successfully while its child failed to start because --cwd '$HOME' was treated as a literal directory. The shared run path now expands $HOME and ~ locally, while preserving those anchors until remote dispatch reaches its target. This fixes direct runs and resumed conversations alike.

1.22.111

  • Remote session resume accepts the resolver’s launch metadata (PHNX-3940). A reachable peer’s valid session row could be rejected because it included a mode or custom harness, leaving the session undiscoverable. The reader now accepts both fields while keeping private transcript paths out of the resolver contract. Malformed responses are reported as invalid data instead of an unreachable device.

1.22.110

  • Session rows are named by what YOU asked for, not by the agent's latest message (PHNX-3797). Every headline in agents sessions, the picker, and the AGI EXT Sessions/Fleet list used to be the agent's last transcript line ("Both seams verified on the real shipped artifacts…") — verbose, rolling, and unrecognizable. The headline ladder is now /rename label → a daemon-generated title → your own first message; the agent's live line stays in the separate preview slot where a rolling status belongs. The new session-title daemon service generates a short, descriptive action + object headline ("Triage the AGI board", not just "Triage") with a cheap model through a swappable SessionTitleProvider (cloud default; a local ollama-style backend can be dropped in later without touching the tick), read-only plan mode, once per session and persists it in the session index against a hash of the message it came from, so a titled session costs no further model calls and only re-titles when that first message changes. Nothing generates per client or per tick: the value rides the existing sessions watch --json stream and the fleet session mirror, so a remote box's rows show the same title with no per-row SSH. Until the titler reaches a session it shows your own words — never an agent line. agents sessions backfill titles [--session <id>] [--refresh] runs it on demand; agents daemon services can turn the service off.

  • Session rows carry a ranked secondary line (PHNX-3797). Beside the bold headline, every live row now folds on importantMessage — the single most important recent agent message, ranked so a pending question or a needs-you block (plan review, permission, input-required) beats generic current activity. It rides the same sessions watch --json / mirror feed as the title (via the row spread), so AGI EXT renders a bold title over a dim secondary line with no extra query; agents sessions --active prints the question / needs-you line dim beneath the row. Source: src/lib/session/title.ts, src/lib/daemon/session-title-service.ts, src/lib/session/active.ts, src/lib/session/db.ts, src/lib/session/mirror.ts, src/commands/sessions.ts, src/commands/sessions-backfill.ts.

  • Codex account sessions record their originating account after hook sync, including macOS homes relocated for Unix socket limits. Registration refreshes trust for the runtime home and removes the obsolete builtin tracker that could overwrite account metadata; unrelated hooks and explicit disabled states are preserved.

  • Session resume preserves device and picker filters (PHNX-3940). sessions resume --device <host> <id> and the equivalent device-after-ID form now open on the selected origin device. Agent, account, directory, time, and limit filters reach the shared picker; an unspecified limit remains 200. Repeated different devices produce an error.

  • Usage snapshots stay fresh enough for balanced to auto-pick (PHNX-3940 W3). A headed box polls only the accounts it holds native logins for; a setup-token-only worker never hits the usage endpoint. A changed snapshot is published immediately (statusline ingest or poll) instead of waiting for the 15-minute tick. isUsageVerified trusts a row that arrived via sync from the account's poller for that 15-minute cadence, and keeps the 5-minute bar for a local capture. auth-sync and usage-sync take the shared-repo lock on different offsets so they no longer starve each other. Source: cli/src/lib/usage-refresh.ts, cli/src/lib/accounting/rotate.ts, cli/src/lib/accounting/usage-sync.ts.

1.22.109

  • agents sessions backup-setup provisions the managed session-backup endpoint (PHNX-3726). The operator command that deploys sessions.agents-cli.sh — the Cloudflare Worker + R2 bucket a signed-in user's agents sessions export --to-r2 backs up to with no r2.backups bucket of their own. Mirrors agents traces setup: reads Cloudflare credentials from the cloudflare secrets bundle (agents secrets exec cloudflare -- agents sessions backup-setup), idempotent (re-running redeploys the current Worker template in place). It is NOT a per-user step — signing in with agents auth login is all a user does; this is how the first-party endpoint itself is deployed. The Worker it deploys is Phoenix-only with no static token (PHNX-3726). Source: cli/src/commands/sessions-backup-setup.ts, cli/src/lib/session/sync/provision.ts (+ provision.test.ts), cli/docs/sessions.md.

  • agents doctor flags a leaked daemon no owner record names (W4, PHNX-3736). A headless e2e session launched agents __daemon-run under HOME=/tmp/pin-e2e-<pid> and never stopped it; the daemon ran 4+ days, invisible to the pid-file takeover because it keeps its own pid file under the temp home. agents doctor now reports any same-uid __daemon-run whose pid is neither the service manager's unit main PID nor the recorded daemon.pid — checked against both the caller's HOME and the real account home, so a test-harness caller never accuses the production daemon — as a leaked-daemon warning carrying the process's HOME and start time, with a kill <pid> remediation. Source: cli/src/lib/daemon/leaked-daemons.ts, cli/src/lib/devices/doctor-findings.ts.

  • agents daemon start refuses under a redirected HOME unless AGENTS_ALLOW_TEST_DAEMON=1 (W4, PHNX-3736). RUSH-3021 gated auto-start from a sandbox/test HOME but left the explicit startDaemon() open — the path the e2e harness took to leak the /tmp/pin-e2e-<pid> daemon. The launch now throws RedirectedHomeDaemonError (printed without a stack, exit 1); a deliberate test/e2e launch opts in with AGENTS_ALLOW_TEST_DAEMON=1 and owns stopping what it starts. Reporting an already-running daemon — and therefore agents daemon stop of a leaked one — is not gated. Auto-start side effects (routines add, webhook fires, monitors add) state the refusal and leave the foreground command green, the same tier split as the auto-start circuit breaker; only the explicit start commands fail loud. Source: cli/src/lib/daemon/daemon.ts (startDaemon).

  • The managed share Worker now proxies same-origin human collaboration to Prix (PHNX-3835). The Cloudflare Worker that fronts a managed share gains a /__collab/* transport so authorized humans can review a hosted artifact together (comments now; agent mentions/wake-up later). It stays the trust boundary exactly as the page GET does: every /__collab request loads the R2 object FIRST (a missing/just-deleted share 404s, so existence is never enumerable), re-derives the share id (SHA-256(ownerId + NUL + normalized path)), current revision (etag), owner, visibility, org domain, and producer provenance SERVER-SIDE from R2 metadata — the browser-supplied share is a lookup key only, never trusted as identity — then reuses the EXACT page read gate (me/org identity gate + private viewer-token gate) with every denial normalized to 404. Public/unlisted readers may read anonymously; every write additionally requires a verified signed-in Phoenix human; private still requires its share token. It proxies to the Prix /v1/artifact-collaboration routes (context, threads list/create, thread replies, comment/thread PATCH, SSE events, and a best-effort authenticated purge on share delete) with the service token in Authorization (never surfaced to the browser) plus trusted X-Artifact-* / X-Phoenix-Actor-* headers, preserves the client Idempotency-Key and Last-Event-ID, streams SSE through unbuffered (cancel/disconnect propagated), and marks every collaboration response no-store. A visibility downgrade is immediate (re-gated per request) and a deleted share drops its comments the instant the object is gone. The whole surface fails closed (404) when PRIX_ARTIFACT_COLLAB_BASE / ARTIFACT_COLLAB_SERVICE_TOKEN are absent — so BYO endpoints (no Phoenix identity) never expose it and the artifact page GET is unaffected — and stays dormant until an operator sets both secrets, which agents artifacts share update/setup now apply on a managed deploy (both or neither). Because the Worker template changed, already-deployed managed endpoints read outdated until re-run through agents artifacts share update. Source: cli/src/lib/share/worker-template.ts, cli/src/lib/share/provision.ts, cli/src/lib/share/backend.ts, cli/src/commands/share.ts, cli/docs/share.md.

  • agents sync --dry-run no longer mutates every native home (PHNX-3923). The umbrella verb (bare agents sync, including --local/--yes/--json) ignored --dry-run entirely: it ran the full reconcile, evicted central browser profiles, verified, and repaired — mutating every installed version home despite the preview flag. The umbrella composes only mutating stages (repo git pull, refresh() reconcile, device sync, repairAfterSync) with no non-mutating preview, so it now refuses --dry-run LOUD and up front — before any change — and points at the scoped path (agents sync <agent> --dry-run), which honors it non-destructively. --json reports { ok: false, mode: 'umbrella', dryRun: true, error, hint, installedAgents }. Source: cli/src/commands/sync.ts.

  • Terminal session resumes preserve follow-up prompts, including spaces and shell characters. Remote terminals use the origin directory or explicit --cwd; run pickers retain --remote-cwd precedence, and home-relative paths expand on the target. An invalid remote directory or failed terminal launch reports failure.

  • Publishes the session-resume fixes from the unshipped 1.22.108 candidate: account homes refresh their session tracker, Codex hook trust follows registration, empty local picks are skipped, and remote picks are validated on their origin device. The 1.22.108 tag is preserved; npm publication resumes with this patch after correcting its release notes.

  • agents view shows OpenCode's real model, account, plan, and last-active (PHNX-3982). The OpenCode row was the only one in the table with nothing usable in it — 1.18.15 (default) default id:meta+openai+opencode-go — while every other harness showed a model, an email, and a plan. Three separate reads were pointed at the wrong place. (1) The model came from <home>/.opencode/settings.json, which is agents-cli's own plugin-enablement file; OpenCode reads ~/.config/opencode/opencode.{jsonc,json}, and ships no catalog default, so the row fell through to the literal placeholder default. Both spellings are now read comment-tolerantly, and when nothing is configured the row shows the model OpenCode itself will start with, from the selection it persists to $XDG_STATE_HOME/opencode/model.json. (2) The account showed only the provider join because the code asserted OpenCode's auth.json carries no identity — it does, for type: 'oauth' providers, in the same namespaced JWT claims case 'codex' already decodes, so the email and plan now render (secrets are never read out, only claims). (3) lastActive was blank because the per-file session walk finds nothing for a harness that keeps every session in one sqlite file; it now dates from opencode.db. Live usage windows remain unavailable for OpenCode — it records no rate-limit state locally and each provider would need its own reader. Source: cli/src/lib/agent-spec/agents.ts, cli/src/lib/models.ts.

  • Codex trace error rates now reflect failed tool calls (PHNX-3761). The tool-call index recognizes Codex's script completion and failure markers, preserves text across input blocks, and re-indexes older rows instead of reporting failed calls as unknown.

  • agents sessions --cloud <id> and agents cloud view open OpenCode cloud transcripts (PHNX-3845). Cloud discovery and fetch accept OpenCode sessions, and normalized cloud transcripts are parsed separately from local SQLite sessions. Both bash and shell tool names appear as command events.

1.22.108

  • Account sync now refreshes the native session-tracking hook in every account home, replacing stale registrations from previous CLI installations so new sessions retain their account identity. Codex tracker installation also refreshes hook trust so headless runs can execute the registered hook.

  • Resume selection validates peer-owned transcripts on their origin device instead of treating a missing local copy as a missing session. Local empty picks are still skipped; explicit local replay still requires readable local content.

  • The resume picker skips a local session with no transcript before opening a tab (PHNX-4080). Empty local selections are reported as skipped, and a batch with nothing left to resume exits 1. Peer-owned sessions and mirror entries are validated on their origin device. Explicit local replay still requires readable local content; the executing device refuses recovery when no transcript exists. Source: src/commands/sessions-resume.ts, src/lib/session/recovery.ts.

1.22.107

  • A bare interactive run places itself like --device auto (PHNX-4083). agents run <harness> with no prompt on a real TTY (no --json) now auto-places onto a fleet worker — the same engine, pool, and banner as --device auto — instead of always running on the machine you typed it at; headless runs (any prompt, --json, teams/routines/hooks) are unchanged and still run in place. To keep a local interactive run, pass --device <this machine> or pick this machine (listed first) in the <harness>@ device picker; when placement finds no healthy device (empty pool, or the PHNX-4051 stale-usage refusal) the run fails loud with the placement error plus Run here instead: agents run <harness> --device <this machine> — never a silent local fallback. Source: src/commands/exec.ts, src/commands/run-account-picker.ts.

  • Run pickers move to # (account) and @ (device), #@ asks both (PHNX-4083). agents run claude# now opens the account picker (the old claude@ menu, same rows and code path); agents run claude@ opens the new fleet device picker — this machine first, offline rows disabled, cached-state age in the prompt — and claude#@ asks the account first, then the device, dispatching with the picked account label. A picker combined with an explicit pin of the same thing ([email protected]#, claude#work#, claude@@) or a conflicting flag (--account, --device/--on/--computer/--host) fails loud; a cancelled menu launches nothing. Source: src/commands/exec.ts, src/lib/hosts/dispatch.ts.

1.22.106

  • Fixed release packages omitting the session-tracking hook. Release qualification now uses the complete CLI build, so installed agents can record the account that owns each native session.

  • Resume conversations by account with one shared picker (PHNX-3940). ag run claude --resume opens the same history picker as agents sessions resume --agent claude; claude#work applies --account work. Selected conversations retain their account, model, permissions, cwd and run options while using the installed binary. Canonical account homes and short Codex homes are discovered independently of version installs. Explicit terminal backends work with session IDs. Native context is verified after cold discovery, and archived replay requires an explicit choice. Origin account identity survives alias updates and index rescans; a model refusal does not disable unrelated models or accounts.

1.22.105

  • Computer setup installs standalone Computer CLI 0.1.2, whose application code is obfuscated before compilation to increase inspection effort. Agents continues to provide permissions and session tracking through its adapter.

  • Fleet test runs use separate temporary workspaces and clean up their own copies. Router account fixtures run only with isolated file-backed secret storage, preserving native keyrings.

  • Device picker module for agents run <agent>@ (PHNX-4083). New run-device-picker command module builds the fleet device menu entirely from on-disk state — the device registry, the cached fleet stats, configured roles and descriptions, and the fleet-synced account catalog — with zero SSH, zero network, and no re-probing of stale rows. Source: src/commands/run-device-picker.ts.

1.22.104

  • Interactive Claude runs on a worker fail loud instead of dropping to the login screen (PHNX-3502 sibling). agents run claude --interactive --device auto/<worker> whose selected account had no synced setup-token used to fall through to Claude Code's "Select login method" screen — an interactive OAuth on a headless box that never persists, so Anthropic re-prompted on the next run (the repeated re-login loop). The run is now refused before spawn with the real fix (pin a live account with agents run claude#<name>, set the fleet default with agents accounts default claude <name>, or mint the account's token on a headed box with agents accounts login claude#<name>). Headless runs are unchanged (they already fail loud with a 401), and headed boxes still show their normal native-login prompt. Source: cli/src/lib/harness/adapters/claude.ts, cli/src/lib/exec.ts.

  • agents view and agents accounts list read as name + usage (PHNX-4051). The account table printed IDENTITY, STATE and WHERE on every row — an email, UNVERIFIED and on 9 boxes repeated down the whole list — which pushed the usage bars past the terminal width and wrapped every line. A row is now the account name and its usage bars, and nothing else when the account is healthy. Only a state worth acting on trails the row: rate-limited (yellow), expired / revoked / missing (red), partial fleet coverage (usable on 3 of 5 boxes), then the repair command. A blocking marker (out of credits) now renders after the bars instead of in front of them, so one throttled account no longer misaligns the table, and a stale window is 48%* (period ended 2h) rather than 48% · stale (period ended 2h ago). The identity and per-device state the columns carried are still one command away — agents accounts list --fleet, agents accounts view <name>, --json. A discovered login nobody has named is listed by its identity (the email) rather than unnamed. Source: cli/src/lib/account-catalog.ts, cli/src/lib/accounting/usage.ts, cli/src/commands/view.ts, cli/src/commands/accounts.ts.

  • agents computer now uses the standalone computer CLI (PHNX-4075). Install it with npm i -g @phnx-labs/computer-cli; agents setup computer also installs it when missing. The standalone package owns desktop commands, native helpers, remote provisioning, and VNC transport. Agents retains fleet device resolution, permission groups, session identity, and action history. The separate computer npm executable and native helper sources now ship from the standalone project.

  • Release cleanup preserves unfinished work. Dirty orchestration, publication, and attestation worktrees remain available with their paths printed. Generated files are restored only after their exact contents are saved in the release PR head; later edits are preserved.

  • A launched agent's own secrets calls hit the store agents-cli reads for it (PHNX-4081). buildExecEnv now exports SECRETS_HOME (the user agents dir, unless the launching environment set one), the same root the process client already passes the standalone. Before, an agent's secrets exec inside a Claude Code or Cursor session ran against a second broker under ~/.secrets with its own event log, so an unlock in one store was invisible in the other. The pinned standalone moves to @phnx-labs/[email protected], whose broker keeps --durable unlocks across restarts and lets a user-driven agent raise the Touch ID sheet instead of asking for secrets unlock by hand. Source: cli/src/lib/exec.ts, cli/src/commands/setup-secrets.ts.

  • One fewer cold secrets process on every command that touches secrets (PHNX-4082). Each synchronous secrets request spawns a fresh secrets __serve, so it pays a full Node boot of the standalone — 0.3–2.6 s on a desktop at load average ~100. The client spent one of those purely to ask which protocol version the standalone speaks, before the first real request of every process. It no longer does: every response envelope already carries v, and parseResponse checks it on each one. That is also stricter than the handshake it replaces — a standalone that answered the handshake as v1 and a later op as v2 used to surface as "malformed response envelope" and now reports secrets speaks protocol 2; this agents-cli needs 1. Measured on agents run claude: 7 standalone spawns before, 6 after. Source: cli/src/lib/secrets-client.ts, cli/src/lib/secrets-client.test.ts, cli/docs/secrets-client.md, cli/docs/specifications.md.

1.22.103

  • agents run claude no longer dies with secrets request failed: spawnSync sh ETIMEDOUT on a loaded machine (PHNX-4078). Every synchronous secrets request spawns a fresh secrets __serve process, so its bound has to cover a cold Node boot of the standalone, not just the tens-of-milliseconds operation. The bound was 3 s; on a desktop at load average ~100 each spawn measured 0.4–2.6 s and sometimes more, so the account listing on the launch path timed out and the run aborted before Claude started (also why AGI EXT tabs opened to a bare shell there). The sync bound is now 30 s — enough to boot under load, still well under the standalone's own 60 s self-deadline — and a timeout names the cause ("the standalone secrets CLI did not answer within 30s … check with secrets --version") instead of the raw spawnSync errno. Source: cli/src/lib/secrets-client.ts, cli/src/lib/secrets-client.test.ts, cli/docs/secrets-client.md.

1.22.102

  • AGI Menu updates itself on npm-installed Macs (PHNX-4036). The npm tarball ships no helper bundle, and the startup self-heal is network-free by design, so an npm i -g Mac kept whatever helper it had — the crashing 1.1.2, a dead 0.1.0 — until someone ran agents menubar setup by hand. The detached background sync now downloads and verifies the floor release into the helper cache whenever the installed helper is behind it (or no service exists yet and the user has not opted out), and the self-heal treats that cached bundle as a source, so the next agents invocation installs it with no foreground network. agents menubar status names the pending download instead of "missing (cannot enable)". Source: cli/src/lib/menubar/install-menubar.ts, cli/src/lib/auto-pull-worker.ts.

  • agents view STATE is honest and WHERE reads like English (PHNX-3940, PHNX-4051). A probe 429 no longer marks the account LIMITED — the probe keeps the previous verdict within 20 min, otherwise unverified with probe throttled (HTTP 429); the account's real throttle is the usage snapshot (deriveUsageStatusFromSnapshot / applyUsageHonesty). Fleet merge no longer lets a remote rate_limited override a fresh local available read (stale:false) — remote revoked/expired still win and rows stay in --json. The daemon probes each account identity once per tick (version home + slot collapse by accountId) and spaces probes ~150 ms so one box no longer fires 16 requests in 4 s. WHERE now reads this box, on N boxes, or on N of M boxes instead of +N; a gray footer legend explains STATE values and * stale. Source: cli/src/lib/auth-health.ts, cli/src/lib/account-catalog.ts, cli/src/commands/view.ts, cli/src/commands/accounts.ts, cli/docs/credential-management.md.

  • Feed rows carry their PR status. agents feed watch --json now attaches the gh pr view status it already fetches for attention to each agent row's pr (state, isDraft, reviewDecision, mergeable, and a one-word checks verdict), and re-emits the row when that status changes, so AGI Menu and the extension can color a PR chip merged / open / failing without their own gh calls. Source: cli/src/lib/feed/watch.ts, cli/src/lib/feed/pr-status.ts.

  • Restore per-window usage bars in agents view <agent> account rows (PHNX-3940). Account rows now render every blocking window side by side (S: ███░░ 58% (3d) W: ██░░░ 41% (5d)) via the canonical formatUsageSummary / pickCompactUsageWindows path instead of a single max-percent bar. The overview caps at 2 windows and single-harness views show all blocking windows; provider rows with no windows keep their current rendering. Source: cli/src/lib/account-catalog.ts, cli/src/lib/devices/harness-inventory.ts, cli/src/commands/view.ts.

1.22.101

  • agents sync status names what drifted, and a merged system-layer skill or subagent reaches the next agents run on every box (PHNX-4056). The status readout said 11 drifted per install and nothing more, although AgentVersionStatus.resources already carried the rows; both human renderers (agents sync status, the drift summary agents sync prints) now list each drifted or missing resource as drifted skills/artifacts under its install line, with the differ's detail when it has one. And when ~/.agents/.system/skills or subagents changed since the last launch of an (agent, version), agents run rewrites those resources into the version home through the registered writers before spawning, filtered by the active resource profile like the full sync, with the same stat-tier sentinel shape as the rules run-sync: on 2026-09-10 a merged skill change left 31 of 33 installed copies stale until the 4-hour umbrella autosync, because the shim's --launch path mirrors project resources only. Source: cli/src/lib/system-run-sync.ts, cli/src/lib/sync-status.ts, cli/src/commands/status.ts, cli/src/lib/drift-sync.ts, cli/src/lib/exec.ts, cli/src/commands/exec.ts.

1.22.100

  • The daemon's 20-minute auth-probe reuse works again on a box that has uninstalled a version (PHNX-4051). isCachedFleetAuthProbeFresh required EVERY cached auth row to be under 20 minutes old, including rows left behind for versions that are no longer installed. Nothing re-probes those — the probe only walks homes that exist — so their timestamp froze and the check was permanently false: the 3-minute tick live-probed the rate-limited /api/oauth/usage for every account on every tick, re-arming the per-account 429 backoff and keeping the usage refresher parked (the RUSH-2998 failure the reuse window exists to prevent; observed on a worker holding rows dated Sep 2 / Sep 6 for uninstalled Claude versions). Freshness now considers only rows for currently installed (agent, version) homes and account slots, and the tick prunes the orphan rows when it writes the cache, so agents view and fleet status stop reporting a verdict for a version that is gone. agents devices ping [--strict] still forces a genuinely live probe. Source: cli/src/lib/daemon-ticks.ts (installedAuthRows, isCachedFleetAuthProbeFresh, refreshLocalFleetAuthState), cli/src/lib/auth-health.ts (authTargetKey, enumerateLocalAuthInstalls, localAuthTargetKeys, writeFleetAuthRows).

  • Fleet usage-sync transport no longer starves --device auto (PHNX-4051). The daemon's shared-state git exchange failed on every tick two ways, so workers never received a fresh Claude usage snapshot and the 40-min placement gate turned every worker into "no ready device". Fixed both at the source: (1) the usage-sync tick is now the single committer — it also publishes the reserved-auth verdict, and auth-sync no longer runs its own syncFleetSharedStateRepo, so the two ticks can't contend for the one shared-repo lock ("Lock file is already being held"); (2) the untracked-collision scan is scoped to the tracked roots origin carries (git ls-tree origin/<branch>) — every path origin tracks stays in scope, so no path the rebase can overwrite is missed, while an untracked-only tree origin never tracks (like artifacts-cli's ~/.agents/artifact-history/) can never push git ls-files --others past the 1 MiB output cap. auth-sync's credential pushes are also gated on the exchange's freshness marker, so they never push off peer state older than one tick interval. Source: cli/src/lib/fleet-shared-repo-sync.ts, cli/src/lib/daemon/usage-sync-service.ts, cli/src/lib/daemon/auth-sync-service.ts.

  • An untitled Arc Space no longer hides every Arc Space from agents browser. parseSidebarSpaces treated the display-only Space title as part of a Space's identity and rejected the whole sidebar when one Space had none, so agents browser start warned that a configured arc-gmail "can't launch on this machine" while agents browser profiles doctor arc-gmail saw Arc and the Space. Identity is the stable Space id and its profile binding; a Space Arc stored without a title (key absent or null) is now listed as arc-space (two or more get their Space-id prefix appended, as same-titled Spaces already do). A mismatched id, a duplicate id, an unknown or malformed profile mapping, and a title that is present but not text still fail discovery closed. Source: cli/src/lib/browser/arc-discovery.ts, cli/src/lib/browser/arc-discovery.test.ts, cli/src/lib/browser/testdata/arc/.

1.22.99

  • agents setup secrets installs the published secrets CLI instead of only printing a hint (PHNX-3989). A missing secrets binary tries agents clis install secrets then a pinned npm i -g @phnx-labs/[email protected], then hands off to secrets migrate. Docs point at the standalone engine; the broker is secrets _agent-run on macOS, not the agents daemon. Source: cli/src/commands/setup-secrets.ts, cli/docs/secrets.md.

  • agents accounts list / agents view no longer report a signed-in account slot as MISSING. A slot's STATE now comes from a live read of the slot's own credential and from the daemon's auth probe, which now covers account slots (slot:<accountId> rows) as well as version homes. Before, a slot's verdict was written only by accounts add/login, the daemon never re-derived it, and signedIn was read from an unrelated version home — so a slot re-materialized as unconfigured while the device doc was unreadable (a duplicate YAML key from a shared-repo autostash merge) stayed MISSING forever even though agents run used its login fine.

1.22.98

  • agents run codex#<account> on macOS now runs as that account. The SUN_LEN short-home relocation keyed CODEX_HOME by version alone, so an account slot (long enough to overflow too) was silently redirected to the default version's ~/.agents/.codex-homes/<version>/.codex and its login — agents run codex#getrush opened the gmail session while the banner still said getrush. The short home is now keyed by the origin (a-<account id> for a slot). A version/account reinstall that recreates a fresh .codex adopts its existing short home (which holds the login) instead of failing, and a slot mis-linked onto a foreign home by the old layout is repointed to its own home rather than run as the wrong account.

  • A Firefox task no longer parks the browser-task reaper, and record on Firefox says "recording" (PHNX-4043). The daemon's idle reaper asks every live task for its recording status before closing it; on a Firefox (or native Arc) task that query threw a capability error, so one Firefox task failed every reap tick and the browser-task-reap service parked after three failures, leaving abandoned tabs open for every profile on that box. Recording status on a backend that cannot record is now a truthful "not recording". agents browser record start/stop and start --record on a Firefox profile still fail loud, now naming recording instead of screenshot (screenshots do work on Firefox). Source: cli/src/lib/browser/service.ts recordStart/recordStop/recordStatus.

1.22.97

  • agents browser --help and the agents setup browser install hint no longer claim Firefox is unsupported: they now state Chromium-family over CDP, Arc natively, and Firefox over WebDriver BiDi (Safari stays unsupported). tabs --all gains a help example and rows in the Arc and Firefox capability tables.

1.22.96

  • agents browser tabs --all shows every tab open in the profile browser, yours included (PHNX-4043). Until now tabs listed only the task's own tabs, so an agent sharing an Arc Space, a Comet store, or a Firefox profile with you could not see what else was open. --all lists every top-level tab: OWNER names the agents-cli task that owns it, or you for a tab no task owns; * still marks the task's current tab. Read-only on every backend (Chromium Target.getTargets, the Arc Space's tabs, Firefox browsingContext.getTree). Source: cli/src/lib/browser/service.ts profileTabs, cli/src/commands/browser.ts.

1.22.95

  • Firefox automation over WebDriver BiDi (PHNX-4043). agents browser now drives Firefox, which dropped the Chrome DevTools Protocol in 129. Every Firefox profile in profiles.ini is discovered read-only as a firefox-<name> browser profile (firefox-default, firefox-default-release, …) pinned to that profile directory; agents launch it headless with a debug port (or attach to a running one, failing loud when a portless Firefox already holds the profile). Supported verbs: start, navigate, tab add, tabs, evaluate, refs, click, fill/type, scroll, screenshot, done — with the same same-task reopen semantics as the rest of the service. A trusted pointer click goes through input.performActions. Network capture, upload, and PDF fail loud with a structured error naming a Chromium-family profile. Source: apps/cli/src/lib/browser/drivers/firefox.ts, apps/cli/src/lib/browser/firefox-discovery.ts, apps/cli/src/lib/browser/service.ts.

  • Comet's own profiles are browser profiles, and discovered profiles reach the whole fleet (PHNX-4042). On a Mac, agents browser profiles list now shows one profile per entry in Comet's profile menu (comet-work, ...), read from Comet's Local State and pinned to Comet's real user-data dir and --profile-directory, next to the arc-* rows for Arc Spaces. Agents and you share one Comet window per profile: agents attach when it runs with remote debugging on the profile's port, launch Comet on your own store when it is not running (over a TCP port, so the instance outlives a daemon restart and the ownership guard can verify it), and fail loud with the relaunch command when it runs without a port. The daemon's fleet-sync tick publishes every discovered profile into this device's declaration, so other boxes list it with WHERE=<device> and --profile arc-gmail from a worker routes to the Mac that owns it. AGENTS_COMET_DIR points discovery at another store. Source: cli/src/lib/browser/chromium-discovery.ts, cli/src/lib/browser/profiles.ts, cli/src/lib/browser/chrome.ts, cli/src/lib/browser/drivers/local.ts, cli/src/lib/daemon/usage-sync-service.ts.

1.22.94

  • Agents drive your running Arc, one profile per Arc Space (PHNX-2399, macOS). On a Mac with Arc, agents browser profiles list shows every Arc Space as a profile — arc-gmail, arc-work, arc-dev, named after the Space title — discovered read-only from Arc's User Data/Local State and StorableSidebar.json. A Space already carries its Arc profile's logins, so it is the browser profile agents pick with --profile; there is no separate Space flag and nothing to create. agents browser use arc-gmail makes it the default. Tabs are created, navigated, evaluated, and closed through Apple Events against the Arc you already have open — no debug port, no relaunch, never a second Arc. Agents only ever touch the tabs they created: durable task state addresses them by stable window, Space, and tab ids, a crash-safe marker intent guards creation, and closing a task removes only its own tabs. Creating a tab makes Arc select it for about 100 ms before the tab you had selected is restored; navigate, evaluate, and close are silent. Native refs, click, fill/type, scroll, and tab focus work; screenshots, promise evaluation, trusted input, network/console capture, uploads/downloads, and PDF fail with ArcNativeCapabilityError and point at a Chromium-family profile. Source: cli/src/lib/browser/arc-discovery.ts, cli/src/lib/browser/drivers/arc.ts, cli/src/lib/browser/arc-dom.ts, cli/src/lib/browser/profiles.ts, cli/src/lib/browser/service.ts, cli/src/commands/browser.ts.

1.22.93

  • An idle reminder is not a permission request — attention is classified from explicit harness evidence, and time-based signals expire (PHNX-3999). The fleet feed was rendering Claude's idle_prompt Notification ("the turn ended and you have been idle") as a permission with Approve/Deny — a session whose whole transcript was "reply with exactly: pong" → "pong" got an approval banner — and the state engine labelled any tool call that had run quietly for two minutes a permission too. Now a notification block is a permission only when its recorded subtype is permission_prompt and the session's transcript corroborates it (no tool result or new turn stamped after the block, process alive); the feed-publish hook no longer publishes idle_prompt at all, an idle_prompt block left on disk yields no request, an elicitation_dialog is a question with no invented buttons, and a pending tool call reads as working however long it has run. One unresolved record per host/session/generation is resolved by later evidence — a transcript event after the block, or a dead process — never by dismissing the banner. What the CLI cannot confirm (a stale hook block with no transcript cursor to check, a notification with no recorded subtype, an older peer's elapsed-time permission claim) is the new unverified kind: the banner and the feed watch stream say "Could not verify request" with an Open terminal action only, and agents feed answer refuses it. computeLiveSignals now memoizes only the parsed transcript tail on mtime and re-classifies against the current clock every call, so the 30-minute prose-question decay expires on schedule instead of freezing until someone types; rows carry lastEventMs, the harness stamp on the last meaningful event. Source: cli/src/lib/feed/attention.ts, cli/src/lib/session/state.ts, cli/src/lib/session/active.ts, cli/src/lib/feed/feed.ts, cli/src/lib/feed/answer.ts, cli/src/lib/daemon/attention-notify-service.ts.

  • AGI Menu 1.1.1: three fixed tabs (PHNX-3999, PHNX-4036). The menubar helper floor moves to 1.1.1, the first release cut from phnx-labs/agi-menu: the status item opens a popover with Sessions (working first, then confirmed requests), Projects (the tracker's projects → milestones with cycle chips → issues, expanding in place, from one linear projects overview --json call) and Settings, replacing the long dropdown. Fresh installs and agents menubar setup fetch it from the public menubar/v1.1.1 release. Source: cli/src/lib/helper-versions.ts.

  • AGI Menu moved out — the menu-bar helper now lives in phnx-labs/agi-menu (PHNX-4036). cli/menubar/ (the SwiftPM source, its build/test scripts, and the build-text test) is gone from this repo, the same split AGI EXT got in RUSH-3189. Nothing user-facing changes: agents menubar setup/enable still download and verify the signed, notarized MenubarHelper.app.zip from the menubar/v<floor> release tag (src/lib/helper-versions.ts), at the same URL, with the same bundle identity and designated-requirement pin — that release is now cut by agi-menu's scripts/release.sh, which also uploads a menubar-source.txt provenance sidecar. Release tooling never builds the helper again: a new scripts/stage-menubar-helper.sh downloads the published menubar/v<floor> asset, verifies its sha256, and (on macOS) extracts it into bin/MenubarHelper.app behind codesign --verify --deep --strict, spctl --assess, and the DR-pin gate (scripts/verify-menubar-helper.sh, no longer a prepack gate); --fetch-only downloads and sha-verifies on any OS. release-attestation-produce.sh --with-helpers records the menubar helper-manifest row from that published asset (asset digest = sha256 of the zip, source = the sidecar's repo/commit/tag when the release carries one) instead of a fresh build, keyed on src/lib/helper-versions.ts as its input — a floor bump is what re-records it; a missing or corrupt release fails closed naming the agi-menu publish step. remote-sign-mac.sh stages the published helper on the home base instead of running swift build. The installer's dead cli/menubar/dist/ source candidate is removed; bin/MenubarHelper.app (staged, or copied from an agi-menu checkout) remains the working-tree source. Source: cli/scripts/stage-menubar-helper.sh, cli/scripts/release-manifest.sh, cli/scripts/release-attestation-produce.sh, cli/scripts/remote-sign-mac.sh, cli/src/lib/menubar/download-menubar.ts.

  • A Claude account-slot launch now runs in its own slot home, and worker slots come pre-onboarded (PHNX-3940 follow-up). agents run claude --device <worker> picked an account slot and pinned CLAUDE_CONFIG_DIR to it, but the versioned alias (~/.agents/.cache/shims/claude@<version>) re-exported the shared version home unconditionally, so the run authenticated as the picked account (the setup-token env survived) while reading and writing config, onboarding state and history in whatever home the version carried — on yosemite-m1 that home belonged to another account and had never onboarded, so every dispatched run opened on Claude Code's theme picker. buildExecEnv now stamps a slot launch in AGENTS_EXEC_HOME; every shim that pins a config-dir env (claude, grok, opencode, kimi, muse, copilot — bare shim and direct alias, schema 32 / alias 21 — the daemon self-heal or agents doctor --fix regenerates on-disk copies) yields its pin to that slot through one shared slotAwareConfigEnvBash and consumes the marker before exec, so a nested launch never inherits its parent's slot and a bare <agent>@<version> from a terminal still gets the version home; the same override was live for all six, codex and cursor already yielded. A re-seed that still reads as unseeded (a slot config file that exists but cannot be parsed) is reported as an error each tick instead of a false "provisioned". Separately, provisionWorkerSlot seeded only the identity email into a slot's .claude.json, never hasCompletedOnboarding — a worker has no human to finish onboarding — so every provisioned slot re-onboarded; it now seeds both, and the daemon's reconcileLocalWorkerSlots re-provisions a durable claude slot that lacks either instead of skipping it as "already provisioned", so the slots provisioned before this converge on the next tick. Source: cli/src/lib/harness/adapter.ts, cli/src/lib/harness/adapters/*.ts, cli/src/lib/exec.ts, cli/src/lib/installations/shims.ts, cli/src/lib/claude-account-token.ts, cli/src/lib/secrets-policy.ts, cli/docs/credential-management.md.

1.22.92

  • AGI Menu 1.1.0 is the helper the CLI installs (PHNX-4007). The menu-bar helper floor moves from 1.0.0 to 1.1.0, so agents menubar setup and agents menubar enable download and install the release that carries the dispatch form, the Sessions window, actionable notification banners, the screenshot OCR index, and the FeedStream data layer (PHNX-3999). The startup self-heal stays network-free: it reinstalls only from a bundled or cached copy, so an existing 1.0.0 install upgrades on the next agents menubar setup. Source: src/lib/helper-versions.ts.

1.22.91

  • agents sessions search/list execs the standalone sessions CLI (PHNX-4012). Read queries intercept in index.ts before bootstrap so they skip the 6K-line in-process engine. Lifecycle verbs (resume, stop, inject, watch, --active, --markdown) still load the in-repo command. The sessions alias shim is retired like secrets so it cannot recurse. Install: npm i -g @phnx-labs/sessions-cli. Source: cli/src/index.ts, cli/src/lib/sessions-client.ts, cli/scripts/postinstall.js.

1.22.90

  • accounts migrate --apply no longer aborts on a worker whose accounts already hold slots (PHNX-3940). The daemon's auth-sync provisions a durable slot per account on every worker, so --apply hit Slot already exists on the first credential-bearing legacy home and cleaned nothing. The plan now routes such a home to trash (restorable with agents trash restore) with the reason "account already holds a provisioned slot on this device", rewrites any <harness>@<label> binding to the account, and clears the stale home record; a canonical install in that state keeps its binary, has its stale home moved to ~/.agents/.history/trash/homes/… (manifest key <harness>@<label>#home), and gets an empty home back. A regular file at the slot path is still not a slot, so the apply-time guard still fails loud on that corruption. Source: cli/src/lib/accounts/migrate.ts.

  • AGI Menu: Sessions window, feed-driven RECENT, Notifications submenu (PHNX-4003). Cmd-Shift-I (or the new Sessions row at the top of the dropdown) opens a persistent Sessions window: live sessions across the fleet grouped by project on the left, with Recent / Needs you / Running / Done today filter chips, a Cmd-F search (label, topic, project, ticket, device), and a device strip that marks stale heartbeats; a session card on the right shows the phase, PR chip, request excerpt, progress tally, latest action, today's rendered artifacts with an Open button, and the attention block when the agent is waiting — numbered option buttons, permission Allow/Deny, plan-review Approve/Send back — answered atomically through agents feed answer. A reply field routes by capability (agents feed answer, agents sessions inject, agents cloud message, agents teams message) and shows the CLI's own result inline; keys 1–9, Return, Cmd-Return, Cmd-K answer, reply, open the terminal, or nudge. The dropdown's RECENT section now renders the same feed-driven rows (status dot, n/N checklist, working · 18h · host, latest action + PR chip) grouped by project, each opening the window on that session, and a Notifications submenu lists today's feed banners unanswered-first. Blocked sessions leave the NEEDS YOU strip, which keeps only load, scheduler, and routine triage. Source: cli/menubar/Sources/MenubarHelper/SessionsWindow.swift, cli/menubar/Sources/MenubarHelper/SessionCard.swift, cli/menubar/Sources/MenubarHelper/SessionRowModel.swift, cli/menubar/Sources/MenubarHelper/Reply.swift, cli/menubar/Sources/MenubarHelper/RecentSectionBuilder.swift.

  • AGI Menu delivers attention banners on UNUserNotificationCenter with real action buttons (PHNX-4004). The deprecated NSUserNotification toast is replaced for actionable notices: the banner carries the mapped category, a time-sensitive interruption level for a blocked ask, a per-session thread so a session's banners group, and the harness avatar as an attachment. The persistent menu-bar instance is the delegate; a tapped action runs one bounded agents feed answer / agents open command off the delegate thread and posts a "Could not deliver your reply" follow-up when it fails, or a "Could not open the report/PR" follow-up when a done banner carried no link for that button. When macOS declines notification authorization it degrades to the old plain banner so a notice is never lost. Source: menubar/Sources/MenubarHelper/Notifier.swift, menubar/Sources/MenubarHelper/AgentAvatar.swift.

  • AGI Menu palette: dispatch form with remembered defaults, project pulse, and session-id minting (PHNX-4005). The Cmd-Shift-O palette now carries the dimensions a dispatch actually needs below its text field — mode (Plan / Auto / Edit), run on (This Mac, auto, or a fleet device by name), surface (Interactive / Headless), and watchdog (Off / Keep moving / Hands-off) — collapsed to one summary line (Claude 2.1.263 · this-mac · agi · Auto · Interactive · Keep moving) with a ▸ details disclosure. Every dimension is remembered per project, so switching projects restores the agent, mode, and target you last used there and one keystroke dispatches. Cmd-P switches this one dispatch to Plan without touching the remembered defaults. A Claude dispatch mints its session id and passes --session-id, which lets the palette apply a non-default watchdog policy at dispatch and register a launching placeholder with the feed stream: the row shows in the project pulse strip as ◌ launching · <name> (and in any window that reads the feed rows) until the real session reports, and reads "did not start" — with the CLI's own stderr — if the launch exits non-zero or no row arrives within 60 s. Under the project row a pulse strip shows the headline milestone from agents projects status, open tickets and PRs, live running / need-you / idle counts with NOW chips (not-progressing first), and the NEXT UP ticket (Cmd-N attaches). Source: cli/menubar/Sources/MenubarHelper/DispatchForm.swift, cli/menubar/Sources/MenubarHelper/DispatchDefaults.swift, cli/menubar/Sources/MenubarHelper/ProjectPulse.swift, cli/menubar/Sources/MenubarHelper/FeedStream.swift, cli/menubar/Sources/MenubarHelper/AgentsCLI.swift.

  • AGI Menu palette: find a screenshot by the text on it (PHNX-4006). Every capture in the palette's screenshot folders (the system screencapture location, CleanShot's export path, and the clip attachments dir) is now OCR'd once, on-device, and indexed so you can search screenshots by the words on them. With the palette key, Cmd-Shift-F focuses a search field above the strip: typing filters the thumbnails to captures whose recognized text matches every token (case-insensitive, tokenized AND), newest first, with an n of m match hint; clearing it restores the newest-six strip. A more disclosure expands the strip into a grid grouped by hour (last 24 h) then day — each cell shows the thumbnail, the time, and the first recognized line — where a click attaches and a double-click previews the full image in Preview.app. Nothing leaves the machine: recognition runs through Vision locally (.fast, no language correction) on a background queue, images over 20 MB are skipped and the rest downsampled to 2000 px before recognition, each capture is keyed by the SHA-256 of its bytes so a rename or duplicate is never OCR'd twice, a deleted capture drops out of the index (search and grid never return it, and the next scan prunes its row), and the index is a local SQLite file at ~/.agents/.history/menubar/screenshots.db. Source: cli/menubar/Sources/MenubarHelper/ScreenshotIndex.swift, cli/menubar/Sources/MenubarHelper/ScreenshotGrid.swift, cli/menubar/Sources/MenubarHelper/PromptPanel.swift.

1.22.89

  • Worker daemons now actually relaunch onto a new agents-cli release, so a fix that ships reaches every worker without a hand agents daemon restart (PHNX-3940 rollout). Two defects kept every fleet daemon on the code it booted with: resolveRunningPackageRoot(__dirname) resolved one level up from the calling module, which from dist/lib/daemon/self-update-service.js is dist/lib, so every self-update tick failed with … is not an npm-managed install (eight Linux workers logged it for days and ran 1.22.79 code while the disk sat at 1.22.88); and the tick only exited after an install it performed itself, so an install written by an operator's agents command never relaunched the daemon, and on a box with a second agents on PATH the tick declined silently. The resolver now walks up to the package.json naming this package; the tick compares the on-disk version with the version it booted with and, when the disk is newer and the install has settled (package.json at rest for a minute and every bin entry present, because bun's write is not atomic), exits for the OS-supervisor relaunch without downloading anything. The shadow decline is logged once per daemon process, and auth-sync logs accounts skipped because their durable key is not readable on the box. Source: cli/src/lib/self-update.ts, cli/src/lib/daemon/self-update-service.ts, cli/src/lib/daemon/auth-sync-service.ts.

  • AGI Menu palette: real projects, Linear from the binding, pin, live screenshots, image paste (PHNX-4001). The Cmd-Shift-O palette now picks a project, not a directory: it lists every agents projects definition by name (with a type-ahead filter and the root as a tooltip) instead of the last eight session working directories, so a project you have not worked in today is still one keystroke away, and two definitions that share a checkout — prix and rush both live in the same monorepo — stay two entries. Dispatch passes --project <name>; recent session cwds now only offer a worktree or subdirectory inside the chosen project. The ticket list is scoped by the project's own Linear binding rather than by matching folder names against Linear project names, which is why picking agi now shows the AGI board instead of "no Linear project matches agents-cli"; a project with no binding says so and prints agents projects link <name> --linear. The palette can be pinned (header button, or press Cmd-Shift-O while it is already focused) so it survives another app taking focus. The screenshot strip is live — a shot taken while the palette is open appears within about a second, with no re-summon — and images can be pasted with Cmd-V or dropped onto the panel; Backspace on a focused thumbnail removes it. Attachments now travel to the agent as <host>:<abs-path> references (the same form Cmd-Shift-V types), so a dispatch to another device can fetch them. The ticket list starts folded and Cmd-T toggles it, remembered. On a machine with no projects defined the palette falls back to recent session working directories, and with neither a project nor a recent directory it now refuses to dispatch instead of launching an agent with no working directory (which resolved to /). Source: cli/menubar/Sources/MenubarHelper/PromptPanel.swift, cli/menubar/Sources/MenubarHelper/ScreenshotWatcher.swift, cli/menubar/Sources/MenubarHelper/LinearTickets.swift.

  • The CLI now posts one actionable desktop banner per new attention item, so a session asking a question, a permission prompt, or a plan review is answerable straight from the notification (PHNX-4004). A new supervised attention-notify daemon service (tick 5 s, reader-independent) reconciles this host's live sessions each tick and posts a banner carrying the category, the attention key, the session id, and the answerable choices — Approve / Approve for session / Deny for a permission, the options plus a typed reply for a question, Approve / Send back for a plan review, Open terminal for a stall — which the macOS helper routes back through agents feed answer <key> --choice <id>. A deny/send-back choice delivers a real Escape keystroke (no trailing Enter) rather than the letters "esc", so cancelling a permission or plan prompt never confirms it. agents run --notify finish banners now carry category: done|failure; the finish-notification shape also carries the session id and open-report / open-pr choices when the caller supplies a session id, report path, or PR url. Idempotency is a filesystem ledger under ~/.agents/.history/feed/notified/, pruned at 14 days, so a daemon restart never re-posts a banner already sent. The --notify argv gains --category, --key, --session, and repeated --choice <id>=<label> (see docs/menubar.md → Actionable notifications). Source: src/lib/menubar/notify-desktop.ts, src/lib/daemon/attention-notify-service.ts, src/lib/feed/attention.ts, src/lib/answer-router.ts, src/lib/run-notify.ts.

1.22.88

  • agents run no longer fails with secrets request failed: spawnSync sh ETIMEDOUT when resolving an account credential. The synchronous secrets path (agents view, the account catalog, and account resolution on the agents run hot path) fed the standalone secrets __serve its request over a named FIFO on fd 3. The standalone wraps fd 3 in a net.Socket, and a Socket over a named FIFO reads the request but never fires EOF on macOS — so its read loop blocked until the 3s sync bound fired and every credential lookup timed out. The request now rides spawnSync's stdin (a real pipe/socketpair — the same fd type the async path already hands the standalone) dup'd onto fd 3, so it EOFs and a real handshake/lookup completes in tens of ms. Source: cli/src/lib/secrets-client.ts (serveOnceSync).

1.22.86

  • Workers now materialize a slot for every registered Claude account, not only the ones added through the v2 accounts add flow (PHNX-3940). The daemon's slot reconciliation skipped any account row without a workerCredential field — every account registered before the v2 model — on the assumption that a legacy row "resolves its token at spawn". That holds for agents run claude#<name> but not for the interactive picker, so on a worker whose auth bundle already held all eight setup-tokens agents run claude --interactive --device auto listed the installed version labels, showed most of them logged out, and launching one put a Claude login screen on a headless box. reconcileLocalWorkerSlots now resolves each row through the same reservedSyncTargets the push plan uses (a v2 row's reserved __claude__ key, a legacy row's email-keyed auth token), so every account whose token is on the box gets a durable slot with a 0600 .oauth_token and the seeded identity the adapter injects from. Two related fixes ride along: the credential publisher is now a ready headed device (electPublisher, headed-first then by name) instead of the alphabetically first ready box, which had made a worker the fleet's source of truth; and slot reconciliation runs first in the auth-sync tick, so a failed shared-state git exchange no longer postpones local provisioning. Source: cli/src/lib/secrets/reserved-sync.ts, cli/src/lib/daemon/auth-sync-service.ts.

  • agents secrets no longer recurses into the legacy secrets shim after an in-place upgrade (PHNX-3989). Boxes that ran agents-cli before the standalone secrets engine still carry ~/.agents/.cache/shims/secrets, a command shim that execs agents secrets. It sits first on PATH and survives an upgrade because its baked entrypoint still exists, and 1.22.85 resolved the standalone with a plain which secrets — so on every such box agents secrets … (and the daemon's reserved-store sync, which uses the same resolver) re-entered itself through the shim without bound. The resolver now skips agents-cli's own shims dir when looking for the standalone (SECRETS_BIN still wins; a miss still fails loud with the install command), and the self-heal shim pass removes that legacy secrets shim even though its target install is alive, since it can only recurse. The package's postinstall — which used to write that very shim on every install, so an upgrade kept bringing it back — no longer writes a secrets alias at all and removes one a previous install left behind; the bare-command aliases are now sessions, browser, pty, and teams. Install the standalone with npm i -g @phnx-labs/secrets-cli if agents secrets reports it missing.

1.22.85

  • agents run codex marks an exhausted account so rotation stops re-picking it (PHNX-3859). When a headless codex run ended with You've hit your usage limit … try again at <date> (or an out-of-credits refusal), that account's throttle was never recorded, so the balanced picker kept selecting it from a stale usage snapshot and every subsequent run died the same way instead of rotating to a healthy sibling. Codex now persists a per-account refusal marker the same way Claude already did — the usage cache is identity-keyed and shared across harnesses — so collectRunCandidates excludes the dead account and picks a healthy one. A usage limit is noted with its reset clock (it auto-clears when the window resets), a genuine credit/quota exhaustion is noted clock-less (cleared by a later successful run), and a limit with no parseable reset is left untouched rather than persisted as an unexpirable marker. Source: cli/src/lib/exec.ts (classifyCodexRunRefusal, parseCodexUsageLimitReset).

  • Account list and view render seams (PHNX-3940). agents accounts list <harness> emits only that harness's group — a provider credential appears under the harnesses it authenticates that match the filter, and empty groups are omitted. agents view <harness> prints the ACCOUNT / IDENTITY / STATE / WHERE / USAGE / FIX header once per harness block. USAGE is its own column (bar + percent) between WHERE and FIX; FIX holds only the repair command and stays empty when the verdict is unverified with nothing to run. WHERE reads this box when only this device reports, and accounts list --fleet names devices whose daemon-state carries no account verdicts (older release) instead of leaving a blank column. accounts migrate --dry-run still counts a busy canonical as the kept install and says so. Source: cli/src/lib/account-catalog.ts, cli/src/commands/accounts.ts, cli/src/commands/view.ts, cli/src/lib/devices/harness-inventory.ts, cli/src/lib/accounts/migrate.ts.

  • Account slots and reserved credential stores (PHNX-3940). Each account is a HOME-shaped slot at ~/.agents/.history/accounts/<harness>/<id>/ (device-doc deviceAccounts.slots, never central). A harness-auth capability table covers every installed agent; connect still drives claude/codex from that table. Secrets refuse user bundle names starting with __; reserved stores __<harness>__ accept only a setup-token or API key. The legacy auth bundle stays a readable alias. Source: cli/src/lib/accounts/slots.ts, cli/src/lib/harness-auth-capabilities.ts, cli/src/lib/secrets/reserved-stores.ts.

  • One managed installation per harness (PHNX-3940, track T2). agents add <harness> now installs or reuses the harness's single managed installation (label main) via ensureHarnessInstallation; agents add <harness>@<release> pins that SAME installation — identical to agents update <harness> --to <release> — instead of creating a second home, which now requires --isolated. Bare agents update <harness> updates the single managed installation (the @<label> selector stays for legacy layouts but is hidden from help), agents view --versions is a hidden legacy diagnostic, and account credential-slot dirs never appear as installations. Source: cli/src/lib/installations/store.ts, cli/src/commands/versions.ts, cli/src/commands/update.ts, cli/src/commands/view.ts.

  • Account lists now show live authentication verdicts, fleet coverage, and exact repair commands (PHNX-3940). agents accounts list [<harness>] and agents view <harness> share one account-first renderer; --json emits the version 2 account schema, and --fleet renders accounts by device without exposing reserved credential stores. A provider credential that authenticates a harness appears in that harness's rows (ready/missing); one no harness uses is listed under Other accounts. Unfiltered --json emits one kind: "provider" entry per harness the credential authenticates (same id, each with its own harness), plus one harness: null entry for an orphan; a harness filter emits only that harness's entry. The daemon sends one important owner notification when an account changes from live to expired or revoked. Worker usage scope failures are shown as unverified, never as a credit claim.

  • agents accounts add <harness> [name] is the account onboarding verb (PHNX-3940, track T4). On a headed device it reuses the harness's one managed installation, creates a HOME-shaped credential slot per account (no second installation), drives the native login into that slot, registers the fleet-wide account row (provisioning, createdOn), and mints the durable worker credential into the reserved __<harness>__ store keyed by account id — claude via a setup-token drive in the slot (plus the legacy auth bundle key this release), codex/grok/cursor/opencode via --api-key or an interactive prompt (never derived from OAuth), codex additionally via --per-device for a ChatGPT-plan seat; --no-worker-token skips the mint with a warning, and an ambient CLAUDE_CODE_OAUTH_TOKEN refuses it. Workers are refused before any side effect, with the laptop command and the automatic provisioning named. agents accounts login <harness>#<name> re-authenticates into the SAME slot (fail-closed on an identity mismatch) and re-mints/re-syncs; a per-device harness (kimi) may log in on any box. agents accounts default <harness> [name] is the one write path for the per-harness default (picker with no name). The legacy connect, name, label, mint, attach, detach, switch, and set-default verbs still execute for one release but are hidden from help and print a one-line pointer to their replacement. The provider form accounts add <name> --provider <p> --auth <t> is unchanged when the first arg is not a harness id; mixing the forms fails loud as ambiguous. Source: cli/src/lib/accounts/add.ts (renamed from connect.ts, runConnect kept as an alias), cli/src/lib/auth-mint.ts (api-key collection flows in MINT_FLOWS, seedReservedStoreKey, workerCredentialStoreKey), cli/src/commands/accounts.ts.

  • Spawn and rotation read account slots, not extra installations (PHNX-3940, track T5). agents run <harness>#<name> (and --account) now sets the spawn HOME to that account's slot on this device; the binary still comes from the one managed install unless @<label> pins it. A leftover acct-* home still resolves until migration; when no homes label was recorded, spawn matches the account's identity against each installed version home's own credential file (the pre-T5 fallback). A worker with the durable key present materializes the slot; a missing slot fails loud with the add/login hint rather than launching into a wrong home. Balanced/available rotation enumerates slots (plus leftover homes) and never launches a slot whose verdict is outside {live, unverified}. A routine pinned to a native account forwards --account so two slots sharing one managed binary still launch the selected login. The account picker shows account · verdict · usage. Symlink-adopted harnesses (droid, antigravity, gemini, …) hold one active slot per device — accounts default either repoints the adopted ~/.<config> under the auth-op lock or fails loud without writing the default; a run whose adopted symlink does not point at the selected slot fails loud. Remote --device forwards #name unchanged so the peer resolves its own slot. Config-dir env pins (GROK_HOME, OPENCODE_CONFIG_DIR, XDG) are stripped so two slots in one install never inherit each other's dir. Source: cli/src/commands/exec.ts, cli/src/lib/exec.ts, cli/src/lib/exec-account-home.ts, cli/src/lib/accounting/rotate.ts, cli/src/lib/daemon/runner.ts, cli/src/lib/harness/adapter.ts.

  • Worker provisioning generalized to every portable account, per key and per role (PHNX-3940). The daemon's auth-sync tick no longer syncs only the single legacy auth bundle: it plans per ACCOUNT and per KEY (<ENV>_<accountId>) over every account's reserved __<harness>__ store (falling back to the legacy auth bundle for a claude row predating the slot model), so a newly-added account propagates to workers within one tick instead of being hidden behind a bundle-coarse "already present" verdict. Pushes target role=worker devices only — a headed (personal/desktop) peer receives the account row but never a durable key (credential-management.md invariant 7). After a key lands on a worker, the daemon materializes a slot for it (ensureSlot + the durable write: for claude, .oauth_token 0600 + seeded identity; an API-key harness gets a durable slot with no file; a token-less harness gets a per-device slot). An email-only worker slot now resolves its account/org identity from the fleet-synced registry row, so it reads as connected instead of "not connected here". The durable credential moves only over the existing encrypted SSH bundle push and is retained nowhere else; native OAuth/session files are never transported. Source: cli/src/lib/secrets/reserved-sync.ts, cli/src/lib/claude-account-token.ts, cli/src/lib/native-accounts.ts, cli/src/lib/daemon/auth-sync-service.ts, cli/src/lib/agent-spec/agents.ts.

  • Fold leftover per-account installations into one install plus credential slots (PHNX-3940, track T7). agents accounts migrate --dry-run (default) inventories every installation dir per harness and prints the plan; --apply moves each credential-bearing unique identity's home/ into ~/.agents/.history/accounts/<harness>/<id>/, trashes empty logged-out homes and duplicate identities (reversible via agents trash restore), repoints the global default at the canonical install, and re-indexes sessions.db paths in the same transaction. The apply manifest is written with the plan before the first move and updated after each one so a crash lists exactly what moved; agent@label bindings are rewritten per-target (a duplicate home's binding follows the kept identity) rather than collapsed onto one bare-agent binding. A busy home is deferred, never moved. An ordinary upgrade only prints the dry-run report. --device <name> runs the same command on a fleet box. Routines without an explicit account: still resolve a provider binding or per-harness default into injected env. Source: cli/src/lib/accounts/migrate.ts, cli/src/commands/accounts.ts, cli/src/lib/session/db.ts, cli/src/lib/daemon/runner.ts.

  • Account model v2 docs, fleet guidance, and seam cleanups (PHNX-3940). The credential, version, README, and AGENTS reference now describe the shipped one-managed-installation-plus-account-slots model rather than the pre-v2 per-account installations: agents accounts add <harness> [name] is the taught onboarding verb, accounts connect/--versions read as hidden/legacy, and the "no reserved bundle name" claim in docs/credential-management.md is resolved in favor of the code — auth is the reserved legacy alias for the __claude__ store, reserved per-harness stores (__<harness>__) hold durable worker credentials, and provider accounts stay their own named bundles. The provisioning model is rewritten around accounts add plus the daemon's per-key/per-role reserved-store push (headed devices keep native login; workers receive only the durable key). cli/AGENTS.md login remediations teach accounts login <harness>#<name> on a headed device. Live FIX hints (signin-badge / account catalog empty-state) teach the same verbs — never the hidden connect alias. Spawn-home resolution in docs/execution.md matches the shipped order including spawn-time worker-slot provisioning and adopted-symlink target verification. Bare agents update <harness> now names the isolated/legacy installations it left untouched instead of moving the one managed install silently; reserved-sync.ts uses a single AUTH_STORE_ALIAS symbol; and the "nothing installed" hint teaches bare agents add <harness>. Source: cli/docs/credential-management.md, cli/docs/version-management.md, cli/AGENTS.md, README.md, cli/src/commands/update.ts, cli/src/lib/secrets/reserved-sync.ts, cli/src/lib/installations/resolve.ts.

  • Process client for the standalone secrets CLI (PHNX-3989). New internal seam cli/src/lib/secrets-client.ts — the one bounded request/response client through which agents-cli will talk to the extracted standalone secrets executable, over its private fd-3/fd-4 pipe (secrets __serve). It resolves the executable ($SECRETS_BIN, else secrets on PATH; a miss fails loud with npm i -g @phnx-labs/secrets-cli and NO fallback to the embedded engine), caches one protocol-version handshake per process, and exposes async (secretsRequest) and bounded-sync (secretsRequestSync) primitives plus typed wrappers for the bundle/agent/keychain/store/remote/push operations agents-cli consumes. It defaults SECRETS_HOME to ~/.agents so the standalone adopts existing stores in place, and bridges the caller's legacy AGENTS_SECRETS_PASSPHRASE onto the renamed SECRETS_PASSPHRASE the standalone reads so it can decrypt that same file store. No consumer is converted yet and the embedded engine is untouched — that is the next wave. Source: cli/src/lib/secrets-client.ts, cli/docs/secrets-client.md.

  • agents secrets is now a thin passthrough to the standalone secrets CLI, and its browser/ssh/apply/sync/webhook/doctor consumers move onto the process client (PHNX-3989). agents secrets <anything> execs the installed @phnx-labs/secrets-cli binary verbatim — forwarding argv and flags unmodified, defaulting SECRETS_HOME to ~/.agents so it adopts the existing store in place — and fails loud with npm i -g @phnx-labs/secrets-cli install guidance when the executable isn't found; there is no fallback to the in-repo engine (DIST-1). The old commands/secrets.ts registrar (and its secrets-import/secrets-migrate/secrets-rotate-passphrase/secrets-sync/secrets-vault siblings) stays in the tree, unregistered, until every other track's consumers convert. agents setup secrets is now install guidance plus a hand-off to the standalone's own interactive secrets migrate, dropping the old backend/policy wizard. browser, ssh, apply/fleet apply, sync, webhooks serve, fleet capture, doctor, and their library dependencies (lib/browser/chrome.ts, lib/share/config.ts, lib/daemon-webhooks.ts, lib/sync-umbrella.ts, lib/fleet/apply.ts) now resolve bundles, keychain items, and remote-sync pulls through secrets-client.ts instead of importing lib/secrets/* directly; fleet policy (the reserved auth bundle name and backend check) moves to a new agents-owned lib/secrets-policy.ts that calls the client rather than the engine. secrets-client.ts gains bundleBackend, listRemoteBundles/pullBundle, scanUserRcFiles/masterPassphraseInEnv, and the pure secretsKeychainItem/keychainRef naming helpers to cover these call sites. Source: cli/src/commands/secrets-passthrough.ts, cli/src/commands/setup-secrets.ts, cli/src/lib/secrets-client.ts, cli/src/lib/secrets-policy.ts, cli/src/lib/net-close.ts.

  • The run/exec hot path resolves secrets through the standalone secrets process client (PHNX-3989). agents run --secrets <bundle> (incl. --secrets-keys, --allow-expired, and remote bundle@host), the crabbox --lease provider-token / tailscale reads, the Cursor and Antigravity cloud API-key reads, and the --lease Claude credential read now go through cli/src/lib/secrets-client.ts instead of the embedded lib/secrets/* engine — a missing secrets executable fails loud with install guidance, never a silent fallback (DIST-1). Bundle precedence (profile < account < auto-share < bundles < --env) and the agents-owned bundle@host policy (fleet-alias resolution, flag validation) stay in agents-cli and are passed into the client; behaviour is unchanged. Source: cli/src/lib/exec.ts context, cli/src/commands/exec.ts, cli/src/lib/crabbox/{cli,runtimes}.ts, cli/src/lib/cloud/{cursor,antigravity}.ts.

  • Accounts, profiles, and the reserved auth bundle now go through the standalone secrets CLI (PHNX-3989). agents accounts (add / set-key / rename / remove / view / sync / mint), the harness profile token store (agents harness add --from-secrets, provider keys, legacy profile-auth migration), agents lease setup, the harness wizard's bundle browse, Claude usage/probe setup-token reads, and the Claude OAuth keychain read/write no longer load the embedded secrets engine: every read and write is one call on cli/src/lib/secrets-client.ts, which spawns the installed secrets executable. The seam is strict — a missing secrets fails loud with npm i -g @phnx-labs/secrets-cli and there is no fallback to the old engine. Fleet policy stays in agents-cli and is passed in: the reserved auth bundle's file-backend rule now lives in cli/src/lib/secrets/reserved-stores.ts (a keychain- or vault-backed auth still fails loud on read, and the standalone refuses to write one), and the SSH host-pinning guard for credential transport moved to cli/src/lib/hosts/credential-transport.ts. The client grew the wrappers these paths need (renameBundle, rotateBundleSecret, bundleBackend, keychainUsesFileFallback, sync siblings for the bundle and item writers) plus the raw item-naming helpers (secretsKeychainItem, profileKeychainItem, keychainRef, parseBundleValue). Setup-token reads memoize per process for 10 s instead of fingerprinting the engine's on-disk credential file (an in-process mint or rotation clears the memo). The suite installs the pinned published standalone once per machine (tests/secrets-standalone.ts, wired through vitest's global setup) and drives it for real — no in-memory keychain. Source: cli/src/lib/account-registry.ts, cli/src/lib/profiles.ts, cli/src/lib/auth-mint.ts, cli/src/lib/claude-account-token.ts, cli/src/commands/accounts.ts, cli/src/commands/profiles.ts.

  • Read-only status surfaces never hang or crash on a missing/broken secrets (PHNX-3989). The synchronous client (secretsRequestSync) does its fd 3/4 wiring in a POSIX shell — request via a FIFO, response redirected onto stdout (4>&1) — so it works under the Bun runtime the suite spawns the CLI with (Bun's spawnSync drops numbered fds 3+, which had left the standalone deadlocked for 60 s and surfacing as secrets returned a non-JSON response). It is bounded to 3 s, and agents view / agents accounts / the agents run account-rotation pool now degrade to one clear line (secretsUnavailable) instead of hanging or aborting when the standalone can't answer. A non-JSON reply names the first 200 bytes it got on fd 4, and SecretsClientError serializes to a plain {code, message} so folding a failure into a JSON.stringify'd structure can't throw a circular-reference error. Source: cli/src/lib/secrets-client.ts, cli/src/lib/account-catalog.ts, cli/src/lib/accounting/account-pool-collect.ts, cli/tests/secrets-standalone.ts.

  • The in-repo secrets engine is gone — the standalone @phnx-labs/secrets-cli is the only implementation (PHNX-3989 Track D). cli/src/lib/secrets/** (bundle storage, the broker, the keychain/file/vault backends, the keychain-helper Swift source) and the old commands/secrets.ts/secrets-vault.ts/secrets-sync.ts/secrets-migrate.ts/secrets-import.ts/secrets-rotate-passphrase.ts command files are deleted; every consumer (account-registry.ts, auth-mint.ts, claude-account-token.ts, exec.ts, sync-umbrella.ts, crabbox/cli.ts, fleet/apply.ts, teams/agents.ts, devices/doctor-findings.ts, session/sync/config.ts, commands/{daemon,lease,profiles,sync,exec,repo}.ts, and more) now resolves through secrets-client.ts exclusively. Shared wire types moved to a new pure secrets-types.ts; agents-owned reserved-store constants moved to a new leaf reserved-stores.ts (kept deliberately dependency-free to avoid a circular import with secrets-policy.ts's fleet-sync helpers, which absorbed reserved-sync.ts and gained resource-profile scoping — resolveSecretsContextForRun/resolveAllowedBundlesForActiveProfile, wired into agents run --secrets per CTX-1). The daemon no longer hosts or takes over the secrets broker (OWN-1): secrets-broker-service.ts and keychain-reap-service.ts are deleted, secrets-broker/keychain-reap are gone from DaemonServiceId, and agents daemon status/services now only probes the broker's reachability (socketPath/health always null). The __vault-age-helper/__secrets-get/__secrets-ping/__secrets-lock intercepts are gone from index.ts (now a zero-static-import slim shell), and bootstrap.ts no longer reinstalls a keychain helper on upgrade. Release tooling that built/signed/verified the keychain helper (scripts/build-keychain-helper.sh, scripts/verify-keychain-helper.sh, scripts/keychain-entitlements.plist) is deleted, and release-attestation-produce.sh/release.sh/release-manifest.sh/remote-sign-mac.sh/signing-home-base-probe.sh no longer reference it — a release now signs only the menu-bar helper and the standalone CLI binary. age-encryption and the keychain-helper build deps are dropped from package.json. A new scripts/packed-tarball.test.ts proves the packed tarball ships no dist/lib/secrets/, no keychain-helper artifact, and no bundled .app of any kind — DIST-1, executed against a real npm pack + tar tzf, not grepped. Source: cli/src/lib/{secrets-client,secrets-types,secrets-policy,reserved-stores}.ts, cli/src/index.ts, cli/src/bootstrap.ts, cli/src/lib/daemon/daemon.ts, cli/scripts/packed-tarball.test.ts.

  • Workflows sync through one per-harness registry (PHNX-3993). How each workflows-capable harness stores a synced workflow (Claude bundle dir, Kimi flow skill, Antigravity global_workflows file, Goose recipe, OpenClaw Lobster, Grok Rhai) is declared once in WORKFLOW_TARGETS; sync, list, remove, the agents doctor drift check, and the staleness detector are generic over it, where they previously each branched on the harness id and the detector kept its own copy of the ownership-marker parsers. Paths, markers, and error text are unchanged; a foreign Kimi <name>/ skill dir now blocks a sync the same way a foreign file blocks the other harnesses instead of being written into. A completeness test pins the table to capableAgents('workflows'). Source: cli/src/lib/workflows-registry.ts.

1.22.84

  • agents feed watch is cheap enough to leave running (PHNX-3939). The watcher cost ~42% of a core steadily on a box with 1,437 activity logs / 64 MB, because every 500 ms tick re-read and re-parsed the whole activity corpus and re-read every row's block, resolution, and PR status. It now keeps a per-file cursor and reads only the bytes appended since the last tick (ActivityStream), and reconciles attention only when something announced a change — a block or resolution written under the feed dir, watched directly — or when the 45 s PR-status TTL has expired. Measured on the same box over 5 minutes, steady state: --local 41.9% → 0.31% CPU (RSS 313 MB → 140 MB) and the full 13-peer fleet fan-out 50.0% → 0.37% CPU (RSS 606 MB → 143 MB), with an identical envelope stream. Source: cli/src/lib/feed/activity-stream.ts, cli/src/lib/feed/watch.ts.

  • A fleet peer that cannot be reached no longer gets a fresh ssh every few seconds (PHNX-3939). agents feed watch and agents sessions watch respawned each peer's --local subscription on a bare 2 s timer with the child's stderr discarded, so an offline box burned a ConnectTimeout-plus-2s cycle for the whole life of the watcher and reported only ssh exited 255. Both fan-outs now share one implementation with per-peer exponential backoff (2 s doubling to a 60 s cap, reset on a healthy protocol event), the peer's own stderr surfaced as the unavailable reason, and a peer parked after three consecutive failed spawns until the device registry changes or the capped delay elapses. Source: cli/src/lib/session/remote/peer-stream.ts.

  • agents accounts connect refuses on a worker before any install or browser login (PHNX-3940). A worker never runs an interactive OAuth flow (credential-management.md invariant 7). On a non-headed device (worker or unmarked), connect throws and does nothing — no slot, no install, no browser. Add the account on a personal/desktop box; workers are provisioned from the durable credential. Source: cli/src/lib/accounts/connect.ts.

  • Every session row carries the request the agent was given and a timeline of what it did (PHNX-3939). agents sessions --active --json, sessions watch --json, feed watch --json, history rows and the fleet session mirror gain three optional fields: request (the LATEST genuine user turn, tidied — the user's own sentence, with screenshot/clip paths, @dir mentions and pasted terminal echo pulled out into attachments / pastedLines, never rewritten), timeline (narration-anchored steps — one per line the agent said, with the tool calls under it folded into per-verb counts, failures, blocked calls and milestones; the last 8 in full plus a counter for everything older), and files (the paths the session created / modified / deleted, from the harness's own ledger where it keeps one). The fold is deterministic and needs no model; the optional summarizer now takes the narration as input and stays off by default. Computed by the daemon inside its existing reader-gated 15s tick, reading only the bytes each transcript grew by, and cached in the new stamp-validated session_timelines table — zero cost on the request path. Source: cli/src/lib/session/timeline.ts, cli/src/lib/session/timeline-pass.ts, cli/src/lib/session/db.ts.

  • A /model echo or a skill body can no longer become a session's title (PHNX-3939). extractSessionTopic consulted a shorter skip list than cleanFirstUserMessage, so harness scaffolding — <local-command-stdout>, Base directory for this skill:, hook feedback, <hook_result>, <notification> — became a session's topic and then its displayed name. The two lists are now one, and the row title / userPromptClean are derived from the raw latest genuine user turn (with the row's attachments in hand) instead of the already-collapsed topic. Source: cli/src/lib/session/prompt.ts, cli/src/lib/session/active.ts.

  • agents sessions trace <id> --steps prints the step list the sidebar shows. The same fold as the session row, as text: NN +MMs source headline · 6 run · 2 blocked · worktree created, with a totals footer. The CLI door to the timeline for an agent working without the extension. Source: cli/src/commands/sessions-trace.ts.

  • The parser keeps the per-tool label every harness writes (PHNX-3939). Claude's Bash input.description, Kimi's tool.call.description, OpenCode's state.input.description / state.title and Codex's parsed command now ride the normalized SessionEvent as label, so a live row's now-line reads as the sentence the model wrote for it instead of a re-derived command summary. Codex additionally gains a reader for its typed item_completed stream (AgentMessage phase, parsed_cmd, FileChange, Extension, SubAgentActivity, ImageView, ContextCompaction) — kept separate from parseCodexContent because the two describe the same turn, and an unknown item type folds to a counted call rather than throwing. Claude permission/hook denials are now distinguished from command failures (blocked vs failed). Source: cli/src/lib/session/parse.ts.

  • sessions watch history rows cover every transcript-writing harness. readPreviousSessionsForWatch queried only claude/codex/muse/opencode, so a Kimi, Grok, Cursor or Droid session never appeared as a Previous row. Source: cli/src/lib/session/remote/watch.ts.

  • The timeline redacts and de-escapes the transcript text it ships. projectTimeline is the single place folded text leaves the fold, so step headlines, the now-line and milestone marks now run through redactSecrets (on by default; sessions trace --no-redact is the only opt-out) and always through sanitizeForTerminal. Without it a Bash call with no description fell back to raw command text, so a pasted token rode the session row into the git-tracked ~/.agents/devices/<device>/daemon-state.json, and a transcript carrying ESC sequences could clear the operator's screen on sessions trace --steps. --steps now honours the command's own --redact default, and sanitizeEvent covers the new label and phase fields. Source: cli/src/lib/session/timeline.ts, cli/src/lib/session/parse.ts, cli/src/commands/sessions-trace.ts.

  • A 4-16 MiB transcript on a non-resumable harness gets a timeline. Whole-file eligibility was gated on the per-session allowance, capped at 4 MiB however idle the tick was, while unavailable only fired above 16 MiB — so a kimi/grok/gemini/cursor/opencode/droid session in that band got no row at all (not ready, not partial, not unavailable) and was counted reused, leaving the daemon log reading healthy. It is now gated on what the tick can actually afford, the byte budget is debited by the bytes each branch really reads (the whole file for a whole-file re-parse, not its growth delta), and a session that genuinely does not fit writes a partial row naming the budget. A per-session fold that throws is logged and counted skipped instead of swallowed. Source: cli/src/lib/session/timeline-pass.ts.

  • Cursor and Droid keep their per-call label too. Both write the same human one-liner through the shared Anthropic-message parser — Cursor as description, Droid as summary — and neither reached SessionEvent.label, so a Droid Execute{command:"ls -la", summary:"List files"} rendered its now-line as ls -la while the equivalent Claude call read List files. Source: cli/src/lib/session/parse.ts.

  • A tool cut short with Ctrl-C counts as blocked, not failed. toolUseResult.interrupted joins toolDenialKind as an input to blocked, so interrupting an agent no longer inflates its failure count. Source: cli/src/lib/session/parse.ts.

  • now ships only on the live step, and ready is never an empty step list. attachTool sets the now-line on every step as it folds; every finished step kept a stale one, so a consumer rendered a now-line on completed work at ~700 bytes a row. A fold that produced no step now reports its state honestly instead of ready with nothing in it. The fleet mirror also carries mix and live through the consume path, which rebuilt each step from a field list that dropped both while keeping now. Source: cli/src/lib/session/timeline.ts, cli/src/lib/session/mirror.ts.

1.22.83

  • The Claude status line shows the registered account NAME (PHNX-3987). A login named with agents accounts (work, dev, …) now renders that name between the host and the model — yosemite · work · Fable 5.1 · … — which is what tells several same-harness logins apart at a glance; an unnamed login keeps the email label. The lookup is the one agents view and the device inventories already use, lifted into findNativeAccountByIdentity so the three copies share it. Source: cli/src/lib/claude-statusline.ts, cli/src/lib/account-registry.ts.

  • agents accounts rename / remove / view honor per-harness account names (PHNX-3988). Native labels are unique per harness (PHNX-3887), but rename/remove still ran a fleet-wide uniqueness check and resolved a bare name to whichever harness row the store ordered first — so renaming Codex's cxicloud to icloud failed because Claude already owned icloud. Rename now scopes uniqueness to the target row's harness, so one harness can take a name another already uses; a duplicate within the same harness is still refused. Rename, remove, and view accept <harness>#<name> and refuse an ambiguous bare name rather than guessing. Source: cli/src/lib/account-registry.ts, cli/src/commands/accounts.ts.

  • Feed activity polling reuses unchanged log tails (PHNX-3939). The activity reader caches file-version-validated tails and timestamp summaries within a bounded process-local budget. Cursor polls skip unchanged historical events without parsing them again; appends, rewrites, replacements and tail-budget changes invalidate the cached snapshot. Returned events remain independently mutable, with timestamp, filter and limit behavior preserved.

1.22.82

  • The Claude status line names the signed-in account before the model (PHNX-3987). The managed agents __claude-statusline render now reads host · account · model · 5h n% · 7d n% · ◆ reminder, where the account is the email the running Claude is signed into (plus the org name for a Team/Enterprise seat, the same label agents view renders), read from the .claude.json of the home Claude is actually running with — the version home under the launch shim, $HOME otherwise. A never-signed-in home omits the part. Source: cli/src/lib/claude-statusline.ts.

1.22.81

  • Same-task page reopen refreshes the SAME tab instead of duplicating it (PHNX-2399). When an agent reopens a URL that is already live in one of its browser task's OWN tabs, agents browser navigate / tab add now issue a real Page.reload on that same tab — keeping its tab id and CDP target, marking it current without stealing window focus — and report refreshed: true with the note Tab already open—refreshed, rather than opening a second copy. Borrowed Arc/user tabs are excluded, a stale registered tab or a failed reload never reports a phantom refresh, and the lookup→reload/create→persist section is serialized per task (and first-use creation per caller) so concurrent requests converge on one tab and the same id. The implicit first-open at the create boundary is reported as a genuine open, not disguised as a refresh. navigate / tab add gain --json ({ ok, task, tabId, url, created, refreshed, message }), and agents browser tabs again marks the current tab. Source: cli/src/lib/browser/service.ts, cli/src/lib/browser/ipc.ts, cli/src/lib/browser/types.ts, cli/src/commands/browser.ts.

1.22.80

  • agents prune cleanup collapses duplicate account homes to one per account — and keeps the RIGHT one (PHNX-3940). After the automatic-update pass moves every home to the latest release in place, an account can be left with two homes: the original identity-captured login and a freshly re-logged-in home that only carries an email ("usage unavailable"). Consolidation now groups those together and keeps the home that best represents the account — captured identity first, then a signed-in credential, then the newest running release — instead of the blind highest dir-name semver, which was the just-created duplicate and whose selection trashed the working login. When the retired duplicate held the global default, the default is repointed onto the keeper first, so the duplicate actually collapses instead of being skipped. Retirement stays a reversible soft-delete (agents restore). Source: cli/src/commands/view.ts (planDuplicatePrune).

1.22.79

  • Managed harnesses follow the latest release without replacing account homes (PHNX-3940). Automatic updates are on by default and configurable with updates.auto and per-harness updates.<harness>.auto. Updates preserve existing home paths and identities, defer active installations, and stage and verify the executable before committing it. A concrete --to release pins an installation; --to latest resumes automatic updates. agents update --check previews without changing local state. Source: cli/src/lib/installations/update.ts, cli/src/lib/daemon/harness-update-service.ts, cli/src/commands/update.ts.
  • agents accounts connect <harness> [name] — stable accounts independent of releases (PHNX-3940). A new account gets a fresh opaque installation label with its own isolated home, installs the current release into it (even when that release is already installed under another label), and drives the harness's native OAuth login there — the credential is never copied or fleet-synced. Reconnecting an existing account by name reuses that account's home and refuses to bind a different identity after login. Supported for the version-scoped, isolable harnesses (claude, codex first); others refuse with a clear reason instead of faking a flow. Source: cli/src/lib/accounts/connect.ts, cli/src/commands/accounts.ts.
  • agents accounts logout honors @label / #account selectors (PHNX-3940). agents accounts logout claude@<label>, claude#<account>, or a bare native account name now signs out that exact home instead of always selecting the global default, and the sign-out runs under the harness's canonical config-dir env (CLAUDE_CONFIG_DIR / CODEX_HOME) rather than HOME alone. Source: cli/src/commands/accounts.ts.
  • Account-first read model for agents accounts / view (PHNX-3940). loadAccountCatalog() / buildNativeCatalog() group installed homes by identity and derive a live connection state — a registered account whose homes are all signed out reads reconnect-needed, never a bare connected on the registry label alone — with the release/home kept as secondary diagnostics. Source: cli/src/lib/account-catalog.ts.
  • installVersion accepts an installationLabel independent of the release (PHNX-3940). The npm install path can now stage the current release under an opaque label distinct from the version string, so many accounts can share one upstream latest under separate isolated homes; the frozen installation record keeps label and releaseVersion separate. Source: cli/src/lib/installations/versions.ts.
  • agents accounts connect allocates account homes safely — no cross-account overwrite (PHNX-3940 security fix). Connect slots are now RANDOM opaque labels allocated to be disjoint from every home already owned by an account or currently signed in, so connect work after work was renamed personal can never land on and overwrite personal's login. Retry-idempotency is provided by a device-scoped pending-connect map (cleared on success), not a name-derived slot. Connection state is now strict — a live credential in the home, not a bare metadata identity, so a stale/expired login reads reconnect-needed. A first connect selects the harness default only when none is configured, and an unnamed connect forces no name (it returns a hint to name the login). Source: cli/src/lib/accounts/connect.ts, cli/src/lib/account-registry.ts, cli/src/lib/account-catalog.ts, cli/src/lib/state.ts.

1.22.78

  • The launch env carries the human's Phoenix ID profile picture. A resolved human actor now gets AGENTS_ACTOR_AVATAR (https URL) when the Phoenix ID session signed in on the device belongs to the same email, so tools an agent runs — artifacts render, shares — can show the person instead of initials. A shared box signed in as someone else lends nothing to whoever SSH-ed in, and a child spawn inherits the value like the rest of AGENTS_ACTOR*. agents auth whoami now also refreshes the persisted picture from /api/v1/auth/me, which Phoenix ID returns as of phnx-labs/phoenix-id#7. Source: cli/src/lib/actor.ts, cli/src/commands/auth.ts.

1.22.77

  • agents repo pull / agents sync no longer wedge on a CLI-regenerated ~/.agents/agents.yaml (PHNX-3968). The shared central config was rewritten by config commands without being committed, so between the daemon's publish ticks the tree sat dirty on agents.yaml; an incoming peer commit that also touched it made the pull refuse with "incoming changes touch uncommitted paths: agents.yaml", forcing manual autostash gymnastics on the fleet. Central config edits are now committed synchronously by the command that makes them (after the meta lock releases, skipped for the daemon whose own publish tick owns its commits), so the tree is clean at rest and the trip cannot occur. A genuinely dirty-and-differing central file now refuses the pull (data-safe, self-heals on the next commit) rather than silently taking the remote copy. The user-repo CHANGELOG.md (a duplicate of the shipped one) is de-tracked via .git/info/exclude so it stops churning the tree. Source: cli/src/lib/state.ts, cli/src/lib/git.ts, cli/src/lib/browser/registry.ts, cli/src/lib/project-resources.ts.

1.22.76

  • Per-session summarizer: bound the mirror publish (PHNX-3939 follow-up). The fleet session mirror now caps checkpoints/summaryChecklist on the publish side (50/100 items, 400-char text) exactly as it already did on consume, so a box's own summarizer output can never ride unbounded into the git-synced daemon-state.json every peer fetches — matching the snippet() cap already applied to goal. Source: cli/src/lib/session/mirror.ts.

  • Per-session summarizer on the session stream (PHNX-3939). A new opt-in daemon service computes a 1–2 line goal, progress checkpoints, and a detailed checklist for each running session and delivers them on agents sessions watch / agents feed watch --json (as goal / checkpoints / summaryChecklist / summaryState), so the AGI extension sidebar can render them. It is off by default and never on the request path: the model call runs only in the background session-summarizer service, reader-gated and bounded per tick, and a computed summary is cached in the transcript-keyed session_summaries table so it never recomputes on unchanged bytes and agents sessions <id> timing is unchanged. Enable it with agents config set summarizer.enabled on plus a local Anthropic-wire endpoint — agents config set summarizer.baseUrl http://localhost:11434 and agents config set summarizer.model qwen2.5:3b (env overrides: AGENTS_SUMMARIZER_ENABLED / AGENTS_SUMMARIZER_BASEURL / AGENTS_SUMMARIZER_MODEL). Summaries ride the fleet session mirror, so a peer's sessions carry them too. When disabled or unconfigured, zero model calls are made and rows read summaryState: "skipped". Source: cli/src/lib/summarizer/, cli/src/lib/daemon/session-summarizer-service.ts, cli/src/lib/session/db.ts.

  • agents config unset project.root now actually clears it. projectRoot is a machine-local key persisted to the device doc, which writeMeta only clears when the key is present-but-falsy; the unset path dropped the key entirely, so agents config get project.root still returned the old value afterward. Source: cli/src/commands/config.ts.


type: fixed

Keep fleet account and device routing consistent: poolless teams now use the active worker pool, remote readiness rejects stale account evidence, removed devices lose cached stats, partial device writes preserve unrelated metadata, paths are passed without shell reparsing, and harness forks can translate across native hosts.

  • Managed share: collision recovery, race-safe republish, and OAuth avatars (PHNX-3942, folding PHNX-3547 + PHNX-3579). Three correctness gaps on the artifact-sharing path are closed. (1) A same-local-part account move no longer dead-ends on the permanent 409: the Worker's __handles/<handle> claim now records the verified email, and a request whose verified email matches the claim but whose userId differs (the same human re-authenticated under a new account id) transfers the claim and re-stamps the old account's objects instead of refusing; when the handle genuinely belongs to someone else, agents artifacts share --handle <name> publishes under an alternate first-writer-claimed namespace (new x-share-handle Worker header). (2) A concurrent republish no longer silently discards a body: the Worker's read → archive-revision → overwrite sequence is now a bounded compare-and-swap loop (onlyIf.etagMatches, the primitive PATCH and the usage ledger already used) — a conflicting write 409s the put, the loser re-reads, archives the winner's body as a revision, and lands its own as canonical, so every accepted body survives as canonical or revision. (3) Share attribution prefers the hosted OAuth profile image: the Phoenix session now carries avatarUrl (captured at login and refreshed from /api/v1/auth/me), and publish stamps it before falling back to the email Gravatar. Also, publish failures stop appending 'Check the write token' to quota/rate rejections (413/429) — the Worker's actual reason is surfaced, and only 401/403 mention the token (or agents auth login on the managed endpoint). Source: cli/src/lib/share/worker-template.ts, cli/src/lib/share/publish.ts, cli/src/lib/identity/client.ts, cli/src/lib/identity/index.ts, cli/src/commands/auth.ts, cli/src/commands/share.ts.

  • Release recovery and exclusion now hold under rebases, interruption, and concurrent releasers (PHNX-3945). A retry validates the merged target version and the recorded/fetched release PR identity, then tags and promotes the exact attested PR head even when the merge tree differs. Attestation fallback is capped at 45 seconds total (15-second optional fetch plus a 30-second poll), content-addressed records publish through a same-directory atomic rename, and release-lease acquisition/reclamation uses one expected-old-ref compare-and-swap instead of overwritable custom-ref pushes or a delete/create gap.


type: changed

  • agents watchdog uses the standard enable/disable verbs (PHNX-3949). The device-local daemon watchdog pass is now toggled with agents watchdog enable / agents watchdog disable, matching the enable/disable shape menubar and daemon already use for the same concept. The old on / off spellings keep working as aliases, so no existing invocation or script breaks. Source: cli/src/commands/watchdog.ts.

type: changed

  • CLI verb vocabulary unified behind one canonical alias table (PHNX-3949). A new cli/src/lib/verbs.ts defines the standard short-form aliases (list→ls, remove→rm, view→show, rename→mv) and a withAliases helper, applied uniformly across skills, hooks, commands, rules, permissions, mcp, subagents, workflows, and memory so every resource group answers to the same shortcuts. No primary command name changes. Source: cli/src/lib/verbs.ts.

  • agents route now uses the standard verbs and pluralizes its noun (PHNX-3949). The router command group matches the shape harness already set: read-one is route view <name> (old show kept as an alias), create is route add <name> (old create kept as an alias), list gains the ls short alias, and the whole tree resolves under the plural noun agents routes .... New route rename <old> <new> re-keys a user-layer router in place, preserving every field (harness allowlists, weights, linked accounts, hijack flag); it fails loud on a missing source, a name collision in any layer, or a router that resolves from a non-user layer. Source: cli/src/commands/route.ts, cli/src/lib/routers.ts.

  • plugins moves to the canonical view/add verbs, and monitors gains an rm alias (PHNX-3949). agents plugins info is now agents plugins view and agents plugins install is now agents plugins add; the old names keep working as aliases, so no existing invocation breaks. plugins list gains an ls alias and plugins remove gains rm. agents monitors remove <name> gains the rm alias. Source: cli/src/commands/plugins.ts, cli/src/commands/monitors.ts.

  • agents open is now the machine-only _callback verb; handler setup moved under agents setup url-scheme (PHNX-3949). agents open <url> was never a user command — it is the OS callback the agents:// deep-link handler invokes when you click a session link in a rendered artifact. It is now hidden as the machine-only agents _callback verb (humans resume with agents sessions resume), and the register / unregister / status plumbing moved to a visible agents setup url-scheme group. Nothing breaks: agents open <url> stays a hidden alias so OS handlers written by older CLIs keep resolving until agents setup url-scheme register re-writes them to the new verb, and agents open register|unregister|status still work as hidden back-compat subcommands. Source: cli/src/commands/open.ts, cli/src/commands/setup.ts, cli/src/lib/deeplink/register.ts.

  • One canonical Comet: attach-only browser profiles + closed port-squat (PHNX-3967). A browser profile now carries a launchPolicy (attach-only | launch) and a durable --user-data-dir. An attach-only profile (agents browser profiles create <name> --browser comet --attach-only, and Arc always) NEVER spawns a rival window — agents attach to the browser you already started with remote debugging, else fail loud with the exact open -a <app> --args --remote-debugging-port=<port> --user-data-dir=<durable> relaunch. Before adopting an endpoint the local driver now verifies ownership beyond browser family: it compares the running instance's --user-data-dir to the profile's durable dir and rejects a foreign port-squatter (e.g. a logged-out /tmp Comet on the canonical port) instead of driving it — and if an occupant is serving CDP but its --user-data-dir can't be read, the guard refuses to attach to the unverified instance (fail loud) rather than driving it silently, with profiles doctor surfacing the same unverified state instead of a green check. The durable dir lives under ~/.agents/.history/browser-profiles/<name>/ (outside .cache), so a one-time sign-in survives quit+relaunch and profiles remove. agents browser profiles doctor flags a foreign-user-data-dir Comet on the canonical port. Source: apps/cli/src/lib/browser/drivers/local.ts, apps/cli/src/lib/browser/{profiles,chrome,types}.ts, apps/cli/src/commands/browser.ts.

  • agents doctor is now diagnose-only; agents sync is the single fixer. doctor --fix is removed — running it prints a one-line redirect to agents sync and exits non-zero — and every doctor remediation now points at agents sync. sync gains the repairs doctor --fix used to own: managed-hook-runtime-shim repair (the "source mismatch" fixer), hook re-wiring, and full home-rot heal, running on every sync path — the umbrella, agent-scoped, single-version, and the agents sync status / menu-bar drift path — so one agents sync now converges a version that doctor flags, instead of two commands that could disagree. Repair failures surface in --json output and the process exit code. The machine-wide stale-CLI purge no longer runs on a routine sync; it is now the explicit agents sync --prune-clis flag. Source: cli/src/lib/reconcile-and-repair.ts, cli/src/commands/sync.ts, cli/src/commands/doctor.ts, cli/src/lib/drift-sync.ts.

1.22.75

  • Account labels are per-harness, and survive a daemon publish (PHNX-3887). One identity signed into several harnesses can now carry the SAME label on each — claude#icloud, codex#icloud, and grok#icloud all resolve to that harness's own login, instead of the first-labelled harness squatting the name and forcing prefixes (cxicloud, gkicloud) on the rest. Uniqueness is scoped to (agent, label); provider accounts, selected by bare name via --account, stay globally unique, and a duplicate label within one harness is still refused. Separately, agents accounts label wrote version-scoped rows into the central agents.yaml as a plain file write with no commit, so the daemon's publish <device> daemon state tick — which committed only the per-device doc and then rebased --autostash over the dirty central file — silently destroyed them: every box lost its account labels on its next publish. The publish now commits agents.yaml alongside the device doc, so labels persist and actually sync fleet-wide as accounts label --help already promised. Source: cli/src/lib/account-registry.ts, cli/src/lib/fleet-shared-repo-sync.ts.

  • Keep one daemon alive across concurrent stop/start (PHNX-3941). Prevent a replacement daemon from exiting while stop holds the lifecycle lock, so concurrent stop/start cannot leave zero instances, and extend real-boot supervisor-wiring coverage to all 20 services. Source: cli/src/lib/daemon/daemon.ts.

  • agents computer can now drive a Linux GUI desktop over VNC/RFB (PHNX-3948). A third computer backend joins the macOS (Accessibility) and Windows (UIA) native helpers — this one speaks the RFB/VNC protocol directly to an x11vnc/Xvnc server, so a headless Linux desktop (or an LXD container exposing x11vnc on the host's Tailscale IP) becomes drivable with no per-box native helper install. Select it with agents computer --vnc <host:port> (port defaults to 5901; password via --vnc-password or COMPUTER_HELPER_VNC_PASSWORD), then use the same verbs as the other backends: screenshot, click, right-click, type, type-text, key, scroll, wait. It is coordinate-based — RFB carries a framebuffer and pointer/key events, not an accessibility tree — so the AX-tree verbs (describe, get-text, ax-action, element-id targeting) and launch fail loud with a clear message rather than silently no-op'ing. VNC authentication uses a bundled pure-JS DES (Node 24's OpenSSL 3 keeps DES in the off-by-default legacy provider). Source: cli/src/lib/computer/rfb-client.ts, cli/src/lib/computer/des.ts, cli/src/lib/computer/computer-rpc.ts, cli/src/commands/computer.ts.

1.22.74

  • Launching an agent no longer dirties your tracked .gitignore (PHNX-3718). The per-harness resource sync self-manages ignore rules for the generated .claude/, .cursor/, .codex/, … dirs it writes on every launch. Those rules used to be written into the tracked <project>/.gitignore, but they are never committed upstream — so every launch left the repo with a permanent M .gitignore (one managed block per harness that had run there) that in turn blocked git pull with "local changes to .gitignore would be overwritten by merge". The managed block now lives in .git/info/exclude (git's per-clone, uncommitted ignore file, resolved via git rev-parse --git-path info/exclude so it works from a subdir, linked worktree, or submodule), keeping the generated dirs hidden while leaving the working tree clean. On the next launch, a repo already dirtied by the old behavior self-heals: the leftover managed block is stripped from the tracked .gitignore (hand-written rules untouched; an untracked file that held only our block is removed). Source: cli/src/lib/project-resources.ts.

  • sessions preview of a remote session no longer dead-ends on the dispatcher (PHNX-3890). A box that launched a session running on another device kept a transcript-less live row for it whose machine defaulted to itself, so agents sessions preview <full-uuid> — and the interactive browser's preview pane — rendered Live session — full transcript not indexed here. instead of fetching the owning peer's digest. A passive peer rendered the same session fine. The launcher shim is now reconciled against the fleet's own view of which device runs the session, and a full-UUID hit only skips the fleet fan-out when this box can actually answer for it (a transcript on disk, or a row that already names another device). Remote previews fill in asynchronously over SSH; a locally readable transcript or synced mirror still renders with no hop, and an unreachable owner still fails loud. Source: cli/src/commands/sessions.ts, cli/src/lib/session/live-metadata.ts.

1.22.73

  • Ctrl-C works again while agents sessions is reaching other machines (PHNX-3791). The static session list, Searching the fleet…, and the cloud session/fetch paths wrap their long SSH/network waits in ora(...). ora defaults to discardStdin: true, which on a TTY flips stdin into raw mode for the spinner's lifetime (via stdin-discarder) — and raw mode disables the terminal's own ISIG, so a Ctrl-C keystroke no longer raises SIGINT. ora's compensating "re-emit SIGINT when I read a 0x03 byte" only fires when stdin is in flowing mode, but a fresh process.stdin has flowing === null, isPaused() returns false, the discarder skips its resume(), and prependListener('data') doesn't auto-resume — so the byte is never read and SIGINT is never raised. The global handler in index.ts (process.exit(130)) was correct; it simply never fired. The user-visible result: a sweep that stalls on offline/unreachable boxes (each burning a ~12s SSH ConnectTimeout) could not be aborted — measured, agents sessions --flat had to be SIGKILLed. New lib/spinner.ts#interruptibleSpinner is a drop-in for ora(text) built with discardStdin: false: the terminal stays in cooked mode, Ctrl-C raises SIGINT normally (and the terminal SIGINTs the whole foreground group, reaping the outstanding ssh children), and the process exits promptly — only cosmetic stray-keystroke discarding during the spin is lost. The five cross-machine/network spinners in sessions.ts route through it; short local spinners are unchanged. Source: cli/src/lib/spinner.ts, cli/src/lib/spinner.test.ts, cli/src/commands/sessions.ts.

  • Remote-host session previews render inline, no per-row SSH (PHNX-3792). On the interactive device, a session that originated on another box used to show as a bare [host/<peer>] row with no topic, and its preview pane fetched the peer's digest live over SSH — slow, and blank when the peer was asleep. Each box now mirrors its own sessions' lightweight preview/metadata (topic, first user message, last activity, agent+version, cwd, ticket, PR) into its conflict-free devices/<device>/daemon-state.json, which the daemon's existing bounded Git transport already delivers fleet-wide. The picker, agents sessions list, and focus read the local mirror, so a peer session shows its real topic/preview inline — even while the peer is offline (last-synced). The live SSH digest fetch remains the fallback for a never-synced session, and space still reads the full transcript live. No transcript leaves the worker; the mirror is bounded and pruned by age. Source: src/lib/session/mirror.ts, src/lib/fleet-shared-state.ts, src/lib/session/db.ts, src/lib/daemon/usage-sync-service.ts, src/commands/sessions-picker.ts.

  • Ctrl-C works during agents repo git operations too (PHNX-3793). Follow-up to PHNX-3791. cli/src/commands/repo.ts wrapped its seven git network ops — clone, git-backing, pull, push, adopt, sync — in bare ora(...) spinners, whose default discardStdin: true raw-modes the TTY and swallows Ctrl-C for the whole operation (same trap PHNX-3791 fixed for the session sweep). A git clone/pull stalled on a slow or dead remote could hang longer than a fleet sweep with no way to abort. All seven now use lib/spinner.ts#interruptibleSpinner (discardStdin: false), so Ctrl-C raises SIGINT and the process exits promptly. Source: cli/src/commands/repo.ts.

  • agents sessions --json now carries the initiator's Phoenix ID (PHNX-3798). The actor resolver already mapped a run's tailnet identity to a phoenixId (the stable internal work identity) and rode it through the exec env, but it was never persisted per session, so a durable listing couldn't attribute a session to a person's work identity. The phoenixId now flows the same path the actor/initiatedBy fields already take — stamped into the durable actor sidecar at launch, joined into a new write-once phoenix_id column via a v47 schema migration, and emitted by the sessions feed. Best-effort throughout: a session with no mapped Phoenix ID (the common case) simply omits the field rather than emitting a null/empty sentinel. This is the persistence half of the ownership work whose resolver landed in PHNX-3798's first PR. Source: src/lib/session/actor-sidecar.ts, src/lib/session/db.ts, src/lib/session/types.ts, src/lib/exec.ts.

  • agents packages materialize materializes a schema-v3 agent.yaml package into an ephemeral Claude, Codex, or OpenCode home (PHNX-3838). The noun-first surface takes --harness, --harness-version (root --version is the CLI version flag), and --output-home. It is a thin front door over the canonical native-home materializer (agent-spec/materialize.ts): it resolves the package once and projects every declared resource (instructions, skills, subagents, mcp, hooks) into the output home, then prints the materialization receipt (agent ref + digest, harness, per-resource targets, warnings); --json is the Factory/Prix Cloud handoff and is byte-identical to the materialization-receipt.json written into the home. It writes only under --output-home: it never mutates the live user home, never copies secrets, and never execs a harness. A non-portable harness, a non-exact --harness-version, a missing/invalid package, and an output-path escape (including the live ~/.claude/~/.codex/~/.opencode homes) all fail loud. Source: cli/src/commands/packages-materialize.ts, cli/src/lib/packages/output-home.ts, cli/src/lib/agent-spec/materialize.ts.

  • agents packages materialize is hardened against four path-traversal / symlink escapes (PHNX-3838). (1) materializeAgentPackage now realpath-verifies every per-resource write/delete target against the canonical output home before touching disk, and --output-home additionally refuses the live home ROOT and canonicalizes symlinked ancestors. So neither --output-home "$HOME" (the materializer appends .claude/.codex/.opencode), a symlink aliasing $HOME, nor a symlink planted at the harness-config-dir join point inside the output home can write into the live harness home — and a direct (Factory / Prix Cloud) caller that skips the CLI front door is protected too. (2) The stale-prune step strictly validates the unsigned materialization-receipt.json schema and realpath-checks every resources[].target as a contained path at deletion time, so a planted receipt can no longer make it rm -rf a ../victim, an absolute path, or a path under a symlinked ancestor outside the output home. (3) A package hook.name must be a safe single path segment, re-checked for containment before the script is copied and chmod +x'd. (4) Package resources reject symlinked file/directory/hook sources and realpath-check every source under the package's real root, so instructions.md -> /etc/passwd or a skill dir symlinked outside the package can no longer be read or copied. Source: cli/src/lib/packages/output-home.ts, cli/src/lib/agent-spec/materialize.ts, cli/src/lib/agent-spec/package-resolve.ts, cli/src/lib/paths.ts.

  • agents packages materialize closes two more output-home boundary gaps (PHNX-3838). (1) When any protected ~/.claude/~/.codex/~/.opencode home is a DANGLING symlink or dangling chain (following relative links and chains), resolveOutputHome now fails closed and refuses EVERY output home until the link is repaired — because the materializer's first mkdir -p would otherwise follow the link and re-create the operator's live harness home at the materialized tree, and the absent target it points at has no canonical spelling to compare a specific candidate against. (2) Materializing a package that carries hooks no longer runs the normal orphan-shim sweep against the ONE process-global shim dir (~/.agents/.cache/shims/hooks/): that sweep deletes every shim absent from the manifest it is handed, so with a package's hooks-only manifest it would wipe the operator's unrelated hook shims. registerHooksToSettings gained an isolated skipGlobalShimSweep option that the materializer sets; normal install/sync keeps the sweep. Source: cli/src/lib/packages/output-home.ts, cli/src/lib/hooks/install.ts, cli/src/lib/agent-spec/materialize.ts.

  • agents packages materialize fails closed on a dangling protected home instead of approximating filesystem collation (PHNX-3838). An earlier iteration compared the live-home identity case- and Unicode-normalization-insensitively (NFC + toLowerCase) on macOS/Windows to catch a spelling-equivalent alias of a DANGLING ~/.claude (e.g. an absent .../Real-Claude vs --output-home .../real-claude). An independent macOS review proved that approximation wrong: APFS treats e.g. U+017F ſ and ASCII s as identical while JS toLowerCase does not, and an OS-derived case-sensitivity flag is wrong for per-volume semantics. The guard no longer tries to reproduce a volume's collation from the path text — when a protected home dangles it refuses every output home (fail closed, above), and an EXISTING protected home is compared with exact realpath identity, which needs no approximation. The pathIdentityKey/pathIsWithin/FS_CASE_INSENSITIVE helpers added for the approximation are removed. Source: cli/src/lib/packages/output-home.ts, cli/src/lib/paths.ts.

1.22.72

  • The self-managed .gitignore block now ignores the .agents-managed.json manifest too, so a synced harness dir is actually clean in git status (PHNX-3717). The project-resource sync's gitignore reconciliation (shipped in 1.22.71) listed the synced commands/skills but not the manifest marker file the sync also writes into each harness dir — so .factory/ (etc.) still showed as untracked on the strength of that one unignored file, defeating the point. reconcileProjectGitignore now feeds .agents-managed.json through the same anchoring + escape guard, and the block persists (shrunk to just the manifest) while that file exists rather than fully pruning. Covered by a new test that asserts a real git status --porcelain no longer reports the generated dir. Source: cli/src/lib/project-resources.ts, cli/src/lib/project-resources.test.ts.

1.22.71

  • The daemon now keeps itself current instead of running stale code for days (PHNX-3695). The long-running daemon (agents __daemon-run) deliberately opted out of the interactive CLI's auto-update (AGENTS_CLI_DISABLE_AUTO_UPDATE=1 forced on for __daemon-run), so new agents-cli code never reached a running daemon until a human ran agents daemon restart — R5 ("the installed CLI auto-updates") silently did not hold for the one process that runs unattended the longest. A new supervised self-update service checks npm roughly every 75 minutes, installs and byte-verifies a newer version with the same primitives agents upgrade already uses (never a bare npm install -g), best-effort pulls the .system companion repo and reconciles with agents sync --local, then exits so the OS supervisor (launchd KeepAlive / systemd Restart=always) relaunches it onto the new code — clients reconnect on their own, and the scheduler's atomic (routine, scheduledFor) claim dedupes any routine mid-fire across the restart. It fails closed on every step: an install or verify failure leaves the running daemon untouched and retries next tick, and it no-ops on a dev build or a shadowed install. A version-skewed browser-IPC client no longer just prints "run agents daemon restart" — reconcileDaemonVersion now asks the daemon to run this same fail-closed path on demand (request-self-update), sharing one in-flight attempt with the periodic tick so concurrent requests can't race two installs. That on-demand trigger is DECOUPLED from the install: because every version-skewed agents browser <verb> routes through it, the daemon kicks the self-update off in the background and answers "triggered" immediately rather than making the browser verb wait out the whole check→download→install→verify (tens of seconds, worst case ~15 min) — the daemon installs, verifies, and exits on its own, and the browser reconnects. Source: cli/src/lib/daemon/self-update-service.ts, cli/src/lib/browser/ipc.ts.

  • The daemon's event loop is no longer starved by synchronous subprocess spawns on its service ticks (PHNX-3695). Every daemon background service runs on one Node event loop, so a synchronous execFileSync/readFileSync inside a service tick froze that loop for the whole call — and while frozen the supervisor's per-tick deadline timer could not fire and the browser IPC server could not answer, which is the "accept but never reply" wedge the trivial version probe (cli/src/lib/browser/ipc.ts) surfaces. A new async, deadline-bounded, process-group-killable exec helper (cli/src/lib/exec-bounded.ts execFileBounded) replaces the tick-reachable sync spawns. The heartbeat tick is now fully async: it reaps exited routine children through reapExitedRunningJobs (an async twin of the sync monitorRunningJobs, sharing one reconciliation core so the CLI routines list/status builders keep their synchronous, off-loop path) with an execFileBounded-bounded ps identity probe and an async sshExecAsync read of a host:-placed run's remote exit (a synchronous ssh there would freeze the loop on a host timeout), plus the async terminal-routine reaper; the self-heal and state-dir-check service ticks moved their existsSync/readFileSync to fs/promises. The worst halt was the event-log file lock (withFileLock → lockfile.lockSync + Atomics.wait, up to 30s), reached from every ctx.log on every tick as well as the watchdog and routine-reaper emits; a new withFileLockAsync + emitAsync (cli/src/lib/fs-atomic.ts, cli/src/lib/feed/events.ts) acquire it without freezing the loop. The other unconditional every-tick halts are converted the same way: usage-sync/auth-sync publish through updateFleetSharedDeviceStateAsync, keychain-reap's whole-process-table ps uses execFileBounded, watchdog/browser-task-reap read config via async paths, and tmux-reap's /proc scan uses fs/promises. A structural guard test (cli/src/lib/daemon/guard-no-sync-io.test.ts) scans every *-service.ts tick/start body for a synchronous fs/exec/lock call AND pins that each hot tick call site uses its async variant (failing if a fix is reverted to its sync twin). Startup and stop/uninstall lifecycle code (pid/lock, launchctl/systemctl, the ps daemon-identity probe) stays synchronous — it runs in a short-lived CLI process, never on the served loop. Source: cli/src/lib/{exec-bounded,fs-atomic,device-config,fleet-shared-state}.ts, cli/src/lib/feed/events.ts, cli/src/lib/secrets/{reaper,reserved-sync}.ts, cli/src/lib/accounting/usage-sync.ts, cli/src/lib/tmux/{session,orphan-reap}.ts, cli/src/lib/daemon/{runner,daemon,heartbeat-service,watchdog-service,usage-sync-service,auth-sync-service,keychain-reap-service,browser-task-reap-service,self-heal-service,state-dir-check-service}.ts, cli/src/lib/hosts/reconcile.ts, cli/src/lib/routine-process-cleanup.ts, cli/src/lib/daemon/AGENTS.md.

  • The required PR check no longer reinstalls 273 packages on every run. Measured across recent Linux required runs, bun install took 14–22s on every run — whether the impact plan selected 78 test files or 2 — while a release check's actual test execution is 0.27s. The install was a fixed cost every pull request paid, and the largest single item in the required-check budget. tests.yml now restores ~/.bun/install/cache, cli/node_modules, and packages/session-tracker/node_modules from an actions/cache keyed on both cli/bun.lock and packages/session-tracker/bun.lock — keying on the CLI lockfile alone would have left the session-tracker tree outside its own key. The key also carries a node -v/bun -v fingerprint, because cli/package.json trusts a native addon (@homebridge/node-pty-prebuilt-multiarch) whose prebuilt binary is Node-ABI-specific and nothing pins the runner's Node. restore-keys is deliberately omitted: a prefix fallback would let bun install on top of a different lockfile's modules, and --frozen-lockfile only guarantees the tree matches the lock from a clean or exact-match state — correctness beats a warmer cache when an attestation binds the tested tree to the published tarball. A dependency change misses the cache and reinstalls, which is exactly the previous behavior, so a miss can never be slower than not caching. Source: .github/workflows/tests.yml.

  • The CI dependency cache now actually hits. The cache added a change earlier was dead weight in practice: GitHub scopes caches per ref — a run can restore only from its own branch or the default branch — and the required test job runs on pull_request only, so every entry it saved landed under refs/pull/<N>/merge, invisible to every other pull request. Measured after it shipped: deps-cache-hit=0 with packages installed [21.63s] on unrelated PRs, i.e. a permanent cold miss for everyone except a re-run of the same PR. A push-to-main warm-dep-cache job now writes the identical key and paths, so the first PR after any lockfile or toolchain change pays the install once and every later PR restores it. The job is push-only and never joins the required check identity. tests-gate.test.ts pins the two keys equal, because drift between them would be silent — the warm job would populate an entry the required job never asks for, and the cache would look healthy while never hitting. Source: .github/workflows/tests.yml.

  • Managed session backup: harden the storage boundary (PHNX-3726). Follow-up security fixes to the managed sessions Worker landed in the same ticket. The Worker is now Phoenix-only — the static WRITE_TOKEN principal is removed (and no longer provisioned): the zero-knowledge --byo path talks to your own R2 bucket directly and never reaches this Worker, so a token here was only a Phoenix-and-quota bypass. Mandatory encryption is now enforced at the storage boundary, not just client-side: a PUT whose body is not an encrypted bundle is rejected 422, and a managed restore fails loud on any plaintext object instead of importing it. DELETE now refunds the per-user quota ledger via a delta CAS applied BEFORE the object is removed, and fails loud (503, object left intact and charged) when that CAS stays contended — instead of deleting first and silently dropping the refund on a contended ledger. Source: cli/src/lib/session/sync/{worker-template,provision}.ts, cli/src/commands/sessions-import.ts, cli/docs/specifications.md.

  • Signed-in users back sessions up off-box with NO r2.backups bucket to set up (PHNX-3726). agents sessions export --to-r2 / import --from-r2 now default to a managed, Phoenix-gated store (sessions.agents-cli.sh) when you are signed in (agents auth login) — the same zero-setup pattern agents artifacts share and agents traces sync already use, resolved through the one shared selectStorageBackendKind policy. It is managed-first: a signed-in user backs up to managed even with a stale r2.backups bundle present; only --byo / AGENTS_SESSIONS_BACKEND=byo flips to your own bucket. Every managed backup is mandatorily AES-256-GCM encrypted under a per-account key that is minted once, cached at ~/.agents/.cache/state/sessions-backup-key.json (mode 0600), and escrowed at the bearer-gated, immutable Worker key <userId>/__key/backup-dek so a fresh box signing in recovers it and decrypts prior backups with zero setup — the managed path never uploads plaintext. Both the command and Worker reject plaintext/malformed managed objects, and restore revalidates every at-rest bundle before decryption. Honest trust boundary: confidential vs a raw R2/Cloudflare bucket read (ciphertext at rest) but NOT zero-knowledge vs Phoenix (the DEK is escrowed on Phoenix infra); --byo keeps the zero-knowledge own-bucket path (the key stays only in your r2.backups bundle). The isolated Worker verifies the Phoenix bearer on every PUT/GET/LIST/DELETE, enforces segments[0] == userId (401/403, no public GET), and meters a per-user CAS quota (__usage, 413 over quota). Same-key managed PUT/DELETE operations share an expiring R2 lease; every object change is etag-conditioned, DELETE leaves a GET-404/LIST-hidden tombstone, and expired recovery fences the predecessor's etag before settling quota. The ledger retains an idempotent pending delta plus a terminal generation per path in the same CAS object, preventing late predecessor charges, replacement deletes, double refunds, stranded paths, and phantom quota. A CAS-reserved historical-path budget is consumed before persistent per-path state is created and is not refunded on DELETE, so authenticated create/delete churn cannot grow tombstones, leases, or ledger history past the hard cap. Reserved __usage/__key prefixes are never listed. The BYO object layout is byte-for-byte unchanged (the <userId>/ prefix is added inside the client) and the Worker is integration-tested against real workerd. Source: cli/src/lib/session/sync/{backend,managed-config,managed-key,net-client,provision,worker-template}.ts, cli/src/commands/sessions-export.ts, cli/src/commands/sessions-import.ts, cli/docs/{sessions,specifications}.md.

  • Daemon: a host-placed run finishing on the heartbeat tick no longer freezes the event loop (PHNX-3727). Fast-follow to PHNX-3695. finalizeHostRunAsync (the async, tick-reachable finalize path reached via reapExitedRunningJobs) called applyHealedHostRun, which ended by emitting routine-end through the synchronous emitRoutineEnd — whose event-log file lock (withFileLock → lockSync + Atomics.wait, up to 30s under contention) is exactly the loop-freeze PHNX-3695 removed elsewhere, on a path guard-no-sync-io couldn't see (it lives in runner.ts, not a *-service.ts). So a host:-placed routine run completing could still wedge the browser-IPC "accept but never reply" state under lock contention. applyHealedHostRun now takes an injected emitter: finalizeHostRun (sync CLI path) keeps the synchronous emit, while finalizeHostRunAsync (the tick) passes emitRoutineEndAsync (withFileLockAsync), so no daemon tick reaches the synchronous lock. guard-no-sync-io gains a call-site pin so reverting it fails loud. Source: cli/src/lib/daemon/runner.ts, cli/src/lib/daemon/guard-no-sync-io.test.ts.

  • agents traces sync now surfaces silent behavioral failures as ranked issues, not just a friction counter (RUSH-2988). A session where the agent went idle after its last event and a human had to nudge it is a real failure with no error code, so it never clustered through computeInsights (which only groups outcome === 'error' tool calls) and was previously only a needsAttention severity input. The per-session silent-stall friction (silent stall: 5-15m/15-60m/1h+, already computed by computeInsightFacets) is now promoted into cross-session FailurePatterns with a new behavioral failure cause (cli/src/lib/traces/insights.ts computeBehavioralPatterns), ranked into the same top-K failurePatterns by wasted idle time (a bucket's midpoint, bounded by the same 30-min MAX_GAP_ATTRIBUTION_MS cap the tool-error path uses) and summed into wastedMsTotal. TraceFailureCause gains behavioral (cli/src/lib/traces/classify.ts); it is producer-derived and never returned by classifyCause, so the failures.byCause tool-error split reports it as 0. The console renders the new cause in a follow-up web change.

1.22.70

  • agents browser stop --profile is now gated by the remote-control consent gate (PHNX-3317). stop --profile <name> (no --task) is handled entirely by bindTask's early return in ipc.ts, so it never reached resolveOrCreateTask — the one chokepoint every other page/close/attach verb's assertRemoteControlAllowedForRequest gate runs through — and case 'stop' called BrowserService.stopProfile() directly with no consent check at all. That left one destructive fleet-remote path (kills the profile's browser process, clears its runtime dir) reachable from browser --device <device> stop --profile even with remote-control off on the target. stopProfile() now takes the same { fleetRemote, actor } opts as start/showUrl/resolveOrCreateTask and asserts the gate first; a local (non-fleet-remote) call is unaffected. Source: cli/src/lib/browser/service.ts, cli/src/lib/browser/ipc.ts.

  • The PHNX-2939 headless-account regression test now genuinely exercises the headless path (PHNX-3352). The real-macOS-keychain test in account-registry.keychain.test.ts set AGENTS_RUNTIME='test-headless-account-launch', a made-up value isHeadlessSecretsContext() never matches — its set is exactly headless/teams/terminal — so the resolve never entered the headless keychain guard and the test passed identically with OR without the PHNX-2939 biometry-ACL fix, reproducing nothing. It now sets AGENTS_RUNTIME='headless' and asserts isHeadlessSecretsContext() is true before resolving, so the test fails against the pre-fix code (a direct getKeychainToken read of the policy-never, no-ACL account item is rejected as if it required Touch ID) and passes only because the fix routes through the bundle path that attests silentNoAcl. Source: cli/src/lib/account-registry.keychain.test.ts.

  • agents daemon stop reclaims a stale browser.sock when the daemon vanishes mid-stop (PHNX-3618). stopDaemonLocked captured the browser IPC socket's inode ONLY when a live pid resolved (pid !== null && browserSock ? …). If the daemon died in the window between the CLI liveness precheck (isDaemonRunning() was true) and the locked read, resolveLiveDaemonPid() returned null, so the owner was never captured — and the socket the daemon's ungraceful death left on disk hit the else branch, was reported "browser IPC socket ownership could not be verified", and was left behind, so the next daemon start raced a dead binding. The socket inode is now captured independently of the pid, and the stale binding is reclaimed once no live daemon — the signalled target OR any successor for this state dir — is proven to own it. PHNX-3607's never-delete-a-successor invariant is preserved: a successor that rebinds during the stop gets a new inode (identity no longer matches) or shows up as a survivor, and in either case its socket is left untouched. Source: cli/src/lib/daemon/daemon.ts.

  • agents run --cloud no longer rejects a non-expiring Phoenix bearer (PHNX-3645). A Rush session stores expires_at: 0 for an opaque Phoenix pid_ bearer, which by contract means NON-EXPIRING. Three freshness checks read that 0 as an absolute timestamp — new Date(0) = 1970-01-01 — and refused a valid, fresh session with Rush session expired at 1970-01-01, so no Phoenix-authed box could dispatch a cloud run, push/pull secrets over Rush, or list cloud sessions. The expires_at === 0 (and missing) special-case now lives in one place, isRushSessionExpired (cli/src/lib/rush-session.ts), which cloud dispatch, the secrets Rush driver, and the cloud session source all route through — mirroring rush/cli's own isFresh (RUSH-1310, PHNX-3637). Source: cli/src/lib/rush-session.ts, cli/src/lib/cloud/rush.ts, cli/src/lib/secrets/drivers/rush.ts, cli/src/lib/session/cloud.ts.

  • Owner alerts now render per destination — Slack gets tappable links, iMessage stays plain (PHNX-3698). agents notify / agents send --to owner (and an important feed post that fans out to the owner) resolve every channel in owner.policy.normal and composed ONE plain string for all of them, so a Slack owner destination received a wall of prose with no <url|label> links and no tappable session crumb — the labeled-link support already in composeBroadcastMessage never fired on the path the owner actually uses. The owner fan-out (sendToOwner) now re-renders the body PER destination, keyed on each channel's resolved provider (the same notify.transports remap delivery uses): a Slack destination turns every TEAM-N key the post names into <https://linear.app/<ws>/issue/<KEY>|<KEY>> and the Sent from claude/<id> on <host> crumb into <https://prix.dev/console/sessions/<full-id>|claude/<id>> (the 8-char crumb upgraded to the full indexed id first), while iMessage / owner-scoped rush / desktop / command: sinks stay the plain human sentence with no URLs. Each delivery echoes the exact body it sent, so agents notify --dry-run --json now shows the two different bodies. Source: cli/src/lib/notify.ts, cli/src/lib/owner-message.ts, cli/src/lib/sink-format.ts, cli/src/lib/feed-broadcast.ts, cli/src/lib/channels/send.ts, cli/src/commands/send.ts.

  • A release no longer starves waiting for the tip of main to be attested (PHNX-3705). release.sh phase 2 required an attestation for origin/main's tree as of the instant the release ran, but attest-main.yml produces that by running the full suite — minutes of work. On a repo where agents merge continuously, main advances faster than attestation completes, so every release lost the race and died at missing exact attestation key. Measured 2026-09-01: six merges to main in ~30 minutes with attest-main runs still pending or superseded; the tip was never attested long enough to release from, which also makes the R2 bar (ordinary release under 60s) unreachable by construction. The release does not need the tip — it needs a green base to cut the release commit from and derive the release-tree attestation against (PHNX-3696), and any attested ancestor serves. Phase 2 now requires an attestation for the tree it is actually releasing from ($BASE_SHA) rather than the live remote tip, and derive_release_attestation inherits from that same commit — the release commit's real parent — instead of the tip. New cli/scripts/release-attested-base.sh resolves the newest attested commit at or below origin/<default> (bounded lookback, fails closed when none is found), release-worktree.sh cuts the release worktree from it and says so on stderr when it is behind the tip, and release.sh's base check now requires an ancestor of the remote tip rather than the tip exactly. The supply-chain guarantee is unchanged: the published tarball is still bound to a tree whose suite passed, and derive's allowlist still fails closed on any code file — the release simply ships from a slightly older tree than the tip, which was always true of an attested tree. Source: cli/scripts/release-attested-base.sh, cli/scripts/release-worktree.sh, cli/scripts/release.sh.

  • Docs: sign-in unlocks managed sharing, not only team spaces; traces setup marked advanced/self-host. The README said signing in unlocks "the one thing" — team spaces — which predated managed, private-by-default sharing; it now names both hosted surfaces (team spaces AND agents artifacts share/agents traces sync on our storage with zero Cloudflare setup, private by default when signed in). The agents traces setup command description no longer reads as a normal user step — it is now labeled advanced/self-host, since agents auth login syncs to the managed endpoint with no setup. Source: README.md, cli/src/commands/traces.ts, cli/docs/command-index.{md,json}.

1.22.69

  • agents monitors no longer fires an action on a poll FAILURE (PHNX-3510). An [on-change] monitor treated a failing poll — a gh pr list … | jq that intermittently printed GraphQL: API rate limit already exceeded … — as a value change: the empty→error→empty flap read as two changes and dispatched a full agent run each time on a premise that was false (~10 sessions burned re-verifying two already-merged PRs). A command/poll source now flags a snapshot that exited non-zero, or whose output matches a transport/auth/rate-limit error shape (caught even on exit 0, since a pipe to jq swallows the failing half's exit code), as an observation failure: the engine skips it, leaves watched-state untouched, does not fire, and records it as a failed check so a sustained streak escalates to the owner as a drought. agents monitors test labels a failed poll and reports Would fire: no. Note: a non-zero exit is now an observation failure rather than a watchable value — a monitor that wants to watch a command's success/failure should emit a stable token (… && echo UP || echo DOWN, always exit 0) so the state it cares about diffs as a value; sustained failures still reach the owner via the drought escalation. Source: cli/src/lib/monitors/sources/failure.ts, cli/src/lib/monitors/sources/command.ts, cli/src/lib/monitors/engine.ts, cli/docs/automation.md.

  • agents fleet worktrees surfaces the worktree-sweep's held set as buckets, not a silent count (PHNX-3520). The nightly worktree-sweep (PHNX-3503, in phnx-labs/.agents) reclaims merged agent worktrees and correctly HOLDS anything dirty, unmerged, or undeterminable — but it only ever emitted a held=<n> count, so the held set became a permanent, growing residue nothing resolved. Measured 2026-08-30 it is consistently LARGER than the reclaimed set (~800 worktrees fleet-wide), and one bucket inside it is real stranded work: a worktree whose branch carries commits on no remote (the PHNX-2951 / PHNX-2732 class — an agent finished, its commits never reached a remote, nothing surfaced them). The new read-only command classifies every held worktree into its three buckets — unmerged-commits (the one that matters), uncommitted-changes, and undeterminable (status-unreadable / merge-state-unknown, failing closed exactly as the sweep does) — and reports each with device/repo/worktree/branch/reason/age/size. --json is the structured form for machine callers; --fleet fans out over every online device and aggregates; --bucket <name> filters. The safe recovery action for the stranded bucket is --push: it PUBLISHES each unmerged-commits branch that is on no remote (re-reading the merge state immediately before pushing and refusing anything already on origin), making the work visible — it never removes a worktree or a branch. The destructive reclaim stays in the sweep; this is the surfacing half the sweep can consume. Source: cli/src/lib/worktree/held.ts, cli/src/commands/ssh.ts, cli/src/lib/worktree/held.test.ts.

  • agents run starts ~0.6s faster on a box with many secrets bundles (PHNX-3585). The AGI EXT "New Claude" boot runs agents run claude --interactive, and the whole wrapper cost is spent BEFORE the harness prints anything. Profiling that pre-exec window (new AGENTS_PROFILE_BOOT=1 per-stage tracer, cli/src/lib/boot-profile.ts) showed the dominant stage was --strategy balanced account rotation: readAccountRegistry → listBundles decrypted EVERY file-store bundle's metadata to find the account bundles, and each decrypt runs a fresh scrypt KDF (~12ms) because every .enc file carries its own salt — ~690ms of pure KDF on a box with ~40 bundles, on every launch. Bundle metadata is non-secret by contract (it holds keychain:/env:/literal refs, never the secret bytes, and is already stored no-ACL so secrets list enumerates without Touch ID), so the file store now caches the decrypted metadata JSON keyed by each .enc file's (mtime,size) plus a passphrase fingerprint. Any bundle write (new mtime/size) or passphrase change (new fingerprint; rotation re-writes every file) misses and re-derives — no staleness, self-healing on any miss — and secret VALUE items never enter the cache. Measured on a worker box: agents run claude pre-exec resolve-version dropped from ~690ms to ~31ms (warm), total pre-exec ~1040ms → ~370ms. A committed cli/scripts/bench-boot.sh keeps it measured. Source: cli/src/lib/secrets/filestore.ts.

  • Grok's cheap scan now fills firstUserMessage from a bounded chat_history.jsonl prefix read (PHNX-3621). The summary-only path never parsed the log. The prefix reader skips Grok <user_info> / synthetic_reason scaffolding, prefers the first prompt_index user turn, and unwraps <user_query>. Existing Grok rows backfill through CONTENT_INDEX_VERSION v4. Source: cli/src/lib/session/discover.ts.

  • Worker homes provisioned before seed-on-attach self-heal their account identity at resolve time (PHNX-3660). resolveClaudeSetupToken required the version home's .claude.json to carry oauthAccount.emailAddress; homes whose .oauth_token was written by a pre-fix accounts attach had none, so exec-env injection and per-account usage reads were silently inert for them (they authenticated only via the shim's .oauth_token fallback). The resolver now recovers the email by matching the home's .oauth_token value against the auth bundle (re-encode-guarded slug decode, so a lossy or unmatched token can never invent a mapping), writes it back with seedClaudeWorkerHomeIdentity so the home converges after one read, and still fails closed — writing nothing — when the token matches no bundle key. Ambient no-home resolves never rewrite the operator's real ~/.claude.json. Source: cli/src/lib/claude-account-token.ts.

  • Resume /continue no longer launches the exhausted origin login when the pick is a provider account (PHNX-3674). After native-first rotation (PHNX-3626), a usage-limited origin whose transcript is not in the origin home (trash/backup/reinstall, or local fallback from an unreachable peer) fell through to /continue on the provider-inclusive pool without carrying RecoveryAccount, so exec authenticated as the rate-limited native login. That continue target now injects the healthy provider the same way native rotation does. Source: cli/src/lib/session/recovery.ts, cli/src/commands/exec.ts.

  • Private share OG covers are no longer cacheable by shared proxies (PHNX-3676). Token-gated visibility=private pages already served HTML as Cache-Control: private, no-store, but the generated /user/slug.png cover used public, max-age=31536000, immutable after the token gate. RFC 9111 lets a shared cache reuse a public Authorization response for later unauthenticated requests keyed on the unauthenticated URL, so a Bearer fetch of the cover could leak the preview. managedCoverHeaders now treats private like me/org (private, no-store + X-Robots-Tag: noindex). Source: cli/src/lib/share/worker-template.ts.

  • agents sessions inject can nudge a live remote tmux session that has no session id, and no longer crashes on --pane --device (PHNX-3688). Two bugs are fixed. (1) A live session whose agents sessions --active id column shows - — only a tmux label like ag-claude-214edaae:0.0 is known — was untargetable: inject <shortid> failed No active session matches. The lookup now matches the ag-<agent>-<shortid> tmux name's suffix (and the full name / pane id), the only selector such a row exposes, and the discovered row carries its tmuxName so the match has something to key on. (2) inject _ "text" --pane %122 --device <box> crashed with TypeError: host.startsWith is not a function: under optsWithGlobals() the parent sessions command's variadic -D, --device <target...> shadows this subcommand's scalar --device, so a single --device box arrived as ['box'] and the array flowed straight into sshExec. It is now coerced to one host (and fails loud on more than one, since inject delivers to exactly one terminal). A bare session id combined with --device now resolves ON that device — its tmux panes live there, not on the box you typed the command from — by re-running the inject over SSH, the tool-native form of the old agents ssh <box> "agents sessions inject <id> …" workaround. Source: cli/src/commands/sessions-inject.ts, cli/src/lib/session/active.ts.

  • agents releases no longer wedge waiting for a human to mint an attestation (PHNX-3696). RUSH-2666 made an exact-tree attestation for the release commit mandatory but shipped no producer for it, so every release.sh --apply since 2026-08-15 stopped at missing exact attestation key and required an operator to hand-run release-attestation-produce.sh and copy the tarball into the store. attest-main.yml already attests every push to main; the release commit differs from that base only by the version bump, the folded changelog, and the regenerated command index — exactly the set release-attestation.sh derive allowlists — so release.sh now derives the record itself (derive_release_attestation, inheriting the green base's suite via --inherit-suite-from, no suite re-run). Soundness is unchanged: derive still fails closed if the tree diff touches anything outside that allowlist, so a code change can never inherit a stale pass, and the derive is best-effort — any failure falls through to the previous poll-then-require, which still fails loud (the call site guards it with || true, which is load-bearing under set -euo pipefail: a bare call returning non-zero would abort the release before that fallback ran). The regression survived review because release.test.ts asserted against release.sh as text; the new coverage executes the real derive_release_attestation body against a real git repo and store. Source: cli/scripts/release.sh, cli/scripts/release.test.ts, cli/AGENTS.md.

  • Owner phone pings are short and tappable — but only where a sink can render a link (PHNX-3698). Every owner-bound message goes through one composer (composeBroadcastMessage), and the shared {message} now surfaces its links per-sink instead of dumping naked URLs after the footer. A Slack channel: sink gets mrkdwn labeled links <url|label>: the Sent from claude/6fc1db18 on zion crumb becomes <https://prix.dev/console/sessions/<full-id>|claude/6fc1db18> (an 8-char crumb is first upgraded to the full indexed id, since a truncated id would 404), and every TEAM-N key the title or body names — not just the session's own ticketId, so a ping that only mentions PHNX-3689 in prose is still tappable — becomes <https://linear.app/<workspace>/issue/<KEY>|<KEY>> in place. iMessage, the owner-scoped rush message, command: sinks, and every other channel stay plain — they cannot render a labeled link and a dumped naked URL reads as noise, so the message is the human sentence with no URLs (keys and crumb as text); there is never a trailing URL line on any sink. agents notify and agents send --to owner route through that same composer on the owner-scoped (plain) path instead of shipping the raw body dump; a non-owner agents send is still delivered verbatim. An important/--blocked post (and an owner send) fires a best-effort, opt-in background agents traces sync so the linked console page exists when a Slack crumb is tapped (gated exactly like the run-exit arm; AGENTS_NO_TRACE_SYNC=1 opts out). The Linear-key detector is canonical in one place shared with transcript ticket detection (session/linear.ts). Source: cli/src/lib/feed-broadcast.ts, cli/src/lib/owner-message.ts, cli/src/lib/session/linear.ts, cli/src/lib/session/db.ts, cli/src/lib/run-trace-sync.ts, cli/src/commands/send.ts, cli/src/commands/feed.ts.

  • An ordinary CLI release no longer signs or notarizes anything on macOS, and the macOS attestation producer works again (PHNX-3699, PHNX-3631). release-attestation-produce.sh gated its sign+notarize block on uname == Darwin alone, with no --with-helpers condition — so a CLI-only attestation produced on a Mac codesigned the CLI binary and rebuilt + signed both helper .apps, none of which ship in the tarball (RUSH-3026 removed the binary, RUSH-3100 the bundles). That violates the R3 requirement ("no signing, no notarization on the ordinary path") and, because agents secrets exec apple.com cannot unlock a Touch-ID-gated bundle headlessly, it killed the release outright — hit live cutting 1.22.69 right after PHNX-3696 made release.sh produce this attestation itself. The block is now gated on --with-helpers, so cutting a helper release still signs and an ordinary CLI release does not. Shipped alongside the PHNX-3631 fix in the same path: ATTEST_TMP used mktemp ".../agents-cli-attest.XXXXXX.json", but BSD/macOS mktemp only substitutes trailing X's and treats a template with a suffix after them as a literal filename — so the first call created that exact file and every later call died with mkstemp failed … File exists. Now agents-cli-attest.json.XXXXXX, which GNU accepts identically; the same pattern in remote-sign-mac.sh is fixed too. Together these take release-attestation-produce.test.ts on macOS from 22-of-29 failing to 31/31 passing. Source: cli/scripts/release-attestation-produce.sh, cli/scripts/remote-sign-mac.sh.

  • agents projects view <path> auto-detects the cwd's project; projects for-cwd is gone (PHNX-3704). agents projects view [nameOrPath] now accepts a DIRECTORY as well as a project name. A path-shaped argument — ., .., a ~-prefixed value, a /-containing value, or an explicit --path [dir] (bare --path = cwd) — auto-detects the project that contains that directory via the existing path-based projectNameForCwd (def-root containment, longest wins); a bare token stays a project name. view <path> --json prints {name, linear:{name,projectId}, root} in ONE call, so a consumer gets the project name AND its Linear binding without a second projects list round-trip. Fail-open: an all-null shape when nothing contains the path, exit 0, never throws. The projects for-cwd subcommand and its formatForCwdOutput helper are deleted — view <path> --json subsumes them. Source: cli/src/commands/projects.ts.

  • agents artifacts share is private by default when signed in, and storage + visibility now live in one shared module. A publish with no --visibility flag is now me (owner-only, Phoenix-gated) instead of public — signed in, your share is private unless you opt into --visibility org (everyone at your email domain, no org to create) or --visibility public (the gallery). A BYO (signed-out) publish still defaults to public, since the Worker refuses me/org without a Phoenix owner. The managed-vs-BYO selection policy and the visibility model are now the single cli/src/lib/storage/ module (selectStorageBackendKind + publishVisibility/defaultVisibilityForBackend), consumed by both agents artifacts share (lib/share/backend.ts) and agents traces sync (lib/traces/backend.ts) instead of each re-deriving it — the seam the sessions backup surface reuses next. Failed share calls (publish/list/revisions/delete) now surface the Worker's own error — a 400/413/429 shows its {"error":"…"} reason and a 429's Retry-After — via one bounded extractor (lib/share/http-error.ts) that never dumps an arbitrary body. Source: cli/src/lib/storage/, cli/src/lib/share/{backend,publish,delete,http-error}.ts, cli/src/commands/share.ts.

  • agents view grok shows the last-known usage % again instead of a numberless "run grok once to refresh usage". Two bugs compounded. (1) getGrokUsageInfo read unified.jsonl from the home it was given, but agents view fetches usage per INSTALLED VERSION, passing each version's isolated home (~/.agents/.history/versions/grok/<ver>) — whose .grok/logs/unified.jsonl never exists, because Grok writes its billing log only to the user's shared real home ~/.grok. So every version fell back to the refresh hint while the real reading (e.g. week 42%) sat unread in the shared home. Grok usage now resolves the requested home's log first and falls back to the shared ~/.grok log where Grok actually writes (a per-version log, if one ever appears, still wins). The shared last line is attributed only to the identity that owns ~/.grok (matching auth.json, or the real home itself) — other version-scoped Grok accounts stay as no-recent-usage rather than inheriting that meter. (2) The usage cache serializer persisted only fresh windows, dropping the staleWindows Grok's collector pre-partitions an ended-period reading onto — so the daemon-refreshed cache the plain agents view grok reads rendered the plan alone (no bar), even right after a --refresh had shown "W: 42% · stale". The serializer now persists the union of fresh and stale windows; deserializeClaudeUsageSnapshot re-runs the freshness gate on read, so the round-trip is preserved for every collector (Claude, which returns raw windows, is unchanged). Source: cli/src/lib/accounting/usage.ts.

1.22.68

Fix OpenCode session history to index and backfill the genuine first user request.

  • agents sessions resume is native-first with same-harness account rotation, and records the origin version so it stops falling back (PHNX-3626). Resume continues the SAME session with the harness's own native resume (claude --resume <id>, codex/grok/opencode native) on its recorded agent@version + device, rather than spinning up a fresh /continue transcript replay. When the origin account is rate/usage/session-limited, recovery rotates to a healthy INJECTABLE provider account of the same harness and stays native in the origin home (--strategy balanced, the same weighted-by-headroom picker dispatch uses); /continue replay is now the last-resort fallback only when no native path or no healthy same-harness account exists. The origin version is recorded forward at launch — buildExecEnv exports AGENTS_RUN_VERSION, the SessionStart hook joins it to the harness's real session id in the durable sidecar (like the launch mode), and the scan joins it at row build (meta.version ?? actorRec.version) and write-once COALESCEs it into the index — so a codex session whose transcript carried no derivable version (its relocated .codex-homes/<version>/ home, which the scanner now also reads) no longer degrades to /continue for a missing recorded origin. Resume prefers the recorded device and falls back to a clearly-labelled local /continue replay from the synced mirror when that device is genuinely unreachable. Source: cli/src/lib/session/recovery.ts, cli/src/lib/exec.ts, cli/src/commands/resume.ts, cli/src/lib/session/actor-sidecar.ts, cli/src/lib/session/db.ts, cli/src/lib/session/discover.ts, cli/src/lib/accounting/account-pool-collect.ts, packages/session-tracker/src/hook.sh.

  • agents artifacts share --private/--unlisted had no read authentication — private shares were world-readable by URL (PHNX-3654). The share Worker's GET served any object with cache-control: public after only existence + expiry checks, so --private/--unlisted were obscurity, not authentication, and a private share could get a title-derived guessable slug. Two fixes ship together. (A) An unlisted/--private publish now prints a loud stderr warning that unlisted is NOT authenticated and points at the real controls, and forces a 64-bit random slug tail (regardless of --label/title) so the capability URL can't be guessed. (B) A new --protected flag (= --visibility private) is token-gated read auth: the CLI mints a random viewer token, the Worker stores only its SHA-256 hash (viewer-token-hash) and serves the object — its page, generated OG cover, and revision list — only to a request whose ?k=<token> or Authorization: Bearer matches under a constant-time compare, returning 404 (never 401, no existence leak) otherwise. The key rides only in the emitted https://<host>/<user>/<slug>?k=<token> URL; the namespace owner reads their own private page without it, and it works for BYO endpoints too. private is publish-only — the in-place share visibility / PATCH route rejects it with a 400. Source: cli/src/lib/share/publish.ts, cli/src/lib/share/worker-template.ts, cli/src/commands/share.ts, cli/docs/share.md.

1.22.67

  • Keep the shared daemon client-safe (PHNX-3605). Browser version reconciliation no longer restarts the process; browser and routines lifecycle commands now enable, disable, and reload only their own services while sibling work keeps running. Source: src/lib/browser/ipc.ts, src/commands/browser.ts, src/commands/routines.ts.

  • Daemon stop/start races can no longer erase a successor or signal a reused PID (PHNX-3607). stopDaemon() now holds the same daemon.lock as start/claim through signal, wait, postcondition, and cleanup; it trusts a recorded PID only when the live process command ends in __daemon-run, and compare-deletes the PID/socket identity it captured instead of unlinking a successor's replacement. The stray-daemon reaper now waits for SIGTERM death, escalates to killTree when needed, waits again, and retains the registry marker if a duplicate somehow survives. Source: cli/src/lib/daemon/daemon.ts, cli/src/lib/browser/ipc.ts.

  • Daemon supervisor hardening: abandon hung ticks, thread an AbortSignal, and bring account-state/catch-up under the supervisor (PHNX-3608). A supervised tick that awaited something that never settled used to park the service but hold its in-flight guard forever, so the backoff restart, agents daemon services restart, and SIGHUP reload were all blocked — a service could freeze for the daemon's life while still looking healthy (the 12h usage-dark class). On a deadline breach the supervisor now ABANDONS the tick: it aborts the tick's new AbortSignal, releases the in-flight guard immediately, and schedules the backoff restart right away, draining the runaway promise separately. PeriodicService.tick(ctx, signal) receives that signal so a service can bound its own I/O. start()/stop()/restart() are now bounded by a lifecycle deadline so a wedged bind/close can't stall daemon startup or shutdown. The account-state usage/auth refresh — previously its own un-deadlined setInterval behind a usageRunning latch (the exact hang that froze usage) — is now TWO supervised PeriodicServices (account-state usage + account-auth) with a real deadline + abort and INDEPENDENT circuit breakers, so a run of usage-refresh failures parks only usage and never starves the slower auth refresh; the deadline signal is threaded through to each provider fetch. Catch-up recovery moved off its bare setInterval onto a supervised catchup service. Disabling the monitors service now routes through supervisor.stop, and a stopped monitor engine dispatches nothing on tick — so a disabled monitors service actually stops firing instead of being ticked with its last-loaded set. Source: cli/src/lib/daemon/supervisor.ts, cli/src/lib/daemon/service.ts, cli/src/lib/daemon/account-state-daemon-service.ts, cli/src/lib/daemon/catchup-service.ts, cli/src/lib/daemon/monitor-engine-service.ts, cli/src/lib/monitors/engine.ts, cli/src/lib/daemon/daemon.ts, cli/src/lib/daemon-services.ts, cli/src/lib/daemon-ticks.ts, cli/src/lib/usage-refresh.ts, cli/src/lib/accounting/usage.ts, cli/src/lib/auth-health.ts.

  • A cold agents browser navigate on a worker now follows the fleet browser hub instead of failing locally (PHNX-3610). A bare agents browser start already forwards to the hub (config.defaultBrowserDevice), but a page verb with no bound task — agents browser navigate|screenshot|click|… run without a preceding start — did not: resolveTaskRoute's no-task branch returned the local box, so the verb ran on the worker and failed with "no default browser on this machine." Agents on other devices could only drive the hub's logged-in browser if they knew to start first. The no-task branch now follows defaultBrowserHub() (passed in as resolveTaskRoute's hub option so the leaf stays pure), and the task-routing hook forwards a taskless drive verb to the hub. So the hub creates the caller's OWN task, the dispatch now forwards the caller's session/launch identity (AGENT_SESSION_ID/AGENT_LAUNCH_ID, the same surface-parity mechanism run --device uses) — without it a forwarded verb resolved BLANK identity on the hub and its no-identity task bucket could collide with an unrelated agent's or a human's browser. A cold CLOSE verb (done/stop) is deliberately not forwarded when taskless — with no task it would target a context this session never opened — so it can never blindly stop a hub task. Behavior-only, no new flags; the existing isSelfHost/isFleetRemoteInvocation guards mean the hub never forwards to itself and a re-exec never loops. Source: cli/src/lib/browser/task-index.ts, cli/src/lib/browser/caller-identity.ts, cli/src/commands/browser.ts.

  • The canonical session watch now carries the genuine request and recoverable history (PHNX-3621). Transcript indexing stores firstUserMessage separately from the one-line topic, skips harness-injected plugin/developer scaffolding, preserves long multi-line requests, and backfills existing rows through the versioned content index. agents feed watch --json also folds in a bounded set of durable Previous rows on reset, marks ended live rows as Previous, and emits agent.remove before their replacement so presentation clients can render and resume history without parsing transcripts or issuing a second session fetch. Source: cli/src/lib/session/prompt.ts, cli/src/lib/session/discover.ts, cli/src/lib/session/db.ts, cli/src/lib/session/active.ts, cli/src/lib/session/remote/watch.ts, cli/src/lib/feed/watch.ts.

  • Auto-sync traces when a local run exits (PHNX-3628). Trace upload was manual-only — the sole caller of syncTraces() was agents traces sync, so the Prix console went stale between hand-runs. A local, headless agents run now spawns a detached agents traces sync on exit (mirroring run-notify; the incremental ledger means it pushes essentially just the finished session). It fires only when you have already opted into the traces store (signed in AND synced at least once); opt out per-run with --no-trace-sync or AGENTS_NO_TRACE_SYNC=1. Local runs only. Source: src/lib/run-trace-sync.ts, src/lib/traces/sync.ts, src/commands/exec.ts.

  • The shared daemon now contains lock-refresh and reload failures instead of crashing, and concurrent health writers preserve the restart-backoff streak (PHNX-3606). Every direct proper-lockfile lease used by device/team registries, routine launch claims, and usage refresh coordination overrides the library's timer-level compromise throw with an observable log-and-continue barrier. The daemon's SIGHUP reload callback is guarded independently of service supervision, and health.json updates now serialize the complete read-modify-write under a file lock and publish through atomic rename, so parallel processes cannot erase each other's failure counts or expose partial JSON. Source: cli/src/lib/lock-compromise.ts, cli/src/lib/daemon/daemon.ts, cli/src/lib/daemon-health.ts.

  • Daemon usage/auth sync no longer blocks the event loop or opens a per-tick SSH mesh (PHNX-3609). Headed devices publish one identity-keyed usage snapshot into their conflict-free tracked ~/.agents/devices/<device>/daemon-state.json; each daemon automatically commits only its owned file and exchanges the user repo under one cross-process lock and a 45-second hard process-tree deadline, so workers receive and merge snapshots newest-wins without a human agents repo sync user. auth-sync uses the same file for non-secret ready/missing/invalid verdicts, elects one ready publisher, and opens device-to-device SSH only to provision a peer known to be missing the reserved bundle. That exceptional secret transfer is fully async and every import/read-back operation has a 20-second deadline; timeout disables SSH multiplexing, sends SIGTERM, then SIGKILLs an uncooperative client after a short grace so both the local promise and direct remote connection are bounded. OAuth/setup-token values never enter the Git store. Source: cli/src/lib/fleet-shared-state.ts, cli/src/lib/fleet-shared-repo-sync.ts, cli/src/lib/accounting/usage-sync.ts, cli/src/lib/secrets/reserved-sync.ts, cli/src/lib/secrets/push.ts, cli/src/lib/ssh-exec.ts.

1.22.66

  • Fleet usage/auth sync now reaches FQDN-only-pinned workers (PHNX-3505). The usage-sync and reserved-auth-sync push/pull planners judged a peer "pinned" by its bare device name, but a tailnet-discovered worker's host key is pinned under its FQDN (yosemite-m6.tail….ts.net). So every worker not also pinned under its short name was silently skipped — its claude-usage.json went days stale and balanced routed into weekly-maxed accounts, and its auth token never propagated. The pin check now resolves the same host string the ssh connection dials (isDevicePinned), matching either form. Source: cli/src/lib/devices/known-hosts.ts, cli/src/lib/accounting/usage-sync.ts, cli/src/lib/secrets/reserved-sync.ts.

1.22.65

  • agents view shows the last-known usage number with its age instead of hiding it (PHNX-3453). When the usage endpoint 429-backs-off an account so a window is not refreshed within its own lifetime (Claude's 5h session window) or a billing period rolls over (Grok's weekly credit window), the view now renders the last reading with a staleness suffix — S: █▌░░░ 30% · 6h old, W: ██▏░░ 42% · stale (period ended 1h ago) — instead of S: ┄┄┄┄┄ unavailable or a numberless run grok once to refresh usage. The dropped windows ride a new VIEW-ONLY UsageSnapshot.staleWindows field; routing still reads only windows, so isUsageVerified/hasStaleUsage/deriveUsageStatusFromSnapshot continue to treat a stale/expired number as unverified and never route on it. Source: cli/src/lib/accounting/usage.ts.

  • A worker's interactive Claude run no longer lands on the login screen (PHNX-3502). claudeAdapter.applyExecConfigEnv deferred to the per-version native login for EVERY interactive run (ctx.interactive || headedDevice), even on a worker-role device that has no such login — only a headless run on a worker injected the auth-bundle setup-token. A keychain-less worker with no .credentials.json/.oauth_token therefore got no credential at all on agents run claude --interactive --device <worker>, landing on Claude Code's theme-picker/login screen. The condition now keys purely on device role: only a personal/desktop (headed) device defers to its native login; every worker run, interactive or headless, injects the setup-token. The credentialPresence routing gate that feeds --strategy balanced is also fixed to stop reading a false-healthy signal: its claude entry now requires the REAL credential (.credentials.json/.oauth_token, via the same floor getAccountInfo applies) rather than the bare presence of .claude.json (stale account metadata a version can carry with no usable credential behind it). Relatedly, a provider account folded into the run-candidate pool now derives signedIn from its record's secretPresent field instead of a hardcoded true — keeping that field honest rather than changing today's routing, since localRegistryRecords already filters out token-less accounts before the map. Source: cli/src/lib/harness/adapters/claude.ts, cli/src/lib/agent-spec/agents.ts, cli/src/lib/accounting/account-pool.ts, cli/src/lib/accounting/account-pool-collect.ts.

  • Include clickable ticket links in every feed broadcast (PHNX-3572). Ticket-backed posts now put the canonical Linear URL in the shared message delivered to private owner destinations and configured Slack channel sinks; repeated URL lines are deduplicated. Source: cli/src/lib/feed-broadcast.ts, cli/src/commands/feed.ts.

  • agents view <agent> --device <host> no longer flashes and vanishes from a real terminal (PHNX-3583). The --device passthrough set interactive = process.stdout.isTTY and forwarded read-only renders over ssh -tt — a forced PTY. On the render's clean exit the PTY teardown + local-terminal restore (restoreLocalTerminal / TERMINAL_MODE_RESET) wiped the just-drawn output, so the whole view flashed and disappeared, leaving the prior screen. The piped (non-TTY) path never had this problem. Pure read-only render commands (view, inspect, insights, doctor) now forward over a plain pipe instead of a PTY, and inject FORCE_COLOR=1 plus COLUMNS/LINES into the remote env so the piped render still comes back colored and correctly wrapped (terminalWidth() already reads $COLUMNS for exactly this reason); FORCE_COLOR is withheld under --json so machine output stays clean. A render command's narrow interactive sub-path — view --prune's confirm — keeps the PTY, and every genuinely interactive command (run, secrets browse, pickers) is untouched. Source: cli/src/lib/hosts/passthrough.ts, cli/src/lib/hosts/passthrough.test.ts.

  • Remote provisional Claude tabs reopen once instead of bouncing across the fleet (PHNX-3587). When VS Code reloaded before an offloaded Claude launch had written its first transcript, agents sessions resume found the dispatcher's synthetic session row, hopped to the target, then let the target search the fleet and rediscover the same synthetic row. The repeated routing printed reconnect banners and eventually returned to the shell without Claude. An owner hop now performs one local lookup: it resumes a materialized transcript, or recreates the exact forced Claude session identity when the transcript has not materialized yet. Source: cli/src/commands/resume.ts.

  • Cap and smooth the daemon's usage-refresh traffic with a per-provider budget. The refresher enforced only a per-account hourly cap, so endpoint load scaled linearly with account count: 8 Claude accounts × 12/hr = ~96 usage calls/hr from one box, against Anthropic's ~100/hr /api/oauth/usage ceiling — which 429'd accounts into Retry-After parks (measured on zion: 7 of 8 accounts parked, never refreshed in their window, so agents view showed S: unavailable and balanced routing read stale usage). A per-provider budget (PROVIDER_HOURLY_BUDGET, 30/hr across all of a network provider's accounts) now bounds aggregate traffic at a fixed rate that no longer grows with account count, paced smoothly (one fetch per ~2 min, round-robin, stalest account first) so it never bursts-then-stalls. Actively-used accounts stay fresh for free via the statusline ingest, which now also re-derives their headroom and suppresses a redundant API refresh. Source: cli/src/lib/usage-refresh.ts, cli/src/lib/daemon-ticks.ts.

  • Route-refuse only on genuinely stale usage, not merely budget-paced usage. Because proactive refreshes are now paced, an idle account is deliberately refreshed on a stretched cadence. Routing keeps its tight 5-min bar for weighting an account, but the fail-loud NO_VERIFIED_USAGE refusal now fires on a wider 40-min bar (USAGE_STALE_REFUSAL_MAX_AGE_MS) — so a budget-paced fleet routes normally while a genuinely broken refresh (hours-old readings) still refuses. Source: cli/src/lib/accounting/rotate.ts.

1.22.64

  • agents accounts attach bootstraps a keychain-less Linux worker (PHNX-3502). Attaching a Claude account to a version home on a headless worker now seeds the home's identity and writes its .oauth_token from the account's non-rotating setup-token already synced in the auth bundle, instead of refusing because the home was never interactively signed in. This is what lets all accounts show as logged in — and interactive worker runs authenticate (the shim's Linux .oauth_token fallback) instead of dropping to the login screen. No rotating OAuth credential is ever copied. Source: apps/cli/src/commands/accounts.ts, apps/cli/src/lib/claude-account-token.ts.

  • Fixed Rush-backed feed channel sinks failing on Linux workers by handing delivery to a reachable macOS fleet peer with the Keychain-bound transport.

  • Auto-produce the origin/main release attestation on merge (RUSH-2666). A new attest-main.yml workflow runs the full suite on every push to main and uploads the exact-tree attestation + pretested tarball to a rolling main-attestations GitHub Release, keyed by tree hash. release.sh now prefetches that proof into the local store before waiting, so an ordinary release promotes without running the suite inline — no more human running the suite by hand to unwedge a release. Purely additive and fail-safe: on any fetch miss or error it falls back to exactly the prior poll-then-require behavior, and a fetched proof is trusted only because the existing exact-tree require re-verifies it. Source: cli/scripts/release.sh, .github/workflows/attest-main.yml.

  • Phoenix ID now uses the branded id.byphoenix.com API hostname by default (PHNX-3543). New CLI sessions start and poll device authorization, resolve whoami, and bind managed share/traces Workers against the same canonical Phoenix ID base. The legacy hostname remains live for older installed clients. PHOENIX_ID_BASE remains an environment override for development and private deployments. Source: src/lib/identity/client.ts.

  • A pre-launch run.launch event makes a launch into a logged-out version visible instead of silent. agents run <agent> --device auto --strategy balanced (the VS Code "New Claude" flow) launched a version that was LOGGED OUT on the target box: --device auto (applyDeviceAutoToOptions) only ensures SOME account is ready on the device, not that the SPECIFIC version launched is signed in there, and the failure was invisible — the pinned/default path in resolveRunVersion returns without emitting rotation.resolved, and run.dispatched only fires at run FINALIZE (post-exit), but a logged-out agent sits at the login screen and never finalizes, so nothing was recorded. A new run.launch event is now emitted RIGHT BEFORE the harness child is spawned, on the device that will run it, so it fires even when the agent then sits stuck at a login screen. Both live launch paths are covered by one shared emitter: spawnAgent (after the tmux-durability gate, for the tmux-wrapped AND bare spawns) and the Windows execShimPassthrough shim (resolvedVia: 'shim'), which was the second blind spot. Payload: module: 'run', agent, version, strategy, signedIn (the launchable-signed-in verdict for the SPECIFIC launched version on THIS device — REUSED from the rotated pick's rotationResult.picked.signedIn when the command already computed it, else derived via the new isVersionLaunchableHere helper, which applies the same getVersionHomePath -> getAccountInfo -> isLaunchableSignedIn gate as collectRunCandidates), launchedLoggedOut (the headline flag, signedIn === false), email, and resolvedVia; the device hostname is auto-stamped by emit(). It sits in the AUDIT lane — the sibling of run.dispatched and the more reliable stuck-launch signal — so agents events --level audit --include runs surfaces it. Purely additive observability — the emit is best-effort (it can never break a launch) and no routing/launch behavior changes; refusing to launch a logged-out version is a separate follow-up. Source: cli/src/lib/exec.ts, cli/src/lib/accounting/rotate.ts, cli/src/lib/feed/events.ts, cli/src/lib/event-families.ts, cli/src/commands/exec.ts.

  • Live session rows carry outcome-card metrics and deliverables (PHNX-3574). agents sessions --active --json and agents sessions watch --json now enrich each live row with indexed tokenCount, durationMs, and subAgentCount, plus created plan/artifact documents detected by the existing bounded transcript-tail state engine. Thin clients such as AGI EXT can render the initial request, progress, deliverables, team fan-out, runtime, and token use from the one canonical stream without polling or parsing transcripts themselves. Source: cli/src/lib/session/active.ts, cli/src/lib/session/state.ts.

1.22.63

  • Managed share endpoint enforces a per-user storage quota, object limit, per-file size cap, and publish rate limit (PHNX-3542). The managed share.agents-cli.sh Worker authenticated any Phoenix ID bearer and then accepted unbounded writes into shared R2 — no quota, no rate limit, no size cap — which blocked opening publishing to third parties. Each managed (Phoenix-identity) publish now charges a per-user usage ledger stored in R2 at __usage/<owner> (a conditional-put CAS object, mirroring the existing __views/__handles precedent — no Durable Object, no new binding): free tier is 200 MiB total, 150 canonical pages, 20 MiB per file, and 60 publishes/hour. Enforcement measures the real request body (bounded-buffered so a streaming body can't exceed the cap) and rejects on the true size before any write, so a spoofed-low declared size can't bypass the caps or destroy an existing page. It fails loud — 413 for a file, object-count, or byte-quota overage, 429 (with Retry-After) for the rate limit — and refunds bytes + object count on delete and on lazy expiry. Covers/views are server-generated overhead and excluded from the quota. BYO (WRITE_TOKEN) publishes write to the operator's own bucket at their own cost and are unaffected (a deliberate, documented policy). A SHARE_PLANS map is the seam for future paid tiers (billing follow-up PHNX-3569). Source: cli/src/lib/share/worker-template.ts.

  • Let feed channel sinks customize their delivered body with existing post placeholders, including fail-closed {ticket} routing for clickable tracker links in team channels.

1.22.62

  • Owner notifications fan out across the configured normal-severity channels (PHNX-3567). agents send --to owner, deprecated agents notify, monitor notifications, and an important feed's owner sink now attempt every addressable entry named by owner.policy.normal in humans.yaml, instead of silently selecting only the first. Each Rush-backed destination that cannot deliver on a Linux worker forwards its explicit channel and target to a capable Mac, avoiding both shell quoting and policy re-expansion/duplicate sends. Partial failures stay visible while successful channels still deliver; legacy single-channel configs retain their old behavior. Source: cli/src/lib/humans.ts, cli/src/lib/notify.ts, cli/src/lib/channels/owner-forward.ts, cli/src/lib/feed-broadcast.ts.

1.22.61

  • agents harness wizard: model catalog, connection test, and edit matrix (PHNX-2218/2220/2221/2222). The create/edit wizard now picks the model from the host's own catalog (getModelCatalog) with a free-text escape hatch, gates the endpoint step to hosts that actually carry one, and runs a real pre-save connection test — agents run <name> "say alive in one word" --headless --timeout 60s classified into pass / auth / endpoint / model — behind a confirm with --test/--no-test, offering keep / edit / delete on failure rather than saving a broken harness silently. A resolver-sourced harnessEditable matrix disables (with a reason) any param the host's API format can't carry. Source: apps/cli/src/commands/harness.ts, apps/cli/src/commands/harness-wizard.ts, apps/cli/src/commands/harness-hooks.ts, apps/cli/src/lib/harness-connection-test.ts.

  • Cap per-machine Linear API spend with a shared per-key request budget (PHNX-2310). With ~13 concurrent drain agents on one Linear API key on a single box, nothing stopped them collectively exhausting Linear's 2,500 requests/hour limit and throttling ticket-status reads for every agent on that box. The agents projects card fetch now reserves against a proactive request budget keyed by the API key before each request; when the hourly pool is spent it serves the last cached snapshot (marked stale) instead of forcing a 429. The pool is lock-free on-disk state under the machine-local cache (one stamp file per request, a sliding one-hour window), so the many separate agent processes on a box coordinate without a shared mutable document to race on. Scope is per machine: the state is not fleet-synced, so two boxes sharing one key each budget independently — cross-device aggregation is a known limitation, not yet covered. This complements the existing read cache and reactive 429 backoff. Source: cli/src/lib/linear-rate-limit.ts, cli/src/lib/linear-project-counts.ts.

  • agents browser drives your RUNNING Arc instead of spawning a second one (PHNX-2399). Arc is single-instance: relaunching the Arc binary with a fresh --user-data-dir produced a stray window and no CDP endpoint rather than a debuggable instance. An Arc profile now attaches to the Arc you already have open (point its endpoint at the port you launched Arc's remote debugging on), and when that Arc exposes no CDP endpoint on the profile's port it fails loud with the one relaunch that fixes it (open -a Arc --args --remote-debugging-port=<port>) — it never silently launches a duplicate. Bind the profile to the tab/Space you want to drive with --target-filter (accepted for --browser arc, not just --electron): it is consulted when Arc picks which existing tab to drive, so navigate reuses your bound Space instead of an unrelated tab, and refuses rather than borrowing one when nothing matches. The documented first-use flow — agents browser navigate --profile arc --url … on a fresh profile — now attaches to an open tab instead of erroring. Opening a brand-new tab remains the one CDP-only op Arc can't survive (Target.createTarget crashes it), so tab-creating verbs fail clearly and steer you to reuse an existing tab or a Chromium-family browser. Source: cli/src/lib/browser/drivers/local.ts, cli/src/lib/browser/service.ts, cli/src/commands/browser.ts, cli/src/lib/open-url.ts.

  • agents sessions watch --json rows now carry a canonical phase (PHNX-2484). Each live/recoverable session row projects a coarse lifecycle bucket — running | waiting | failed | done | idle — derived once at the source from the finalized status (derivePhase in cli/src/lib/session/active.ts), so thin-client consumers (the AGI EXT Fleet panel) read phase off the row instead of re-deriving it from the status word. It also fixes a drift a status-only re-derivation had: orphaned and crashed now bucket to failed (dangling/dead — needs attention) rather than falling through to idle and hiding a dead agent.

  • balanced/available routing never auto-picks an account from entirely stale usage (PHNX-2526). When every signed-in account's usage snapshot is older than the 5-minute decision horizon and none can be verified, the initial route no longer guesses on a stale number (the yosemite-s1 incident, where a 26h–2.7d-old snapshot read 48% while the account was at its weekly cap). An interactive run now shows the account picker so a human chooses with the stale numbers in view; an unattended run (--headless/--json/no TTY, or a routine) fails loud with NO_VERIFIED_USAGE. The stale candidates are preserved only for bounded post-rejection failover, never the first pick. A pool that is merely blind (no snapshot at all — a worker box whose usage endpoint 403s) is unaffected and still draws a pick. Source: apps/cli/src/lib/accounting/rotate.ts, apps/cli/src/commands/exec.ts, apps/cli/src/lib/daemon/runner.ts.

  • Raise SSH ControlPersist to 10 minutes so repeated fleet touches reuse warm connections (PHNX-2582). The OpenSSH multiplex master now lives for 10 minutes after its last use (SSH_CONTROL_PERSIST_SECONDS) instead of 60 seconds. The master only helps a repeated touch of the same host that arrives while it is still alive, and the ad-hoc --device / fan-out calls that touch a box repeatedly (sessions --active, fleet ping, doctor, teams, a --device command run a few times) arrive in bursts spread over minutes — so at the old 60 s window any two more than a minute apart both paid a fresh TCP+auth handshake (~100-300 ms direct, up to ~500 ms relayed). 10 minutes keeps a multi-minute burst warm (~3-5 ms per reuse) while staying bounded so an idle master to a slept host is reaped rather than lingering. Source: cli/src/lib/ssh-exec.ts.

  • A running agent whose owning IDE window died is now surfaced as orphaned (PHNX-3183). foldHostLink promoted only idle/input_required sessions to orphaned; a still-running agent was never flagged, so a genuinely-stranded remote agent — alive in tmux after its owning window crashed, rebooted, or dropped its SSH link — stayed hidden as a healthy "running" row. It now promotes a running session to orphaned when, and only when, its owning window was LOST: the window's registry slice went stale past HOST_HEARTBEAT_STALE_MS after having republished it (hostWindowLost). Client ABSENCE alone still never flags a running agent — a tmux attached-client count of zero is the normal steady state for a detached agents run --device pane (RUSH-3125), so promoting on it would relabel every unattended remote agent as orphaned (the over-report reverted in 6d973b823). This narrows spec SES-18a to the window-loss case rather than the blanket precondition-drop that was reverted. Source: cli/src/lib/session/host-link.ts, cli/src/lib/session/active.ts, cli/docs/specifications.md, cli/docs/sessions.md.

  • Teammates can no longer silently self-merge their own PRs (PHNX-3236). A write-capable agents teams teammate authenticates as the repo owner, so it could merge its own PR straight past the required non-author-review gate (the RUSH-2988 wave-1 dispatch self-merged PRs #1817/#1820 with zero reviews). Team dispatch now appends a self-merge boundary (TEAMMATE_PR_POLICY, via one withTeammatePrPolicy helper) to every write-capable (non-plan) teammate's prompt, fresh or resumed, at all three dispatch surfaces — local, --device remote (both via buildRunArgv), and --cloud (cloudDispatchOptions) — so none can drift: open the PR and hand it off; do not merge your own PR without a posted non-author verdict. The hard enforcement is merge-guard.sh, the PreToolUse hook a teammate inherits, whose verdict check now excludes the PR author's own reviews/comments (phnx-labs/.agents #395) — but it only reaches hook-capable local/remote teammates; cloud teammates (provider sandbox, no inherited hook) and hook-incapable harnesses (Warp) get the prompt as their only layer, a residual documented in cli/AGENTS.md §6 that server-side branch protection closes. Source: cli/src/lib/teams/agents.ts, cli/src/commands/teams.ts.

  • Blanket Bash now auto-approves shell on Grok, so fleet agents stop punting on ssh/scp/agents ssh (PHNX-3294). A blanket Bash grant was translated to a Grok rule with pattern:"*", but Grok's * is a SINGLE-level wildcard, so it never auto-approved a multi-token command like ssh host cmd or scp a b — a Grok agent on a box without permission_mode=always-approve prompted and handed the command back to the user. Blanket Bash (and its Bash(*)/Bash(**) forms, allow and deny) now emit Grok's documented "bare prefix matches all invocations" rule — a bash rule with no pattern key — the true allow-all-shell form, matching how Kimi, Droid, and Claude already express it. This reads back as Bash(*), so the round-trip is unchanged. Source: cli/src/lib/permissions.ts.

  • agents traces sync: per-session roster in index.json (PHNX-3483). The index shard now carries sessions — one flat scalar row per AGENT session (id, title, harness, model, repo, mode, projectType, startedAt, durationMs, toolCount, errorCount, needsAttention, best-effort costUsd) — so the Rush console can filter the session set and re-aggregate its headline metrics client-side instead of being stuck with the pre-rolled scalars. durationMs is the ACTIVE duration (sessionActiveMs, the value behind stats.medianMs; 0 when unmeasured) and mode encodes the AGENT-vs-INTERACTIVE segmentation (headless/interactive) so a mode-split median over the measured rows reproduces stats.agentMedianMs/stats.interactiveMedianMs (the segmented stats skip null-duration rows, which the roster still carries at durationMs: 0). Utility rows are excluded, so the roster length equals stats.sessionsImported. Source: apps/cli/src/lib/traces/sync.ts.

  • agents insights perf run now actually records agent.run timings (PHNX-3497). The startup sub-phase surface added in PHNX-3468 had no data on a foreground run: recordPerfTiming wrote the perf-spool row behind a fire-and-forget import().then(...), and the agents run process exited before that deferred write landed — so every foreground run's agent.run sample was silently lost. The spool append is now synchronous (recordSample is an appendFileSync that never opens SQLite), so agent.run rows and their startup p50/p90 phase line persist and show up. Source: cli/src/lib/feed/events.ts.

  • gh pr checks now escapes the shared GraphQL rate limit automatically — no new command to learn (PHNX-3501). The whole fleet shares one GitHub token, and the merge loop's gh pr checks/view/list are all GraphQL-backed, so the fleet collectively drains GitHub's 5000-point/hr GraphQL budget while REST core sits ~95% idle — after which every agent's gh pr checks --watch dies with GraphQL: API rate limit already exceeded. A generated PATH shim (like the existing browser shim) now transparently intercepts the agent's trained gh pr checks and routes it to a hidden agents __gh verb backed by REST (commits/{sha}/check-runs + /status, anchored to the PR's live head SHA); every other gh verb execs the real binary byte-for-byte. --watch owns the REST poll loop (head-SHA-anchored, so a superseded run's red can never be reported — closes PHNX-3042); a one-shot runs real gh first and falls back to REST only on the exact rate-limit stderr. The shim self-heals — a leftover shim execs real gh whenever agents-cli is gone or uninstalled, so it can never break gh. Installed on agents sync (POSIX in v1). Source: cli/src/lib/github/rest.ts, cli/src/lib/github/gh-overload.ts, cli/src/lib/installations/shims.ts.

  • agents doctor now detects content drift for mcp, permissions, subagents, workflows, and memory (PHNX-3504). The resource-diff engine compared these kinds by NAME only (or, for workflows/memory, not at all), so an edited MCP command/args/env, a swapped permission rule, an edited subagent prompt, a changed workflow, or an edited knowledge-memory fact all read as a false ok under an unchanged name. Every kind syncResourcesToVersion writes is now content-aware: mcp structurally compares the home server def against the resolved source; subagents re-render the source through the registry transform and byte-compare; workflows compare layout-aware per harness (copied tree or transformed file); memory (~/.agents/memory/*.md facts, tracked via the .agents-cli-memory.json manifest) byte-compares each fact; permissions compare per-rule in the harness's native vocabulary for the representable harnesses (claude/opencode/cursor/droid/openclaw) and stay presence-only with an honest detail: 'format cannot verify content' for the lossy TOML/flag harnesses rather than faking ok. workflows and memory are now real --kind filters; promptcuts (not version-scoped) is dropped. A completeness test binds DOCTOR_ALL_KINDS to the writer set so a future synced kind cannot silently become a blind spot, and agents doctor --fix reaches the newly-covered kinds. Source: cli/src/lib/doctor-diff.ts, cli/src/lib/resource-content-diff.ts, cli/src/lib/subagents-registry.ts, cli/src/lib/mcp.ts, cli/src/lib/permissions.ts, cli/src/lib/workflows.ts, cli/src/lib/heal.ts, cli/src/lib/devices/doctor-findings.ts.

  • A balanced/available route now records WHY it picked an account, so a bad pick is debuggable from agents events alone (PHNX-3564). rotation.resolved / rotation.unresolved used to carry only { version, healthy: <n>, excluded: <n> } — a device-local version number (meaningless across the fleet) and two counts. When a run landed on a weekly-maxed, logged-out, or absent account (PHNX-3505 / PHNX-3479), the event could not say which org, how fresh its usage was, or why it won the draw, so every occurrence was debugged blind. buildRotationDecisionEvent (cli/src/lib/accounting/rotate.ts) now emits a full decision record: picked { usageKey (per-org quota key), email, tier }; a pickReason read straight from the router's own result (verified-weighted for the healthy path, unverified-<tier>-draw for a blind/stale fallback, refused-no-verified for the fail-closed exit); a per-candidate map { usageKey, accountKey, providerAccount, tier (verified/stale/blind), source, capturedAt, ageMs, windows, eligible, excludedReason, credentialVerdict } keyed by pool index (not version or usageKey, which a RUSH-3182 provider-account pool shares across rows); and a freshness tally { verified, stale, blind } over the healthy pool. usageKey is the org uuid — the only identity that joins the same account across devices — and ageMs is measured by the routing host's clock, so a last_seen snapshot showing tier: verified beside a large ageMs is the cross-host clock-skew failure made visible. Two details are dictated by the event sink's generic sanitizer (feed/events.ts): candidates are a keyed object (not an array) because arrays are truncated to 10 entries, and the verdict field is named credentialVerdict (not authVerdict) because any key matching /auth/i is redacted — an end-to-end test drives a real emit() into a redirected sink and asserts the persisted line to lock both in. This event now also carries account emails to the local-only event log (~/.agents/.history/events, 0600) — a new local PII surface for this event type. Pure observability: no routing-logic change, and every field is read from data the router already computed; the build+emit is wrapped so an observability bug can never abort a launch. Live-verified on zion (8 claude accounts: 1 verified, 7 stale — every synced row already past the 5-min decision window on arrival). Source: cli/src/lib/accounting/rotate.ts, cli/src/lib/accounting/rotate.test.ts.

  • agents reminders + Claude statusline reminders. Personal operating reminders kept in ~/.agents/reminders/reminders.yaml (each a short/full pair) now surface succinctly in the Claude statusline — one per session, chosen deterministically from the session id so concurrent agents each show a different one and it stays stable within a session. Presence of the file with at least one entry is the opt-in; a malformed file is swallowed by the statusline (a broken prompt is worse than a missing line) but surfaced by agents reminders. The file syncs across the fleet via agents repo push/pull. Source: cli/src/lib/reminders.ts, cli/src/commands/reminders.ts, cli/src/lib/claude-statusline.ts.

1.22.60

  • Richer schema-2 trace shards (PHNX-3442). The per-session trace shard the evals console reads is now a typed ToolExecution discriminated union instead of a thin SessionStep with one output blob. Bash steps carry per-action argv, effective program, build/test/git/network categories, and a conservative danger/DESTRUCTIVE classification (recursive-force-delete, git reset --hard, git clean -f, force push incl. the +refspec form, git checkout -- ., DROP TABLE/TRUNCATE/DELETE-without-WHERE, kill -9, dd of=, mkfs, redirects over block devices); edit/write steps carry file mutations, diff hunks, and a cross-step revert ledger (only an exact hash round-trip stamps revertedByStep, never a partial edit); read/grep steps carry counts; permission/hook events are first-class steps. It emits by default with no flag: the prix/web console already decodes both schema 1 and schema 2 and is the only shard consumer, so the change is backward-compatible by construction — a fleet-wide producer must not depend on an operator setting an env var. The bash unwrap/tokenize/classify, trajectory step-pairing (pairSteps), and shard meta helpers are reused, not reimplemented. category/risk/categoryMetrics are deliberately omitted — the consumer backfills neutral defaults. Source: cli/src/lib/traces/schema2.ts, schema2-build.ts, schema2-danger.ts, cli/src/lib/traces/sync.ts, cli/src/lib/session/trajectory.ts.

  • Detect stranded uncommitted work in agents teams status (PHNX-2951). A teammate that exits COMPLETED with no PR and uncommitted changes in its local worktree now reports STRANDED instead of done, and the status line names the worktree path so the work can be rescued before cleanup. The stranded count is computed over the full team even when --filter narrows the displayed agents, so agents teams status <team> --filter running still surfaces stranded completed teammates. Team rollups and teams list --status classify these teams as stranded, not done. Source: cli/src/lib/teams/delivery.ts, cli/src/lib/teams/api.ts, cli/src/commands/teams.ts.

  • Track agent-launch boot cost: persist + surface the startup phase (PHNX-3468). agent.run already timed a startup phase (entering spawnAgent → child spawn) but only the total run duration reached the perf warehouse, so boot cost was untrackable across the fleet. The timer now persists its sub-phase marks into each perf.timing sample's meta_json, aggregateSamples folds them into a per-label phases break-out (p50/p90 over the samples that carried each phase), and agents insights perf run renders a └ startup: p50 … p90 … (n=…) sub-line under agent.run. This is the measurement the boot-perf work is graded against. Source: cli/src/lib/feed/events.ts, cli/src/lib/perf/db.ts, cli/src/lib/perf/types.ts, cli/src/commands/perf.ts.

  • agents sessions preview <id> now pulls the transcript digest from the owning device for host-dispatched sessions (PHNX-3481). A host dispatch leaves a synthetic local index row with machine = <peer> and an empty filePath, but that row does not carry the live fan-out's _remote marker. Full-UUID preview resolved the local row and then treated it as a local live session, rendering "full transcript not indexed here" without making any SSH hop. Transcript reads now follow one file-aware predicate: an explicit remote row still reads on its peer; a row naming another machine also reads there when no transcript exists on this disk; and a synced mirror with a real local file still renders locally. The direct preview, sessions <id>, picker view, and picker/browser digest pane all share that rule. Live-registry metadata now also stamps _remote when its execution machine differs from this box, and an unreachable owner remains a loud error instead of a local placeholder. Source: cli/src/commands/sessions-picker.ts, cli/src/commands/sessions.ts, cli/src/lib/session/live-metadata.ts.

1.22.59

  • agents devices disable/prefer now actually change --device auto placement (PHNX-2092). The per-device auto-launch.enabled / auto-launch.preferred flags (written by agents devices disable/enable and prefer/unprefer, and by agents devices config <name> auto-launch.*) were stored and synced but never consulted by the CLI's one placement path, so a disabled box was still picked and a preferred box got no boost. They now feed the single automatic-placement rule: filterAutoPool drops any device with auto-launch.enabled = false from EVERY auto path (run, teams, ssh auto, the AGI EXT launch commands, which resolve placement through the CLI) exactly as a personal/desktop role does, and pickBestDevice ranks an auto-launch.preferred device ahead of its load-equal peers — after the signed-in tier, before load, so an operator boost overrides load-based ordering without overriding hard health. A fleet-wide default (--fleet) reaches doc-less devices via the candidate roster. No second placement path was added — the existing filterAutoPool/pickBestDevice rule was extended. Source: cli/src/lib/devices/pool.ts, cli/src/lib/teams/scheduler.ts, cli/src/lib/smart-launch.ts.

  • agents sessions stats coverage now reports scan coverage, not with-usage, so a completed backfill clears the "run the backfill" hint (PHNX-2301). The coverage line read sessionsWithUsage / sessionsIndexed — a ratio that stays near-zero (~1.2% on a real fleet) even after agents sessions backfill resources has fully run, because most sessions genuinely invoke no skill/slash-command and a non-recording harness contributes none by construction. The nag therefore never cleared and the low number read as a coverage bug when it was not. resourceUsageCoverage() now also returns scanned — the count of sessions carrying a resource_scan_ledger row at the current RESOURCE_INDEX_VERSION, the honest "has the backfill folded in history?" signal (the ledger is stamped for EVERY scanned session, including ones that invoked nothing). The human line shows both — N/T sessions scanned · M carry an explicit invocation — and the backfill hint keys on scanned/total, so it appears only when history really is un-scanned. The --json coverage object gains sessionsScanned alongside the unchanged sessionsWithUsage/sessionsIndexed (SES-IF-4b envelope preserved; schemaVersion bumped to 2), and signal.recording now names the recorded set (skills: claude+kimi, commands: claude) so a machine consumer can tell a zero from a non-recording harness apart from genuine non-use. The zero-invoked heading reads "never explicitly invoked" to match. Recording coverage itself is unchanged — extending the Skill-tool set to another harness needs a verified transcript tool-name, not a blind capability-table edit. Source: cli/src/lib/session/db.ts (resourceUsageCoverage), cli/src/commands/sessions-stats.ts.

  • Menu bar: favorite (pin) devices below the current Mac (PHNX-2376). Each device's submenu in the menu bar's DEVICES section now has a ★ Favorite / Unfavorite toggle. A favorited device renders with a ★ and sorts immediately below the current machine (<name> (this Mac)), above all other devices, with a divider between the favorites and the rest. "Favorite" reuses the EXISTING per-device auto-launch.preferred config that automatic placement already honors (PHNX-2092) — the toggle writes agents devices config <name> auto-launch.preferred on|off, not a new store — so a box favorited from the menu bar is also boosted in --device auto placement, and vice-versa. Source: cli/menubar/Sources/MenubarHelper/StatusItemController.swift, cli/menubar/Sources/MenubarHelper/AgentsCLI.swift.

  • Built-in monitors ship visible + enabled, monitors list is fleet-aware, and built-ins are tagged (built-in) (PHNX-2506). A monitor shipped in the system mirror (~/.agents/.system/monitors/) used to be the lone system-layer resource that shipped disabled and untagged — readMonitorFile special-cased system scope to enabled: false, so a shipped built-in read off on every install and nobody could tell it came from the system layer. It now defaults to enabled like every other system resource (rules, hooks, commands, skills), on for every install unless the user shadows it with enabled: false (via agents monitors pause, which materializes a user copy — the system mirror stays pull-only; there is deliberately no enable/disable verb). Each monitor now carries its read-time scope (user/system), so agents monitors list tags a built-in (built-in) and --json emits scope + builtin, mirroring routines. And agents monitors list now fans out across the fleet (the same gatherRemoteAgentsJson sweep sessions --active and the add-time duplicate guard use), so every monitor on every device is visible with the box it lives on — the original "monitors shouldn't be device-local" complaint was a visibility gap, not a git-sync one (monitors are per-work-item watchers and are deliberately NOT synced through the DotAgents repo). --local pins the listing to this device; a peer answering the fan-out reports itself only, so the duplicate guard's bare-array contract is unchanged. Enabled-by-default is not, on its own, permission to fire on every daemon (SING-9a). A shared-input built-in — one whose source polls a fleet-shared queue such as gh pr list --author @me, pr-merge-on-green being the canonical case — is placed on a single owner in code, not just by a device: pin in the shipped YAML: an unpinned scope: system monitor is treated as shared-input unless it sets sharedInput: false, and fires only on the resolved owner (interactive.host, else the sole box on a single-device fleet, else nowhere) via requiresSingleOwner / monitorRunsOnThisDevice. So even a built-in whose YAML forgot the pin can never fan out across the fleet and race to merge the same PR. A device-local built-in (input = the firing box's own state) opts back into fleet-wide firing with sharedInput: false; a user monitor keeps its fleet-wide default and opts into owner-only with sharedInput: true. Source: cli/src/lib/monitors/config.ts, cli/src/lib/monitors/remote.ts, cli/src/commands/monitors.ts, cli/src/lib/state.ts.

  • A version-home plugin left behind by a marketplace move is now reconciled away instead of shadowing the current copy (PHNX-2618). Plugin orphan detection keyed on plugin name alone, but a version home installs plugins per marketplace (marketplaces/<name>/plugins/<plugin>). When a plugin moved marketplaces — e.g. code once shipped from the user repo (the agents-cli marketplace) and later from the system repo (agents-system) — the stale agents-cli copy was never cleaned, because the name code was still active via agents-system. Fleet boxes then carried two code plugins: the current one, plus a shadow serving skills the repo deleted on purpose (code:quality / code:ship / code:verify), and resolution order decided which code:review an agent got. cleanOrphanedPluginSkills / diffVersionPlugins now key on the (marketplace, name) pair: an installed copy whose marketplace source repo is present but no longer ships that plugin is trashed (soft-deleted to ~/.agents/.trash/plugins/), even when another marketplace still ships the same name — so a plain agents sync reconciles the shadow away. When a marketplace's source repo is absent (a project we're not in, a removed extra repo) the original name-only test is kept, so an unrelated sync from another cwd never trashes a plugin whose source simply is not reachable right now. Source: cli/src/lib/plugins/plugins.ts (cleanOrphanedPluginSkills, diffVersionPlugins, isOrphanMarketplacePlugin), cli/src/lib/staleness/writers/plugins.ts, cli/src/lib/installations/versions.ts.

  • agents sessions --device all/fleet --json now actually searches the whole fleet (PHNX-2673). The documented "search the whole fleet" sentinel was filtered to an empty host set — correct for --active and the interactive listing, which fan out by default — but the HISTORICAL --json listing does not fan out by default (it stays a deterministic local slice for scripts), so the sentinel was silently dropped and a fleet-wide historical query returned local rows only. Naming devices explicitly (--device box-a --device box-b) reached the fleet; --device all did not. The --json listing path now remembers the sentinel and runs the SAME peer SSH sweep the interactive listing uses (gatherRemoteList over the registered online devices, whole-index per peer), merging peer rows in machine-first. A bare --json (no --device) stays local-only, --local still pins to this machine, and dead peers are skipped, never fatal. Source: cli/src/commands/sessions.ts.

  • agents pty now works on macOS/arm64 and Node 25/26 (PHNX-2740). The pinned @homebridge/[email protected] shipped no darwin-arm64 prebuild above Node 24 (ABI 137), so on a Mac running Node 25 (ABI 141) or 26 (ABI 147) the native binding never resolved and every pty command died with a bare Cannot find module '.../pty.node' MODULE_NOT_FOUND. Bumped to 0.14.1, which publishes darwin-arm64 (and darwin-x64) prebuilds through ABI 147, covering current Node. The PTY sidecar now also fails loud when the binding genuinely can't load: instead of a raw MODULE_NOT_FOUND it prints the platform, the running Node ABI, and a concrete remediation (npm rebuild @homebridge/node-pty-prebuilt-multiarch / reinstall the CLI), preserving the underlying error. Source: cli/package.json, cli/src/lib/pty-server.ts.

  • An upgrade can no longer strand a box with the package installed but no working agents command (PHNX-2768). agents upgrade (and every agents fleet update box, which runs it) now owns the global bin links: after installing and verifying the new version, it checks that <prefix>/bin/{agents,ag,browser,computer} resolve to the freshly-installed copy and restores any the package manager dropped. This closes the failure that left zion upgraded to 1.22.40 with /opt/homebrew/bin/{agents,ag,browser,computer} gone — every agents invocation "command not found" until the links were relinked by hand. It covers the sibling entrypoints (ag/browser/computer), not just agents. A link it cannot make resolve fails the upgrade loud (non-zero exit), so a genuinely-broken box is reported failed by the rollout instead of a stranded ok/unverified. Scope: the npm-prefix POSIX layout — bun and Windows use their own bin shims. Source: cli/src/lib/self-update.ts (ensureGlobalBinLinks), cli/src/bootstrap.ts, cli/src/commands/ssh.ts.

  • The auto-pulled system repo is verified against its expected origin before a fast-forward — a repointed origin is refused, not executed (PHNX-2957). The system repo (~/.agents/.system/) ships hooks that register as shell command strings run on every tool event, and its checkout auto-fast-forwards from origin (on agents use, and on the opt-in AGENTS_AUTO_PULL=1 background worker). Neither path verified that origin was the repo the operator actually chose — so an origin repointed to an attacker's fork (or a clone seeded from one) would silently fast-forward arbitrary hook code that then ran as shell on the next command. Both pull sites now route through tryAutoPullSystemRepo, which pulls only when origin is the canonical system repo (isSystemRepoRemote) or the exact AGENTS_SYSTEM_REPO the operator pointed at; an unexpected origin is refused loud (agents use prints the offending remote and the re-point/AGENTS_SYSTEM_REPO fix), never pulled. The canonical system repo and a legitimate AGENTS_SYSTEM_REPO override still sync exactly as before — no regression to trusted pulls. Source: cli/src/lib/git.ts (isExpectedSystemRepoRemote, tryAutoPullSystemRepo), cli/src/commands/versions.ts, cli/src/lib/auto-pull-worker.ts.

  • agents monitors add --watch-pid <pid> (PHNX-3023). A reliable, daemon-polled watcher for a backgrounded process — the fix for "will re-invoke me" watchers that never fire because a harness's exit hook only wakes the agent when the watched process dies, and a watch loop (gh pr checks --watch, a long sleep, a tick poll) never itself exits. --watch-pid has the monitor engine's own poll loop check the pid's liveness independent of the arming session, defaults its condition to fire on exit, and fails loud at creation time when the pid is already dead instead of silently arming a watcher that can never fire. Source: cli/src/lib/monitors/pid-watch.ts, cli/src/commands/monitors.ts.

  • agents doctor --fix now reconciles hooks / permissions / subagents / rule aliases on Windows instead of an unactionable "hold" (PHNX-3187). On win-mini --fix healed commands/skills/rules/plugins but reported hooks, permissions, subagents, and the CLAUDE/GEMINI rule aliases as "couldn't reconcile" and left the box permanently red — with real guards (git-guard, rm-guard, secrets-guard, ask-user-question-guard, public-artifact-guard) uninstalled. Four Windows-specific defects, each fixed at its source: (1) subagents — parseSubagentFrontmatter/getSubagentBody split on '\n' and compared the fence with === '---', so a git-CRLF-checked-out AGENT.md ('---\r') was rejected and the subagent silently dropped from discovery, so it could never be installed; now CRLF-robust (/\r?\n/). (2) permissions — buildPermissionsFromGroups extracted rules with a line regex anchored on the closing quote ("$), which a trailing \r broke, extracting zero rules and writing an empty permission set; now CRLF-robust. (3) rule aliases — git checks the rules/CLAUDE.md and rules/GEMINI.md symlinks out as plain text files on Windows, so lstat().isSymbolicLink() was false and the diff treated them as independent rule sources no sync could ever produce; a new isCheckedOutSymlink detector skips them on every platform. (4) hooks — the heal pass fed the resource diff's extensionless hook names (git-guard) back to the sync writer, whose available.hooks set carries the source filename with its extension (git-guard.sh), so the exact-set match found nothing and no flagged hook could be written; a new basename-tolerant resolveHookSelection maps them, and the orphan sweep still prunes any stale extensionless hook copy left in a version home (one would be invisible on Windows, lacking both an extension and an exec bit). The subagents writer also now surfaces a per-item write failure with its reason instead of swallowing it in a bare catch, so a genuine failure fails loud rather than reading as a silent "hold". Source: cli/src/lib/subagents.ts, cli/src/lib/permissions.ts, cli/src/lib/doctor-diff.ts, cli/src/lib/installations/versions.ts, cli/src/lib/staleness/writers/subagents.ts.

  • Fix the agents-cli discovery skill/plugin install commands to use the real GitHub repo path phnx-labs/agi-cli (they pointed at phnx-labs/agents-cli, which only resolved via GitHub's rename redirect). The npm package stays @phnx-labs/agents-cli. Also maps the plugin manifests + skill to agents-cli-plugin.test.ts in CI impact analysis so a manifest/skill edit runs its test on the PR. (PHNX-3337 review follow-up)

  • Ship a cross-harness agents-cli discovery skill + Claude plugin marketplace (PHNX-3337). New skills/agents-cli/SKILL.md is an authoritative skill whose description carries the exact intents a developer types — run multiple coding agents in parallel, manage multiple Claude Code accounts, I hit my usage limit, resume a session on another machine, pin the agent CLI version — each with a verified agents command recipe (teams, accounts, run --fallback/-b/auto, sessions resume, add/use). A repo-root .claude-plugin/marketplace.json + .claude-plugin/plugin.json make the repo installable via claude plugin marketplace add phnx-labs/agents-cli and npx skills add phnx-labs/agents-cli; the plugin's source: "./" bundles the discovery skill plus the existing per-command skills. Harness parity is registry-driven, not per-harness copies: the one SKILL.md is authored once and the existing capability-gated skill sync (supports(agent, 'skills', …)) fans it into every skill-capable harness home. claude plugin validate . passes on the committed manifest, and a real (no-mock) test reads the repo-root files and runs the CLI's own validateClaudePluginManifest. Source: skills/agents-cli/SKILL.md, .claude-plugin/marketplace.json, .claude-plugin/plugin.json, cli/src/lib/plugins/agents-cli-plugin.test.ts, README.md.

  • agents cloud providers no longer reports Rush as ready when the session token is expired (PHNX-3382). capabilities().available checked only that ~/.rush/user.yaml existed — it returned true even for an expired session, so the provider appeared ready but every dispatch failed with a cryptic HTTP 401. readToken() also silently returned an expired token, giving the same bad error on every cloud call (dispatch, status, list, stream, cancel, message). Both paths now check expires_at (Unix seconds): capabilities() returns available: false for a missing file, missing token, or expired session; readToken() throws Rush session expired at <ISO>. Run 'rush login' to refresh. — an actionable message instead of a 401. The exported isRushSessionValid(yamlPath?) helper is testable in isolation. Source: cli/src/lib/cloud/rush.ts.

  • Traces: session duration is populated for every harness, and the console duration median is active-time, not calendar span (PHNX-3457). Only claude/codex/droid/gemini/opencode ever derived a duration_ms at scan; rush/grok/kimi/cursor/muse/antigravity left it NULL — 52% of sessions, 100% of the dominant rush usage — so the Evals console median was computed over only the ~48% that carried it and skewed misleadingly short. resolveDurationMs now fills the span at the single upsert boundary from the timestamps the row already stores (a v43→v44 migration backfills existing rows in place, no re-parse), so every harness gets a span. The agents traces sync index shard's medianMs/p90Ms now run over active time — span minus every idle gap > 120s (before the first tool call, between calls measured from each call's end, and after the last call to the session end), derived from the ordered tool_calls already loaded — so a session resumed after hours or left idle mid-turn no longer inflates them (it killed a 345h calendar-span outlier). The stat keys are unchanged, so the fleet-aggregate worker keeps averaging them; the raw span stays available per session as SessionDetail.meta.spanMs, which also gains activeMs. Source: cli/src/lib/session/db.ts, cli/src/lib/traces/sync.ts.

  • Traces: treemap tiles are drillable — each topic bucket carries example session refs (PHNX-3408). The agents traces sync index shard now emits up to 30 most-recent sessions ({id,title}) per topic bucket, so the Evals console can drill from a treemap tile into that category's session list instead of rendering every tile display-only. The tile's count stays the true total. Source: cli/src/lib/traces/sync.ts.

  • agents run --device auto no longer lands a launch on a fleet box that is logged out for the target harness (PHNX-3466). The --device auto placement gate already excluded a device whose harness reads signed-out — but it judged a REMOTE candidate by agents view --json's display signedIn, which is true whenever the box's active/global HOME carries a login even if the per-version home the isolated run actually launches has no credential of its own. The LOCAL candidate, by contrast, used the strict per-version launch truth (collectRunCandidates → isLaunchableSignedIn). So a worker whose selected harness version home was not launchable passed the remote gate, got picked, and the launch died at spawn — from AGI EXT the dispatched tab exited 1 and vanished. agents view --json now emits a per-version launchable field (the same isLaunchableSignedIn signal the local path uses), and remote placement (viewAgentAccountEligibility) gates on it, so both paths agree. A device with no launchable account for the harness is excluded from the --device auto candidate set; when that empties the pool the existing fail-loud no healthy device error fires instead of a silently-lost launch. An older remote CLI that omits launchable falls back to signedIn, so a rolling fleet does not regress. No second placement path was added — the single CLI gate is hardened, so both the CLI and AGI EXT (which delegates placement to it) benefit. Source: cli/src/commands/view.ts, cli/src/lib/view-types.ts, cli/src/lib/hosts/ready.ts.

  • agents traces sync now emits SEGMENTED active-time medians so the console can headline agent runs, not the corpus blend (PHNX-3472). 63% of sessions are one-shot queries (≤2 messages, ~15s active), so the blended medianMs/p90Ms sat at ~15s while substantial agent runs have a ~15-minute median — the blend headlined neither. The index-shard stats now classify each session as an AGENT run (any tool call OR more than 8 messages) or INTERACTIVE, and carry agentMedianMs/agentP90Ms (active-time median/p90 over agent sessions), interactiveMedianMs, and measuredFraction (share of sessions with a non-null duration) alongside the unchanged blended medianMs/p90Ms. Reuses the PHNX-3457 active-time computation (span minus idle gaps > 120s); no calendar span reintroduced. Source: cli/src/lib/traces/sync.ts.

  • agents traces sync classifies session kind and EXCLUDES internal utility calls from the Evals corpus (PHNX-3474). ~68% of the raw corpus is machine plumbing — single-shot calls (no tool call AND ≤2 messages) plus known internal-prompt signatures (title generation, watchdog ticks, commit-message writes, factory workers), all spawned under the claude harness by the Rush app — and it poisoned every console statistic (count, median, need-attention, tool-error-rate). Each session now carries a kind (utility vs agent, classifySessionKind in traces/sync.ts) and every index statistic is computed over the AGENT set ONLY: sessionsImported is the real agent count (not the raw row count), and the medians / needAttention / toolErrorRate / topic-bucket counts all exclude utility. A new top-level utilityCount reports how many were dropped. Each session ref (topic-tile sessions[] and needsAttention) also emits kind and harness so the console can filter by both. Pure reclassification — utility rows are tagged and excluded at shard-build time, never deleted from sessions.db. Source: cli/src/lib/traces/sync.ts.

1.22.58

  • agents notify is deprecated in favor of agents feed post (PHNX-3323). The command still works for existing callers, but it now prints a stderr deprecation notice naming agents feed post as the replacement, and agents notify --help carries a [DEPRECATED] label and examples that use agents feed post. Source: cli/src/commands/send.ts.

  • A remote browser task survives a browser-daemon restart on the driving box (PHNX-2663). When the local daemon restarted, its in-memory task map was rebuilt from disk for local-CDP profiles but skipped ssh:// tunnelled tasks — so an agent driving a browser host over --device got "Unknown browser task" / "Tab not found" for a tab that was still alive on the far side, and gave up. attachRunningProfile now re-establishes the SSH tunnel and reconnects CDP for a tunnelled task during rehydrate (reusing connectSSH, which attaches to the already-running remote browser via isOwnTunnel — it never launches one), carrying the tunnel teardown so it can't leak across the next restart. A failed reconnect falls through to disk reconcile exactly like the local-CDP path. Source: cli/src/lib/browser/service.ts.

  • agents prune cleanup no longer over-reports source-present hooks as orphans (PHNX-2693). Orphan detection diffed the hook files in a version home against the registered-hook manifest, but sync copies helper / test / benchmark scripts into every version home alongside registered hooks without registering them — so each one (e.g. permission-handler, verify-work-state, *_test, benchmark_*) read as an orphan, and agents prune cleanup [hooks | --all] offered to trash ~1000 in-use files across version homes. It now diffs against the resolved source set (user + system + enabled extras hook dirs), so a hook present in any configured source is never an orphan, exactly as the command's own help promised — while a genuinely dead file (present in a home, absent from every source) is still flagged. Same detector backs agents doctor's orphan warning. Source: cli/src/lib/hooks/install.ts.

  • agents prune cleanup no longer offers to delete plugin-provided skills (PHNX-3185). The skill orphan detector compared a version home's skills against skills/ + .system/skills/ + extras only, and never credited plugin-bundled skills (plugins/<plugin>/skills, .system/plugins/<plugin>/skills) — so every skill a plugin legitimately installed (design, plan, review, blog, …) read as an orphan. On a real machine that was ~95% false positives, and agents doctor framed the same set as ⚠ orphans … → agents prune cleanup --all, so following the tool's own guidance destroyed the working skill library. The detector now credits plugin skill sources when deciding orphans, and the skill-source resolver finds them too so a plugin skill reads as up-to-date instead of drifted. A genuinely dead skill (no central, extra, or plugin source) is still flagged. Source: cli/src/lib/plugins/skills.ts, cli/src/lib/staleness/writers/sources.ts.

  • agents sync stops reporting ✓ reconciled while the drift it was asked to fix stays put (PHNX-3186, PHNX-2955). agents sync status over-reported four classes of phantom drift that the sync writer never creates and can never clear — so re-running sync forever printed success while a phantom "N drifted / N missing" stuck (42–46 drifted per version observed fleet-wide). diffVersionResources now mirrors what the writer actually installs: (1) directory docs (README/AGENTS/CLAUDE/GEMINI) in commands/ are documentation, not commands, so they are no longer counted as missing/drifted/orphan commands; (2) on command-as-skill agents (Codex ≥ 0.117, Kimi) a command whose name collides with a real same-named skill is reported present (the skill wins the shared skills/<name>/ slot and the wrapper is deliberately never written), not the phantom "missing"; (3) version-gated presence-only kinds (e.g. subagents below the harness floor) are capability-gated so a version that structurally cannot hold the resource is not reported missing it; (4) diffRules now finds an agent's non-.md instructions file (Cursor's .cursorrules), which an .md-only scan never saw, so the rules row stops false-reporting missing. On top of that, every agents sync now runs a post-reconcile verification: it re-reads each version it wrote into and, if any drift the reconcile was asked to fix survives, names the exact residual resources instead of printing a bare ✓ (the umbrella line reads ⚠ sync: reconcile INCOMPLETE, and --json carries residualDrift with ok:false; the exit code stays 0, matching the declined-write precedent so the fleet fan-out never discards the residual payload). Source: cli/src/lib/doctor-diff.ts, cli/src/lib/sync-status.ts, cli/src/lib/refresh.ts, cli/src/lib/sync-umbrella.ts, cli/src/commands/sync.ts.

  • A stale per-version plugin marketplace copy is now reported as drift instead of everywhere/ok (PHNX-2955). describePluginDrift (the content-aware plugin diff behind agents sync status / agents doctor / the post-reconcile verification) compared only the presence of a mirror's skills/commands, so a skill whose bytes went stale — an edit pulled into ~/.agents/.system but never refreshed into a version's marketplaces/… mirror — read as fully synced while agents ran the OLD skill text. It now content-compares each skill/command present in both central and the mirror and reports stale skill: <name> / stale command: <name>, so the drift is visible and a --yes reconcile (which rewrites the mirror) clears it. (Out of scope, left as follow-ups: the interactive Already in sync short-circuit does not yet consult this signal, and agents plugins update for marketplace-resolved plugins is unchanged.) Source: cli/src/lib/doctor-diff.ts.

  • agents sync on a box where ~/.agents is not a git repo adopts it in place instead of silently no-op'ing the pull (PHNX-3239). A partial install — runtime state present but no .git — made the umbrella repo-pull stage fail quietly while agents sync still reported success, so fleet dotfiles and resources never propagated and nothing said so. The umbrella now runs the same self-heal agents sync user does (adoptUserRepoIfNeeded): it materializes the tracked resource set from origin and continues, or fails loud with the reason (and the agents repo pull user <git-url> hint) when there is no recorded remote to adopt from — never a silent no-op that looks like success. Source: cli/src/lib/sync-umbrella.ts, cli/src/commands/status.ts.

  • agents browser sessions --profile <name> surfaces the profile's real task store instead of an empty one (PHNX-3317). A browser's live tasks and captures live under a composite runtime dir (<name>@<device>), but the sessions reader used the --profile value as a raw directory name and read the bare legacy <name>/ dir — which is empty on a machine where the daemon writes the composite key — so a driving agent saw "no captures" / "Unknown browser task" while 13 real tasks sat one dir over. The reader now resolves a profile to every cache dir that belongs to it via listProfileCacheDirs/keyBelongsToProfile — the same single rule status() and findTask already use (RUSH-2709) — so the bare name and its @<device> stores are unified in the listing. Source: cli/src/lib/browser/sessions-list.ts.

  • Failure phenotype folded into the traces insight-engine fingerprint via a persisted per-session cache (PHNX-3327). computeInsights() grouped cross-session failure patterns by (tool, cause, normalized-error) but did not carry a phenotype (false-termination / premature-completion / out-of-order / failure-to-act) — that classification needs the full derived trajectory, which flat tool_calls rows don't have. It is now computed per-session once (classifyPhenotype) and persisted in a new mtime+size-keyed session_phenotypes cache (same shape as session_topics / session_insights), then read for the whole corpus on every sync and folded in as a fourth grouping dimension. Reading the cache for all rows — never just this run's freshly-parsed batch — is what keeps two identically-signatured sessions in ONE failure cluster regardless of which incremental sync first saw each (the fragmentation an in-memory, batch-only map would cause). The signature output ({ tool, cause, key }) is unchanged; FailurePattern gains a phenotype field and the pattern id incorporates it, so the same signature under two phenotypes is two deep-linkable clusters. Source: cli/src/lib/traces/insights.ts, cli/src/lib/traces/sync.ts, cli/src/lib/session/db.ts.

  • agents doctor and agents setup no longer crash on macOS when the Keychain helper source is unavailable (PHNX-3385). Both read the reserved auth secrets bundle as an optional status check; on a macOS box where the bundled/local Keychain helper couldn't be found (a source checkout, or a fresh npm install before agents setup secrets downloads the signed helper — RUSH-3100 dropped it from the tarball), that check threw and took the whole command down with it instead of reporting the finding as unavailable. Source: cli/src/lib/secrets/bundles.ts (inspectReservedAuthBundle), cli/src/lib/auth-mint.ts (hasMintedSetupToken).

  • agents traces sync reports a truthful run outcome — recover-then-succeed is completed, not errored (PHNX-3387). SessionDetail.meta.outcome was derived as errorCount > 0 ? 'errored' : 'completed', so a run that hit a tool failure but recovered and finished the task was mislabeled errored — and the Evals console's "green run with hidden tool failures" callout (the LangSmith trap) could never fire honestly because surfacedToolFailures only existed when the run was already errored. Outcome is now derived from a causal-recovery predicate (recoveredAfterErrors, shared with the false-termination phenotype in phenotype.ts): a run with tool errors is completed only when a substantive, non-human-facing tool step succeeded strictly after the last error and resolved the failed work — its work signature (the effective program for a shell step, the tool identity otherwise) matches an errored step's. A run that ended in error, punted to a human (AskUserQuestion), or whose only post-error success is unrelated work (a failed bun test followed by an incidental ls) stays errored — no regression. surfacedToolFailures is retained on completed runs so the console can honestly show "recovered from these failures." Source: cli/src/lib/traces/sync.ts, cli/src/lib/traces/phenotype.ts.

  • --strategy balanced no longer routes to a weekly-exhausted Claude account on worker boxes; a missing usage snapshot is treated as unknown (fails closed) instead of full capacity (PHNX-3392). A worker's usage cache is empty (the setup-token lacks the user:profile scope, so /api/oauth/usage 403s — RUSH-2392), and capacityWeight(null, …) scored that absence as 100 — max weight — making a blind, actually-exhausted account the top pick. The null arm now draws UNVERIFIED_WEIGHT (1): any verified-healthy account outranks an unverifiable one, while an all-unverified pool still draws a pick. Pinned by spec GWT-E5c, a pure unit test (capacity.test.ts), and a real-cache-path regression test proving a persisted 7d-100% week window makes the account ineligible. Source: cli/src/lib/accounting/capacity.ts, cli/src/lib/accounting/{capacity.test.ts,rotate.test.ts}.

  • A worker pulling Claude usage from a Windows primary no longer silently blanks its cache (PHNX-3392 follow-up). The usage-sync pull path (pullUsageFromPrimary) now wraps the primary's JSON in stripClixml before parsing, exactly as every other remote-JSON boundary does. Without it, a headed Windows box serving __usage-export prepends a PowerShell CLIXML progress banner to stdout, every pull fails as malformed JSON, the worker's usage cache stays null, and the capacity floor becomes the only thing keeping --strategy balanced from picking a blind, exhausted account. Source: cli/src/lib/accounting/usage-sync.ts.

  • agents upgrade self-heals the orphaned npm reify staging dir that dead-ended it forever (PHNX-3393). npm stages an upgrade in a hidden sibling directory next to the running install, deterministically named from the install's path, then renames it into place; a crash mid-upgrade left that directory behind non-empty, and every subsequent upgrade's rename onto the same path failed ENOTEMPTY — recurring across boxes with no re-run ever clearing it. agents upgrade now sweeps any such orphan before reifying, and agents doctor --fix / the daemon's periodic self-heal do the same fleet-wide (with a 10-minute age guard so a concurrent in-flight upgrade is never touched). Source: cli/src/lib/self-update.ts (sweepStaleInstallStaging), cli/src/bootstrap.ts, cli/src/lib/self-heal/checks/install-staging.ts.

  • The Evals console's "all agents" view now renders real fleet data (PHNX-3397). The CLI writes traces per-device (<userId>/<hostname>/…), but the console asks for the fleet-wide <userId>/all/… view, which was never a stored key — so every request 404'd and the console fell back to sample data. The traces Worker now synthesizes /all on read: it lists the owner's device prefixes (one delimited list, not every session object) and merges their shards for all/index.json, and resolves all/sessions/<id>.json to whichever device holds that session. A single device is an exact passthrough; across devices, counts sum and failure patterns fold by id (median/latency are session-weighted approximations, exact when filtered to one device). No CLI change and no extra upload — the per-device shards already in R2 are the source. Owner auth is unchanged: /all still requires a Phoenix bearer whose id matches the path prefix. Source: cli/src/lib/traces/worker-template.ts.

  • agents traces sync no longer silently ships a stale Evals dashboard when the sessions DB is busy (PHNX-3401). buildIndexShard writes freshly-classified topics/insights back into the local sessions.db while building the console index. If another process (e.g. the Rush app) holds that DB, the write waits out the 30s busy_timeout and throws SQLITE_BUSY — which used to escape the build and get swallowed by a bare catch {}, so per-session shards uploaded but the aggregated index (wasted-time, failure clusters, latency) never refreshed and the console sat stale while sync reported a clean run. The cache write-backs are now best-effort (they only warm a cache the shard doesn't read from), and a genuine index build/upload failure is surfaced as SyncResult.indexError plus a ⚠ console index not refreshed warning instead of a silent success. Source: cli/src/lib/traces/sync.ts, cli/src/commands/traces.ts.

  • A release now redeploys the managed share OG-cover Worker so prod can't drift from the shipped template (PHNX-3403). The Worker that renders share preview cards was deployed only by a manual agents artifacts share update run, decoupled from the release train — so a release could publish a CLI whose worker-template.ts changed while the deployed Worker stayed stale (the gap that made PHNX-2835 look shipped while every new share still 404'd its cover). release.sh now takes --deploy-worker <auto|on|off> (default auto): after publish, on the home base, it redeploys the Worker from the just-published CLI when the shipped template differs from what the endpoint deployed (auto), always (on), or never (off). The decision is a pure local render+hash via the new agents artifacts share update --check (needs no Cloudflare credentials), so an unchanged template is a cheap no-op; the promote preflight verifies the share endpoint and cloudflare.com token on the home base before publishing, and a deploy failure fails the release loud with the manual fallback. Source: cli/scripts/release.sh, cli/scripts/promote-home-base-probe.sh, cli/src/commands/share.ts.

  • A browser task created over agents ssh <host> 'agents browser …' is owned by the real caller, not UNRESOLVED@<host> (PHNX-3405). The agents ssh browser-drive passthrough stamped the fleet-remote consent marker (AGENTS_FLEET_REMOTE=1, PHNX-3065) but never forwarded the caller's actor, so the peer's daemon re-resolved identity from its own empty env and collapsed every such task to UNRESOLVED@<host> — making the browser look contended by phantom owners. markFleetRemote (the single seam behind buildSshInvocation, the fleet passthrough, and leasedBoxRemoteCmd) now forwards AGENTS_ACTOR*/GIT_* alongside the marker, in both POSIX and PowerShell dialects, marker-first so the consent gate and idempotency guard are unchanged. This closes the same RUSH-2028 actor-provenance gap on the agents ssh seam that the --device dispatch already closed via withActorEnv. Source: cli/src/lib/devices/connect.ts.

  • Monitor --run actions now require an authenticated account and resolve routine hooks (PHNX-3406). Claude routine selection verifies the balanced account with Claude's own local auth status instead of trusting stale identity metadata, then launches with the operator HOME plus that version's CLAUDE_CONFIG_DIR, matching ordinary agents run so both the native login and portable ~/... hooks resolve. agents monitors list and its JSON output also surface the latest asynchronous action failure, and the docs clarify that --run creates a new headless conversation rather than resuming the caller. Source: cli/src/lib/{sandbox,daemon/runner}.ts, cli/src/commands/monitors.ts, cli/docs/monitors.md.

  • agents sessions fork now works cross-device and cross-harness (PHNX-3409). Forking a session that lived on another box used to dead-end with transcript not found — fork resolved the source with a local-only lookup and copied the transcript off local disk, and it only worked for Claude. It now resolves the source across the fleet (the same path agents sessions preview uses) and launches a NEW same-harness session, load-balanced, seeded with a recap of the source (label, cwd, ticket, last state, changed-files, and the source id for /continue) — so a session on any device, in any REPL harness, forks and picks up where it left off. Add --device <host> to place the sibling on the fleet or --terminal to open it in a fresh tab; --name still labels it. Source: cli/src/commands/fork.ts, cli/src/lib/session/fork.ts.

  • agents routines now says WHY a routine failed, not just that it did (PHNX-3410). The list, the interactive detail's Recent runs, and agents routines logs showed only the status enum (failed/missed/skipped) — so diagnosing a routine that keeps failing on auth_failed: Please run /login, a wedged active-run lock, a readiness block, or a missed slot meant drilling into the run dir. A new runFailureReason(run) maps the RunMeta already on disk (errorMessage / readiness / skipReason / missed) to one compact line, rendered inline in the Last Status cell, the detail's Recent runs, and a reason: line in the logs header. Visibility only — no scheduling behavior changes. Source: cli/src/commands/routines.ts.

  • The daemon warm-tick session indexer no longer re-derives an active non-Claude/Codex session's whole tool history every tick — the root cause behind the wedged event loop and browser-IPC stalls (PHNX-3411). The symptom fix (above) made a wedged browser daemon fail loud; this removes what wedged it. The warm-tick indexer parsed AND re-redacted every tool call in a changed session on every tick, in toolIndexMode: 'replace'. claude/codex resumed from a stored parse offset; the other 11 harnesses (kimi, grok, opencode, antigravity, cursor, droid, …) did not, so an active large session on the interactive hub re-sanitized its entire tool history several times a second — synchronous work that blocked the daemon event loop, so browser.sock accepted connections it never answered. The full-file harness path is incremental now too: the tool ledger records how many events were folded plus the collector snapshot, and a later scan of the same append-only stream re-derives only the newly appended tool calls (mode: 'append'), falling back to a full re-scan for a first scan, a truncated/rewritten transcript, or an extractor-version bump. Measured on a 400-tool-call Grok session indexed once per tick, re-derive+redact work drops from 80,200 tool calls to 400 over 400 ticks (~201× less), with the index content byte-for-byte identical to a full re-parse. Source: cli/src/lib/session/{db,tool-calls,tool-store}.ts.

  • agents browser detects a wedged daemon instead of hanging on it (PHNX-3411). A unix-socket connect succeeds at the kernel level even when the browser daemon's shared event loop is blocked, so isDaemonReachable (a bare accept probe) reported a busy-but-unresponsive daemon as healthy — browser.sock accepted every connection while the daemon never replied, and cross-device browser drives to a busy hub (e.g. zion) hung or surfaced a confusing Timeout waiting for browser daemon socket. Readiness is now a reply: before serving a verb the CLI requires the daemon to actually answer a version probe (isDaemonResponsive, retried so a transient GC pause never condemns a healthy daemon), and a reachable-but-wedged daemon fails loud — naming the blocked event loop, the daemon log, and the agents browser stop --daemon reset — instead of hanging (which also fixes reconcileDaemonVersion's own untimed version probe). Also adds a critical-path regression guard proving the daemon's session-index warm tick resumes an actively-growing Claude session incrementally (per-tick cost tracks appended bytes, never a full O(session) re-parse) — the invariant whose violation would re-starve browser IPC. Source: cli/src/lib/browser/ipc.ts, cli/src/lib/session/__tests__/active-session-warmtick-incremental.test.ts.

  • A routine whose dispatch account is signed out now records a blocked run instead of a doomed failed one (PHNX-3415). When the daemon fires a routine, it preflights the rotation-resolved account against the auth-health cache; if that account is provably signed out (revoked or unconfigured — the "OAuth revoked" / "Please run /login" cases), it records a terminal blocked / agent_auth_failed run carrying the exact re-login repair (agents run <agent>@<v> -- login) and spawns nothing — instead of launching a run that 401s, lands as failed, and burns a session (surfaced from the 2026-08-28 reliability investigation, where signed-out accounts were the top cause of routine failures). The check is cache-only and fails open on any still-usable or indeterminate verdict (rate_limited/unverified/expired/error/absent), so a stale probe never wedges a routine. Broadens and corrects the prior preflight, which only caught revoked and mislabeled it failed. Source: cli/src/lib/routine-readiness.ts (fireTimeAuthReadiness), cli/src/lib/daemon/runner.ts; spec RT-12.

  • agents fleet apply on an empty roster now tells you how to fix it (PHNX-3422). When fleet.devices is {} (the default on a fresh box), apply/apply --plan used to print a gray No target devices — nothing to apply. that read like the command was broken. It now prints an actionable fleet.devices is empty — nothing to converge. plus the two ways to declare a roster (fleet: { devices: all } or a named map), and still exits 0. Other zero-target cases (a devices: all fleet with no online peers, or a named roster whose every device was unresolved — off-tailnet/ignored/typo) keep the plain note, since their reason was already surfaced above. Source: cli/src/lib/fleet/manifest.ts (emptyTargetsMessage), cli/src/commands/apply.ts.

  • Traces now attributes a failed tool call's OWN blocking duration to wasted time, not just the bounded gap to the next call (PHNX-3437). tool_calls gains an end_timestamp column (the call's result-record time, already available at ingestion) so the insight engine books end - start — the time a call actually blocked — as its wasted time, independent of whether another call follows. Before, a call that hung for minutes and then failed registered as ~0 waste whenever it was the last call in its session or was followed quickly by an unrelated call. This is what makes a fail-fast fix measurable on the Evals console: a channel that stops hanging ~5.5 minutes on stdin and instead fails in under a second (PHNX-3407) drops from ~5.5 minutes of attributed waste to ~0. The existing 30-minute-bounded inter-call gap heuristic is kept as the fallback for rows an older extractor stored (NULL end), so nothing crashes or yields NaN, and — when the end time is known — the post-call retry/stall gap is measured from the call's END so the blocking duration is never double-counted. wastedMs/wastedMsTotal/failurePatterns shard shapes are unchanged. Source: cli/src/lib/session/{db.ts,tool-calls.ts,tool-store.ts}, cli/src/lib/traces/{sync.ts,insights.ts}.

  • agents view: an idle Claude session window no longer renders as a full red "unavailable" block (PHNX-3453). When a Claude account has no usage in the current rolling 5-hour window, the usage API returns five_hour.utilization = null, so no S: session bar exists — and agents view drew that missing window as a solid red █████ unavailable, which reads as "100% used / maxed-out", the opposite of what it means. It now renders a neutral dim dashed row ┄┄┄┄┄ unavailable, visually distinct from both a real 0% (░░░░░) and a full 100% (█████). The reading itself is unchanged and honest — an idle account genuinely has no current 5-hour number; the bar fills in the moment that account runs Claude, or on agents view claude --refresh when the box isn't being usage-endpoint rate-limited. Source: cli/src/lib/accounting/usage.ts (formatUsageSummary, NO_DATA).

1.22.57

  • Removed two duplicate commands: agents list and agents trash restore (PHNX-3391). agents list was a long-deprecated full duplicate of agents view (it already printed "agents list is now agents view" before delegating) — agents view is now the one version-listing surface. agents trash restore <target> was an exact duplicate of the top-level agents restore <target> (both call restoreVersion); agents restore stays, and agents trash keeps trash list. Both removed names now error as retired top-level commands rather than auto-correcting. Source: cli/src/commands/versions.ts, cli/src/commands/trash.ts.

  • agents perf moved to agents insights perf (PHNX-3391). Performance metrics are an insight, not a top-level noun — latency now sits beside agents insights cost / output under the one insights group. Every subcommand is unchanged (agents insights perf [hooks|commands|run|friction]), the disposable warehouse at ~/.agents/.cache/perf/perf.db is untouched, and bare agents perf now errors (retired top-level name) rather than auto-correcting. Source: cli/src/commands/perf.ts, cli/src/commands/insights.ts, cli/src/cli/command-registry.ts.

  • agents insights mix is now the one counter surface — the eight per-recipe shortcut commands, recipes, and trends are gone (PHNX-3391). agents insights mix already printed the whole recipe board, yet each section was also a standalone command (agents insights harness-mix, model-mix, …), also reachable as mix <recipe>, also listed by agents insights recipes, and the board was also aliased by agents insights trends — one query engine wearing five surfaces (each registered twice, under insights and sessions insights). It collapses to one: agents insights mix (board), agents insights mix <recipe> (one section, e.g. mix harness-mix), and agents insights mix --list (the recipe ids). The removed spellings now error and point at mix. This is surface removal, not hiding — 20 command registrations deleted. Source: cli/src/lib/analytics/mix-commands.ts.

  • Worker boxes now show real Claude usage (S:/W:) bars in agents view (PHNX-3392). A rate limit is metered per account, so the number is identical on every box — but only a headed device (personal/desktop) can read it, because the usage endpoint needs the user:profile scope only the interactive login carries. Headless workers, which have just the user:inference setup-token, got a 403 and showed blank bars. A new usage-sync daemon service closes the gap: each personal/desktop daemon pushes its per-account usage snapshot to worker peers, which merge it newest-wins (via the hidden agents __usage-ingest verb). Role-gated (personal/desktop publish, workers consume), single-executor per destination, and idempotent. A synced bar reads as last-seen, never a live fetch. Source: cli/src/lib/accounting/usage-sync.ts, cli/src/lib/daemon/usage-sync-service.ts.

  • agents devices gains a desktop role and stops double-labeling your interactive box (PHNX-3392). agents devices role <name> now accepts a third value — desktop — for a headed always-on box (the release/credential home, e.g. a Mac mini): it is excluded from --device auto like personal (agents never auto-land there), but authenticates from its own interactive login like personal rather than a worker setup-token. And agents devices list no longer prints both ★ interactive and personal on the same row — a personal box IS your interactive seat, so the star is folded into the role (it still shows for a non-personal box pinned as interactive.host). Source: cli/src/lib/device-config.ts, cli/src/commands/ssh.ts.

  • The system DotAgents repo phnx-labs/.agents-system was renamed to phnx-labs/.agents on GitHub, and both names are now recognized as the system origin (PHNX-3394). The -system suffix was redundant once the layering itself is the role. The clone target stays gh:phnx-labs/.agents-system (GitHub's rename redirect makes that resolve fine, so there is no forced cutover), but a checkout under either name is recognized as the system origin across all transport forms (ssh / https / scp-style), via a RENAMED_REMOTE_ALIASES fold in canonicalGitRemote and the pure isSystemRepoRemote behind isSystemRepoOrigin. The companion extras repo phnx-labs/.agents-extras keeps its name but is now private and opt-in (agents repo add, not cloned by default). Source: cli/src/lib/git.ts.

1.22.56

  • Scheduled routines claim a durable per-occurrence slot, so a duplicate delivery or a catch-up of the same slot can't double-launch (PHNX-3215). The forward-timer path keyed its single-fire claim on croner's currentRun(), which is the jittered wall-clock fire instant, not the aligned schedule boundary — so two timer callbacks for one occurrence minted different run ids and both launched, and a live fire never collided with its catch-up twin. The scheduler now floors the fire to the aligned (routine, scheduledFor) boundary (fireSlot/alignedSlotForFire), the same derivation catch-up uses, making the atomic slot claim a structural guarantee (SING-15/16, self-overlap SING-13). Source: cli/src/lib/scheduler.ts, cli/src/lib/scheduling/routines.ts, cli/src/lib/overdue.ts.

  • Managed-share Open Graph cards render in Cloudflare instead of failing on every new publish (PHNX-2835). The Worker upload now carries Yoga and resvg as compiled WebAssembly modules alongside the bundled JavaScript and fonts. The previous single-file bundle decoded the WASM into byte arrays and compiled it at request time; Node accepted that in tests, but Cloudflare workerd forbids runtime WASM code generation, so a new share's lazy <slug>.png render failed. A real-workerd regression test now renders and validates the PNG, and a genuine renderer failure returns a diagnostic 500 instead of falling through as a missing cover. Source: cli/src/lib/share/{worker-template,provision}.ts.

  • A dangling per-harness default account no longer hard-fails every launch (PHNX-3326). When agents.yaml accounts.defaults.<agent> pointed at an account id absent from the local registry, resolveSpawnAccount threw Unknown account '<uuid>' and the launch exited before doing any work. The default is now a preference, not a requirement: a stale pointer emits a warning naming the harness and the remedy (agents accounts clear-default <agent>), then falls back to the existing balanced-selection path. accounts set-default/switch now store the portable account name instead of the per-device uuid; legacy uuid entries continue to resolve, and accounts remove/switch correctly treat both name- and id-stored defaults as references. Source: cli/src/lib/account-registry.ts, cli/src/commands/accounts.ts.

1.22.55

  • Managed shares generate their Open Graph cover server-side (PHNX-2835). Publishing HTML to share.agents-cli.sh no longer launches local Chromium. The Worker lazily renders a deterministic 1200×630 AGI card with Satori and resvg-wasm, bundles Inter and JetBrains Mono, inherits the canonical page's visibility gate, and caches the PNG in R2. BYO endpoints keep their local screenshot fallback. An explicit missing AGENTS_SHARE_BROWSER or PUPPETEER_EXECUTABLE_PATH now fails loudly instead of silently falling through. Source: cli/src/lib/share/{capture,publish,worker-template}.ts.

  • agents routines status --json — the daemon-owned scheduler status surface (PHNX-3215). routines status printed human-only scheduler health and upcoming fires with no machine-readable form; the JSON that existed (routines list --json) is definition-shaped and does not answer "did this routine fire here, and is one running right now?" The new --json emits a scheduler block (state, pid, heartbeat age, enabled/total counts) plus a routines array where each row carries the routine's single owner device (ownerDevice, with ambiguousDevicePin when devices: names more than one), whether it fires here (runsHere), the last local fire's outcome and error (lastStatus, failureReason, lastRunStartedAt/CompletedAt), and any in-flight spawn (inFlight: { runId, pid, startedAt, triggerKind }) — surfaced only for a run that is genuinely live on this device (a live local child or a host-placed run), never a provisional pre-spawn claim. The routine set and counts are the schedulable ones (matching plain routines status), so the JSON never inflates totals with discoverable-but-unmaterialised project routines. Source: cli/src/lib/scheduling/routines.ts (buildRoutineStatusRows), cli/src/commands/routines.ts.

Fixed

  • Daemon service restarts now replace periodic timers instead of multiplying their tick rate, and a timed-out tick parks until its real work settles instead of overlapping teardown or another tick. Live-session metadata publishing, monitor evaluation, heartbeat/process reconciliation, tmux and browser-task reaping, broker self-heal, and signed webhook ingress now run under the per-service supervisor with measured health and isolated failure handling.

  • agents view --json and agents devices harnesses/accounts --json no longer leak the internal 'stale' sentinel in their usage-error fields (PHNX-3348). Without --refresh, a read-only usage lookup on a never-cached account returns the internal USAGE_NOT_COLLECTED_MARKER ('stale') — a cache signal, not a message — and the JSON builders emitted it verbatim (as usageError, and as quota.unavailableReason), contradicting those fields' "human-readable" contract. Both surfaces now normalize through a new shared usageErrorForDisplay to a plain-language, actionable string (Usage not collected yet — run \agents view --refresh` to fetch it.); every genuine, already-human error passes through unchanged, and the human terminal renders were already correct and are untouched. Source: cli/src/lib/accounting/usage.ts, cli/src/commands/view.ts, cli/src/lib/view-types.ts, cli/src/lib/devices/harness-inventory.ts`.

  • agents artifacts share open <target> — open your own published page signed in as the owner, so its inline visibility control is actually usable (PHNX-3370). The shared page's Public/Unlisted/… chip is an interactive dropdown only for the signed-in owner, but nothing ever handed a browser the login it needed — the Worker could redeem a phoenix_ticket cookie yet nothing minted one, so opening your own link left the chip a static cue. Now the share Worker mints a short-lived, single-use, self-signed login ticket at POST /__ticket (authenticated by the caller's Phoenix bearer — it grants nothing the caller didn't already have, and is domain-separated from the identity cookie so neither can be replayed as the other), and share open appends it to your page URL so the browser trades it for the __share cookie and the chip becomes a live control. Managed (Phoenix) endpoints only; run agents artifacts share update once to redeploy a Worker that predates the mint route. agents artifacts share visibility <target> <level> remains the no-browser way to re-scope in place. Source: cli/src/commands/share.ts, cli/src/lib/share/worker-template.ts.

  • The new human task taxonomy actually applies to existing sessions now. The traces topic classifier's output changed (Feature work / Bug fixes / Refactor / … in 1.22.54), but classifyTopic's result is cached per-session in session_topics keyed by SESSION_TOPIC_EXTRACTOR_VERSION — which wasn't bumped, so every already-classified session stayed a cache hit at v1 with its old Engineering/Operations label and agents traces sync kept re-uploading the old labels. Bumped the version 1→2 so every session re-classifies with the new taxonomy on the next sync, and the Evals console treemap reflects it. Source: cli/src/lib/session/db.ts.

  • agents view claude shows live session + week usage bars again on a personal device (RUSH-2392). A foreground agents view at a TTY on a personal device now falls through to the interactive Claude login for the usage read — the only credential carrying the user:profile scope the usage endpoint needs — so agents view --refresh repopulates the session (5h) + week (7d) window for every signed-in account instead of showing S: █████ unavailable on idle homes. 3f3554c51 (RUSH-1822) had removed that fallback for every caller to stop the daemon's unattended usage/auth-health loops from firing the interactive token at Anthropic and getting the fleet logged out; that guarantee is intact — the read is now gated by allowInteractiveUsageLogin(role, isTTY), so every background caller (daemon usage warm, auth-health probe, watchdog) and every --json/piped reader stays setup-token-only and default-closed. #2987's blanket-wording change was treating the symptom; this restores the data. A usage read still never refreshes an access token — an expired interactive login reports expired-credential. Source: cli/src/lib/accounting/usage.ts, cli/src/commands/view.ts, cli/docs/secrets.md, cli/docs/specifications.md.

1.22.54

  • agents accounts mint claude (also agents auth mint claude) mints a setup-token and seeds it — the first-class close of the mint-auth recipe (PHNX-2364). claude setup-token is driven through the same injectable PTY driver agents fleet login uses; the authorize URL is opened on this machine; the captured value is accepted only when it is a single-line sk-ant-oat01-… token (the #1767 TTY-banner capture is refused). The command seeds a named provider account (policy never) for agents run --account / agents accounts sync and the reserved FILE-BASED auth bundle keyed per email, which usage/probe reads. --token-stdin skips the browser dance; --fleet / --device syncs both bundles to workers. --json (including --code --json) emits only the machine-readable result on stdout — progress URLs are suppressed, matching agents fleet login. Native rotating OAuth is never copied. Other harnesses fail loud (agents fleet login for device-code, agents accounts add for API keys). agents setup accounts surfaces the same step in the onboarding hub. Source: cli/src/lib/auth-mint.ts, cli/src/commands/auth-mint.ts, cli/src/commands/setup-accounts.ts.

  • Reserve the auth secrets bundle as file-backed (PHNX-2365 / SEC-GAP-3). agents secrets create/import auth now forces the file backend and fails loud on keychain or vault, instead of letting a keychain-backed auth bundle be silently ignored by usage/probe. resolveClaudeSetupToken throws the same error rather than returning null, and agents doctor emits auth-bundle-wrong-backend. Source: cli/src/lib/secrets/bundles.ts, cli/src/lib/claude-account-token.ts.

  • Auto-sync the reserved auth bundle across the fleet (PHNX-2371). File-backend secrets export --device no longer forwards AGENTS_SECRETS_PASSPHRASE (that keyed remotes to a secret their daemons lacked while still printing "Imported N key(s)"). Import and remote push now read-back-verify decryptability. The daemon auth-sync service, plus fleet apply and repo push/pull user, push a local file-backed auth bundle to pinned devices that lack it. Source: cli/src/lib/secrets/reserved-sync.ts, cli/src/lib/secrets/remote.ts, cli/src/lib/daemon/auth-sync-service.ts.

  • The menu-bar helper no longer permanently forgets refresh children after a crash (PHNX-2413). Child ownership is persisted before spawn in a versioned, cross-process-locked registry, completed with PID/PGID/start-time/executable/argv identity, and recoverable from an unguessable provenance marker carried by the group-leading supervisor if the helper dies in the spawn-to-registration window. Reaping now retains individual ambiguous entries and registry I/O failures for retry, and deletes a child only after its process group is confirmed absent. Existing two-field records migrate conservatively. Source: cli/menubar/Sources/MenubarHelper/ChildProcess.swift.

  • Honor an OpenCode custom-harness --model pin (PHNX-2577). agents run of a harness forked from opencode now forwards the pinned model as --model on opencode run. OpenCode does not read OPENCODE_MODEL, so the pin previously vanished and the host used its configured default. Source: cli/src/lib/profiles.ts, cli/src/lib/exec.ts.

  • agents accounts now fails with a clean CLI error instead of an uncaught Node stack (PHNX-2578). A missing or locked --from-secrets bundle, or inspect of an unknown name, prints the user-facing message and exits 1. Ctrl+C at the add/set-key secret prompt still exits 130 with no error. Source: cli/src/commands/accounts.ts.

  • agents monitors add no longer auto-starts the daemon when daemon.enabled is false (PHNX-2637). The kill switch already stopped routines add/start/catchup and webhook auto-start; monitors add called startDaemon() directly and brought the daemon up (and wrote the systemd autostart symlink) on a box whose config disabled it. Auto-start now goes through the same assertDaemonEnabled() gate: the monitor is still created, the refusal names agents daemon enable, and the engine-pickup wait is skipped. Source: cli/src/commands/monitors.ts.

  • Unpinned dispatch no longer dies on a logged-out default version (PHNX-2685). Version selection now consults per-device auth state: a logged-out (or revoked) workspace/global default yields to a signed-in sibling on the execution device instead of spawning into a credential-less home, and fails loud naming each excluded version when nothing is signed in. --strategy pinned still forces a rate-limited default; an explicit @version pin is unchanged. Off macOS, a Claude home whose .credentials.json is missing (and which has no setup-token) reports signed out even when leftover .claude.json still names an email. Source: cli/src/lib/accounting/rotate.ts, cli/src/lib/agent-spec/agents.ts, cli/src/commands/exec.ts.

  • Codex permissions list now shows deny rules, and a later apply with no deny clears them (PHNX-2703). toCanonical used to skip .codex/rules/agents-deny.rules, so agents permissions list codex omitted every forbid the writer had already emitted. The reader now inverts that file, and the writer deletes it when a later apply has an empty deny so a removed forbid does not persist. Source: cli/src/lib/permissions-registry.ts, cli/src/lib/permissions.ts.

  • Content search returns indexed transcripts the listing page missed (PHNX-2767). agents sessions "tmux pane" intersected FTS hits with the in-memory pool (cwd-scoped, default-capped at 50), so grep-visible project transcripts dropped out and the query returned 0. FTS hits are now unioned with the pool and hydrated from the index when the page does not already hold them. --project / --agent / --routine still apply after that union, so a content hit outside the requested scope does not reappear as a multi-match. Source: cli/src/lib/session/discover.ts, cli/src/commands/sessions.ts.

  • A non-ASCII share title / --label / --meta value now publishes in full instead of degrading to (unnamed) or dropping glyphs (PHNX-2786). publishToEndpoint still sends the latin1-safe folded value in x-share-<field> (so an already-deployed Worker keeps rendering exactly as before — the ByteString crash stays fixed), and now ALSO sends a percent-encoded x-share-<field>-u companion plus an x-share-encoding: percent opt-in whenever the fold was lossy. An updated Worker (agents artifacts share update) prefers the decoded companion, so a Japanese/Arabic/Hindi title or an emoji renders as written; a Worker that predates the scheme ignores the unknown headers and folds gracefully. --meta rides its whole raw object in one percent-encoded x-share-meta-u companion. Source: cli/src/lib/share/publish.ts, cli/src/lib/share/worker-template.ts.

  • Claude Code's native per-project memory now survives an agent version upgrade (PHNX-2817). getVersionHomePath gives every installed Claude version its own isolated HOME, so .claude/projects/<project-key>/memory/*.md — the freeform notes Claude writes into itself, distinct from the agents memory resource — was a THIRD, unmanaged per-version copy: a note written under one version was invisible under another (empty dir, no symlink, no copy). Every version home's copy is now a symlink into one canonical dir shared across versions for that project, wired into the same sync path as agents memory (agents sync / agents run / agents doctor --fix); a pre-existing real directory's content is migrated into the canonical dir once instead of discarded. Source: cli/src/lib/memory.ts, cli/src/lib/installations/versions.ts.

  • agents monitors --run no longer records ok just because the agent exited 0 (PHNX-2842). A fire's frozen ok was reconciled against the run's later status (RUSH-2690), but completed still counted as success — so a merge-on-green monitor whose agent ran, hunted the fleet, and never merged showed ok while the PR stayed OPEN. run/routine actions now take an optional postcondition (CLI: --postcondition <cmd>, YAML: action.postcondition) — a shell command, {event} interpolated, that must exit 0 after the run settles. agents monitors runs/view then show no effect (not ok) when it fails, and persist the result on the fire record. --run without a postcondition still records ok on completed and warns at add time. Source: cli/src/lib/monitors/{state,config,engine}.ts, cli/src/commands/monitors.ts.

Provider accounts created with accounts add now resolve silently in headless --account launches on macOS. Their policy never credentials are stored without a biometry ACL and are read through the policy-aware secrets bundle path instead of being rejected by the raw-keychain Touch ID guard.

  • agents ssh <box> agents browser … is gated by browser.remote-control (PHNX-3065). The --device passthrough already stamped AGENTS_FLEET_REMOTE=1 so the far-side consent gate could refuse a drive when remote-control is off. agents ssh built its invocation with only askpass vars in the local env — which OpenSSH does not forward — so a browser drive over ssh skipped the gate entirely. buildSshInvocation now prefixes an agents browser / ag browser / standalone browser remote command with the same markFleetRemote helper the fan-out uses (env AGENTS_FLEET_REMOTE=1 … on POSIX, $env:AGENTS_FLEET_REMOTE='1'; … on PowerShell). Non-browser commands and interactive logins stay unmarked. Source: cli/src/lib/devices/connect.ts, cli/src/lib/hosts/passthrough.ts.

  • Feed terminal answers now write a resolution tombstone (PHNX-3074). The TypeScript recordAnswer / removeBlock paths already recorded resolutions/<id>.json before clearing an open block, but the Python UserPromptSubmit hook (the path that fires when a human types in the TUI) unlinked the block with no tombstone. A later reconcileAttention consumer would then resurrect that generation from a stale session re-read at the same cursor. The hook now writes an answered tombstone (generation + sourceCursor from the still-present block) before unlink, matching the TS writer. Source: cli/src/lib/feed/feed.ts.

  • Daemon PATH now includes ~/.rush/bin and ~/.local/bin (PHNX-3075). systemd/launchd pin a PATH that never sources a login shell, so which rush failed inside the daemon even when an interactive shell on the same box resolved ~/.rush/bin/rush. Monitor notify actions and dispatched agents run children inherit that PATH and silently never delivered (rush CLI not found on PATH) while the watcher kept polling. Both user-bin dirs now sit after the agents shim and Node dirs (so a ~/.local/bin/agents cannot shadow the daemon binary) and before the platform system dirs. Source: cli/src/lib/daemon/daemon.ts.

  • Kimi expired usage state says "run Kimi once" and --json --refresh exposes the refresh result (PHNX-3198). An expired Kimi OAuth credential used to render "re-auth for usage", even though a normal Kimi launch refreshes its own token. The usage error now reads "run Kimi once". agents view <agent> --refresh --json now actually forces a live refresh and includes a usageError field on each version so programmatic consumers see why a snapshot is missing. Source: cli/src/lib/accounting/usage.ts, cli/src/commands/view.ts, cli/src/lib/view-types.ts.

  • Share metadata can be corrected without republishing (PHNX-3278). agents artifacts share edit <target> now sets/removes labels and merges, replaces, or removes validated arbitrary metadata through an authenticated Worker PATCH. The Worker streams the exact stored body back to R2 while preserving HTTP metadata, publication time, visibility, expiry, provenance, cover, and revisions, and deliberately creates no revision. agents artifacts share list --meta key=value is repeatable and filters exact metadata values in both human and --list-json output. Reserved keys and missing targets fail loudly.

  • Fail loud when codex's Linux sandbox can't start, instead of landing zero tools (PHNX-3285). Codex ≥0.146 sandboxes its read-only and workspace-write runs on Linux with a bundled bubblewrap that needs an unprivileged user namespace. On Ubuntu 23.10+ (kernel.apparmor_restrict_unprivileged_userns=1) that's denied, so bwrap dies with bwrap: setting up uid map: Permission denied and a headless codex run — an agents teams codex teammate, or agents run codex, always headless + sandboxed — burned a turn and wrote nothing while still reporting a completed turn. agents run now preflights the box before spawning a headless sandboxed codex and fails loud with the one-time fix, rather than silently under-delivering. The preflight is scoped to codex + Linux + a headless + sandboxed (non-skip) run — an interactive TUI, a --mode skip (no-sandbox) run, macOS/Windows, and every other harness are untouched, and the intended auto=workspace-write + approval_policy=never config is preserved, never downgraded to danger-full-access. cli/scripts/enable-codex-sandbox.sh re-enables unprivileged userns on a box (sysctl drop-in, idempotent, self-verifying) so codex's sandbox works with its isolation intact; run it once per fleet worker. Source: cli/src/lib/linux-userns.ts, cli/src/lib/exec.ts (codexSandboxPreflight), cli/scripts/enable-codex-sandbox.sh.

  • New hook-manifest self-heal check — a hook registered at an unresolvable path no longer fails silently. resolveHookScriptPath only resolves a manifest script: under <root>/hooks/, and resolveContainedHookPath rejects any candidate escaping that root; an entry pointing anywhere else returns null and the hook is dropped with no error, no warning, and no trace in agents doctor. This is how main-branch-guard — declared in agents.yaml as rules/subrules/truly-agentic-git-workflow/main-branch-guard.sh, authored and covered by a 100+ case suite — reached zero of 25 settings files across three machines, and stayed there until four agent sessions had already written into a primary checkout it exists to prevent. The check walks the manifest, resolves every relative script:, and reports each one that resolves to nothing. Detect-only: guessing where a script was meant to live could wire the wrong file into a PreToolUse gate, so it names the broken entry instead. Absolute scripts (subrule-composed hooks, which bypass the hooks/ resolver) and enabled: false entries are skipped. Source: cli/src/lib/self-heal/checks/hook-manifest.ts. (PHNX-3312)

  • Device-scope is now the DEFAULT for new config keys, not a hardcoded set (PHNX-3315). Routing a Meta key used to hinge on a decorative Record<keyof Meta, 'central' | 'device'> string map whose values nothing consumed — the real split was a hand-written destructure, so a newly added key that nobody wired silently landed in the synced top-level agents.yaml (central), the very trap behind the recurring per-box churn and key-loss. Classification is now an OPT-IN CENTRAL_META_KEYS allowlist that DRIVES routing: a key not explicitly marked fleet-shared is device-scoped by default and round-trips through this box's devices/<host>/agents.yaml under its own name, with no bespoke wiring — so a slipped-through key can never leak to the shared file. A compile-time check keeps the classification exhaustive (a nudge, not a safety gate — the runtime default is still the safe per-box file), and a foreign key an older CLI does not model but a newer one wrote to central is left in place and preserved verbatim (never relocated). Behavior-identical for every existing key. Source: cli/src/lib/state.ts.

  • agents sync status now surfaces config drift — a box that hasn't drained its device-scoped state (PHNX-3315). The one-shot fold-and-delete migrations (P1's frozen-header heal + central browser: tombstone drain; P2's fleet.discovery/fleet.ignored, hosts:, and device-scoped accounts: folds) run on config access, so a converged box shows nothing — but a box that never re-synced kept carrying that state centrally, and it was INVISIBLE: the report only showed per-agent "N missing", so the un-drained box was found the hard way, as a mystery agents repo pull conflict. A distinct drift line now flags (1) a stale top-level agents.yaml header (!= the current header) and (2) any lingering central fleet / hosts / accounts (device-scoped) / browser block that should have folded into the device doc. The detector is read-only and never triggers the migration; the machine-readable UnifiedSyncStatus gains a config field. Source: cli/src/lib/config-drift.ts (new), cli/src/lib/{sync-status,state}.ts, cli/src/commands/status.ts.

  • Root --help now surfaces the measured front-door groups and points to the full surface (PHNX-3320). agents --help renders the ten most-used groups (Quick start: setup, view, run, sessions; Most-used: teams, browser, secrets, devices, accounts, add) and a pointer to agents --help-all, which lists all 69 command groups. Previously 29 groups were never mentioned on root help. Source: cli/src/lib/help.ts, cli/src/cli/command-registry.ts, cli/src/bootstrap.ts.

  • agents focus <id> --attach-only now names the real session id in its recovery hint when the live row has not registered one yet (PHNX-3356). A live IDE/plain-terminal session with no sessionId used to print the literal agents sessions resume <id> placeholder even though the caller already had the indexed id. Attach-only still refuses rather than forking. Source: cli/src/commands/go.ts, cli/src/commands/focus.ts.

  • Every agents insights subcommand now honors --json / --since / --by (PHNX-3360). These flags share their long names with the insights parent command, so commander bound them to the parent at parse time and the subcommand actions — which read their own opts() — never saw them: --json printed the human table (invalid for machine callers), --by fell back to the default agent grouping, and --since was ignored (7d default). The whole group is affected — output, cost, mix, trends, recipes, query, and the per-recipe leaves — and all now read command.optsWithGlobals(). Unblocks the yc:workweave shipped-output engine, which shells agents insights output --by project --json. Source: cli/src/commands/output.ts, cli/src/commands/cost.ts, cli/src/lib/analytics/mix-commands.ts.

  • agents sessions content search now indexes what the agent ANSWERED, not just what you asked (PHNX-3363). session_text's extractor pushed only role === 'user' text — every harness parser's assistant branch recorded tokens/skills/tools but never the reply itself — so agents sessions "<an assistant-only phrase>" returned 0 hits even when the transcript on disk plainly had it. Every harness parser (Claude, Codex, Gemini, Rush, Hermes, Muse, Droid, Cursor) now accumulates the agent's answer text the same way it already accumulates the user's prompt text, into a new session_text.assistant FTS5 column with a lower BM25 weight than content (an equivalent user-prompt match still ranks first). A scan_ledger.extractor_version + CONTENT_INDEX_VERSION (lib/session/db.ts) lever makes the change-detector (filterChangedEntries) re-extract an already-indexed, byte-unchanged session on its next scan — the mechanism that backfills every existing session's assistant text without a destructive DELETE FROM scan_ledger (which would also drop the Claude/Codex resumable parser_state). ftsSearch's FTS5-tier hit now carries a short bm25 snippet() excerpt (auto-picking the best-matching column), surfaced on SessionMeta.snippet and included, unlike _matchedTerms/_bm25Score, in --json output. Source: cli/src/lib/session/db.ts, cli/src/lib/session/discover.ts.

  • agents artifacts share visibility <target> <level> changes a published page's visibility in place. Until now the only way to re-scope an already-shared page (public ↔ unlisted ↔ me ↔ org) was a full re-publish or a delete. The new verb re-stamps only the visibility on the stored object: the slug — and so the URL — is preserved, the body is untouched, and (like agents artifacts share edit) it is a metadata-only rewrite that creates no revision. It reuses the delete target parser (full URL, <user>/<slug>, or a bare slug in your namespace) and the same PATCH metadata-edit route/client — visibility is a first-class edit field alongside label, never a --meta entry (visibility is reserved). me/org require a Phoenix session and fail loud with an agents auth login hint when signed out; org is refused on a public-inbox email domain. A BYO endpoint whose deployed Worker predates the visibility edit fails loud (agents artifacts share update) rather than silently reporting success. Adds a visibility field to the Worker PATCH route, so already-provisioned BYO endpoints must run agents artifacts share update to pick it up (their template reads outdated until then); the managed endpoint is updated by the release. Source: cli/src/commands/share.ts, cli/src/lib/share/worker-template.ts, cli/src/lib/share/publish.ts.

  • The cross-platform CI matrix moved off the release path to a nightly lane (PHNX-3237). The six-job ci.yml matrix (ubuntu + macOS + Windows × Node 22/24) used to fire on every release/** branch — 16-53 min of macOS-10×/Windows-2×-billed work that gated nothing: it is not a required check, and release.sh gates the merge on test + gitleaks and the publish on an exact-tree attestation, never this matrix. Worse, a macOS-only failure had it red on every recent release, adding a false "release is failing" signal. It now runs on a nightly schedule plus manual workflow_dispatch, so cross-platform regressions are still caught without putting slow, billed, non-gating work on the release. Source: .github/workflows/ci.yml, .github/workflows/ci.test.ts.

  • A release-tree attestation can now inherit the suite result instead of re-running the full suite (PHNX-3237). An ordinary release ran the ~13k-test suite twice — once for the default-branch tree, once for the chore(release) commit tree — even though the second differs from the first only by the version bump, the folded changelog, and the regenerated command-index. New release-attestation.sh derive --base <green-attestation.json> --tarball <release.tgz> mints the release-tree record by inheriting the base's suite pass, verifying that the tree diff touches only package.json, .changelog/**, CHANGELOG.md, and docs/command-index.{md,json} — it fails closed on any other changed path, so a code change can never ride a stale pass. release-attestation-produce.sh --inherit-suite-from <base.json> uses it to skip the suite while still building + packing the real release-tree tarball, cutting the second full-suite run per release. The derived record inherits the base's lockfile/policy/toolchain identity, which the allowlist proves are byte-identical to the release tree's, so the release's require() still keys to it exactly. Source: cli/scripts/release-attestation.sh, cli/scripts/release-attestation-produce.sh.

  • agents view names why a Claude usage bar is empty instead of printing a blanket "usage unavailable" (#2987). Three distinct states shared one generic label, so an operator chasing empty bars was sent to claude setup-token for a remedy that cannot work. (1) A signed-in Claude account with no provisioned setup-token reported the shared usageNoCredentialError — "sign in, or provision a long-lived token" — advice whose first half the usage path will never honour: a read-only caller resolves only a file-based setup-token and never the interactive login (RUSH-1822), so an account that IS signed in still reads as unreadable. It now reports usage unavailable (no usage credential) and states both constraints, including that a setup-token cannot read usage either (RUSH-2392). (2) headless-scope was listed with the generic bucket in formatUsageErrorKindLabel, so the RUSH-2392 phrase appeared only on the surface that also passes the headless flag — every other caller printed "usage unavailable" for the same account. It now renders usage unavailable (headless) from the classified kind alone. (3) The read-only cache-miss sentinel ('stale', now the exported USAGE_NOT_COLLECTED_MARKER) had no classification arm and fell through to 'rejected', so a cold cache — no request made, nothing failed — rendered as a failure; it classifies as not-collected and renders usage pending. Source: cli/src/lib/accounting/usage.ts, cli/docs/secrets.md.

  • agents insights now breaks the Bash lump down by binary. The tool mix used to render every shell call as one undifferentiated Bash bar (and one failed tool loop: Bash line), which said nothing about what ran. Two new sections split it by the actual executable + subcommand, reusing the existing bucketKey classifier: Shell commands (git commit, gh pr, agents ssh, find, ssh→git pull, …) and Shell command failures (which binary was running when a tool loop failed). It is cross-harness — keyed on the command string, which Codex's exec_command carries too, not just Claude's Bash. The Actions block also now prints a one-line pointer to drill a sample session id into its full timeline (agents sessions trace <id>); the report itself stays aggregate by contract (SES-IF-4c). Source: cli/src/lib/session/insights.ts, cli/src/commands/insights.ts.

  • release.sh --apply from a non-interactive shell now fails loudly instead of exiting 0 having published nothing (PHNX-3176). The confirmation gate reads [y/N] from stdin; with no TTY (a backgrounded run, a sandbox, < /dev/null) read gets EOF, the prompt default-declines, and the script exited 0 having done nothing — no attestation, no PR, no tag, no publish. A caller that backgrounded the release, checked $?, and saw 0 then believed a release shipped when none did, which is exactly how a silent version gap opens. --apply without a TTY and without --yes now aborts with a non-zero exit before any work, naming --yes as the sanctioned non-interactive path. The internal --home-base-phase / --orchestration-phase re-execs are exempt (they inherit the already-checked stdin). Source: cli/scripts/release.sh.

  • The release attestation no longer dead-loops on a changed computer-mac helper (PHNX-2943). release-attestation-produce.sh never rebuilds computer-mac (it is signed on a separate macOS path), so when its source drifted it died telling the operator to run publish-computer-helper-mac.sh and re-run — but that script recorded nothing, so the re-run hit the identical error forever (workaround used live cutting 1.22.43). The publish script now uploads a computer-mac-input-digest.txt sidecar naming the exact source it built from, and the producer records the published binary against the current source only after that sidecar matches (verifying the downloaded zip against its .sha256). A mismatch, a missing sidecar, or an undownloadable release fails closed with the exact publish command, so a stale binary can never be attested against changed source. Source: cli/scripts/release-attestation-produce.sh, cli/scripts/publish-computer-helper-mac.sh.

  • release.sh now refuses to fold the changelog queue when an earlier release's version-bump PR is still open, closing a cross-version note-misattribution (PHNX-3084). The decoupled release (RUSH-2395) merges the version-bump PR asynchronously after publish; if that merge is stuck on a CHANGELOG conflict, v1.2.3's .changelog/next/* fragments stay queued on main, and a later release.sh 1.2.4 re-read them and folded v1.2.3's notes under v1.2.4. The same-target STUCK_BUMP_PR retry only ever queried release/v<current-target>, so it was blind to an OTHER version's stuck PR. Before folding, release.sh now detects any other open release/v* bump PR (via the new unit-tested scripts/release-other-bump-prs.sh) and fails loud with the exact merge command, rather than silently re-attributing an earlier version's release notes. Source: cli/scripts/release.sh, cli/scripts/release-other-bump-prs.sh.

  • Shared images / videos / PDFs now get the attribution bar too, via a viewer page (PHNX-3332). The attribution bar only injects into HTML; a directly-shared non-HTML asset had no "shared by / visibility" chrome. Now a browser navigating to a shared image, video, audio, or PDF gets a lightweight viewer page — the same attribution bar on top, the asset centered below. A non-browser fetch (Accept without text/html: an <img src>, an OG crawler, curl) still gets the raw bytes, so embeds and social previews are never broken; the viewer's own media element points back at the asset with ?raw (which also re-runs the me/org gate, so a private asset stays private inside its own viewer). Non-viewable types (JSON, arbitrary downloads) are served raw. Source: cli/src/lib/share/worker-template.ts.

  • The share attribution bar now always shows the sharer's avatar. Every shared page (and every asset viewer page) renders an avatar in the bar next to "Shared by …": a coloured initials circle derived from the handle, always present, so a share is never faceless — especially useful for an org/company share where knowing who posted it matters. When the sharer is signed in, the CLI stamps a Gravatar (keyed on a SHA-256 of the lowercased email — only the hash lands in public metadata, never the raw address) so a real profile photo layers over the initials; if the user has no Gravatar the photo request 404s and the bar cleanly falls back to the initials circle (onerror, honoured because the share page sets no CSP). Source: cli/src/lib/share/worker-template.ts (render + x-share-avatar → customMetadata.avatar), cli/src/lib/share/publish.ts (resolveShareAvatar). Deployed by re-running agents artifacts share update; a hosted-photo pipeline for users without a Gravatar is tracked as a follow-up.

  • Shared pages now show page stats and let the owner change visibility inline. The attribution bar injected on every shared HTML page gained a right-side stats cluster — 👁 <n> views · updated <rel> — where views is a per-slug visitor count (owner views and ?raw/embed fetches are not counted) and the relative time comes from the page's last write. For the page's owner (the signed-in viewer whose handle is the namespace), the visibility chip becomes a live control: click it for an inline dropdown of Public / Unlisted / Only you / Anyone at your org, pick a level, and the bar PATCHes the same in-place edit route share visibility uses (with credentials:'include'), flipping the chip optimistically and reverting with the server's error text on failure (e.g. org from a public-inbox domain). Everyone else keeps today's static read-only cue. The view counter is a separate __views/<path> R2 object, so counting a view never rewrites the page (which would corrupt "last updated"). Source: cli/src/lib/share/worker-template.ts (renderAttributionBar owner control + stats, readViews/writeViews, gateVisibility, owner-cookie write principal in authorizeWrite). Because this changes the Worker template, a deployed endpoint reads outdated until its owner redeploys with agents artifacts share update — a pure Worker update, no new bindings.

  • agents artifacts share list can now show the owner's hidden pages. The default remains the public gallery (--scope public). Pass --scope unlisted|me|org or --all to include pages that are hidden from the public listing; the CLI sends the owner's bearer and a scope=mine hint to the Worker's JSON listing route, which returns hidden pages only after verifying the caller owns the namespace. Each human row now shows the page's visibility so public vs hidden is obvious at a glance. Source: cli/src/commands/share.ts (--scope/--all filter, owner bearer forwarding, visibility column), cli/src/lib/share/worker-template.ts (resolveListingScope + owner-gated hidden inclusion in renderListing). The Worker change only takes effect on a live endpoint after its owner runs agents artifacts share update.


type: fix

agents sessions trace now recognizes a session's shell steps consistently across every surface. The "is this tool a shell command?" test lived as six hand-synced copies that had drifted apart — the tool-call indexer treated Codex's exec/execute/run_command as shell, but the trajectory model, the directory-touched scan, and both the HTML and stream renderers each used a narrower or differently-cased list. They now share one case-insensitive isShellExecTool predicate (shell-programs.ts), so a Codex or Droid shell step is colored, program-resolved, and counted the same as Claude's Bash everywhere the trace reads it.

  • Phoenix Evals topic buckets now read as human task types, not five generic labels. The session-topic classifier (agents traces sync) collapsed every code session into Engineering and every ops session into Operations, so the console treemap showed coarse, robotic labels. classifyTopic now emits the taxonomy the approved console mockup always intended — Feature work · Bug fixes · Refactor · Debugging · Code review · Release · Blog & docs · Fleet / ops — as an ordered rules table (still metadata/tool-mix only, no transcript read), keyed within the five stable TraceTopicGroup groups so the treemap grouping and existing consumers don't move. One-time: a session previously bucketed as engineering/operations/content re-keys on next sync, so bucketHistory/driftSignals show a single-day discontinuity the day this ships, then track the new keys. Source: cli/src/lib/traces/classify.ts.

1.22.53

  • Custom-harness runs launched via a profile now show their real harness name in agents sessions instead of 'claude' (PHNX-2935).

  • Watchdog un-addressable stalls now include the same recovery command as agents focus (PHNX-3070). Rotate, needs-human declared-block, and nudge-refuse paths append addressabilityRecoveryHint so an un-addressable session prints agents sessions resume <id> / tmux wrap instead of a reason-only skip. Source: cli/src/lib/watchdog/runner.ts.

  • Un-addressable live sessions now surface a clear recovery hint at the point of use — agents focus, sessions inject, and the session picker report when a bare interactive session has no addressable terminal rail (not tmux/iTerm/an IDE terminal/a pty sidecar) instead of failing silently, and the hint now names the session's real id in agents sessions resume <id> rather than a placeholder (PHNX-3070).

  • Vitest can no longer launch the developer's browser (PHNX-3072). tests/setup.ts sandboxed HOME but not desktop-opener binaries, so a test that reached a real spawn('open' | 'xdg-open') opened the URL on the developer's machine — green on Linux CI, where xdg-open is absent, and disruptive locally. The harness now prepends stub open / xdg-open / gnome-open binaries to PATH so a spawn cannot reach the real handler; an unauthorized spawn fails the file. Source: cli/tests/opener-sandbox.ts.

  • Feed attention stamps sourceCursor at block-write time (PHNX-3073). coveredByResolution conservatively suppresses a candidate with no comparable cursor when a tombstone exists, which is correct for a stale lifecycle re-read but buried a genuinely new open block whenever session.lastActivityMs was unresolvable (cloud / remote / index-lag) because neither writer stamped a cursor. buildDeclaredBlock and the feed-publish hook now stamp sourceCursor.lastActivityMs from the write instant, so a fresh generation compares strictly newer than any prior tombstone. Source: cli/src/lib/feed/feed.ts.

  • Cursor auto-titles go through the same skill-scaffolding cleaner as Claude (PHNX-3123). readCursorMeta took chatMeta.title verbatim, so a Cursor session whose server-generated title echoed the injected Base directory for this skill: … line kept that path as SessionMeta.label — the field that wins on every surface for the session's whole life. Claude's ai-title was already collapsed to /<skill> at label composition (PR #2995); that collapse now lives in one shared helper (cleanGeneratedSessionLabel) and Cursor's title takes the same path. An ordinary Cursor title, and one that merely names a skills/… path, is unchanged. A Claude /rename (custom-title) is still never rewritten. Codex / OpenCode / Kimi / Droid put auto-titles on topic, not label, so they are out of this change. Not retroactive: already-indexed labels stay until the transcript next changes (RUSH-3122). Source: cli/src/lib/session/prompt.ts, cli/src/lib/session/discover.ts.

  • Traces insight engine — cross-session failure clustering + wasted time (PHNX-3141). agents traces sync's index.json now carries failurePatterns[] (top-K, ranked by wasted time, not raw occurrence count — a rare 8-hour retry loop still outranks a frequent 2-minute one), wastedMsTotal, and latency (time-to-first-tool p50/p90/p99/max). Failed tool_calls are grouped by (tool, cause, normalized error) so near-identical failures (a GitHub rate-limit message with a different countdown each time) fold into one pattern instead of a wall of near-duplicates. Per-session SessionDetail also gains surfacedToolFailures — every failed step, listed regardless of the run's overall outcome, so a session that ultimately succeeded still shows the tool failures it hit along the way. Source: cli/src/lib/traces/insights.ts, cli/src/lib/traces/sync.ts.

  • Traces insight group-by dimensions (PHNX-3141). cli/src/lib/traces/segments.ts derives the console bar axes — agent (model × harness), task type, failure timing — and the time-to-first-tool latency insight (firstToolMs p50/p90/p99/max). Pure functions; the sync integrator wires them into the shard. Source: cli/src/lib/traces/segments.ts.

  • Native-account labels live on the central accounts.native rows in agents.yaml (already fleet-synced via agents repo push/pull). remove/rename/label operate on every row matching (agent, identityKey), so a post-merge duplicate cannot silently survive (PHNX-3206).

  • The top-level agents.yaml header heals itself instead of freezing (PHNX-3315). serializeCentral parses the committed file to preserve its hand-written body comments, but that also preserved the leading metadata header verbatim — so a top-level agents.yaml written before the agi-cli rename kept its stale 3-line header (repo agents-cli, no $schema: line) forever while every freshly-written device doc got the current header. The header is now rewritten to the current META_HEADER on the next central write, textually (the yaml library folds the leading comment onto the first key when that key already carries a comment, so doc.commentBefore is unreliable), leaving body comments and every config key intact, and byte-stable once healed so it never re-churns. Source: cli/src/lib/state.ts.

  • The legacy central browser: tombstone drains itself on agents sync (PHNX-3315). New browser profiles write the per-device doc, but profiles created before the device-scoped store lingered in the shared top-level agents.yaml and churned every fleet pull until someone ran agents browser profiles claim by hand. agents sync now auto-evicts a lingering central profile into this device's doc and clears it from central — but ONLY for remote (ssh://) profiles. An ssh:// endpoint names a specific host, so the profile is fungible by design: any box resolves it to the same browser, so a concurrent cross-machine double-claim is harmless. A local/cdp:// profile has no per-machine ownership signal ("that browser is installed here" is not "I hold this profile's credentialed session"), so auto-claiming it would let two boxes with the same common browser each claim the same tombstone and flip a credentialed profile from identity-bearing to fungible fleet-wide — the exact logged-out-browser failure this module exists to prevent. Local/cdp tombstones are left central for an explicit agents browser profiles claim. The claim is selected and committed inside a single meta-lock acquisition, and non-throwing (a config conflict or unhostable profile is left central rather than wedging the sync). profileRegistry is left as the single source of truth: a claimed profile lives in the device doc alone, never double-counted across two stores. Source: cli/src/lib/browser/registry.ts, cli/src/commands/sync.ts, cli/src/lib/state.ts.

  • fleet.discovery / fleet.ignored are device-scoped with a union-on-read view (PHNX-3315). Every box used to rewrite one shared fleet.discovery/fleet.ignored map in the top-level agents.yaml (each dismissal stamped ignoredOn:<host>), so two boxes dismissing different peers produced guaranteed git line-conflicts on the next pull. Each box now records ONLY its own discovery decisions and dismissals in its tracked device doc (devices/<host>/agents.yaml fleet: block), and the effective fleet view is recomputed as a deterministic, order-independent UNION across every device doc at read time — ignored beats approved for a conflicting name, and the newest ignoredAt wins a shared dismissal — so every box computes the identical view and no box ever rewrites another's file. A fold-then-delete migration moves the existing central maps into the owning box's device doc before the central keys are dropped (the device-doc write lands before the central strip, so a crash re-folds idempotently). agents devices register/ignore/unignore/ignored, discovery reconcile, the sync pending-diff, and agents fleet capture all read the union. Source: cli/src/lib/devices/{discovery-policy,registry,device-docs,config-migration}.ts, cli/src/commands/{ssh,fleet-capture}.ts, cli/src/lib/state.ts.

  • Agent-host registrations are device-scoped (deviceHosts) with a merged read (PHNX-3315). Locally-discovered SSH hosts and inline registrations landed in one shared hosts: map in the top-level agents.yaml, so every box that enrolled a host rewrote the fleet-shared file. Registrations now write this box's device doc hosts: block; the effective host directory is the cross-box union (newest addedAt wins a name collision), and remove only edits this box's own doc. A fold-then-delete migration relocates the existing central hosts: map into this box's device doc and drops the central key. Source: cli/src/lib/hosts/{providers/local,registry,remote-os}.ts, cli/src/lib/devices/{device-docs,config-migration}.ts, cli/src/lib/state.ts.

  • scope:'device' native accounts move off the shared file into the device doc (PHNX-3315). addNativeAccount accepted scope:'device' but always wrote the central accounts.native map, so a per-box login (and its email / identityKey PII) landed in the git-tracked, fleet-synced agents.yaml. A native login is machine-local, so its home now follows its scope: a scope:'device' identity (and any binding that targets it) writes this box's device doc accounts: block, keeping its identity PII off the shared file; genuinely fleet-shared account defaults and scope:'version' identities stay central. Every account reader (listNativeAccounts, findUnifiedAccount, binding resolution, rename/remove) merges the fleet-shared central store with this box's device slice. A fold-then-delete migration relocates existing scope:'device' natives and their bindings out of central (forward-only; no history rewrite). Source: cli/src/lib/account-registry.ts, cli/src/lib/devices/config-migration.ts, cli/src/lib/state.ts.

  • tmux.enabled now gates remote --device runs too — no more forced tmux wrap (PHNX-3316). resolveTmuxWrap (cli/src/lib/exec.ts) no longer lets remoteDispatch override the device's tmux.enabled: off (the default) means a followed agents run --device <host> spawns the agent directly, with no tmux session on the peer. Durability for a bare remote run comes from the existing auto-reconnect in cli/src/lib/hosts/reconnect.ts, whose target (agents sessions focus <id> --local) already resumes the harness session in place when no pane exists — a dropped link costs the in-flight turn, not the conversation. One case still wraps regardless: a followed remote run whose launcher has no TTY (CI, scripts, another agent driving the CLI), where the detached pane is the run's only interface. undurable is now reached only when a wrap was actually wanted (opted-in device, or the no-TTY case) but the peer has no tmux; its refusal message names the agents ssh <host> 'agents devices config <host> tmux.enabled off' alternative (the key is machine-local, so it must be set on the peer) alongside --raw. RUSH-3125's forced wrap conflated durability with an ergonomics preference and wrapped boxes whose operator had explicitly left tmux off. hosts/reconnect.ts's header premise, the hosts/dispatch.ts REMOTE_INTERACTIVE_ENV comments, and the tmux.enabled help text (device-config.ts) are updated to match; wrap-matrix tests cover the new gate.

  • Share attribution bar: full-width chrome, not a floating box. The bar was injected position:sticky inside <body>, so on a page whose body is a narrow centered column (max-width + margin:auto) it inherited that width and rendered as a floating black rectangle. It's now position:fixed, full viewport width, with an injected html{padding-top} pushing the page content down beneath it — clean top chrome on any page layout. flex-wrap:nowrap + left-side ellipsis keep it one line so the push height stays correct. Source: cli/src/lib/share/worker-template.ts.

1.22.52

  • agents artifacts share --visibility me|org — identity-gated share pages (PHNX-3260). Extends the P1 public|unlisted tiers (RUSH-3135) with two Phoenix-gated GET visibilities. me is visible only to the signed-in owner (the viewer's Phoenix userId must equal the stamped owner); org is 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 and share list, like unlisted. Publishing org from a public inbox (gmail/googlemail/outlook/hotmail/live/icloud/me) is refused with 400, and me/org PUT without a Phoenix identity is a loud 400 (a BYO WRITE_TOKEN alone cannot publish them). Browser GET of an unauthenticated me/org URL 302s to ${PHOENIX_ID_BASE}/login?return=<url> (the same Google OAuth as /device, no new IdP); the returned one-time phoenix_ticket is redeemed once via POST /api/v1/auth/ticket and exchanged for an HttpOnly, HMAC-signed __Host-phoenix_share cookie on the share host. An existing CLI Authorization: Bearer still works with no redirect. me/org responses send Cache-Control: private, no-store and X-Robots-Tag: noindex; public/unlisted GET is unchanged (anonymous 200). Because the Worker template changed, already-provisioned BYO endpoints need agents artifacts share update to 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 status no longer errors on macOS with ps: etimes: keyword not found — the process-uptime probe now shells the portable ps -o etime= and parses it with the same helper the keychain reaper uses. waitForSocket raises 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 throws Timeout waiting for browser daemon socket. New agents browser stop --daemon stops the browser daemon and clears a stale/wedged browser.sock, failing loud if a live server still holds it, so the next start comes up clean. And agents 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 --device instead of failing with a misleading No 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'd handle taken when any page's stamped owner differed from the writer's Phoenix userId. But a BYO WRITE_TOKEN publish stamps owner = 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. Requires agents artifacts share update on already-provisioned endpoints (the Worker template hash changed).

  • sessions resume/sessions attach/sessions focus no 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 full ag-<agent>-<8hex> alias agents tmux ls prints, or its bare 8-hex suffix when it names exactly one live local pane — attaches with zero SSH, for bare sessions resume <alias> and deprecated sessions attach <alias> too, not just --attach-only. Previously collectSessionCandidates ran first — two fleet sweeps (transcript pool + live roster) that stall on offline devices (~2 minutes measured on yosemite-s0, two copies of unreachable or no agents CLI — skipped) — and bare resume of 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. --device skips 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 attach tears 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 in tmux ls — the v6 pane-died hook only detach-clients when a client is attached, and the attach verbs never killed the remain-on-exit husk. tmux attach, sessions focus / resume --attach-only, and go'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) local agents run --interactive no longer wraps in tmux, so session-tracker tests cannot leak live "trust this folder" panes. agents tmux kill with 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 ls prints 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 start no longer silently mints a logged-out auto-chrome (PHNX-3296). With no configured default and no existing launchable profile, start now errors and points at agents setup / agents browser use <name> / the browser.device fleet hub instead of popping a signed-out Chrome. A pre-existing auto-chrome or legacy default still 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 stop of 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 skips gatherRemoteActive entirely, so a sleeping peer cannot print unreachable or no agents CLI or 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 / focus with no id stays all-settle. Source: cli/src/commands/go.ts, cli/src/commands/detach.ts, cli/src/commands/sessions-stop.ts.

  • agents monitors add refuses 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 user self-heals a non-git or partial ~/.agents by 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 with Not a git repo: ~/.agents and 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-level agents.yaml from origin (backing up the previous copy to .history/agents.yaml.pre-adopt.bak first), preserves gitignored runtime state, and surfaces (never clobbers) real local edits. The remote URL is resolved from an existing origin, AGENTS_USER_REPO_URL, or a device-local record left by a prior healthy sync — never hardcoded. agents sync status now flags a non-git ~/.agents as 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 rush CLI) is macOS-only, so agents feed post --level important / --blocked, agents notify, and monitor notify actions run from a headless Linux worker used to record the post but fail to reach the owner with owner 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 reroute agents message already 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_FORWARD guard). 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 frontmatter title:, then the filename, so repeat publishes of a long-running artifact update the same URL; --slug remains 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 via test.sh --shard N by default, resolving N from the eligible workers agents devices pick reports (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,c overrides mirror test.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 with agents 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.sh cuts the helper's own tag (PHNX-3228). It published ComputerHelper.app.zip to the CLI's v<version> release, but since the client was repointed at per-helper tags the resolver asks for computer-mac/v<x.y.z> (helperTag in cli/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 cuts computer-mac/v<x.y.z>, requires the helper's version explicitly instead of defaulting to cli/package.json (that default was the coupling), and refuses an existing tag, since the upload uses --clobber and would otherwise replace a binary an installed CLI already pins. Symmetric with publish-computer-win.sh. Source: cli/scripts/publish-computer-helper-mac.sh. It also cuts the tag itself now. gh release create --verify-tag refuses to invent a tag absent from the remote, and nothing else pushed computer-mac/v<x.y.z> — release.sh delegates 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 manual git 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-zero gh is not evidence of absence — unauthenticated, offline, and rate-limited all exit non-zero — so reading that as "not published" would have carried on into gh release upload --clobber over 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 what gh actually said.

  • agents traces sync retains 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's file_mtime_ms > watermark filter 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 classified transcript-unavailable (the file is gone — expected history, not re-read, aged out after 14 days), parse-failed, or upload-failed (both retried); agents traces sync prints the breakdown (… errors (N transcripts no longer on disk · M parse/upload failures — will retry)) and agents traces status lists 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_REPO still named phnx-labs/agents-cli after the repository was renamed to phnx-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 in helper-download.ts; this removes the reliance on the redirect. The agents.yaml $schema URL, the CHANGELOG link, the star nudge, and the package.json repository/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.sh carried the old slug too, and that one is a write path: REPO_SLUG reaches gh release view (the immutability guard), gh release create, and gh release upload --clobber. Publishing signed helper binaries was going through the rename redirect. Found by sweeping scripts/ 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:170 WIN_HELPER_RELEASE_REPO (a separately hardcoded constant — the download URL for computer-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; and factory/snapshot.ts:30, which polls this repo's PRs. installations/migrate.ts also wrote the old URL into the agents.yaml header it generates. Separately, .github/workflows/tests-windows-host-e2e.yml:54 gated on github.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 $id moved with it — state.ts and migrate.ts write that URL as the $schema hint into every user's agents.yaml, so a mismatched $id would make editors reject their own config.

  • The menu-bar helper's staleness check compares the HELPER's version, not the CLI's (RUSH-3230). installAndStartService stamped getCliVersion() as the installed helper's version, and menubarSetupStale() compared against getCliVersion() 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, which KeepAlive then 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: release with its helper version, or local with the source path + mtime for a dev build (menubar's build.sh hardcodes CFBundleShortVersionString, 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 / doctor now print three labelled lines — helper installed, helper available, CLI version — instead of conflating two axes into one, and the permanent false "(mismatch — agents menubar setup updates 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). downloadWinHelperExe built its error message from `v${version}` — the CLI's tag shape — while the URL it had just tried came from helperTag('computer-win', version). So a genuine 404 sent the reader looking for v1.0.0, which does not exist, instead of computer-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 a getCliVersion import 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-statusline from a dev install, or an absolute path) was not recognized as us. Every status-line render then spawnSynced that command, which read the same delegate and spawned another — unbounded recursion. Observed on a real box: ~4,900 live node __claude-statusline processes 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-statusline subcommand under any binary name or path (isStatusLineSelfReference), installClaudeStatusLine refuses to persist such a command as a delegate (and deletes an already-poisoned delegate on re-install), a AGENTS_CLAUDE_STATUSLINE_DELEGATED env marker hard-caps delegation at one hop, and the delegate spawnSync now 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 status text/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 --after blockers instead of spinning until --max-waves. teams start (no --watch) reports teammates that failed during the wave in a Failed this wave section (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 identical teams add --name <name> after a failure requires teams remove <team> <name> first. Successfully resuming a failed teammate clears the prior attempt's evidence as the replacement enters running; 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 sync now fire-and-forget POSTs the Phoenix bearer to api.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-hosted AGENTS_TRACES_WRITE_TOKEN is never sent to Prix. Source: cli/src/lib/traces/sync.ts.

  • Ship traces-daily-sync routine (PHNX-3258). Daily agents traces sync at 02:00 UTC to accumulate bucketHistory for 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 balanced now spreads across your provider accounts, not just native logins (RUSH-3182). A setup-token or API-key account added with agents accounts add is 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 --account couldn't select them. The run path folds those provider accounts into the candidate list and injects the picked one through the existing --account path (a setup-token still authenticates via CLAUDE_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.sh gained --with-helpers so an ordinary release does no helper work, but release-attestation-produce.sh kept the same unconditional release-manifest.sh verification — so the coupling survived one step upstream. Found live: a one-line comment fix in native/computer-mac/scripts/build.sh (an apps/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-helpers flag, 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 on computer-win/v<x.y.z> rather than the CLI's v* 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-exe builds, 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 a v-prefixed or non-semver version, refuses an existing tag because the upload uses --clobber and an installed CLI may already pin it, and is dry-run by default. Symmetric with publish-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 auto run uses, so role=worker/role=personal marks 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. Requires agents ≥ 1.22.49 for devices 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 the devices pick --json dependency, so sharding also works from a machine whose installed CLI predates 1.22.49. Source: cli/scripts/test.sh.

  • --shard fails loud on a count below 2 and on a conflicting target flag (RUSH-3230). --shard 0 previously passed the numeric check, ran zero shards, and still printed All 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, MODE was last-write-wins with no cross-flag validation, so --shard 6 --device box silently dropped one of the two purely on argument order; conflicting target flags now die naming both. Source: cli/scripts/test.sh.

  • --devices obeys the same 2-worker floor, and is validated before prerequisites (RUSH-3230). --devices onebox derived 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 missing rsync first. 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 existing describe boundaries (per-device config / fleet-wide defaults + role + describe / retired-subcommand tombstones) over a shared device-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 shared daemon-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) and MenubarHelper.app (3.3 MB) were copied into dist/ by build, listed in files, and hard-gated by two prepack checks — 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. build still clears any stale bundle from dist/ 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 prepack refused 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 setup provisions the isolated agents-traces R2 bucket and Worker, uploads the canonical private trace Worker, binds its write and Phoenix identity secrets, enables its workers.dev route, and maps traces.agents-cli.sh.

  • agents traces sync emits the rich Phoenix Evals index shard (RUSH-3142 M3). The per-device index.json now 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-healing session_topics table 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 --device shipped the tree to ~/.agents/test-runs/agents-cli — but ~/.agents is itself a git repo, so anything in the suite calling git rev-parse --show-toplevel resolved to ~/.agents instead of the shipped tree (release-manifest.test.ts went looking for ~/.agents/native/computer-mac/Sources and 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 reason sandbox.sh's ~/workspaces choice 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 the test:remote package.json alias. Offloading was opt-in at each call site because scripts/sandbox.sh took a hand-composed command string rather than a verb, so every call site opted out: scripts/build.sh and scripts/release-attestation-produce.sh both ran bun run test on whatever machine invoked them. New scripts/test.sh is 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 --device command instead of silently falling back. build.sh and 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.sh gains the sibling projects' verb vocabulary (sandbox.sh test), which also fixes its bare default running bun install && bun run test at 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.sh seeded its helper manifest from gh 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 (assets computer-helper-win.exe + .sha256) into the same v<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 on computer-mac — a helper it never rebuilds — with helper computer-mac input changed but this producer never rebuilds it. Observed live: v1.22.48 (helper-only, published 09:54Z) shadowed v1.22.47 and blocked the release. The seed now walks back through recent releases to the newest one that actually carries a release-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 setInterval timers in runDaemon() are now PeriodicServices registered on ServiceSupervisor, 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 through agents daemon services instead of an inferred running (unsupervised) label. scheduler and webhook-receiver remain outside the supervisor (neither fits the PeriodicService shape — see cli/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 services shows every service's live health, with live enable/disable/restart (RUSH-3193 #4). The daemon services view 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; --json gains the full service list while keeping the existing secretsBroker/browserIpc fields. 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 ServiceSupervisor migration (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. PeriodicService gains an optional startupDelayMs (default 0, no change for other services); SelfHealService sets it to 30s. Also fixes a stale comment on watchdog-service.ts claiming an SSH fan-out that runWatchdogPass does 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-host broadcast 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. The usage.primary-host config key (and its interactive.host fallback 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 setInterval closures 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 new DaemonService/PeriodicService contract plus a ServiceSupervisor give every registered service its own timer, a per-service error boundary that can never propagate to process.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, and supervisor.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 list now shows it as session-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. Stale cli-config.json metadata 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 every last_seen source as a failed refresh. Current Grok weekly meters cache normally; an expired or missing event tells the operator which grok@version to 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 pool agents run --device auto draws from, so role=worker / role=personal marks govern it. The name alone goes to stdout so scripts can consume it (box="$(agents devices pick)"); candidates and load go to stderr. --json adds 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.sh now auto-picks a fleet worker by default instead of requiring crabbox. --device auto says the same thing explicitly, --crabbox selects the previous disposable-crabbox path, and --device <box> / --here are 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 --here warning when that machine is itself a pool worker.
  • scripts/release-attestation-produce.sh gains --test-crabbox, so all three of test.sh's lanes are reachable from a release. It previously exposed only --test-device and --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.ts used to build releases/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 in helper-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.zip is served as Agents.CLI.app.zip while the CLI requested the spaced name — a permanent

    1. The asset is now published as Agents_CLI.app.zip: a name GitHub preserves verbatim, chosen deliberately rather than mirroring GitHub's rewrite (matching Agents.CLI.app.zip would work only for as long as that normalization rule holds). helperAssetUrls additionally 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 Windows helper exe is decoupled too. ssh-tunnel.ts is a separate implementation that never routed through helper-download.ts, and it still keyed computer-helper-win.exe to v${cliVersion} — the identical coupling, meaning every CLI release had to re-stage a ~165 MB binary or the download 404'd. It now resolves computer-win/v<x.y.z> from the same floor table.

Removed

  • release.sh no 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 under Agents CLI.app.zip, a name GitHub rewrites to a dot and no client can fetch. The hard die on a missing ComputerHelper.app.zip is 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 the release argument on a machine holding a Developer ID produced a real signed bundle, passed every self-test, and exited 0 — while spctl reported rejected / source=Unnotarized Developer ID with 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 and prepack already stops them shipping.

    A credential-less debug build on a machine that holds a Developer ID now signs ad-hoc (with the .dev bundle id) instead of failing: SIGN_ID is 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 before Info.plist is 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-level await import('bun:sqlite') in src/lib/sqlite.ts made that module un-lowerable to CJS, so every test that spawns a subprocess through tsx died at transform time with Top-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 through createRequire, 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 old as string cast did for the dynamic import).

    This mattered beyond the suite: release-attestation-produce.sh runs 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 at AGENTS_SYNC_PASSPHRASE rather 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. Four docs-hygiene guards had been failing ever since, which is the alarm working as designed rather than test staleness.

  • Restored the **Severity rubric** section to docs/observability.md, removed by the same reset. doctor-findings.test.ts pins this prose copy against FINDING_SEVERITY, so its absence meant the docs-cannot-drift guard was simply not running.

  • add-targets-user-repo.test.ts no longer enumerates the state.js surface. Its partial mock hand-listed every export, so adding getCliVersionCachePath to state.ts took the whole file down with No "getCliVersionCachePath" export is defined on the mock. It now spreads importActual and 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): staging ComputerHelper.app.zip onto the CLI's v<version> tag, which publishes an asset no client requests; and release-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.sh flag is inert until it is merged (RUSH-3216). release.sh execs release-worktree.sh, which checks out origin/<default> and re-runs the script from there, so the second parse is the merged copy and an unmerged flag dies as unknown flag even though the local parser handles it. Pass --orchestration-phase to 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 share now publishes to share.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-saved file://…#section TOC 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 --device run 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 shows Session <uuid> on <host> and agents 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/resume over SSH, or a remote tmux attach ends, the CLI writes the full session id and agents sessions resume <id> under OpenSSH's Shared 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.sh compared bin/Agents CLI.app to beb02d…, but @phnx-labs/[email protected] and keychain/v1.0.0 both contain a49080…. Linux npm pack therefore 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). classifyHostLink decides 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 to connected. 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 --device session whose pane lives on another machine, since every signal it reads is local). There is now an unknown link, which says the question is open rather than answering it; connected is returned only on positive evidence — a counted client or a republishing window. unknown is not a loss signal: it never promotes a status, and it no longer clears a derived attached presence, so a plain terminal session you are sitting in keeps its marker. Everything else is unchanged and deliberately so — a deliberate agents sessions detach is still excluded first, tmuxClients === 0 is still authoritative, an absent client count still never reads as zero, and per SES-18a a running session 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 run wraps an interactive agent in a tmux session named ag-<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 harnesses ag-claude-c8c4a2c8 carries a real, resumable short id while ag-codex-364cd550 carries a throwaway that resolves to nothing — and the two are indistinguishable on sight. createSession now stamps two session-scoped tmux user options, @ag_session_id and @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 something agents sessions <id> would reject. The id is published only when the harness actually received it — a new isHarnessKnownSessionId mirrors the two branches in buildExecCommand that put an id on the command line (a native resume on any harness with a resume spec, and Claude's create-with---session-id), so agents run codex --session-id X without --resume, where X never reaches codex, correctly publishes nothing. This is the in-tmux counterpart to the existing name-parsing recovery tier (shortIdFromName + resolveNamesToSessionIds in cli/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-bucket BucketStats (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 local sessions.db and 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 a SessionDetail (a meta summary — spanMs/turns/tools/errorCount/tokens/cost/outcome/repo — plus a plain-language whereItWentWrong) 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 with agents devices role <name> personal) authenticates every Claude run from its per-version login, whether it opens a TUI or is a headless one-shot like agents 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 reserved auth-bundle setup-token and took the session off your login — surfacing as /status reporting Auth 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.sh now seeds release-manifest.json from the last published GitHub release before falling back to an empty one — a fresh store had no recorded computer-mac inputDigest, so the helper loop read "input changed" and told the operator to run publish-computer-helper-mac.sh, which does not write a manifest, so the instruction looped forever on a byte-identical helper. (2) release-worktree.sh now exports RELEASE_ATTESTATION_DIR to the caller's store when it is unset: the throwaway release worktree resolved its own empty .release-attestations, so require reported missing exact attestation key with ? 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.sh now sources headless-sign-context.sh itself: 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 in codesign with errSecInternalComponent unless 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 --json row (which the AGI EXT Fleet reads) now carries a recap so a row reads as the agent's work, not its stale first prompt: title follows a best-source-wins ladder — a /rename/harness label (which also holds an agent-generated title) → the last agent line → the first-prompt topic — and recapSource ('label'|'last'|'prompt') names which rung won, so a session that produced work shows an agent-derived line. The first user turn is cleaned into userPromptClean/userPromptKind (with lastAgentLine exposed) so a screenshot path folds to [image], a pasted $ cmd to the command, and a /skill install path to /<name> — path noise never shows on the "You" line — and the recap card's Prompt: line uses the same cleaning. Separately, a crash-leaked --device tunnel 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-prompt topic (RUSH-3011). Claude ai-title and /rename events 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 parked claude entirely 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.ts records claude@<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, so probeAuthHealth derives a live verdict from it and only fires a network probe when no fresh evidence exists — roughly halving fleet endpoint load. agents devices ping --strict passes forceLive, which skips the derivation entirely — its contract stays a genuinely live request that surfaces revoked immediately — and derivation additionally requires a local credential (signedIn), so a fleet-imported snapshot on an unsigned home never reads as live. 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 doctor no longer claims a healthy just-upgraded helper is "running the OLD binary" (RUSH-3038). The stale-process check compared a pid start time parsed from ps -o lstart, which prints whole seconds, against the installed bundle's sub-second mtime. restartMenubarHelperAfterSwap restarts 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 …353000 and 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 auto stops prompting for approval. Codex declared only plan/edit/skip, so --mode auto silently degraded to edit — and edit is approval_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 real auto: the agents-auto permission profile, identical sandbox to agents-edit (workspace, ~/.agents, the regenerable toolchain cache roots, network on), with approval_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: auto does not widen the sandbox, and --mode skip remains the only mode that removes it. Interactive shim launches (a bare codex at your own terminal) still pin edit, 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_mode predicate (RUSH-3050). A hooks.yaml / subrule hooks.yaml entry may declare matches: { 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-style permissionMode). 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 declare permission_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.yaml under config.description, so it syncs to every machine via agents repo push/pull exactly as role does, and any box may set it for any device (shared visibility). 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. notes is 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.yaml under fleet.ignored, a list of { name, ignoredAt, ignoredOn } entries (who dismissed it, when, on which box — the read side for a future agents 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 carries fleet.defaults). Writes go through the existing updateMeta path (withMetaLock + atomic write), and a malformed fleet.ignored block is still a hard error rather than a silently-emptied set. A one-shot migration folds any existing ignored.json into fleet.ignored (union by name, legacy updatedAt kept as ignoredAt) and removes the legacy file only after the central write lands; running it twice is a no-op. The loadIgnored / isIgnored / addIgnored / removeIgnored signatures 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 run now spawns directly by default (RUSH-3066). tmux.enabled is an explicit per-device opt-in for addressable panes instead of the default wrapper: the wrap's benefit (a unique %pane so agents sessions --active can tell co-located agents apart, and agents focus re-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 with agents config set devices.<name>.tmux on. --no-tmux and --disable-tmux remain 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-redact reads honestly in compare and lineage too (RUSH-3077). The single-trajectory footer already said Unredacted (local only) under --no-redact, but the compare (--compare) and lineage (--tree) renderers hardcoded a Secret-redacted footer 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' own redacted state (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 usage command is removed (RUSH-3079). It was a second, worse surface for the same data agents view already renders — per-account quota/rate-limit usage with the account, version, and auth state beside it — and in practice most harnesses printed stale or "does not publish usage data" there. Use agents view (optionally agents view <agent>, --refresh for an explicit collection, --json for scripts). The name is retired: agents usage fails as an unknown command and never auto-corrects into a live command. The shared usage library behind agents view and 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 browser binds --device at start (RUSH-3086, RUSH-3087). --device stays on agents browser start and is rejected on later verbs; the starting machine records a local task → device index so type/click/screenshot --task post resolve 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.png now 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 list rendered a down box as a bare ci-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 an Agents CLI.app.zip GitHub release asset per tagged version. A machine whose tarball lacks the bundle fetches that asset for its exact CLI version on the explicit agents setup secrets path (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 at agents setup secrets rather 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.app is now ALSO published as a signed + notarized MenubarHelper.app.zip GitHub release asset, so a machine whose npm tarball lacks the bundle can fetch + verify it from the v<version> release. agents menubar enable / agents menubar setup download it when no bundled or local .app is present; a downloaded bundle is verified (sha256 + codesign --verify --deep --strict + Developer ID Team 2HTP252L87 + a designated-requirement pin on bundle id com.phnx-labs.agents-menubar + spctl notarization) 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 .app STILL 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 new src/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 to CFBundleExecutable for both the Accessibility list row (blank icon) and the "would like to control this computer" prompt. The executable inside the bundle is now named AGI 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), the orphan-reap.ts protected-service regex, and the Swift side's SingleInstance.swift/ChildProcessSelfTest.swift, now reading a shared HelperIdentity.executableName constant. 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-download first. helper-download.ts imported two pure sha256 helpers (parseSha256Asset, sha256File) from computer/ssh-tunnel.ts, whose import graph runs browser/drivers/ssh → browser/chrome → secrets/* → secrets/download-keychain, which imports back into helper-download while it is still evaluating — before EXPECTED_TEAM_ID is bound. The result was a hard ReferenceError: Cannot access 'EXPECTED_TEAM_ID' before initialization thrown from secrets/download-keychain.ts:45, which took down the drift-sync apply path and the self-heal isolated-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 label reads 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 at agents view codex, which is organized by version. The first argument now also accepts <harness>@<version>: agents accounts label [email protected] work labels whatever account is signed in there, and the label stays bound to the account identity, so codex#work keeps 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 --account is 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 at share.agents-cli.sh — the Phoenix access_token from phoenix-session.json is the bearer, so there is no R2 bucket, Worker, or write token to provision. share status, share list, share revisions, and unshare/share delete resolve through the same resolveShareBackend seam, so a managed-only user can see and take down what they just published instead of being told to run agents artifacts setup. share analytics is 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 verifies Authorization: Bearer against Phoenix ID (GET ${PHOENIX_ID_BASE}/api/v1/auth/me), stamps customMetadata.owner with the verified userId, and namespaces the R2 key by that userId so one user cannot write another's prefix. --visibility public|unlisted (default public) replaces the --unlisted/--private booleans; those flags stay as hidden aliases mapping to --visibility unlisted. Unlisted GET now sends X-Robots-Tag: noindex (it was already hidden from the gallery/list). org visibility 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 static WRITE_TOKEN is still honored when the bearer matches it (checked first, so the platform endpoint may set both principals); AGENTS_SHARE_BACKEND=byo or a caller-supplied write token forces the BYO path while signed in. Already-provisioned BYO endpoints need agents artifacts share update to 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_BASE so Phoenix-bearer auth survives agents artifacts share update (RUSH-3138). Cloudflare's script-upload API clears Worker bindings/secrets wholesale; the managed Worker reads env.PHOENIX_ID_BASE to verify bearers at ${PHOENIX_ID_BASE}/api/v1/auth/me, so every update wiped it and Phoenix PUTs 401'd while BYO WRITE_TOKEN kept working. The deploy path now re-applies PHOENIX_ID_BASE as a secret_text after every script upload (same Secrets API as WRITE_TOKEN), sourced from the CLI's PHOENIX_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 the browser start command, but navigate, click, screenshot, evaluate and the other page verbs open a browser implicitly when the caller has no live task. A browser navigate --device <box> therefore opened a browser on a machine whose owner never opted in, while browser start --device <box> was correctly refused. The gate now sits in the daemon at the two points that can launch a browser (BrowserService.start and the create branch of resolveOrCreateTask), 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 off means "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 inherits AGENTS_FLEET_REMOTE=1 permanently, 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/--description existed only on create, and updateProfile() had no CLI caller at all. profiles edit <name> reuses create'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-filter edit re-checks the --electron gate. Source: src/lib/browser/profiles.ts, src/commands/browser.ts.

  • profiles doctor flags a profile that resolves to the wrong machine's browser. A profile declared by one device but bound to cdp://localhost:PORT is 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 createProfile runs was never applied on update, and applying it naively would have failed every edit against the profile's own stored port. Extracted as assertLocalPortFree(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 edit refuses a name change because the name keys the on-disk runtime dir, every endpoint/fork dir derived from it, and the browser.profile pointer. 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. rename moves the config (staying in whichever store it already lives in), moves every cache dir belonging to the old name, and repoints both browser.profile and browser.viewer when either pointed there — a dangling browser.viewer sends 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-dir out 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. os joins default as a name a profile may not take — it is the reserved browser.viewer value meaning the OS handler. Source: src/lib/browser/profiles.ts, src/commands/browser.ts.

  • Profile-name validation is shared between create and rename. The shape rule lived inline in profiles create, so a second caller would have accepted names create rejects. Now assertRegistrableProfileName, which also refuses default — 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 navigate honoured browser.profile and nothing else did: agents fleet login, agents devices lease, agents feedback, agents sessions trace --open, and agents browser sessions --open each shelled straight to open/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.viewer config key (device scope) — a profile name, or os to keep using the OS default handler. Unset follows browser.profile. Deliberately distinct from browser.profile: one is the profile agents drive, the other is the browser that shows you a page. Source: src/lib/device-config.ts.

  • New show IPC 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 not navigate. 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 to open. Use it instead of navigate for anything a person will read: navigate binds a task and the reaper closes a task's tabs. --os-browser forces the OS handler; --json reports where it landed. Source: src/commands/browser.ts.

  • --device interactive resolves to the machine the human is at. --device auto picks a box by load; this picks the one box someone is actually looking at, pinned as interactive.host. It resolves in the shared host matcher rather than per command, so browser, run, sessions and secrets inherit it; teams and ssh resolve it explicitly because they leave the fleet passthrough before the matcher runs. A few narrower --device surfaces do not consult the matcher. None of them mis-routes: most fail loud, and devices harnesses filters 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 (beside detach/attach): it stops the interactive process and tears down its tmux/mux session — reusing detach's exact stopInteractive teardown (kill-session when tmux-hosted, else SIGTERM→SIGKILL the pid) and resolution (remote sessions stopped over SSH, cloud/team refused) — but does NOT resume it headless. Use detach to keep an agent working unattended, stop when 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-died hook is now session_attached-aware (AGENT_HOOK_SCHEMA v6): with a client attached it detach-clients as before (so resolveAfterAttach reads the exit status, EXEC-23b unchanged); with no client attached it kill-sessions outright. A wrapped agent that exits unattended — a closed terminal, or a /exit after the user detached — previously left a dead remain-on-exit husk 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.
  • agents accounts list is 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 the run <harness>#<label> / run <harness> --account <name> selector hints inline. Provider bundles keep their own aligned section. --json output 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-tested renderAccountList. 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 list shows WHERE, not a stored scope. The column is the devices whose own devices/<machine>/agents.yaml declares the name — true by construction, no field to drift. --json adds devices and kind (identity when exactly one device declares the name, fungible when several do). profiles add is an alias of create and prints Added "<name>" on <device> (port N). Source: src/commands/browser.ts.
  • profiles doctor fails where on 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 off now gates attaching to a running browser, not just launching one (RUSH-3064). PR #2932 moved the consent gate to BrowserService.start and the create branch of resolveOrCreateTask, closing the implicit-launch bypass — but the two early-return attach paths (an explicit --task, or a lone caller-identity match) returned before it, and tabAdd reached createPageTarget unconditionally. 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 ungated status). The gate now sits at the top of resolveOrCreateTask, 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 inherited AGENTS_FLEET_REMOTE=1 never 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) and prune (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, plus prune. The canonical pair remove <name> / prune (already the pattern in commands, hooks, skills, versions) is now applied to the stragglers: agents route rm / agents devices rm / agents projects rm make remove the primary spelling (rm stays an alias); agents browser profiles delete → remove (alias delete); and the bulk-stale sweeps agents browser gc, agents lease gc, agents mailboxes gc, and agents routines cleanup become prune (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) and agents artifacts share delete/unshare are 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 --device agent 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's tmux.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 exports AGENTS_REMOTE_INTERACTIVE=1 and 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, and AGENTS_NO_TMUX=1 still 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 shared ControlMaster — ControlPath=cm-%C hashes 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 --json now 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 return already_answered without 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 auth help 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-output tests 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's GIT_AUTHOR_EMAIL / GIT_COMMITTER_EMAIL environment variables outrank -c config — 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, collectCommits matched none of them, and four tests failed with a bare expected +0 to be 2. CI never caught it because its runners export no such vars, which made this a local-only failure that also wedged release-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_not was typed, documented, and silently inert — every gated hook fired anyway (RUSH-3116). The predicate shipped in shouldFire() (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 in src/lib/hooks/cache.ts, and the mirror was never updated. So matches: {permission_mode_not: plan} fell through to return True and 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 a permission_mode: plan payload printed FIRE, while the positive permission_mode control correctly printed SKIP. 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 HookMatches surface, so a predicate cannot ship half-implemented again. cache-matches.test.ts already cross-checked the shim's real bash+Python decision against shouldFire() "so the two implementations can't drift" — but only over a hand-written fixture list, and permission_mode_not had no fixture, so the suite stayed green through the drift. A new test reads the HookMatches interface out of types.ts and fails with the offending key name when any declared predicate has no conformance fixture; permission_mode_not, git_dirty and project_has were 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 existing permission_mode is 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 both permission_mode and Grok's camelCase permissionMode, and ANDs with the positive form when both are declared. Motivation, measured from ~/.agents/.cache/perf/perf.db: nine guards fire on every Bash tool 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 — the claude@<version> alias shim agents run actually invokes for a pinned/balanced version — carried a hand-copied subset of the config env that had dropped the Linux .oauth_token fallback block the main shim's claudeAdapter.shimConfigEnvBash has (the alias env now reuses that adapter block verbatim via VERSION_DIR, so the two can't drift again); and (2) nothing wrote the .oauth_token file 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 via buildExecEnv); 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_token fallback, 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 --interactive on 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_token fallback 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 … --lease ran agents setup on the fresh box with >/dev/null 2>&1 || true — output and exit both discarded — so a setup that never left ~/.agents/.system a 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 captures agents 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 ~/.agents config-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 --device run lost its link, only Claude ever reconnected. Claude is handed a --session-id before 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-minted AGENT_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 of agents sessions resume, so the advice printed at the one moment you needed something that worked was stale. They now name agents 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 -tt leaves 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. A stty -g snapshot 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 in sshStream itself, 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 main to hold still — publish is decoupled from live main (RUSH-2395 audit). release.sh used to squash-merge the release PR into origin/main and then refuse to publish unless main's tree byte-matched the attested release tree — so any commit that landed on main during the release (or a CHANGELOG merge 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, so main can 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 a main already at the target version) is unchanged. Source: apps/cli/scripts/release.sh.

  • Fixed: resolve-target.test.ts failed on any machine actually named mac-mini or zion. The fixtures wrote device declarations for two real fleet hostnames and then asserted the resolver would tunnel to them. On a box with that machineId() 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 use peer-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-title from the first turn, so a session opened with /continue (or any skill) was named Base directory for this skill: /home/…/.claude/skills/continue — the scaffolding line the skill injects, not the task. That name lands in SessionMeta.label, which wins on every surface for the session's whole life: the interactive agents sessions preview header, --flat / --tree, the agents feed watch / sessions watch streams, the AGI EXT Fleet row, and the editor tab title. The generated title now goes through classifyUserPrompt and 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 a skills/… 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's ai-title; a harness that supplies its own title verbatim (e.g. Cursor's chatMeta.title) is unaffected and untouched (RUSH-3123). Source: apps/cli/src/lib/session/discover.ts (finalizeClaudeScan).

  • agents sessions preview shows the fan-out a session left behind, on remote rows too (RUSH-3091, RUSH-3095). The Doing line now carries N 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 from SessionMeta alone through formatMetaOnlyBody, 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 flag Bash with run_in_background, grok flags run_terminal_command with background; codex and droid record no such concept and cursor persists no tool calls locally, so those render nothing rather than 0 background shells — a zero would assert "none running" where the truth is "cannot know", and NULL (not scanned) stays distinct from 0 (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 trap agents devices ps documents, so live status stays with sessions --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 trace now reads newer Codex sessions by program, not a wall of "exec". Codex gpt-5.6-sol (codex ~0.145+) runs every shell command inside a JS cell — a custom_tool_call named exec whose code is await tools.exec_command({cmd:"git status …"}). The trace now unwraps that cell to the real shell command, so a Codex trajectory reads agents 68% · git 15% · scp 13% with git fetch origin / sed -n … steps and exit codes, exactly like Claude's Bash and Droid's Execute — instead of exec 100% with every step labeled exec. 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 trace compares 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 --json with layout: '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> --tree renders 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's meta.json parent_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 inline Task/Agent sub-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; --text prints an indented tree; --json emits layout: 'lineage' with a lineage: { 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 trace v2 — 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 shared extractShellPrograms parser; sudo/env/agents ssh unwrapped, bare cd/export skipped) across every harness's shell tool — Claude's Bash, Codex's exec_command, run_shell_command, shell, Execute — so the mix reads git 94 · agents 81 · gh 75 instead 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 --text renderers off one model field — a Bash-heavy run reads git 56% · gh 33% · agents 11%, never Bash 100%. --text and the --json step model gain additive program + exitCode fields, and the trajectory's toolTimeShare is renamed programTimeShare (program-keyed) in the --json envelope. Source: apps/cli/src/lib/session/trajectory.ts, trajectory-html.ts, trajectory-text.ts.

  • agents sessions trace (alias agents 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. One buildTrajectory() 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 versioned sessions-trace JSON 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 trace steps read by what the command DID, not their cd prefix. A shell step's label now strips the leading throwaway statements — cd <repo> &&, export X=Y;, set -e, source …, and bare VAR=val assignments — so cd /long/path && git fetch origin renders as git fetch origin, and a multi-line script whose first line is cd <repo> shows its real command instead. Before this, most rows in a coding session rendered an identical cd [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 but cd is shown as-is. Fixed at the model (buildTrajectory), so the HTML, text, and --json renderings all benefit. Source: apps/cli/src/lib/session/trajectory.ts.

  • Trace durations roll into hours past 60 minutes. An overnight idle gap now reads 24h01m instead of 1441m18s, in both the HTML and the compact text renderings. Source: apps/cli/src/lib/session/trajectory-html.ts, trajectory-text.ts.

  • agents view stops 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 --refresh rendered SuperGrok Heavy, the very next plain agents view grok rendered usage 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 an available badge — deriveUsageStatusFromSnapshot still 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 claude gains 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 sync pushes derived, redacted trajectories to your Phoenix account (RUSH-3140). A new agents traces command group increments over sessions.db via the file_mtime_ms gate (only sessions modified since the last sync are uploaded), computes a SessionTrajectory for each (steps + gaps + stats, no raw transcript text), applies redactSecrets() before PUT, and stores the result under <userId>/<device>/sessions/<id>.json in 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 uses cache-control: private, no-store on every response. Source: apps/cli/src/lib/traces/{backend,sync,worker-template}.ts, apps/cli/src/commands/traces.ts.

1.22.46

  • agents auth returns, against Phoenix ID instead of a sibling product's backend (RUSH-2581). 1.22.45 removed the account layer that authenticated against Rush's api.prix.dev. It comes back pointed at Phoenix ID (phnx-labs/phoenix-id), agents-cli's own account service: agents auth login runs a device-code flow whose browser page is Phoenix-branded and Google-only, agents auth whoami reports the signed-in account, agents auth logout clears this machine and nothing else, and the team surface nests as agents 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.yaml fallback. Source: apps/cli/src/lib/identity/{client,index}.ts, apps/cli/src/commands/auth.ts.

  • agents auth points at the deployed Phoenix ID service (RUSH-2581). The PHOENIX_ID_BASE default shipped naming id.phnx.sh, a domain that was never registered, so every agents auth login would have failed DNS with nothing behind it. It now defaults to the live service (a workers.dev URL until a custom hostname is attached). Override it with PHOENIX_ID_BASE to 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 when xcrun was 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.app that Gatekeeper rejected on every Mac. The gate now hard-fails when the stapled ticket (Contents/CodeResources) is missing and xcrun is unavailable, and hard-fails when the bundled executable is not a universal (fat) Mach-O binary, checked portably with od on any platform. Source: apps/cli/scripts/verify-menubar-helper.sh.

  • The release-attestation producer no longer arms the real-~/.agents hermeticity guards it needs to get vitest's extended timeout profile (RUSH-3007). CI=true used to control both the vitest hookTimeout/ignore-pool-error profile AND tests/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.sh now sets AGENTS_ATTEST_PRODUCER=1 (and unsets any ambient CI) 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-less out_of_credits marker 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 manifest check: 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 teardown rm (the dominant residual ENOTEMPTY suite flake after RUSH-3021). Probes now run in their own process group via probeCapture, and the whole group is reaped when the probe settles — on clean exit, on timeout, and on parent death (a process.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 old execFile timeout 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 shipped agents auth login/whoami/logout and agents org against the Rush product's backend (api.prix.dev), with entitlement.ts reading the Rush billing tier to cap accounts at 3-per-harness on free and to gate the agents insights friction sections. agents-cli's identity must not ride a separate product's login and database, so all of it is removed: auth and org are retired top-level names (they fail loudly, no auto-correct), account registration is uncapped again, agents accounts output drops the dormant field/suffix, and agents insights renders the full report with no plan/notice JSON fields. The Rush feature integrations are untouched (cloud/rush.ts dispatch, 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 staple writes 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.sh runs in a fresh worktree whose bin/ is empty (the signed .app helpers are untracked), so on any non-Mac box npm pack died at the prepack gates and attestation production — and therefore every release — stayed chained to a Mac. Off a macOS signing box the producer now seeds bin/Agents CLI.app and bin/MenubarHelper.app copy-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. resolveDomainSkill now searches project > user > system > extra repos, first layer with a match wins, mirroring resolveResource precedence; $AGENTS_BROWSER_DOMAIN_SKILLS_DIR stays 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 the unshare alias) 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-user for its own publish-time use, so the same-named flag on list/update/delete was swallowed at parse time even when passed alone (--help still showed it registered, which made it easy to miss). agents artifacts share list --json | jq …, the exact example in docs/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 precedent share revisions already set (--revisions-json/--for-user, RUSH-2683). A CLI-wide enablePositionalOptions() fix was evaluated and reverted: commander copies that setting onto every command created via .command(), so enabling it on the root program broke sessions backfill tools/resources --since … --json …, which relies on a parent command's options staying visible to leaf subcommands via optsWithGlobals(). Source: apps/cli/src/commands/share.ts, apps/cli/src/lib/startup/root-command.ts.

  • agents artifacts share publishes with a partial share: config (RUSH-2837). status and publish both used to treat a missing/empty accountId as "not set up", even when baseUrl and WRITE_TOKEN were present — so a fleet rewrite that blanked accountId (or a partial writeMeta that dropped share:) silently killed PR evidence uploads. Publish now requires only baseUrl + the write token; status reports the endpoint and names an empty account id instead of pretending nothing is configured; writeShareConfig will not persist accountId: "" over a stored id; serializeCentral no longer deletes share: 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 --json exposes each version's cached authVerdict so 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 beta under agents setup beta; retire top-level agents apply; drop harness login/logout (RUSH-2981). Enabling a preview feature is setup, not its own noun — agents setup beta list/enable/disable keep the same ~/.agents/agents.yaml beta.enabled write path. Fleet reconcile is already agents fleet apply / agents devices apply; top-level apply is gone (unknown command, retired from distance-1 auto-correct). Harness credentials live on agents accounts — harness login/logout are 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 org as a deprecated spelling (RUSH-2989). agents unshare moves under agents artifacts unshare (same takedown as artifacts share delete; the top-level name is retired). agents audit moves under agents events audit (still events --include runs, plus verify for the legacy hash-chain). Former top-level agents trends is agents insights mix (also agents insights trends). agents org still works and prints a deprecation line; the canonical tree is agents auth space. Prix login stays off agents 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 view shows Claude usage again — the daemon auth-health probe no longer hammers /oauth/usage into a permanent 429 (RUSH-2998). The daemon's fleet auth-health probe (probeLocalFleetAuth → probeClaudeStatus) hit the rate-limited /api/oauth/usage endpoint every ~3 minutes on every device. Across the fleet that drove one per-account request quota to a persistent 429; the shared Retry-After backoff then parked usage fleet-wide and the usage cache froze (windows stale-zeroed → agents view rendered S: 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, so agents devices ping --strict and the run auth-preflight keep detecting revocation on every host. agents devices ping forces a genuinely live probe (never the throttled cached verdict). Source: apps/cli/src/lib/daemon-ticks.ts (AUTH_PROBE_MAX_AGE_MS, isCachedFleetAuthProbeFresh, refreshLocalFleetAuthState force), apps/cli/src/commands/ssh.ts (runFleetPing).

  • BREAKING: remove agents serve, its --control anchor, and agents devices pair-ios (RUSH-3001). The read-only local web companion (agents serve) duplicated what agents sessions, agents teams status, and the AGI EXT Fleet panel already show, and its --control mode 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's control role) is removed with it; with no writer left, the control-only dial-exclusion filters (isControlDevice) are removed too, across doctor, apply, fleet status, fleet-capture, smart-launch, the devices host provider, session fan-out, and remote login detection. agents serve now reports unknown command and is retired from distance-1 auto-correct; agents devices pair-ios is rejected by devices as an unexpected argument. Migration — if you previously ran agents devices pair-ios, that phone or tablet is still in your device registry with role: 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 all passthrough will dial it and wait out a ConnectTimeout; agents hosts list shows it as dispatchable; --device auto can place an agent on it when no device is marked worker; and routine fleet placement can schedule a recurring job onto it, since an unknown-platform device sorts ahead of a real worker alphabetically. Failures are loud, not silent. Remove it with agents devices rm <name>; the stale role key is dropped anyway on that device's next agents devices sync. Session fan-out is unaffected — it still gates on a windows/linux/macos platform. 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 when kickstart -k can't reach the GUI launchd domain from a non-Aqua shell context, so launchd's KeepAlive relaunches from the swapped binary. Separately, a machine that transitioned ad-hoc -> Developer ID (6fa36f73a) now gets a one-time tccutil reset Accessibility com.phnx-labs.agents-menubar to 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-only agents menubar doctor reports 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 left ensureDaemonStarted()'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 teardown rm (the ENOTEMPTY flake that cost most full-suite attestation rolls on shared boxes). The launch path now refuses under a redirected HOME via the same serviceManagerRegistrationAllowed() signal and AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME test seam; reporting an already-running daemon is untouched, and agents daemon start remains the operator override. Source: apps/cli/src/lib/daemon/daemon.ts (ensureDaemonStarted), regression test in apps/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 from package.json files and the prepack gates; 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 new assert_promote_home_base preflight (npm token + gh auth, via scripts/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 .app helpers (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 sync restores a resource deleted from a version home — no --force needed (#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 --force restored it. The sync manifest now records the artifact paths the last full sync wrote (writtenTargets, reported by each resource writer), and isStale treats a missing path as stale — one existsSync per 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 --fix was 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 list no 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 --refresh and 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 by STATS_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 default devices list / devices status read is itself the cache's writer and a stale value can never be presented as current. This cache feeds the fleet scheduler, --device auto affinity, 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 readClaudeCredentialsBlob still read Keychain / .credentials.json — the exact shape that captured a claude setup-token TTY banner as an Authorization header. That helper is deleted. --lease SING-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). removeVersion reassigns 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. Because agents use <agent>@<v> sets the default and the symlink together, both dangled in lockstep once <v>'s binary went away: agents sync <agent> would print Repointed <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 deliberate agents use choice), 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) — then Latest (the full wrapped last message, formerly Last response:) and one width-capped Details ▸ fold (session id, skills, plugins, hooks, links, dirs, repos) that shows … +N more instead of wrapping and swamping the pane. Msgs/tokens/session-id move out of the header's third line into Cost/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-green can actually select a PR (RUSH-2848). The built-in's poll ran gh pr list --author @me with no --repo, so gh inferred 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 matches merge-guard.sh: a formal GitHub APPROVED review or an APPROVE comment on THIS PR (carried-from citations do not count). Hidden agents _internal mergeable-prs prints owner/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 status under agents sync status (RUSH-2864). status was the unified sync-drift report sitting as its own top-level command next to sync. It now lives as agents sync status (--json UnifiedSyncStatus contract and --yes reconcile unchanged). Top-level agents status is gone, not deprecated: it reports unknown command, and status is retired from distance-1 auto-correct. agents sync <agent> still treats the positional as an agent spec; status is reserved as the subcommand. agents sync status --device from 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 deepseek and agents monitors add --run deepseek used to refuse any name outside the native registry; both now accept a custom harness (agents harness list), and the daemon delegates the job to agents 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 @version pins, balanced rotation, and account-env injection don't apply to these jobs. Separately, agents harness add/fork/edit and the legacy-auth migration stored the account's per-device id in the profile, while profiles sync fleet-wide via agents repo push — every synced custom harness then died on other machines with Unknown account '<uuid>'. The portable account name is stored now, and a profile with a dangling ref reports which harness is broken and the exact agents 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 tickets command (RUSH-2932). Ticket reads go through linear (linear-cli) and gh issue. agents tickets reports unknown command, and tickets is 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/usage per account since ~Aug 5) rendered every account as S: 0% (now) W: 0% (now) with usageStatus: "available" — and balanced rotation kept dispatching into genuinely rate-limited accounts (RUSH-2858). deserializeClaudeUsageSnapshot now 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 view renders usage unavailable plus 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's computeVerdict hardcoded severity: 'critical' for any status: 'missing' resource of any kind, while fleet mode's FINDING_SEVERITY — whose own docblock calls it "the single source of truth" — rates the same fact warning for everything except missing-hook/missing-plugin. A missing command, skill, permission, subagent, rule, or MCP entry now reads warning in target mode too, matching fleet mode; a missing hook or plugin stays critical in both. computeVerdict reads severity from FINDING_SEVERITY via a new missingResourceSeverity(kind) helper instead of re-hardcoding it. Source: apps/cli/src/commands/doctor.ts.

  • BREAKING: nest agents alias under agents setup alias (RUSH-2965). Setting up a PATH shorthand is a setup action, not its own noun. agents setup alias add/list/remove keep the same shim behavior (~/.agents/.cache/shims/ + ~/.agents/aliases.json). Top-level agents alias is gone, not deprecated: it reports unknown command, and alias is 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). launchctl and systemctl --user are 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.ts already namespaces the job label, but that only changes the identifier — the registration still lands in real launchd/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 consults serviceManagerRegistrationAllowed() (false when isolatedHomeSuffix() is non-null) and skips with a stated reason rather than touching the real service manager: daemon startDaemon/stopDaemon, menubar restartMenubarLaunchAgent/disableMenubarService, secrets retireLegacySecretsAgentService, and agents computer start. A test seam AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1 lets 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 inbox command is removed — use agents feed (RUSH-2984). inbox was a Phase-3 observe alias that re-parsed as agents feed (needs-you is already the feed default), the same leftover class as timeline (RUSH-2692). It is gone entirely, not deprecated: agents inbox now reports unknown command, and inbox is retired from distance-1 auto-correct. agents feed / feed post / feed post --blocked are unchanged. Source: apps/cli/src/commands/feed.ts, apps/cli/src/lib/startup/command-registry.ts, apps/cli/src/bootstrap.ts.

  • agents auth and agents org now 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-limited in agents view instead of idle usage.

  • Sandboxed monitor --run and routine children now inherit this host's GitHub CLI auth (RUSH-2860). A monitor fire recorded ok while the spawned child had no ~/.config/gh, so gh auth status reported "not logged into any GitHub hosts" and the PR it was meant to act on stayed open. The routine sandbox overlay replaces HOME with a disposable dir, and gh was never linked into it. prepareJobHome now links the host's gh config dir into the overlay (and only that — ~/.agents is deliberately not linked, since it holds the secrets master key and the encrypted store), buildSpawnEnv pins GH_CONFIG_DIR and forwards GH_TOKEN/GITHUB_TOKEN, and the runner refuses to launch when a routine's own env: overrides GH_CONFIG_DIR such that this host's gh auth would still be hidden — a regression tripwire rather than a runtime guarantee. Not covered on Windows, where gh stores config under %AppData%\GitHub CLI.

  • agents webhooks now advertises /hooks/slack. The receiver has accepted signed Slack deliveries since the Slack bridge landed, but agents webhooks serve and agents daemon webhooks list still 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.agent or run.workflow webhook 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 recorded skipped with an empty allowlist (can only run on: ) while the receiver logged it as fired, and agents routines enable <handler> refuses the name, so there was no way out. Verified live: a real HMAC-signed /agents slash command reached the public receiver, acked 200, and started no agent. The job now carries dispatchedBy: 'webhook', the same escape RUSH-2681 gave monitors; a handler's routine: 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 accounts and agents insights (RUSH-2424). A new apps/cli/src/lib/entitlement.ts reads the live subscription tier from GET /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 / attach now 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 of accounts switch/set-default (excluded from listSwitchableAccounts) and are listed dormant (upgrade to reactivate) in agents accounts. agents insights keeps top-line counts, harness mix, insights mix, and agents perf free on every tier; the Friction / Friction-thrash / Dissatisfaction-corrections sections, grouping --by account (the default), and --narrative are paid — a gated section is replaced by the in-voice notice Friction and account-split analysis are on the paid plan. in both the text report and --json (a new plan: {tierName, isPaid} field, groups: null when 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.start used to overwrite BrowserProfile.name with the composite <profile>@<endpoint> runtime key, so the key leaked into profiles list, status, history, and the feed, and six consumers each invented their own rule for turning one back into the other — which is why status --profile default could 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 branded ConnectionKey built at exactly one site, so connections.get(someProfileName) — the original miss — no longer compiles. One rule, keyBelongsToProfile, replaces the four ad-hoc reconciliations in status, stopProfile, findTask, and listProfileCacheDirs, which also fixes a legacy <name>.<n> fork directory being matched by some of those paths and missed by others. status renders comet-local (endpoint: endpoint-0, port 9222, pid …) instead of the raw key, and status --json carries name (bare), endpoint, and key as 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.

  • default is now purely an alias, and the auto-detected browser profile is named auto-chrome (RUSH-2709). default used to name BOTH a concrete auto-detected profile and the "whatever you configured" alias, and only start honored the alias (its own comment: "narrowly scoped … only here in start") — so --profile default reached one profile in start and a different one, or nothing, in stop / status / navigate. One resolver, resolveProfileRef, now serves every command. Nothing existing breaks: a browser.profile config value of default still resolves (to the auto-detected profile), a profile you literally named default still resolves to itself and outranks the alias, and a machine that already carries a default profile keeps using it — ensureDefaultBrowserProfile reuses 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/logout and agents org create/list/view/invite/members/role/remove/leave — the account-layer client over the existing api.prix.dev backend. agents auth login runs the already-shipped RFC 8628 device-code flow (POST /api/v1/auth/device/authorization + poll /token) and writes its own session file, separate from rush login's ~/.rush/user.yaml — so agents auth logout never signs you out of rush, while agents auth whoami/agents org fall back to a live rush login session automatically if you never ran agents auth login. agents org maps to the backend's /api/v1/spaces routes (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 as accounts set-default, which balanced rotation already honors. Pass an account name to skip the picker; --json lists or reports the result. Native accounts name / attach now 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.); provider accounts add --provider is 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 navigate now reuses a tab in Arc instead of refusing outright (#2786, #2778). Arc crashes on Target.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 raw open, a new tab per call, i.e. exactly the tab-spam #2779 set out to end. Arc does expose page targets and does honor Page.navigate on 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 survives done (the rule adoptTabShowing states for unowned pages, kept here where reuse is unavoidable). When nothing is safe to reuse the actionable refusal stands. The Target.createTarget guard from #2778 is unchanged. Source: apps/cli/src/lib/browser/service.ts.

1.22.41

  • BREAKING: the top-level agents timeline command is removed — use agents feed --filter updates (RUSH-2692). timeline was a pure alias of agents 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 timeline now reports unknown command, and timeline is retired from distance-1 auto-correct so a stale invocation fails loudly instead of running a neighbouring command. agents feed --filter updates is unchanged, and the sibling observe aliases agents inbox and agents roster are 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 preview now opens its header with the session's title (session.label — an agent-generated name / /rename / the --name launch 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.sh helper digests are keyed by path relative to --repo-root, and release-attestation-produce.sh now writes the helper manifest it needs (RUSH-2766). hash_tree used to bake the ABSOLUTE file path into every helper's input-digest, so a digest recorded in one worktree could never match the same recomputation elsewhere — require_helpers structurally could not pass for any release, and v1.22.40 was finished only by hand-recomputing the manifest inside a fixed-path worktree on mac-mini and 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: identical sha256: for all three helpers computed from two independent checkouts). Separately, release.sh has always consumed release-manifest.json (require at :231, upload_release_proof at :976) with no producer writing it — the same consumer-without-producer gap RUSH-2749 fixed for the attestation itself. release-attestation-produce.sh now writes/updates it alongside ATTEST.json: an unchanged helper carries its prior record forward, keychain/menubar are re-recorded from the fresh signed assets this script's own Darwin block builds, and a drifted computer-mac digest 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 secrets no longer prints bundle values — the plaintext export and bundle-key get are removed; run commands under agents secrets exec instead (RUSH-2774). The eval "$(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 export is now transfer-only (--device, --to-1password, --to-file; anything else refuses, naming the alternatives); secrets get <bundle> <KEY> refuses, naming agents secrets exec <bundle> -- printenv <KEY>; secrets view --reveal refuses inside an agent session (AGENTS_RUNTIME/AGENT_SESSION_ID/CLAUDECODE markers) and its non-TTY --reveal --plaintext escape is gone; the raw-item secrets 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 behind secrets exec --device and run --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, both release.sh) now re-enter themselves under agents secrets exec, so values ride the child environment and never touch stdout. Migration for eval scripts: agents secrets exec <bundle> -- <your command>, or VAR="$(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). fetch encodes header values as a ByteString, so a title containing an ellipsis, a curly quote, an emoji, or CJK text threw TypeError: Cannot convert argument to a ByteString mid-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 through toHeaderValue(), 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 render produces 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 a1b2c3d4 prints https://share.agents-cli.sh/<you>/session-a1b2c3d4. Unlisted by default, unlike agents artifacts share — a transcript carries file paths, command output, and error text, so it stays out of the public gallery until --public asks 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 fold keeps the model's reasoning in collapsible sections, --expire never opts 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-scoped browser.profile config key; a bare interactive invocation opens a picker over configured profiles and installed browsers, while a headless invocation prints the current default and usage. --unset and auto restore auto-detect. agents browser profiles use is an alias, and the hidden profiles set-default compatibility alias now points users to browser 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>].hasTrustDialogAccepted in .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 use now carry trust via a new claude-trust manifest 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.json carry. A target entry's stamped-default false is 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_level rides along in config.toml. Source: apps/cli/src/lib/settings-manifest.ts.

  • Arc is a recognized browser type, and agents browser now refuses to drive it with a clear error instead of crashing it (#2779, #2778). arc joins the Chromium-family BrowserType (detected at /Applications/Arc.app, macOS-only; agents browser profiles seed creates an arc-local profile). But Arc answers Browser.getVersion while 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 via agents browser navigate --url file://<path> (one reused tab, refreshed in place) instead of a raw open <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 serve and the daemon webhook-receiver) accepts a third source, POST /hooks/slack: it verifies Slack's v0 request signature with a 5-minute replay guard, answers the one-time url_verification handshake, and parses both slash commands (/agents AGI: …) and app_mention events. A matching ~/.agents/webhooks/*.yml handler with source: slack (and optional command/channel filters) runs an agent scoped to the project named in the message — a handler may now template project/cwd from the delivery (project: "{{slack.project}}"), with the new {{slack.*}} namespace exposing prompt, project, channel, thread_ts, user, text, and command. The agent replies into the same thread through the existing agents send --channel slack --to <channel> --thread <ts> — no new outbound code. The receiver bundle gains SLACK_SIGNING_SECRET (and SLACK_BOT_TOKEN when the reply uses the Slack Web API). Setup, a ready-to-paste app manifest, and an example handler are in docs/routines.md and docs/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-died hooks now self-heal at attach and daemon startup instead of relying on a poll that no longer exists (RUSH-2435). The 5-minute tmux-reconcile daemon routine that used to retrofit a stale hook onto a managed tmux session was deleted in an earlier pass (RUSH-2495), leaving reconcileSessionHooks with 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 new ensureSessionHookRepaired(name, socket) repairs a single session right before it's attached to — wired into prepareSessionForResume (native agents run --resume) and the manual agents focus/agents go/agents tmux attach attach paths. Both share one non-destructive repairSessionHookIfStale implementation so the full sweep and the single-session repair can never drift on what counts as repaired. docs/routines.md and docs/specifications.md no longer describe the deleted builtin-routines.ts/tmux-reconcile machinery 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 on database is locked (RUSH-2477). After a machine crashes and reboots, every previously-open tab relaunched sessions resume <id> at once. Each resume ran the full discoverSessions path, whose tryClaimScan/releaseScan bookkeeping are BEGIN IMMEDIATE writer transactions (lib/sqlite.ts), so even a process that skipped the scan still took the writer lock — dozens at once exhausted busy_timeout and threw an unhandled SQLITE_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 doubled unreachable … skipped list. The direct-id path now resolves against the local SQLite index first through a new resolveIndexedSessionById (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. dedupeSessionsByLogicalId collapses 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 concurrent resolveIndexedSessionById calls complete with zero SQLITE_BUSY, and the resolve issues no SSH fan-out. Source: apps/cli/src/lib/session/discover.ts, apps/cli/src/commands/focus.ts.

  • agents monitors now correctly reports a failed action dispatch — ok:false and the specific reason — when the runner declines to start or fails to start the agent process (RUSH-2500). Previously, dispatchAction in lib/monitors/dispatch.ts treated any non-throwing return from executeJobDetached as success (ok: true). But executeJobDetached calls runWithAttempt, which can return a RunMeta with status: 'skipped', 'blocked', or 'failed' without throwing — e.g. when the non-overlap active_run guard fires because a prior monitor-triggered run is still live, or when the auth preflight fast-path (dead-verdict credit exhaustion) marks the run failed and 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/ got ok: true, so monitors runs showed success while monitors logs showed skipped. The engine's drought escalation also got a false healthy signal and never notified the owner. The fix checks runMeta.status after executeJobDetached and returns { ok: false, error: runMeta.errorMessage ?? runMeta.status } for skipped, blocked, or failed results, making the fire record agree with the runner record. Applies to both run and routine action types. Source: apps/cli/src/lib/monitors/dispatch.ts.

  • agents sessions --active now distinguishes "genuinely nothing running" from "discovery failed and was silently swallowed" (RUSH-2507). Previously getActiveSessions folded every tmux/teams/terminals source failure into a bare [] — a wedged tmux server, a spawn error, or a nonzero list-panes exit 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. listTmuxAgentSessions now throws TmuxDiscoveryDegradedError when the tmux socket exists but couldn't be read (still returning [] when no socket has ever existed — that case is legitimately empty), and the new describeActiveDiscoveryHealth() re-probes it only on an empty result to report which local source degraded. RemoteActiveResult now carries skipped/discoveryFailed from gatherRemoteAgentsJson instead of dropping them. The empty-result message in agents sessions --active names 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 origin cannot 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 gatherRemoteAgentsJson seam as projects status, with a 120-second per-device timeout for large repos and slow links.

  • --device / --devices scope the pull to one or more named fleet boxes.

  • --json emits 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 — matching projects status --json.

  • A device whose answer cannot be verified is reported as unverified and 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 by startIsolatedDaemon no longer inherits the real production AGENTS_HISTORY_DIR (RUSH-2545). The test helper spread ...process.env into the daemon's environment without overriding AGENTS_HISTORY_DIR, so the detached daemon process read its ~/.agents/.history record — 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 adds AGENTS_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>/environ on 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'd agents webhooks serve tied to an agent session: declare a receiver with agents daemon webhooks add --secrets-bundle <name> [--port n] [--funnel-port 443] and the daemon's new webhook-receiver service binds it, restarts it on crash, and brings it back after a reboot. agents daemon webhooks list [--json] and agents daemon webhooks remove <port> manage the declarations. The signing secret resolves headlessly through the secrets broker, so there is no AGENTS_SECRETS_PASSPHRASE; a locked bundle fails that receiver loud in agents daemon logs instead 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 carries fired/runs/handlers, and a post-ack failure surfaces as the new webhook.failed event 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). routers is a new ResourceKind, resolved project > user > system like other resources; agents route create/list/show/allow/link-account/unlink-account/rm manage it. Harness ids and model/tier tokens are validated on create/allow against 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/stop always 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 past browser.task-idle-minutes (default 30, new device config key — 0 disables 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 restart and 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 — but launchctl unload/load/list route 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 real launchctl: when a different plist is already loaded under that label, a new instance's own unload (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 from os.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 $HOME for the whole suite so tests can no longer write into the developer's real ~/.agents. macOS resolves the login keychain from $HOME, so usage.test.ts's setKeychainToken call had no keychain to write and died with security: 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 existing setKeychainBackendForTest seam, 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.ts pinned only specific hot spots (AGENTS_DEVICES_DIR, AGENTS_EVENTS_PATH, hook-shim/cache/logs/perf/state dirs) to fork-private temp dirs, leaving HOME itself untouched — and state.ts's HOME (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 override HOME before its first import resolved against the real home. Two confirmed live offenders: src/lib/routines.test.ts wrote fixture routine YAML into the real ~/.agents/routines/ and ~/.agents/.system/routines/, and a src/lib/daemon.test.ts describe block registered/reaped real daemon-instance pid markers under ~/.agents/.cache/helpers/daemon/instances/. tests/setup.ts now redirects HOME/USERPROFILE to 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 with env: {...process.env} — resolves under the sandbox with no per-test effort; new CI-only leak tripwires assert the real ~/.agents top level and ~/.claude/settings.json are untouched. A new tests/global-setup.ts also 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 run while the routine sat silently dead. The slot is now released the moment a run reaches a terminal state; a still-running record 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). Root README.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 the apps/cli/docs/ markdown headers and body prose that use "agents-cli" as the product name now read "agi-cli". Every github.com/phnx-labs/agents-cli reference in those user-facing files, plus the repository/bugs/homepage URL fields in packages/agi-cli/package.json and packages/swarmify-mirror/package.json and the hardcoded repo defaults in apps/cli/scripts/release.sh, root scripts/release.sh, scripts/bottle.sh, .github/ISSUE_TEMPLATE/config.yml, and scripts/release.test.sh (plus the companion apps/ext/app/package.json repository.url its test asserts against), now point at phnx-labs/agi-cli. This is docs/branding only — no runtime behavior change. Not touched: the npm package name fields (@phnx-labs/agents-cli stays canonical; the package-name flip is a separate ticket), the npm install -g @phnx-labs/agents-cli line in apps/cli/scripts/install.sh, the agents-cli shell-rc marker in src/lib/shims.ts, ~/.agents/~/.agents-system, the agents/ag command 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 / test check is one GitHub-hosted Linux job: scripts/ci-scope.ts plus apps/cli/ci/test-ownership.yaml select 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.sh promotes 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 .tgz and 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 list now answers for every harness whose permissions the CLI writes, not 3 of them (RUSH-2676). applyPermissionsToVersion writes claude, opencode, codex, cursor, antigravity, grok, kimi, droid, copilot, kiro, openclaw and hermes, but the read side was a hand-written 3-arm switch: readAgentPermissions returned null for everything except claude/opencode/codex, and the config-file import behind agents permissions add <path> gated on a .claude/.opencode/.codex substring, 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 new PERMISSION_TARGETS registry (apps/cli/src/lib/permissions-registry.ts) declares each harness's config path plus how to read it back into the canonical PermissionSet, mirroring SUBAGENT_TARGETS; a completeness test pins the key set to capableAgents('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.yaml is 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), which permissions.ts imports for the forward serializers, so the two directions cannot drift into disagreeing about what fs_read means. Every reverse projection is lossy in a way the harness's format forces — Kiro collapses Read/Grep/Glob onto fs_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 list renders 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. goose reads allowlist: false in the capability table, and its converter, writer, detector and config path are gone. Goose's permission.yaml gates 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 goose no longer writes .config/goose/permission.yaml, and applyPermissionsToVersion('goose', …) now refuses with Agent 'goose' does not support permissions rather than silently writing a lossy approximation. An existing permission.yaml on 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.mcp is true for every harness, but the code behind it was four independent switch (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, and agents sync <that harness> printed Already 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 to capableAgents('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: installMcpServers reports it, the staleness writer forwards it instead of discarding it, and agents sync prints a Not 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.json in the user's REAL home — only ~/.gemini/antigravity-cli is symlinked into a version home, so a version-home path lands where agy never reads — and its remote transport is keyed serverUrl, not url; grok MCP is [mcp_servers.<name>] in ~/.grok/config.toml, so the resolver no longer answers .grok/mcp.json and the parser no longer reads that TOML as JSON; kimi is read back from the .kimi-code/mcp.json the 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 reads mcpServers. A malformed existing config is now refused rather than rewritten from scratch — these files hold far more than MCP (hermes' whole config.yaml, openclaw's openclaw.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 --run action 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 as skipped with no output. A monitor synthesizes a one-off JobConfig named after itself and hands it to the routines dispatch seam (executeJobDetached), which gates it on checkJobDeviceEligibility → 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 under monitors/ ever writes one. Every fire therefore recorded skipReason: "wrong_owner" with the empty-allowlist message Job '<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 explicit dispatchedBy: 'monitor' marker and jobRunsOnThisDevice skips the routine activation manifest for it — a monitor already resolves exactly-once ownership through its own device: 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's routine action fires a real routine, which keeps its activation gate, so a routine that is defined but not activated on this device is still refused with wrong_owner. Monitor names are NOT registered into the device routines manifest — that would conflate two ownership models and pollute agents routines.

    A second gate sat directly behind it and would have kept every run action inert on its own: with the manifest no longer swallowing the job first, the run was blocked with execution_context_missing, because resolveJobExecutionContext refuses an agent job carrying neither project nor cwd (lib/routine-context.ts) and MonitorConfig had no field able to supply one. Monitors now take an optional cwd (agents monitors add … --cwd <path>, home-relative or ~/…) and the synthesized job defaults it to the execution target's home, which stays portable across a runOn: SSH hop. Verified end to end on one box: the same monitor fires skipped / skipReason: "wrong_owner" / no output on installed 1.22.39, and completed with 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.

    dispatchedBy is runtime-only and closed at both ends of the schema boundary: writeJob strips it and readJobFileResult refuses a routine definition that carries it (inert-and-loud, like the existing device:/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 calls validateJob.

  • agents sessions preview <id> (and the id behind resume/focus) no longer says "No session matching" for a session that is actually running (RUSH-2682). Transcript indexing was lazy — only discoverSessions wrote the index and nothing scheduled it — so a session THIS box just started was listed as running by agents sessions --active while agents sessions preview <id> answered "No session matching" until an unrelated agents 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 --active reads) 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 share now carries provenance and a title, and agents artifacts share list is a real "what have I shared" gallery, not just slugs (RUSH-2683). Every publish auto-captures agent (AGENTS_AGENT_NAME), session (AGENTS_SESSION_ID/AGENT_SESSION_ID), host (os.hostname()), repo (the current git repo), and date from 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 and share list; omit it and one is derived from the HTML <title>, a Markdown frontmatter title:, or the filename, with a one-line nudge toward --label in 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 list gains --agent/--session/--label-contains filters (named to avoid a real, pre-existing option-name collision with the parent share command — see below) and returns label/agent/session/host/repo/revisionCount/meta per item (every --meta key=value an agent attached is readable again, in both --json and 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 --meta unconditionally in the Worker (not just overwritten when the matching provenance header happens to be present), so a same-named --meta entry 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 --label and every --meta value (previously body-only), since both land in the same public customMetadata as 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-revision skips it). The new agents 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 in share list beyond a revisionCount, and each keeps its own recorded expiry. The Worker template changed (customMetadata capture, revision-on-overwrite, the ?revisions=json route, gallery/listing display) — existing endpoints need agents artifacts share update to adopt it (agents artifacts share status reports when one is due). Verified against the live production share.agents-cli.sh endpoint: 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 parent share command's own option is silently dropped even when passed alone (verified with an isolated commander repro). This is pre-existing on share list/update/delete (predates this change) and out of scope to fix CLI-wide here — a real generalized fix needs enablePositionalOptions() audited across 552 commands, tracked as RUSH-2687; list's new filters were named to sidestep it too (--label-contains instead of --label), though its own --json/--github-user remain affected. share revisions is 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 unshare now also delete a target's retained revisions by default (--keep-revisions to 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 group repo ?? 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 explicit cloud bucket — never the harness or a machine name, no fallback chain (activeSessionProjectKey in src/commands/sessions.ts, LocalState.groupKey in the helper). Separately, the dropdown header hardcoded agents-cli; it now reads agents-cli <version> from the snapshot's cliVersion (getCliVersion(), the value agents --version prints), resolved at runtime so a menu-bar helper left running across an agents upgrade 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/view now show a fire's REAL outcome, reconciled against the run's current status, instead of the frozen ok written at fire time (RUSH-2690). dispatchAction (lib/monitors/dispatch.ts) only checks a synchronous snapshot: executeJobDetached writes status: 'running' before spawning and returns immediately (lib/runner.ts's executeJobDetachedClaimed), while the real outcome — completed, failed, timeout, or a process that exits with no captured output — lands later, asynchronously, in that function's own settle() on child exit/error. RUSH-2500/RUSH-2681 fixed the SYNCHRONOUS skip/blocked/wrong-owner cases; this closes the async gap. A run action's fire record was persisted with ok: true off that transient running state and nothing ever revisited it, so agents monitors runs showed a healthy fire history forever while agents monitors logs (which reads the run record fresh) already showed skipped/failed — the exact divergence the ticket reproduced (fire ok at T, run skipped 3ms later). resolveFireOutcome (lib/monitors/state.ts) re-reads the run's real, current status by runId at render time and corrects ok against it; agents monitors runs and agents monitors view's Recent fires now call it instead of trusting the stored ok, and runs also surfaces the corrected status inline (ok (run failed)) when it diverges from the frozen write. The engine additionally stamps a best-effort runStatusAtFire on 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 answered WHERE 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, ids 01a00504-8ac6-… and 01a00504-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 backing preview/resume/focus it 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 to process.cwd() (the daemon's, $HOME) and caps at 50. The count therefore meant "sessions whose cwd is exactly $HOME, max 50": 0 on 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 an archived_at-writing existence check and a Linear fetch on every tick. The tick now calls a new scanSessionsIncremental (the scan half of discoverSessions, 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. scanOpenCodeIncremental and scanOpenClawIncremental gained 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 ten ResourceHandler implementations, 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 against McpHandler.sync as 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 in installMcpServers. Eight dead handlers is eight ways to repeat that. resources/workflows.ts stays — it is genuinely live (lib/resources.ts:16, :530) — as does resources/types.ts, which it imports. Note lib/resources.ts (singular, resolveResource/listResources) is a different, live module; conflating it with the lib/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 --yes and the --device all fan-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 --json and the umbrella payload still emitted a hardcoded ok: true and omitted declined entirely, and refresh() returned void so the umbrella never saw a decline in the first place. Since the fleet fan-out injects --json on 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 a Not written: block on the human path; the umbrella carries them; both JSON emitters derive ok from whether anything was refused; and the fan-out roster renders N not written for a peer that declined, instead of a flat green ok. Audited every emitJson call site in commands/sync.ts rather 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, or launch, none of which run a resource sync (runLaunchSync returns a LaunchSyncResult and never calls syncResourcesToVersion). Documented in docs/resource-sync.md alongside the pruned precedent. 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.json now imports instead of printing "No permission sets found" (RUSH-2702). Harness detection built its match suffixes by calling each registry target's home(''), 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 against process.cwd(), so the same file detected differently depending on where the CLI happened to be run. Detection now reads an explicit altSuffixes list 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.md said Goose supports permissions. RUSH-2676 removed that support and updated AGENTS.md and docs/resource-sync.md, but missed concepts.md — in the very table whose header names src/lib/agents.ts as 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 prune cleans up the dead ones (RUSH-2716). A profile pins an OS-specific binary: 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 shared agents.yaml and syncing to the whole fleet as junk that nothing removed. agents browser profiles create <name> now writes to this machine's own devices/<machine>/agents.yaml; pass --fleet for a profile that really is fleet config (a remote ssh:// 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-detected default stays machine-local regardless, as it has since 1.22.38. agents browser profiles prune removes local profiles that are dead — their browser is not installed here, or they have never been started — with --dry-run to preview, --fleet to 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 auto default. 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 list no longer breaks its own columns, and default means one thing again (RUSH-2710). The name column was a bare padEnd(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 one padColumn helper every column goes through, never a bare padEnd. Separately, the listing used the word "default" for two different things with no way to tell them apart: the profile literally NAMED default (the auto-detected one) and whichever profile this machine resolves a bare agents browser start to (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 — so default in the name column always means the profile of that name. A new SCOPE column says whether each entry is local (this machine) or fleet (synced), and --json carries scope plus isConfiguredDefault. Source: apps/cli/src/lib/browser/profiles.ts (formatProfilesTable, padColumn), apps/cli/src/commands/browser.ts.

Added

  • agents routines add/edit accept the same launch-target vocabulary as agents run (RUSH-2719): --agent <agent[@version]> splits into persisted bare agent + exact version fields (previously the compound string failed validation with agent must be one of: ...); --strategy pinned|available|balanced (and --balanced) persists a per-routine selection policy that overrides the firing device's run.<agent>.strategy; --run-on auto places the job body on a healthy, signed-in, unloaded fleet device re-picked at each fire via the same picker as agents run --device auto.

Removed

  • gemini is no longer a routine target: dropped from ROUTINE_AGENT_COMMANDS, so routines add --help stops advertising a hard-deprecated harness the add-gate refuses anyway. A legacy on-disk gemini routine still lands a visible blocked run 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 @version no longer slips past the routines add deprecation gate (the check ran resolveAgentName on 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 target and an agents add repair, 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.md hero (<h1>/logo alt) now reads "agi-cli" instead of "agents", and the marketing-domain references in README.md, DESIGN.md, demo/src/AgentsDemo.tsx, and apps/cli/docs/README.md now point at agi-cli.sh (the live marketing domain) instead of the retired agents-cli.sh. This is docs/branding only — no runtime behavior change. Not touched: the share.agents-cli.sh links (that subdomain is not live on agi-cli.sh), the npm package name @phnx-labs/agents-cli and its install lines, and historical CHANGELOG.md/.changelog/ entries. Source: README.md, DESIGN.md, demo/src/AgentsDemo.tsx, apps/cli/docs/README.md.

  • agents browser resolves the task from the caller's identity — no more No task specified / export AGENTS_BROWSER_TASK=… in the common path. --task and $AGENTS_BROWSER_TASK still win when set; otherwise the CLI stamps sessionId/launchId once inside sendIPCRequest (from the session-tracker state file at the harness pid, else the process-table anchor via agentKindFromComm, stamped as launchId so 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 and done/stop never create. Tasks get a human label (--title, else first navigated host, else untitled) while the short machine id remains the address. After a daemon restart, live tasks rehydrate from tasks.json via the profile endpoint. Version reconciliation restarts the daemon forward only so an older CLI rides a newer daemon. status --profile <bare> matches composite name@endpoint keys. Grammar: navigate [url] (alias goto), evaluate [expr] (alias eval), 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-level login/logout). Harness-native OAuth sign-out is agents accounts logout <harness> (API-key accounts keep accounts remove).
    • Spend caps live under agents config budget (was top-level budget).
    • 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-level mine is gone.
    • Fleet poll snapshot moves to agents devices snapshot (not config).
    • Matrix runs use agents run --broadcast (with --list-tasks / --results / --task); top-level bench is removed.
    • Top-level agents profiles is removed — use agents harness (same ~/.agents/profiles/*.yml).
    • Top-level agents cp is removed — use plain scp (or scp -3 for host-to-host); no fleet broadcast advantage.
    • Top-level agents resume folds into agents sessions resume (strict id/prompt path + multi-select picker).
    • Top-level agents roster is removed — use agents 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.
  • 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.yaml config: block (conflict-free by construction — each machine writes only its own folder), and a new --fleet flag (agents devices config --fleet <key> <value>) writes fleet-wide defaults to central fleet.defaults.config. This supersedes #2458's central fleet.devices.<name>.config block, which a one-time migration folds into the per-device docs (central wins); the same migration extracts agents:/isolatedAgents: pins from the tracked docs into the untracked ~/.agents/.history/devices/pins-<host>.json — the root-cause fix for the commit churn that kept devices/ gitignored — and folds the legacy auto-launch.json in too. Reads report effective values; --json gains a per-key source (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-pins now records only this machine's pins (peer pins no longer sync). AGI EXT reads the same per-device docs layered over fleet.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 projects forever — linear.name was written once and never refreshed. agents projects link knew 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 stale name. projects import --from-linear did 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.yaml read name: Agents CLI for a project the board calls AGI, rush-app.yaml read Rush App for Rush, and three defs still pointed at project ids Linear had deleted. The label is not inert — apps/ext/src/core/managedProjects.ts surfaces it as linearProjectName in the AGI EXT Fleet panel, and agents read it when naming the work, so both reported a project name that no longer existed. link and import now write name from the live Linear row every run, link reports the replacement (renaming "Agents CLI" → "AGI" (Linear is authoritative)) and drops a url belonging to the project it just unlinked, and the projects status card leads with the name instead of the bare uuid (linear AGI 8eb8f5b1-…). Re-run agents 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 prune showed 1.50 GB where every other surface showed 1.5 GB; session artifact sizes capped at MB, so a 1.4 GiB transcript rendered as 1433.6 MB; and agents share plus the browser artifact list capped at GB, so a 2.3 TiB entry rendered as 2355 GB and now reads 2.3 TB. Only agents inspect is 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 -N rebuild suffix. supports() and the command version gate each carried a private compareVersions that split on . and dropped the trailing -N, so 2026.2.19-2 compared equal to 2026.2.19. Both now use the canonical comparator in lib/agent-spec/primitives.ts, which treats a higher -N as newer. No capability currently declares a -N floor, 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_AGENTS and the sessions command surface documented accurately. The spec said "exactly these 12" while the code has 13 (muse was missing), and both it and SES-IF-2 required a sessions sync subcommand that has never existed. The command list also omitted seven real subcommands (render, bookmark, stats, insights, optimize, watch, backfill resources) while still listing verbs that 655b22512 retired behind resume. 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 from over 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 inspect shows commands (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 / -D only. The -H/--host routing flag is removed from every command that used it for remote dispatch. Use --device <name> (or -D). Scripts using --host for fleet routing must switch. Legacy --host is still stripped from forwarded remote argv for mixed-version fleets, but is no longer registered or documented as a user flag. Unchanged: agents hosts noun, 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 in meta.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 one accounts / accounts view renderer (text + --json). New positional grammar: accounts name <source> <name>, accounts attach <account> <target> / detach <account> <target>, and accounts 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 exact agent@version. Device-scoped harnesses (Cursor/OpenCode/Antigravity/Kimi/Droid) are unsupported for native naming — their API-key path is a provider account. attach validates the live identity before binding and injects no secret or env; resolveAccountSelection resolves explicit → exact-target binding → device-scoped binding → per-harness default. remove refuses while a binding, a default, or a harness profile still references the account. Bindings are honored end-to-end: agents run and 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) — and agents view plus 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 a cred_fp hash 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-tokens flag, the AGENTS_RUSH_UPLOAD_TOKENS env var, the recorded consent file, and buildAccountTokensPayload are 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 (or setup-token), then dispatch under that account. Source: apps/cli/src/lib/cloud/rush.ts, apps/cli/src/commands/cloud.ts.

  • agents run --host --copy-creds and agents run --lease no 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/gemini auth.json files — 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.md SING-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 with agents accounts sync <name> --device <host> (a policy-never bundle, safe to reuse on many devices). A profile-dispatch --lease run carrying its own portable auth (a BYOK gateway) still works; only the native-login copy is refused. Explicit agents accounts sync and secrets export --host are unchanged. Sources: apps/cli/src/lib/hosts/credentials.ts, apps/cli/src/lib/crabbox/runtimes.ts (buildCredentialScript); shared predicate isNativeOAuthRuntime.

  • agents apply / agents fleet apply no longer propagates a harness login between devices (RUSH-2527, SING-1b). login: sync used to copy each agent's portable login file (Claude .credentials.json, codex/grok/opencode/kimi/ antigravity auth.json) from the source box to every target — a rotating token that a single refresh then invalidates fleet-wide. apply now emits no push-login action and captures no credential (snapshotAuth reads nothing); every login: sync agent that needs a login is surfaced as needs-login with the honest reason and the portable alternative — log in on the box itself, or agents accounts sync <name> --device <host> a policy-never provider account. The internal apply --recv-auth receive 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). compileRulesForProject treated any cwd with .agents/rules as a project — and the user layer's own home at ~/.agents satisfies that test — so it wrote a compiled ~/AGENTS.md plus 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 predicate getProjectAgentsDir already used (isReservedAgentsDir in state.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 default browser profile no longer sits in the fleet-shared agents.yaml, which was wedging agents repos pull user fleet-wide (RUSH-2161). browser is a central key because named profiles a user creates are real fleet config, but the one default entry inside it is machine-local: its binary is an OS-specific path and its endpoint is a locally chosen free port. createProfile/updateProfile have 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, and serializeCentral could not — it deletes whole device-scoped KEYS, and browser is 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: zion chrome + /Applications/Google Chrome.app/..., yosemite-s1 brave + /opt/brave.com/brave/brave, mark-1 the same shape, each with an empty deviceBrowser. Because the pull refuses when an incoming change touches a locally modified path, a permanently dirty agents.yaml blocked fleet config sync outright — those boxes sat 5, 8 and 79 commits behind, so merged config (a notify.owner block, device roles) reached zero machines. A new migration moves the entry into devices/<machine>/agents.yaml and 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 YAML Document, 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 off turns the interactive tmux wrap off for one machine (RUSH-2620). Interactive agents run wraps the harness in the shared-socket tmux session so every agent gets an addressable %pane — that is what lets agents sessions --active tell co-located agents apart and agents focus re-attach without forking. Until now the only ways out were per-run (--raw / --no-tmux / --disable-tmux) or the AGENTS_NO_TMUX=1 env 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-shared agents.yaml and is refused for a peer. Unset still means wrap. agents devices config <name> tmux.enabled off sets the same key. Source: apps/cli/src/lib/device-config.ts (isTmuxEnabled), apps/cli/src/lib/exec.ts (shouldWrapInTmux).

  • agents devices role <name> worker marks which boxes agents run on, and --device auto follows it. Roles are fleet-wide (worker / personal), stored in the shared fleet.devices.<name>.config.role block of ~/.agents/agents.yaml, so a mark set on any box travels with agents 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 device worker turns 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 marked personal (a machine you sit at) is never picked automatically, under any mode, and a paired cockpit stays excluded through its existing registry control role. When roles leave the pool empty, --device auto fails loud naming the fix instead of quietly running on the local machine — through agents ssh auto and the --host auto passthrough too, not just agents run. Nothing marked = today's behavior, every online device. Widen it back with agents config set auto.pool all. agents devices role with no arguments prints who is marked what and exactly which devices --device auto would consider; agents devices list tags marked rows and --json carries role plus an autoPool boolean 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 stateTo triggers 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 a stateTo: Plan handler re-matched on any subsequent Issue/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's updatedFrom to 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.provisionprofile became a committed, tracked file in commit 2567004b4 (RUSH-2535), but release.sh's home-base seed step and signing-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 though git fetch origin (which both already run) had the file the whole time. Both now recover the blob from the freshly fetched origin/<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 under set -e on a freshly bootstrapped checkout (RUSH-2541 follow-up). The DEFAULT_BRANCH="$(git symbolic-ref ...)" assignment added to recover embedded.provisionprofile from origin/<default> ran under home_base_wt_snippet's own set -euo pipefail; git symbolic-ref returns non-zero whenever refs/remotes/origin/HEAD is unset, which is the normal state of a checkout bootstrapped via init && remote add && fetch rather than clone -- plausibly a brand-new fleet home base, exactly the box this feature targets. Under set -e the 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 pattern assert_signing_home_base already established for the identical anti-pattern. Source: apps/cli/scripts/release.sh.

  • Top-level agents set is removed — use agents models set (RUSH-2579). agents set [email protected] --model opus-5 moves to agents models set [email protected] --model opus-5, unchanged otherwise: same selector forms (<agent>@<version>, <agent>:*), same --model/--mode flags, same underlying run.defaults store (agents config get run.<agent@version>.model still reads it). agents set now reports unknown command. Source: apps/cli/src/commands/models.ts, apps/cli/src/lib/startup/command-registry.ts.

  • BREAKING: agents share moved under a new agents artifacts group (RUSH-2580). artifacts is the noun and share the 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, and agents artifacts share list|delete|analytics|join|status|update are unchanged under it. The two provisioning doors collapsed into one: agents artifacts setup replaces both agents share setup (flag-driven) and agents 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-level agents share group and the agents setup share subcommand no longer exist; share is 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 browser stops leaving duplicate and orphan tabs behind (RUSH-2622). The about:blank that a bare agents browser start opens is now registered on the task, so done/stop actually closes it — it never was, which made every bare start leak one tab permanently. agents browser start --url now 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 --fresh skips the reclaim entirely. Tasks also carry a lastActionAt stamp in tasks.json now. 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 of agents.yaml disagreed on collection style. state.ts and manifest.ts pass collectionStyle: 'block', while feed.ts, activity.ts and the notify.owner migration in migrate.ts did not — so an empty map rendered as mcp: with an indented {} from one group and mcp: {} 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 one stringifyDoc() helper that pins the committed shape, so a write produces no diff regardless of which command ran. routines.ts keeps 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 its devices: pin stripped. A source from anywhere else is still copied in.

  • A devices: pin now says it applies to this box only and names agents routines devices <name> --set, instead of silently reading as Devices: all on 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 --daemon processes on one worker. agents sessions reap and the routines daemon's 5-minute sweep now terminate those helpers as well as the dead panes, and killSession collects a session's helpers as it tears the session down. A helper is attributed to its session (tier 1) through the AGENT_TMUX_SESSION_NAME the 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", and agents sessions reap --json's warnings array (plus a daemon log line) says when that happens. Tier 1 needs another process's environment, which is a plain /proc read on Linux; on macOS, modern ps -E no 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's daemon 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 any claude started outside an agents-cli pane) and the env marker when one is present — so a grep/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-run lists 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 --host stops leaving remain-on-exit on server-wide, and lands the migrated session where its reaper can see it (RUSH-2521). The remote launch set the tmux server default to on and 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 to off, matching createSession. 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 as tmux-session-gone on its next tick — it now targets the same agents socket the reaper queries. Source: apps/cli/src/commands/sessions-migrate.ts.

  • release.sh fails fast when the --device home 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-unlockable rush-signing.keychain-db, and no apple.com/npmjs.com secrets bundles -- failed there, after both irreversible acts, leaving a tagged-but-unpublished release (RUSH-2535: npm stuck at 1.22.35 with v1.22.36 tagged). A new preflight runs a read-only readiness probe (scripts/signing-home-base-probe.sh) ON the resolved home base, over the same agents ssh hop 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 / projects and the meta.routines.projects allowlist) is gone, and agents routines resume/pause remain as hidden aliases. agents routines list now shows discoverable project routines from your registered projects as disabled rows. Safety is unchanged: a cloned repo's .agents/routines/*.yml can never auto-fire — enablement lives in user-owned local state and a project YAML's own enabled: is never trusted for firing, and sync only 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-dir grants reach Claude, Codex, Cursor, Kimi, and Grok — not just Claude and Codex. Multi-repo projects already attached sibling checkouts as grants on agents 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-dir flag (same as Claude); Grok always gets a short --rules note naming the siblings, and when a non-off OS sandbox is active (GROK_SANDBOX / --sandbox) writes a project-local .grok/sandbox.toml profile (agents-project) with those paths as read_write and 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.yaml no longer pads flow sequences, so ~/.agents keeps syncing fleet-wide. The YAML emitter defaults to padded flow output ([ a, b ]), but the committed agents.yaml uses [a, b]. Installing the feed / activity-log hooks (ensureFeedPublishHook, ensureActivityLogHook — run back-to-back on agents feed) re-serialized the whole document, flipping any committed flow node — e.g. a notify hook's command: [agents, notify, "{message}"] — to the padded form. That left the git-backed ~/.agents working tree permanently dirty on that one file, so agents repo pull refused 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 committed agents.yaml/routine YAML now pass flowCollectionPadding: false, matching the committed formatting exactly so a write produces no diff: feed.ts, activity.ts, the notify.owner migration in migrate.ts, and the routine-definition writer serializeJob (same synced repo).

  • The generated HTML command reference is now browsable, not search-only. docs/command-reference.html gains 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 root agents card is anchorable at #agents instead of shipping an empty id="". 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 services exposes 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 coarse daemon.enabled and scheduler.enabled kill switches. Now each responsibility is listed under agents daemon services, and agents 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 on SIGHUP reload. 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-broker is disabled, agents secrets unlock/status/start report the disabled state up front, and any keychain-backed bundle read throws a clear error naming the agents daemon services enable secrets-broker recovery 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 via AGENTS_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: events is the source of truth; audit and logs are aliases. The unified event stream already covered ops + agent activity; run-dispatch outcomes now land there too as run.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 / --exclude families: ops, activity, commands, runs, security (sessions-style, mutually exclusive).
    • agents events --exclude commands drops high-churn command.start/command.end noise.
    • agents events --include runs lists dispatched-run outcomes (what audit list used to show).
    • agents events stats / agents events rotate own housekeeping (moved off the logs tree as the canonical home).
    • agents audit ≡ events --include runs; agents audit list same; agents audit verify still 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/rotate re-dispatch. A bare logs <id> redirects to sessions / hosts logs for content.
    • Coverage: secrets/keychain (secrets.*), browser, computer, daemon lifecycle (daemon.start/stop/error/info mirrored from the daemon log), plus every module already covered by the CLI command.start/command.end choke 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-level agents lease command now fails as unknown, while agents run --lease and --box keep their existing behavior. Source: apps/cli/src/commands/lease.ts.

  • Nest Funnel management under the daemon command. Use agents daemon funnel status|up|down for webhook ingress; the former top-level agents funnel command 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 live devices-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.sh no 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 unpublished v<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 and v1.22.36 tagged: 1.22.36 could not be finished at all, because its CI-tested tree predates the prepack version-gate fix (1dffc78bc) and so its own npm publish rejects a correct binary. scripts/stuck-release.sh now takes the resolved bump kind and main's version, and exempts exactly one case — patch-from-main stepping over main's own version, which is precisely what validate-bump.sh documents 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 defaults and agents export commands. Configure run defaults and the projects root through agents config; isolated installs are removed with agents remove <agent>@<version> --isolated.

  • Removed the top-level agents hosts command group. agents devices is 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-host agents hosts check <name> probe has no standalone replacement — fleet health is agents devices status (a rollup over every registered device, no per-host argument), and the per-host readiness probe still runs internally via ensureHostReady at dispatch time. The two dispatched-task commands with no devices equivalent moved under devices: agents hosts ps → agents devices ps (list tasks dispatched with agents run --device <name> --no-follow; reconciles running records; --json), and agents hosts stop <id> → agents devices stop <id> (alias kill). agents hosts logs <id> is dropped because agents logs <id> already views and follows host-dispatch task logs. The dispatch fabric is unchanged: agents run --host/--device, teams remote, and cloud --provider host all still work. Source: apps/cli/src/commands/ssh.ts, apps/cli/src/lib/startup/command-registry.ts.

  • Removed the top-level agents lock and agents helper commands. agents lock (the agents.lock SHA-256 resource manifest, generation + --frozen verification) and agents helper (the macOS Keychain .app install/status/where surface) are gone, along with src/lib/lock.ts and its tests. The signed Keychain helper still installs and repairs itself automatically — the postinstall installs it, and getKeychainHelperPath()'s lazy staleness check reinstalls it on the next agents secrets use (or when you reinstall agents-cli) — so no user action replaces agents helper install. Verify: agents lock and agents helper now print an unknown-command error.

  • Removed the top-level agents publish command. 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 profile and the resource-profile aliases agents profiles use/status no longer register; agents profiles remains 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 pull and agents push are no longer registered; use agents repo pull <alias> and agents repo push <alias> instead. Source: apps/cli/src/lib/startup/command-registry.ts.

  • agents sessions watch --json is 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 NDJSON reset, 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. --local disables fleet aggregation.

  • Fleet readiness now comes from the existing device/account JSON contracts (RUSH-2484). agents devices list --json includes effective config and resource health; agents devices status --json includes each row's effective profile/config; and agents devices accounts --json exposes quota verdict, capture/reset timestamps, and an explicit unavailable reason. agents run auto --interactive --device auto --strategy balanced --mode auto excludes unreachable, overloaded, signed-out, rate-limited, and out-of-credits choices and fails loud when no eligible placement exists.

  • agents tickets list --json is 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, so resolveResource('skills', 'browser') finds a skill that lists browser among its aliases:. The canonical name always wins a collision — a real resource named browser beats any resource that merely aliases it, in any layer — and layer precedence (project > user > system) still applies among aliases. listResources surfaces each resource's aliases. 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 secrets operation 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 SSH ControlMaster socket behind that an unrelated later agents invocation could silently reuse. This matches the posture the --copy-creds dispatch 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 writes watchdog.action with live/stalled/nudged counts onto the unified stream — filterable via agents events --module routine|watchdog or --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-level agents resume, and raw agents 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 an ag-<agent>-<shortid> tmux alias, and takes --attach-only (attach one living process or refuse, never fork a copy). Bare agents sessions resume keeps the multi-select history picker. agents sessions --help drops from 21 commands to 16.

  • Retired, hidden but still working for one release — each prints the replacement on stderr: agents sessions attach <id> and agents reconnect [id] → agents sessions resume; agents sessions go <id> → agents sessions resume <id> --attach-only. agents sessions focus is hidden and becomes the internal lifecycle dispatcher (resume delegates to it), so it is deliberately not warned. One behaviour is deliberately dropped: bare agents reconnect (no id) auto-attached the most recent session started in the current directory with no prompt; bare agents sessions resume opens the picker instead, so that zero-typing path goes away when reconnect is 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 raw agents tmux attach <name> escape hatch.

  • A live tmux alias now attaches instead of being keyword-searched. agents sessions resume ag-kimi-632c1fbc attached 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 no state/sessions/<pid>.json record cannot be mapped back to a session at all — so such a pane had no working selector and raw tmux -S … attach was 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 reap is 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, and agents 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 run wraps 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 returned 0. So an interactive run whose tmux server died under it ([server exited unexpectedly], the agent stranded at an approval prompt) printed a failure banner reading exit 1 and handed its caller 0: anything scripting agents run counted a killed run as a clean finish. Those cases now resolve 1 and print an explicit "outcome unknown" banner, matching the rule the --host follow path already used ("the remote's own exit code, or 1 if unknown"). Exit 0 is reserved for an outcome tmux actually reported: a confirmed-alive pane (a clean Ctrl-b d detach) or a dead pane whose status tmux read as 0. One helper, tmuxRunExitCode(pane, knownAlive), is now the single decision behind every runInTmux return path, so the banner and the returned code cannot disagree. That includes the native-resume attach (agents run <agent> --resume), which previously returned a hardcoded 0 without ever querying tmux — prepareSessionForResume now 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 reports 1; 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 / --computer now name the agent session that drove the task (RUSH-2549). Every browser row used to read unlinked with last known owner: UNRESOLVED@<host>, because task identity lived only in the browser daemon's tasks.json — a file saveTaskState rewrites from the LIVE task map, so agents browser stop erased the link and a daemon restart emptied it. Observed on a real machine: a profile's tasks.json was {} while its sessions/ dir held dozens of task dirs full of captures. Identity is now written once at task start to a durable browser_sessions row and is never deleted, so a finished task still resolves its session. The link also keys on AGENT_SESSION_ID — carried by every agent, measured on 5 of 5 live agent processes — in addition to AGENT_LAUNCH_ID, which was on only 2 of 5 and was previously the sole join key. Computer-use invocations get the same durable computer_sessions row: they already resolved identity correctly (stampProvenance reads 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 as N 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 read unlinked honestly. Known gap: agents browser start --host <device> still records no session, because the SSH dispatch forwards AGENTS_ACTOR* / AGENT_TERMINAL_ID but not AGENT_SESSION_ID (lib/hosts/dispatch.ts withActorEnv); 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 (or interactive.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-agent spawn URI now carries agent / sessionId / title from sessions focus and sessions resume, so remote ssh … tmux attach tabs open with the harness icon and status bar instead of a generic shell. The terminal engine's SurfaceItem / LaunchRequest plumb 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; use agents 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 as agents ssh. Either endpoint is host:path (remote) or an absolute local path. Remote ~ and literal $HOME in 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-sync stamp and fresh *.lock mtimes under the fetch cache), and the multi-install install-root scan is memoized beside .update-check for 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-string KNOWN_TOP_LEVEL_COMMANDS set (same first-seen order for tie-breaks) and registers only the auto-corrected command before reparse. Distance-1 auto-correct and --host re-routing after correction are unchanged. Source: apps/cli/src/index.ts, apps/cli/src/lib/startup/spellcheck.ts.

  • CLI bootstrap no longer loads versions.ts via the brand edge (RUSH-2331). Every agents invocation statically imports brand.js for resolveBrandName / disabledCommandsForActiveBrand. That module used to import agents.js solely for reservedBrandNames() / validateBrandName() (mine/setup only), and agents.js pulls the full versions.ts graph — ~90ms of module evaluation on the --version / secrets-broker / bare-help path. brand.ts now reads the zero-dep agent-cli-commands leaf (pinned equal to AGENTS[*].cliCommand by 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.js is now a slim shell that statically imports only the leaf lib/secrets/sync-commands.js and 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 via await import('./bootstrap.js'). Cold node dist/index.js __secrets-ping was 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 / --version no 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 importing lib/menubar/install-menubar.js and doing its filesystem checks before deciding to no-op. It now carries the same helpOrVersionRequested gate the update check, background sync, and ensureInitialized already use, so help and version skip the self-heal's filesystem work and its installMenubarLaunchAgentOnUpgrade() call. (On the default path the lib/menubar/install-menubar.js module is still pulled into the import graph via the static migrate.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 its notesPlain notes entirely. The importer now extracts a NOTES-purpose field (1Password's notesPlain, 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.

  • buildExecEnv strips an ambient CLAUDE_CODE_OAUTH_TOKEN on 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, rotating CLAUDE_CODE_OAUTH_TOKEN through sanitizeProcessEnv(process.env). When the reserved auth bundle 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 — so agents 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-key stores the API key in the existing prompt-free account bundle, and agents profiles add deepinfra --account deepinfra configures Codex for https://api.deepinfra.com/v1/openai with deepseek-ai/DeepSeek-V3. The daemon-owned BYOK refresh path reads DeepInfra's documented /payment/checklist endpoint so agents view can 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 --host passthrough module graph when no routing flag (RUSH-2374). Ordinary named invocations (agents view, agents sync, …) no longer dynamically import lib/hosts/passthrough.js (~187ms cold module graph measured on yosemite-s1) just for maybeRunOnHost to return false. Bootstrap now gates that import on a leaf hasHostRoutingFlag scan of --host/-H/--device/--hosts/--devices (~0.001ms). Also import machineId from the leaf machine-id.js instead of the session/sync/config.js re-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 status shows PR OPEN instead of bare COMPLETED when 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 is PR 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-id on 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 from agents sessions --active — and agents message <id> resolved purely from that set, so the only recovery was kill-and-redispatch. The scan now reads --session-id from the live process argv (Linux /proc/<pid>/cmdline, Darwin ps), treats argv identity as "own session" so those children are not folded away, and agents message falls 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 view names the setup-token usage-scope gap instead of painting healthy headless Claude accounts as broken (RUSH-2392). claude setup-token mints user:inference only; Anthropic's usage endpoint requires user:profile and returns HTTP 403. The probe and live usage read previously treated that 403 like any other rejection — auth-health mapped it to revoked, and formatUsageSummary stamped cached bars unverified. 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"), surfaces usage unavailable (headless), and auth-health classifies reason: usage_scope as unverified (benign) rather than revoked. 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 run that backgrounds gh pr checks --watch and 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 harness account: 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 KeepAlive with no ThrottleInterval, so a daemon dying during startup was relaunched on launchd's ~10s default — six times a minute, forever — and the systemd unit's Restart=always had no StartLimitIntervalSec/StartLimitBurst to ever give up. Three layers now bound it. The plist carries a ThrottleInterval (the same fix the menu-bar helper already ships), the unit carries StartLimitIntervalSec/StartLimitBurst in [Unit] (where systemd 229+ actually reads them) with RestartSec paced so the bursts fit inside the window, and agents __daemon-run installs top-level uncaughtException/unhandledRejection handlers — there were none anywhere in the CLI — that route the failure into the daemon's own logs.jsonl and exit non-zero deterministically instead of dying on Node's default handler with a raw stack. Separately, daemon-health.ts's consecutiveFailures was write-only telemetry; a new daemon-start record 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 from secrets unlock, browser start, the watchdog) refuses instead of relaunching a broken daemon on every foreground command, and agents daemon doctor reports the streak and its recorded cause. An already-running daemon is still reported, and the explicit agents daemon start override 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 stop now 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 in handleShutdown, which never runs on the escalated killTree path or on Windows — so a wedged daemon that had to be killed left all three behind while the stop reported ok: true. That is not cosmetic: a leftover heartbeat is what resolveLiveDaemonPid consults 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 in released/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() called net.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 stop no 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), so handleShutdown was still inside browserIPC.stop() when stopDaemon gave up waiting and ran killTree. 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 record findSurvivingStateDirDaemons reads and the next stop would report ok: true with it still running; ownership is now decided by the stop's own survivor scan rather than isAlive, which also fixes the zombie case (a SIGKILLed daemon stays signalable until reaped, so kill(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). handleShutdown is 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 as getDaemonLogPath() 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 their setInterval are 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.json write no longer permanently disables orphan-worktree cleanup for every worktree name in every team (RUSH-2429). saveMeta() wrote with a bare fs.writeFile — no tmp file, no rename — so a process killed mid-write left a truncated, unparseable meta.json. loadFromDisk() returned null for that file from a bare catch, indistinguishable from the file never having existed, so loadExistingAgents()/rescanFromDisk() silently skipped it forever and isWorktreeClaimed() (which reads meta.json directly, 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 blocked teams add's orphan-worktree teardown for every name in every team. saveMeta() now writes to a sibling tmp file and fs.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 unparseable meta.json, the case that actually breaks cleanup — by renaming it to meta.json.corrupt with a warning, so it stops masquerading as "no record" and a subsequent isWorktreeClaimed() 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, so loadFromDisk() returns null without 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 run git worktree remove --force over 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 agents binary from the Node prefix (RUSH-2431). The daemon service manifest now puts the directory containing the running agents launcher first on PATH, ahead of the Node bin directory. This stops scheduled routines from shadowing the current binary with an older agents installed inside the same Node prefix. As a runtime backstop for already-running daemons, command routines also inject a shell-function guard (and a PATH prepend fallback) so bare agents invocations use the same binary that launched the routine. agents doctor now reports a binary-shadow warning when another agents binary on PATH or 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 --computer is task-first, grouped by run (RUSH-2432). The computer counterpart of agents browser sessions (RUSH-2407): every computer.action event (already written by every agents computer <verb> and the embedded run --task loop) 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's sessionId/launchId resolves — 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/--json print the flat table. The agents sessions --computer alias also preserves --limit, --host, and --device. Nothing sensitive is added to the ledger: typed text stays length-only, and a --task description 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 (via agents remove's trash, a manual rm, a .history version-home rotation, or a box reimage) and the session silently disappeared from every agents sessions listing 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 the session_text FTS content column, so querySessions/topSessionsByCost now KEEP a file-gone session whose content survives — flagged archived — instead of dropping it, agents sessions <id> --include user renders those user turns from the DB (with an archived — transcript file removed banner) 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/moved file_path) stays suppressed. Merely listing a file-gone session also no longer PURGES its redacted tool-call evidence: that destructive purge-on-read (purgeToolCalls from the querySessions missing-file branch) is removed. A row whose file returns (a recoverable-trash restore) is un-archived on the next listing. A new persisted archived_at column (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-r2 uploads each selected session as its own encrypted object under sessions/<machine>/<agent>/<session> in the r2.backups bucket; import --from-r2 downloads and restores them through the normal placement (deduped, local-always-wins). Bodies are sealed client-side with AES-256-GCM under the shared R2_SYNC_ENC_KEY (zero-knowledge — Cloudflare only ever stores ciphertext); credentials come only from the r2.backups secrets 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/locked r2.backups bundle 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 sync now 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 --force did 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 a Pruned from <agent>@<version> block and carried in the --json payload as pruned: { commands, skills }. Hooks are out of scope here — pruning a hook must also GC its settings.json/hooks.json registration, 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 run from 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 hmackey heal on the hot read path (RUSH-2441). v1.22.7 moved the once-per-machine no-ACL re-store of agents-cli.hmackey onto readHmacKeyRecord so every hashed keychain lookup (including the SessionStart agents devices list stats probe) converged a biometry-ACL'd key to silent. v1.22.10 (bf79dc885 / #1995) put the heal back inside maybeAutoRekey, which is bypassed for the hmackey and hashed-name lookups themselves (prepareServiceName returns early for HMAC_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 on readHmacKeyRecord: first hashed lookup re-stores no-ACL once (guarded by healedNoAcl), silent forever after. Source: apps/cli/src/lib/secrets/index.ts.

  • scripts/install.sh restarts a running routines daemon after a dev install (RUSH-2442). The dev install strips the npm postinstall hook (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.sh now 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 share gains --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 never for permanent). --unlisted (alias --private) stores visibility=unlisted so the public gallery and agents share list omit 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. Requires agents share update on 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 list shows 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 at GET /<user>?format=json (a ?format=json discriminator 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=json still serves its real page rather than a fake empty listing), and agents share list reads it: a human table newest-first by default, or the raw listing with --json (each object carries slug, url, size, contentType, publishedAt, and expiresAt). It lists the ACTIVE pages only — expired links and the sibling <slug>.png OG covers are omitted, mirroring the public gallery. An empty namespace 404s at the Worker, which list reads as "nothing published" (via the template-hash signal) and reports cleanly. Because this adds a route to worker-template.ts, the feature only reaches an endpoint after agents share update (RUSH-2449) deploys the current template; an endpoint that predates the route makes list fail loud with a "run agents share update" hint (reusing the agents share status template-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 update now verifies each box actually runs the new version, instead of trusting exit 0 (RUSH-2446). The rollout ran agents upgrade --yes per device and called a box ok on the exit code alone. That exit code only says the npm global moved — it says nothing about which copy agents resolves to on that box, and on any dev box scripts/install.sh puts a side-by-side build at ~/.local/agents-cli-dev with ~/.local/bin/agents pointing 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 status reports 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 what agents resolves to (symlinks followed) and what version that copy reports. A box on the target reads ok runs <version>; a box whose resolved agents is a dev build reads stale 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 reads stale with both versions and its resolved path. Those boxes are counted in a new N not upgraded tally, excluded from the ok count, and make the command exit non-zero. A box whose probe cannot answer — no POSIX shell, or agents unresolvable — reads unverified and is likewise not counted as a success, because "we could not check" and "it is upgraded" are different answers. A failed or skipped box 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 a worker-template.ts change out to an already-provisioned endpoint (RUSH-2449). agents share setup only 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 update re-deploys the current template to the existing account/worker/bucket with no re-provisioning (no bucket creation, no route/domain changes) and never regenerates WRITE_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 (--force to redeploy anyway). ShareConfig now records a templateHash, and agents share status reports current / outdated / unknown (a config from before this field existed reads as unknown, 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 disband is terminal — PENDING teammates cannot resurrect or re-start (RUSH-2450). Disband deleted log dirs and the registry entry but left the in-memory AgentManager cache intact; a concurrent teams start --watch supervisor then re-persisted those records via saveMeta, so a second disband still found N logs to clear and teams start could 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). saveMeta refuses to re-persist a teammate whose team carries a disband tombstone. A second disband is a clean no-op; teams start on 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> --refresh and 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 update names the partial-failure window when the write token fails to re-apply (RUSH-2453). Cloudflare's script upload clears Worker secrets; update re-applies WRITE_TOKEN immediately 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-run agents 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 / --version no longer load the migration graph (RUSH-2454). Follow-up from RUSH-2346: the menu-bar self-heal was already gated, but foldLegacySystemRepo() and the v19 runMigration() hop still ran on pure documentation paths and dynamically imported lib/migrate.js — whose static imports pull the hosts/routine/teams/daemon/menubar graph (~287 modules). Both hops now share the same helpOrVersionRequested gate. The always-on fold itself moved to a leaf lib/migrate-fold.ts (fs + createLink only), and the full migrate.js import is deferred until a missing/stale v19 sentinel actually requires runMigration(). 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/downloads carried the pinned version string — which happens routinely, since grok self-updates its binary in place — the dispatcher shim, the direct grok@<version> alias, and getBinaryPath all 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, so agents run grok and 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 grepped ls'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 warp installs the new Warp Agent CLI (curl installer), not the removed oz brew cask (RUSH-2461). agents add warp@latest failed with Error: Cask 'oz' is unavailable: No Cask with this name exists. — the registry modeled Warp's older oz platform runner via brew install --cask oz, a cask that only lives in the warpdotdev/warp tap, not homebrew-core. The warp entry now points at Warp's current standalone agent CLI (docs.warp.dev/cli): install via the official cross-platform curl -fsSL https://app.warp.dev/download/agent-cli | bash, binary warp at ~/.local/bin/warp (self-updating, like droid/muse). It is an interactive TUI — bare warp opens 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), so agents run warp launches the TUI and the exec spec drops the stale oz agent run -p --output-format json mapping. 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 (bare warp opens browser sign-in, or set WARP_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-system config-repo files (RUSH-2465). usage-refresh, fleet-cache-warm, session-cache-warm, device-probe, auto-dispatch, watchdog, tmux-reconcile, and launch-health were shipped as routines/*.yml in gh:phnx-labs/.agents-system (RUSH-2353, to gain declaration, run history, pause, and device pin over the old hardcoded setIntervals). 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 of listJobs() — below project > user > system. The same pid-claimed JobScheduler schedules and fires them via agents __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-system YAML shadows it during the removal transition (so exactly one definition ever fires). agents routines list tags each with (built-in) and the --json output carries builtin: true. .agents-system keeps only check-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 accounts now keeps provider, authentication type, optional URL, stable identity, and API key or long-term setup token in the account's own agents secrets bundle. 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 with agents accounts sync <name> --device <device>. Harness-native auth remains owned by each harness and is never copied. Existing v2 accounts.yaml entries migrate transactionally into bundles and retain their stable IDs. Source: RUSH-2470.

  • scripts/release.sh takes --device <name> to pick the release Mac, instead of a hardcoded mac-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 to mac-mini and --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.sh gained 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 with agents 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 zion listed sessions executing on yosemite-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. machine is now the execution host: foldExecutionMachine folds 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/--device means where the session runs, not which box answered — applied inside the single gather so the interactive browser and --active --json cannot 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 field offloadedFrom on ActiveSession / --active --json marks the dispatcher's shim row. Contract: docs/specifications.md SES-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 --match monitor that polled steadily but never matched left state.json empty — and agents monitors view <name> showed state: 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, test evaluating 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 own liveness.json so it never perturbs the baseline logic. agents monitors list and view surface 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. --json on both gains lastCheckedAt, checkCount, lastError, consecutiveErrors, and stalled. agents monitors add now 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 sessions attributes offloaded work to the box it runs on, consistently. Resuming a live host-dispatched run by id (agents sessions <id> for an agents run --device <peer> session) no longer fails with "ambiguous (2 sessions)": queryIndexedSessions kept 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: listTeamsActive folds the teammate's hostName into machine/offloadedFrom the same way agents run --device rows are folded, so agents 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 rush landed in defaultPath ?? root and 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 add gains a repeatable --dir <path...>; agents projects set gains 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 own origin remote, never inferred from its path — a checkout at ~/src/github.com/muqsitnawaz/agents-cli whose origin is phnx-labs/agents-cli records what it actually pushes to. --slug covers a directory with no origin and applies to a single --add-dir. Removals apply before additions, so --rm-dir old --add-dir new re-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 --project keeps 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 receiving agents run expands against the host's own $HOME). agents teams create gains --project <slug>: it is validated at create time rather than at the first teams 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 --devices pool 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 now worktree → --cwd → project directory → process.cwd(), so an explicit --cwd still wins; the grants are attached either way, since the siblings are what the project binds rather than where the teammate sits. A --project slug@worktree run keeps the worktree as cwd and grants the main checkout alongside the siblings. A teammate staged behind --after resolves 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 into workspace_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 --host run, because the target machine has its own checkouts and this box's filesystem must not decide what exists there.

    --add-dir values are now ~-expanded at the consumer (buildExecCommand, for both the Claude flag and Codex's workspace_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 --host run.

    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, not defaultPath, so a monorepo subproject still probes its checkout) and keeps missing directories so it can still report ✗ missing. Also corrects the agents projects help note, which still told users to sync definitions with the long-removed agents push/pull instead of agents 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 status now 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-active reads active, 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. status now carries a Stale code section listing each such pid with its missing entry, marks the Binary: line (MISSING from disk) when it is this device's own daemon, and daemon doctor raises it as a problem with the right remediation — agents daemon restart for your own, kill <pid> for a stray this install owns. --json gains binaryMissing and staleBinaries, each stale row carrying actionable so 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 doctor problem, a non-zero exit, a kill instruction — 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 to kill. 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 /tmp cwd, which lib/daemon.ts documents 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 (statSync with throwIfNoEntry, since existsSync also returns false on a permission error — a root-owned daemon under an unreadable parent would otherwise be named); it considers absolute entry paths only, so node -e '<code>' __daemon-run is never mistaken for deleted code; and that last guard also exempts entry paths containing spaces, which ps renders 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. detectAgentsBinaryShadows was already comparing files by identity, but its test still compared two path spellings through fs.realpathSync. On Windows realpathSync does not expand an 8.3 short name, so a where-resolved path and one built from os.tmpdir() compare unequal even when they name the same file — which is why every PR touching apps/cli saw windows fail on a GitHub runner (C:\Users\RUNNER~1\... vs C:\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 default profile no longer churns the fleet-shared agents.yaml. ensureDefaultBrowserProfile regenerates that profile on every agents browser start without --profile, writing an absolute binary: 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 auto default now 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.

  • projectRoot is machine-local too. ensureProjectRoot infers 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 / deleteProfile did an unlocked readMeta(), mutated, then writeMeta(meta) — persisting a snapshot taken before the lock, so a newer write from another process was silently lost. They now go through updateMeta, 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 off getConfigValue / setConfigValue / unsetConfigValue, so an ordinary agents config get could 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-s0 sat 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.rmSync the per-device devices/<host>/agents.yaml and fs.rmdirSync its 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 capture no longer erases a peer's config. captureFleet rebuilt fleet.devices from the captured roster alone, so a device the capturing box had not seen was dropped along with its config: block — observed for real, a capture on yosemite-s0 deleted zion's entire config from the shared file. A dropped device now carries its config: 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 shared agents.yaml, showing up as a spurious local change on whichever box ran the command. It now drops the key, matching how an emptied hosts: 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 after routines add. scripts/postinstall.js healLongRunningProcesses calls startDaemon on darwin and linux when daemon.enabled is 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 under daemon.enabled=false, postinstall stops it and leaves it down. First-run / --force agents setup calls startDaemon() after the system repo is ready (hub re-entry without --force does not). The CLI hot path is unchanged — no per-invoke ensure. Companion: phnx-labs/.agents-system#291 (check-updates daily + agents repo sync system + agents sync --local -y when HEAD moved). Source: apps/cli/scripts/postinstall.js, apps/cli/src/commands/setup.ts.

  • A dev build no longer takes over the installed agents command. scripts/install.sh published its side-by-side build into ~/.local/bin under the production names agents, ag, and browser, 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 with no such file or directory. The dev build is now exposed as agents-dev (and ag-dev); the script never creates or overwrites agents, ag, or browser, 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. browser leaves 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-daemon instead of automatic, because pinning that daemon to a working-tree build changes what the everyday agents talks to while agents itself still looks untouched. The PATH-precedence warning is gone — a distinct name makes ordering irrelevant. Root AGENTS.md now states the rule for agents working in this repo: never npm i -g from 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 the agents name. If removing the shadow leaves agents unresolvable (npm skipped its own ~/.local/bin link because the shadow already answered the probe), the install says so and prints the one command that restores it, instead of claiming agents is 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); key reads a value back, key value sets it with validation (booleans take on/off/true/false), key --unset restores the default, and notes <text> appends an operator note. Device-scope settings — the existing agents.max-concurrent / scheduler.enabled / daemon.enabled / watchdog.enabled / browser.remote-control / browser.profile / notes, plus new ssh.user, ssh.auth, ssh.bundle, ssh.bundle-key, ssh.identity-file, platform, auto-launch.enabled, and auto-launch.preferred keys — now live centrally in ~/.agents/agents.yaml under fleet.devices.<name>.config (synced and backed up with the repo; a fleet.devices: all declaration upgrades to an explicit roster map on the first write). The ssh.* / platform / user values overlay the registry's discovery record at dial time, so agents ssh, the ssh_config render, host dispatch, and devices list all 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 into devices config. Existing per-device config (devices/<name>/agents.yaml config, defaultBrowserProfile) and .history/devices/auto-launch.json fold into the central block via a one-time migration (first config read/write, daemon boot, or runMigration); 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 --help is 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 existing registerCommandGroups formatter (the same pattern browser, computer, and secrets already use), and the "Typical workflow" block moves from the bottom of the help (.addHelpText('after', …)) into a workflow-first Examples: section right under the description via setHelpSections. 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/factory moved to apps/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 (publisher swarmify, name swarm-ext), so installs and the swarm-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, the factory cloud provider), the beta-gated agents factory Software 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-type remains a "Factory label" because it configures the Software Factory worker, not this dashboard.

  • agents feed post --notify raises a local desktop banner on top of any configured broadcast. A feed post already forwards outward through feed.broadcast sinks (a Linear comment, an owner-channel iMessage), but reaching the local desktop meant hand-declaring a channel: desktop sink in agents.yaml. --notify is the per-post, config-free equivalent — the same notifyDesktop banner agents run --notify fires — added on top of whatever delivery is already configured, never replacing it. It routes through the real desktop channel provider like every other channel: sink, so it appears in the post's broadcast outcomes and the --json payload rather than a side path. Two properties follow: it carries no minLevel, so a routine milestone post can raise a local heads-up without an important-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 an important-level owner/broadcast sink. Source: apps/cli/src/lib/feed-broadcast.ts (withDesktopNotify), apps/cli/src/commands/feed.ts.

  • agents subagents list no longer reports every Codex target as missing while the .toml files 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 resolver resume and preview use), 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 with Session <id> does not match the selected focus filters unless 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. resume already 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:

  1. A billing line with no creditUsagePercent was coerced to 0%, so a fresh period looked empty instead of unknown.
  2. An expired period (e.g. last week's 100%) was still rendered, so one box showed rate-limited after 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 as agents 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 .system append 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. --json keeps its shape (one description value 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 *_test scaffolding instead of registered hooks. Source: apps/cli/src/commands/inspect.ts, apps/cli/src/commands/resource-view.ts.

  • agents inspect --hooks no longer shows a line of shell as each hook's description. All 53 hooks in .system rendered !/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 (an mcp .yaml, a .toml) are unaffected. Source: apps/cli/src/commands/inspect.ts.

  • agents inspect --hooks now 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 out not registered. A repo target resolves those events from that repo's own agents.yaml rather than whatever is installed centrally. Source: apps/cli/src/commands/inspect.ts.

  • agents inspect <repo> --routines shows what a DotAgents repo schedules, and whether it actually fires. inspect could 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 . --routines errored with unknown option. --routines lists 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 a Routines section beside Hooks/Plugins/MCP. The listing folds in live state deliberately, because the YAML alone is misleading: a routine's own devices: pin does not enable it — membership in a device's routines: allowlist does (applyDeviceActivation → routineEnabledOnThisDevice) — so a definition-only view reports git-review → zion for a routine that has not fired in days. When routines sit on no device's allowlist the section says so outright and names the agents routines devices <name> --set <device> fix; --json reports devices (the YAML pin) beside enabledDevices (the live fleet answer), reusing agents routines list --json field 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 legacy device: key, a non-list devices:) is now listed with the reason instead of silently vanishing (readJobFileResult, extracted from the private readJobFile); 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 of devicesWithRoutineEnabled); and ResourceItem.extra now reaches --json for every kind, not just plugins, so the detail view and its own preview pane cannot disagree. The collector reads *.yml files only — routines/ also holds <name>/home/ sandbox overlay HOMEs, and a flat readdir counted 49 routines where the repo declares 24. A bare routines/ directory is deliberately not a DotAgents marker: shipping example routines is a normal pattern (this repo's own apps/cli/routines/), and counting it made agents inspect apps/cli resolve 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.md pair plus a managed _agents-cli.yaml parent index, targeting the version: 1 / agent: agentspec of the older, separate kimi-cli product. @moonshot-ai/kimi-code has no loader for that schema — system_prompt_path appears 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 (frontmatter name/description + body), which kimi-code discovers from its brand home's agents/ 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 the subagents capability is now >= 0.29.0 and older installs skip with a stated reason instead of writing files nothing reads. Verified against a real kimi 0.29.0: --agent no-such-agent reports Available 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.yaml files in ~/.kimi-code/agents/. A one-shot migration (migrateKimiSubagentsToMarkdown, migration schema v19) removes them from every kimi version home on the first run after upgrading — not agents prune cleanup, which could never reach the two .yaml files because they match no subagent enumerator. Without it the leftover <name>.system.md would be listed as a phantom subagent named <name>.system and warned about by kimi-code once per session, since it ends in .md and carries no frontmatter. A legacy pair is matched by its signature (a <name>.yaml with a sibling <name>.system.md), so a subagent you named <x>.system yourself is left alone.

  • Per-machine config no longer lives in the fleet-shared agents.yaml. Every device-scope config key now declares visibility, which asks who READS it. shared keys stay in the synced fleet.devices.<name>.config because a peer resolves them — the ssh fields and platform are needed to dial a box before it is reachable, agents.max-concurrent drives teams placement, auto-launch.* and notes feed fleet views. machine keys — 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-control was 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 the agents ssh <device> form to use instead. Source: apps/cli/src/lib/device-config.ts.

  • A new config key cannot silently pick the wrong home. ConfigKeySpec is a discriminated union, so omitting visibility on a device-scope key is a COMPILE error — the same discipline META_KEY_SCOPE already applies to Meta. 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). every previously returned fire: true for 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, so every re-fires (bounded by rateLimit), where match/on-change would dedupe and never retry. No shipped monitor used every, 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 add refuses a monitor that duplicates one you already have. A monitor's NAME was not its identity: writeMonitor overwrites 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 accumulated open-pr-watch, pr-ci-fail, three stale pr2222-* watchers and an agent-added lander all polling the same PR queue. add now 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-out sessions --active already 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"; --force proceeds 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 bare agents sessions <id>, run --resume, and sessions --resolve no 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 — printing Could 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 from 2 to 1 (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 three resolveSessionMetadataValue call sites in sessions.ts. (RUSH-2492)

  • Dead tmux sessions are now reaped automatically every 5 minutes. Sessions stay open after their process exits because remain-on-exit on is 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 --active now attributes cursor/grok/kimi/droid sessions. The ps-scan identity path fell back to loadHookSessionIndex(), which scans terminals/sessions/ — a directory populated only by the @agents/session-tracker package, which is not deployed on most fleet machines. The SessionStart hook that IS deployed writes state/sessions/<pid>.json instead. The ps-scan path now also tries readStateSessionRecord(pid) after the index lookup finds nothing, so non-Claude harnesses that carry no --session-id argv can be attributed from the deployed hook's state file. Source: apps/cli/src/lib/session/active.ts.

  • agents routines is now an interactive browser. On a terminal, the bare command opens a filterable, grouped picker (reusing the same picker primitive behind agents 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 --json and any non-interactive shell keep the exact agents routines list output, so pipes and the menu bar are unaffected. (RUSH-2503)

  • agents secrets export --host raises Touch ID at a terminal instead of demanding a prior agents 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 — resolveBundleForPush passed agentOnly: true unconditionally, while its sibling reads (view --reveal, exec) already decided this per-invocation with isHeadlessSecretsContext() || !isInteractiveTerminal(). The push is strictly less exposed than the view --reveal that 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, and pushBundleToHost (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-1password are 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, and sessions --resolve --json previously refused to resolve whenever any registered device failed to answer the fan-out — printing Partial session resolution: <peers> did not answer and 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 printed shortId width) 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 add warns and blocks when the base checkout is behind origin/main, requiring --confirm to proceed. Pointing a team at a stale repo (local cwd, or the repo provisioned on a --device host) 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 add now fetches origin, counts how far behind origin/<default> the base is, and refuses with a sync command (git … merge --ff-only origin/main) unless you pass --confirm; with --confirm it 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's run.agent/run.workflow action previously always ran at the target box's $HOME with no repo checkout and a hard-coded auto mode, 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 (named agents projects execution anchor — the run lands in that project's base directory), cwd (portable execution directory, resolved under project or $HOME), and mode (plan/edit/auto/skip/full, default auto). They thread into the dispatched JobConfig for the run.agent/run.workflow path and override the routine: delegate; run.command is unaffected (use a cd in the command). Source: apps/cli/src/lib/triggers/handlers.ts.

1.22.35

  • agents doctor --fix and agents upgrade purge 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. Bare agents doctor --fix and a successful agents upgrade now 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 at agents doctor --fix. Source: apps/cli/src/lib/self-update.ts, apps/cli/src/commands/doctor.ts, apps/cli/src/index.ts.

  • agents share delete (alias agents unshare) takes down a published page (RUSH-2428). agents share publishes to a public URL with no way to take one down — the Cloudflare Worker already implements an authed DELETE, 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>.png OG cover (--keep-cover opts 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-exists treats it as a no-op success. Source: apps/cli/src/lib/share/delete.ts, apps/cli/src/commands/share.ts.

  • Codex edit runs can write under the repo's .agents/ again. Codex's workspace-write sandbox 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 hit EROFS: read-only file system on any write into a worktree (a build's dist/, 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>/.agents writable: agents run codex (and the Windows shim delegate) add it to the agents-edit profile's workspace_roots, and the adopted POSIX codex shim resolves the repo's .agents from $PWD at run time (worktree-aware) and passes it via Codex's own --add-dir. (Routines use a separate overlay-HOME sandbox with their own allow.dirs and are unchanged.) Naming the .agents directory itself is the only override Codex honors — a nested sub-path makes bwrap refuse the mount. Only an existing .agents is added; out-of-workspace and ~/.config writes 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 add no longer strands an agents/<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 next teams add with the same --worktree name died on fatal: 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 --after dependency graph are validated before createWorktree runs (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 exact git worktree remove / git branch -D pair 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 --after dependency 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-failed createWorktree (branch ref created, checkout not) run the same guarded teardown — the fixer path matters because it stages its teammate with --after when 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 --after dependency, or dependency cycle is now rejected before the worktree step, so it surfaces as the raw validation message under the friction id teams/add-precondition-failed rather than wrapped in Could not add <agent> to <team>: under teams/add-failed. Source: apps/cli/src/commands/teams.ts, apps/cli/src/lib/teams/agents.ts.

  • teams add no longer silently discards pending teammates past the retention cap (RUSH-2356). listCompleted() classified every non-RUNNING status — including pending — as "completed", so cleanupOldAgents() reaped staged --after teammates once the team's history crossed the 50-record cap: teams add --after printed 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 cwd corrupted 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 used path.isAbsolute(cwd) — which is platform-relative. A Windows-rooted path (C:\Users\dev\repo\...) is not absolute on POSIX, so it fell through to path.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 corrupted cwd since the fallback was introduced — visible as wrong directory attribution in agents sessions filtering and display, and as a lost worktree_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 --device teammate no longer reports RUNNING forever, and resume relaunches it (RUSH-2366). The remote liveness probe only distinguished ".exit sentinel 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, so agents teams resume kept 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-lived teams start --watch supervisor'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() and rescanFromDisk() now adopt a newer on-disk terminal status instead of overwriting it. Source: apps/cli/src/lib/teams/agents.ts.

  • agents message can now reach a detached agents run --device <host> --no-follow dispatch (RUSH-2366 follow-up). getActiveSessions() has no visibility into a detached dispatch's live remote process — only ~/.agents/.cache/hosts/<id>.json (the record agents hosts ps reads) does — so agents message <name> reported "No running agent... matches" for a dispatch agents hosts ps showed running with a live pid, and the only recovery was killing and re-dispatching it (losing all context). agents message now resolves the same host-task records and reroutes the message over the existing --host passthrough 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 with reconcileRunningTasks before deciding, the same step agents hosts stop/ps already take — a detached record never self-updates, so a finished run stays stamped running on 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 --worktree no 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 dispatching teams add from within its own worktree), createWorktree resolved the placement root via git 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/removeWorktree now resolve via getMainRepoRoot() (the existing git-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). buildExecEnv injected the auth bundle's per-account setup-token into CLAUDE_CODE_OAUTH_TOKEN for every resolved Claude launch, so a hand-driven session on a personal machine reported Auth token: CLAUDE_CODE_OAUTH_TOKEN in /status and 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 is user: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 cursor showed several Cursor accounts and balanced rotation picked one, but every run silently authenticated as the single live ~/.cursor login: buildExecEnv gave 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.json is only metadata). buildExecEnv and the versioned-alias shim now pin XDG_CONFIG_HOME at the version home for Cursor (the same XDG mechanism muse uses), so agents run cursor@<account>, the account picker, and balanced rotation each launch cursor-agent authenticated as that account's own token — isolated per child process, so two accounts run concurrently without clobbering one another. agents view cursor now 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 --browser is 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 whose launchId still 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; enter opens the highlighted capture (or drills into a capture list first for a multi-capture task). New --no-interactive flag prints the previous flat table; --json output is unchanged. Source: apps/cli/src/lib/browser/sessions-list.ts, apps/cli/src/commands/browser-sessions-picker.ts.

  • agents sessions --routine is 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/--tree or 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 status points 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 interactive agents ssh <device> with no command now starts the remote login shell in the same home-relative directory you launched from — agents ssh yosemite-s0 from ~/src/app lands in ~/src/app on the target when it exists, else the remote home — matching agents 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 explicit agents 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 watchdog now 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; --verbose restores every row and --json remains the complete machine-readable result. Source: apps/cli/src/commands/watchdog.ts, apps/cli/src/lib/watchdog/runner.ts.

  • agents daemon start no longer defeats its own launchd/systemd launch (RUSH-2417). startDaemon() and the daemon child's claimDaemonInstance() resolve the same <daemonDir>/daemon.lock, and the launchd/systemd branches busy-waited on waitForPid(3000) while still holding it — the release only happened in startDaemon's outer finally. The freshly-launched daemon therefore hit EEXIST against 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 — after launchctl load / systemctl start / the detached spawn — instead of after the pid wait. The concurrent-start guarantee is unchanged: two startDaemon() 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 by claimDaemonInstance's last-wins takeover (SING-11). Source: apps/cli/src/lib/daemon.ts.

  • Reap orphaned keychain watch-lock helpers 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 only close() used to handle. Hosted and standalone broker close() now wait for net.Server's close event 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.html is 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:index now fails when the HTML or canonical JSON drifts from the CLI. Source: apps/cli/scripts/gen-command-index.ts.
  • agents inspect drill-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. --rules listed the composed AGENTS.md output and its symlinks while hiding every fragment under rules/subrules/, so agents inspect . --rule foundations exited 1 — rules now resolve from subrules/. --hooks did a flat readdir and 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 through listHookEntriesFromDir. Plugin detail gains the bundle's real recursive size instead of (bundle), plus its author, dependencies, and which execution surfaces it ships (hooks/, bin/, .mcp.json, …) — author was present in every plugin.json on disk but missing from the PluginManifest type, so nothing read it. Source: apps/cli/src/commands/inspect.ts, apps/cli/src/lib/types.ts.

  • agents inspect no longer crashes when a plugin.json or agents.yaml field's JSON/YAML type contradicts the interface it is cast to. loadPluginManifest and the agents.yaml hook 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 down agents 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 (for description) the plugin list; and in agents.yaml, a scalar events: PreToolUse or a numeric matches.prompt_contains threw in summarizeHook, breaking bare agents 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/*.yaml no longer takes down agents inspect <repo> with an unhandled stack trace. validateMcpYamlConfig returns null for some bad shapes but throws for others — args that is not a string array, a non-string command, a non-map env — and the three directory scans that call it (discoverMcpConfigsFromRepo, listMcpServerConfigs, and the layered project/user MCP resource scan in versions.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 a null; 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.sh now creates v<version> as an annotated tag whose message is Release <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 project plus a portable cwd; 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 /tmp HOME and invisible to every agents daemon guard, since a different HOME resolves 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/services no longer misreport test fixtures as duplicate daemons to kill, and no longer render healthy for an unreachable service (RUSH-2368). Duplicate detection used to scan every __daemon-run process on the box, so a fixture daemon under its own HOME (and therefore its own getDaemonDir()/instance registry) — a separate install, or a leaked test process — showed up as a stray for the reader to kill, contradicting the reaper (reapStrayDaemons) and stop's postcondition, which already scope by the instance registry. Duplicates are now read from that same registry (findSurvivingStateDirDaemons), never a raw ps match. Separately, the secrets-broker and browser-IPC health lines derived their healthy/down verdict 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 rendered healthy (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, routine version:, and profile that named the old one — and two installations of the same release could not coexist at all. Each install now carries an installation.json with 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 update takes --to <release> (latest by default), --account <label> to disambiguate when several installations match, --json, agents update list <agent>, and routes through --host to 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 harness agents add manages 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 with agents 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 doctor detects 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>.sh target was absent, a dangling symlink, empty, or non-executable — the harness silently never ran the hook. agents doctor now emits a hook-runtime-broken critical 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 reads N critical · M warnings (or all set) from agents doctor --json's findings on 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 doctor row 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-project specials the CLI derives from each routine's projects: 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 emits projectGroup; an older CLI falls back to the previous view. Source: apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift.

1.22.31

  • agents secrets unlock --keys folds in the scoped-hold surface; the secrets lease/leases/revoke commands are deleted (RUSH-2350). unlock now takes --keys K1,K2 to hold ONLY that subset of a bundle behind its own expiry instead of the whole bundle — the one capability lease had that unlock did not, now on the command agents already reach for. Without --keys, unlock behaves exactly as before (whole bundle). An unknown or empty key subset fails closed (Unknown secret lease key(s): … / requires at least one key), --keys scopes exactly one local bundle (rejected with --all/--host), and secrets status now names the held keys of a scoped hold; secrets lock <name> releases it. The duplicate secrets lease/secrets leases/secrets revoke trio is gone — it had zero consumers, and its jobs (list holdings, release one) are already secrets status and secrets 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 --keys reuses it. Source: apps/cli/src/commands/secrets.ts (scopeHeldEnv, the unlock/status actions), 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 / --bookmarks to filter bookmarks, and f to focus the highlighted session through the same attach-or-recover flow as agents sessions focus. Enter still resumes. The non-TTY surface is now agents sessions bookmark; its flag, JSON keys, code symbols, and durable store are bookmark-only, with a one-time migration from ~/.agents/.history/favorites.json to bookmarks.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 --force now 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 --force drives the remote's own import, 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 set AGENTS_SECRETS_PASSPHRASE. Only BundleUndecryptableError qualifies — 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.md now specifies routines normatively: a new §Routine execution & readiness section (RT-1..RT-11) pins that projects (plural) is grouping metadata only while a singular project/--project-anchor is 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 that repo is 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 gain blocked/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.md documents 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-run for the same state dir — from ANY install path (homebrew, .local dev, 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. claimDaemonInstance SIGTERMs 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 separate HOME, 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, and auto-dispatch were hardcoded setInterval timers inside runDaemon() — undeclared, no run history, no pause, no device pin. They are now shipped system routines (gh:phnx-labs/.agents-system routines/*.yml) that invoke the same tick body one-shot via agents __daemon-tick <name> (apps/cli/src/lib/daemon-ticks.ts), fired by the same pid-claimed JobScheduler as every other routine. Same schedules, same effects; each tick now shows up in agents routines list, accumulates history in agents 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 with agents routines devices auto-dispatch --set <device>. Requires the paired phnx-labs/.agents-system release 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 daemon had been deliberately removed while every mechanism it needs (getDaemonStatus, startDaemon, stopDaemon, readDaemonLog, signalDaemonReload) stayed implemented and unreachable. It's back as status (identity — state/pid/uptime/heartbeat/owning install — plus every duplicate __daemon-run process on the box, plus per-service health), start/stop/restart, enable/disable (a new persisted daemon.enabled device kill switch — disable stops routines add/routines start/routines catchup/webhook auto-start; agents daemon start still starts it explicitly), reload (SIGHUP), services, logs (-n/-f/--level/--since/--json), and doctor. 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 no agents daemon jobs — scheduled work stays agents routines; status/doctor point failures at agents routines stats. Source: apps/cli/src/commands/daemon.ts, apps/cli/src/lib/daemon-health.ts, apps/cli/src/lib/device-config.ts.

  • Daemon: stop asserts its postcondition instead of assuming it (RUSH-2355). stopDaemon used 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 via killTree on a wedged daemon, then verifies each resource released — the secrets broker socket, the browser IPC binding, and no surviving __daemon-run for this state dir — reclaiming any stale socket the ungraceful exit left behind. agents daemon stop prints 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 from agents 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). parseOpenCode now projects a tool part to just the fields it reads, dropping state.attachments and capping state.output / an oversized state.input: on a real opencode.db the 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×. Every json_extract in the OpenCode scan and parse queries is now guarded by json_valid — SQLite aborts the entire query on a malformed value, so a single unparseable part or message row previously dropped all OpenCode sessions from agents sessions, with no message in a non-TTY run. The todo read is gated by a sqlite_master row count instead of a blanket catch, so a locked or corrupt database no longer reads as "no todos" — and the count avoids the undefined-vs-null split between node:sqlite and bun: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' OpenCode account field now resolves from auth.json, the same source agents view/agents doctor already use (RUSH-2358). The prior resolver queried opencode.db's account/account_state/control_account tables, 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) — so account always 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 valid auth.json credential. 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 sessions reads what OpenCode's own SQLite DB already records: input_tokens, cache_read_tokens, cache_write_tokens (siblings of the existing output_tokens), cost_usd and model from the session row, duration_ms from its timestamps, tool_call_count from its tool parts, todos from its todo table, recent_directories_touched from the files it edited, and worktree_slug from its cwd. parseOpenCode now truncates only a tool part's output (not the whole part), so large edit parts are no longer dropped. Newer cost/model columns and the todo table are probed, so an older opencode.db still scans. account stays null when no signed-in account is recorded, and account_key/account_org/git_branch/cost_usd_nocache remain 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 bounded lru-cache L1 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; --json emits a versioned envelope. The existing detailed agents sessions <id> renderer and --preview spelling 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. Every agents command 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 secrets drops the vague --for flag: duration is --ttl/--until, harness narrowing is --agent. --for was doing two unrelated jobs on sibling commands — secrets unlock --for <agent> meant a HARNESS while secrets lease --for <duration> meant a DURATION — so an agent that learned the flag from one command passed it wrongly to the other, and unlock --for 8h silently read 8h as a harness name. unlock keeps its existing --ttl/--until and now narrows with --agent <agent>; lease now takes --ttl <duration> (matching unlock) alongside its existing --agent. --for no longer exists anywhere under secrets, and a test asserts Commander's real option tree so it cannot come back. Breaking for anyone scripting secrets 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 cursor and Factory Cursor tabs.

  • agents view and agents run keep custom harnesses independent from their native host. The unfiltered view still lists each fork as its own custom harness block, while agents 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 ~/.local dev 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. ensureAgentRunning now 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/agents binary and the node <shim> JS entry) with different process.argv[1], and the stray-daemon reaper keyed on that path — so the two never reaped each other, duplicates accumulated (78 live __daemon-run observed 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 into failed). 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 from ps. 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 with sh: 1: exec: cursor-agent: not found, behind a ⚠ <agent> looks logged out banner that was also wrong. agents run now probes the executable it is about to spawn and exits 1 with agents: <agent> is not installed on this machine. plus the agents add <agent> fix, before any spawn. The probe is existence-based, so a harness you installed yourself (Homebrew, a vendor curl | 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 own agents 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.json and firing it at api.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-only accessTokenCache path 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" in agents 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 --json keeps its existing default-then-version order for automation compatibility. Source: apps/cli/src/commands/view.ts.

  • agents sessions accepts version filters in the same forms as other harness commands (RUSH-2363). An installed positional selector such as agents 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 accounts discovers distinct signed-in accounts across every installed version and agents accounts name work names 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 read f favorites · * star, presenting two names for one mechanism — * toggles a favorite, f filters to favorited, and both write the single store in lib/session/favorites.ts. The * action, the --favorites flag help, the agents sessions favorite command help, and the docs now all say "favorite"; the ★/☆ glyphs and the favorites/favorite JSON 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 Copy agents 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 via agents 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-minute menubar snapshot --json poll (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 view and agents usage labeling Codex's weekly or monthly quota as session usage when the native CLI publishes the long-duration limit in its primary rate-limit window. Codex windows are now labeled from their reported duration (S, W, or M) 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 remote agents view --json listing during ensureHostReady before the run is marked dispatched. Missing pins exit non-zero naming the box, the pin, what is installed there, and agents ssh <box> -- agents add <agent>@<ver> — so detached fleet drains no longer print Dispatched and 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-mini again under the hermetic device registry. tests/setup.ts (RUSH-2042 / #1572) redirects AGENTS_DEVICES_DIR to an empty fork-private directory so unit fixtures never leak into the real fleet registry. The live ssh-tunnel.e2e / browser/drivers/ssh.e2e suites still need a real DeviceProfile for AGENTS_TEST_WIN_HOST, so after #1572 every tests-windows-host-e2e.yml run failed immediately with Unknown 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 from ssh -G <host>) when AGENTS_TEST_WIN_HOST is 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.ts plugin used PERF_SPOOL.slice(0, PERF_SPOOL.lastIndexOf("/")) to mkdir the perf spool parent. On Windows getPerfDir() is backslash-separated, so lastIndexOf("/") returned -1 and the slice dropped one character — the sample write was fail-silent and never landed. The plugin now imports node:path and uses path.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_SCRIPT in activity.ts) hand-duplicated bash-command.ts's TOOL_REGISTRY and VALUE_FLAGS with a "keep them in sync" comment — the two had already drifted (Python was missing rmdir, and agents/linear two-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 peels export VAR=…, set -euo pipefail, for/until loop bodies, if/then branches, and (command) subshell prefixes that previously classified as other. 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 behind agents sessions --active and the menu-bar badge tick) now memoizes by transcript path + mtime + pidAlive inside 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-home projects/ tree per pid per tick. Source: apps/cli/src/lib/session/active.ts.

  • perf(sessions): throttle headless ps/lsof scan + memoize process table across an active-session poll (#2047)

  • agents routines devices --set / --clear no 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 claude no 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 like claude-opus-4-1 that only appear as a foundry: / cloud-scoped field next to a real firstParty id (claude-opus-4-1-20250805). dropBareLegacyIds now 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 collapsing claude-opus-4-1 into claude-opus-4-10. Source: apps/cli/src/lib/models.ts.

  • Menu-bar dropdown density toggle removed. The Density: Auto/Rich/Compact footer 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 the menubarDensity UserDefaults key and MENUBAR_DENSITY env 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 8h authenticates 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 leases shows 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 pull fast-forwards a clean behind-only checkout instead of failing with "Cannot rebase onto multiple branches" (RUSH-2282). After fetch, a 1-behind tree integrates via merge --ff-only against the tracking ref; only genuinely diverged histories enter rebase. Avoids re-running git pull --rebase on a multi-entry FETCH_HEAD (common after bare fetch when the remote has several branches, or under concurrent fleet fetch). Source: apps/cli/src/lib/git.ts pullRepo.

  • agents perf splits 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 as errorCount, 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 now ERR/BLOCK/TO (err:…% block:…% to:…%). JSON rows gain blockCount/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 --yes drops ~1s/agent of dead work and reuses still-fresh fingerprints (RUSH-2320). Measured on a real install: getActuallySyncedResources was ~1055 ms/agent on the unattended path that never reads it; buildManifest re-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, buildManifest carries still-fresh fingerprints from the previous manifest, and syncResourcesToVersion runs its no-change guard before pattern expansion (accepting a caller-supplied available inventory 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 cursor exited with no healthy cursor account under strategy 'balanced' — excluded: 2026.07.23 (signed_out), the agents run cursor@ account picker marked the logged-out row disabled and offered only No usable accounts — cancel, and agents use cursor only 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 labelled launch to sign in; rate_limited / out_of_credits still 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 --active no 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 excludes queued/closed/crashed outright (still reachable via --queued/--closed/--crashed) and requires a real process row to positively verify its machine, a positive pid, and pidAlive === true; a cloud row stays active on its provider + task id alone. Every process-backed --active --json row now guarantees machine/pid/pidAlive: true, and the human CLI row and the menu bar's session detail both show the matching machine:pid (or provider · taskId for cloud) locator. Source: apps/cli/src/commands/sessions.ts, apps/cli/src/lib/menubar/snapshot.ts, apps/cli/menubar/Sources/MenubarHelper/.

  • agents projects status card scans faster: grouped warnings, a fleet health summary, and a truthful dead label (RUSH-2337). On a busy fleet the card printed each host's git drift twice — once in the inline fleet table, then again as one 2-line block per host in the warnings footer (18 lines for 3 facts) — and read dead 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's emitGroup); (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) the dead row names the status directly when singular (dead 41 crashed); (4) the fleet-wide rollup carries a fleet snapshot · as of HH:MM stamp; (5) linear shows 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 --teams groups sessions by team (RUSH-1997). Instead of one flat list with a [team/handle] tag, --teams now 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 teammate meta.json — headless agents run sessions, or teammates whose team record aged out — sink into a trailing (no team) bucket, so a real agents teams teammate and a bare SDK spawn are never shown as the same thing. --teams --flat/--tree keep the plain inline table, and a search query keeps the interactive picker. TeamOrigin (also on --teams --json) now carries the teammate's startedAt spawn time and a source (meta = teammate, entrypoint = bare spawn). Source: apps/cli/src/lib/session/team-filter.ts, apps/cli/src/commands/sessions.ts.

  • agents run --device auto and agents teams add --device auto now 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 uses agents 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: harnesses lists every installed agent@version across the fleet with its account, signed-in state, quota (highest usage-window utilization; * = cached snapshot), and a single ready verdict (signed in AND not rate-limited); accounts collapses that to one row per account, naming which harnesses share it. Both SSH-probe each online device (bounded like fleet 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>; --json emits 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 --host says unknown 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.ts even swapped in process.cwd() when the recorded directory did not exist locally). agents resume now re-runs itself on the owning device over SSH; --here overrides. The bare agents sessions picker routes the same way, and agents sessions attach hops as an attach (its detach record and the headless process it stops both live on the owner). The multi-select agents sessions resume inherits it: each tab it opens runs the canonical agents 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 with agents 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 under agents 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/--device router answered for commands that do not exist. It runs before commander parses, so agents session resume --host <box> (one letter off sessions, 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 to unknown 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 publish is in the lazy command table. commands/packages.ts registers 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, so agents docto --host <box> corrects to doctor and runs on <box> — previously the corrected command re-parsed locally with a --host it 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 … --json fan-out caps each peer's stdout at 16 MiB instead of buffering it unbounded (RUSH-2065). The shared fan-out (gatherRemoteAgentsJson, behind agents sessions --active, agents feed, and every other fleet-wide JSON sweep) streamed each peer's output into memory with no ceiling, under one Promise.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 in skipped) so the rest of the fleet still renders. The bound and the UTF-8-safe accumulator now live once in apps/cli/src/lib/ssh-exec.ts, shared with the agents sessions browse 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 probeLocalFleetAuth over 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 a 429 that then parked the whole box's usage reads behind a Retry-After penalty (usage-backoff.ts survives 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). runWithFallback resolves each attempted entry's harness/version home and runs the same skip-fast preset synchronization as the primary agents run path, 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 add against 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 a gemini … command against a backend Google retired. executeJob/executeJobDetached now reject a hard-deprecated agent up front, before any version/account resolution or sandbox prep, and record a failed run 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 stale profiles.md doc line still listing gemini as 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-version CLAUDE_CONFIG_DIR / CODEX_HOME home, not the sandbox overlay the archiver scanned — so routine runs were indexed as ordinary origin='cli' sessions and never linked to their routine or run. archiveRoutineTranscripts now reads the same per-version home buildExecEnv writes 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, and agents sessions --routine shows 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 origin and scripts/release-lease.sh status read held for up to the 30-minute TTL with nothing actually releasing. The lease now records the holding host, pid, and that pid's start time, and status reports holder-alive=yes|no|unknown. A holder that is provably gone is reclaimed by the next claim immediately instead of waiting out the TTL, and a new release-lease.sh clear drops 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 output and 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 in session/discover.ts failed on Windows: the OpenClaw scan gated on which openclaw, which is POSIX-only (which throws ENOENT on Windows, so the whole OpenClaw scan silently returned before indexing anything), and the Grok scanner recovered a session's version from summary.grok_home with a /-only regex that never matched a backslash-separated Windows path. The OpenClaw presence check now uses the cross-platform hasCommand, its openclaw invocations route through execFileShellSpec so a Windows .cmd/.ps1 shim 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 #< CLIXML banner before parsing (stripClixml in hosts/remote-cmd.ts), so a fleet-wide rollup that folds in a Windows box no longer drops it on a JSON.parse failure. The banner strip is applied at every remote---json boundary a Windows peer's output flows through: the remote-agents-json fan-out, the session remote-list list/payload/tool-search parsers, the --host fleet passthrough (agents view --host all), and agents 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 output now reports the burn split and a --pricing no-cache scenario (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) — a burn split: line in the text report and the three counts on burn and every breakdown row in --json. New --pricing no-cache reprices 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 column burn(nc)) while --json always carries both costUsd and costUsdNoCache so 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/timeout routine finish, or a pre-spawn failure such as auth_failed, now also pings the owner over the same channel stack agents notify uses (the owner.channels in humans.yaml, or the legacy notify.owner), delivered in-process by the daemon — no shelling out to ssh mac-mini agents notify. This closes the gap where a failed scheduled routine on a headless fleet box was invisible, and specifically covers the auth_failed case the per-routine agents notify prompt 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, and agents bench results: benchmark cells fan out through the existing agents run path 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.md walks a fresh install through agents setup, adding and logging into harnesses, a minimal agents teams smoke test, and setting up a fleet (agents devices sync, agents devices set-interactive, agents apply) — linked from docs/README.md and the root README's Quickstart. Docs only — no runtime change. Source: apps/cli/docs/QUICKSTART.md, apps/cli/docs/README.md, README.md.

  • agents insights detects 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-bucketed silent stall: 5-15m / 15-60m / 1h+ friction signals; resume nudges (continue, keep going, …) after that silence also count as resume after silent stall. Report, actions, --narrative, and /sessions-insights instruct 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 insights splits 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 generic sk- rule can't reach an oat01 token because the hyphen after ant breaks its run — so a captured setup-token could leak verbatim into a run log or session export. resolveClaudeSetupToken also now validates the stored value: a corrupt auth bundle entry (e.g. a captured claude setup-token TTY banner+ANSI blob, the exact #1767 shape) is refused instead of being injected as CLAUDE_CODE_OAUTH_TOKEN → an invalid Authorization: Bearer header 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 and MAX_ATTEMPTS bounded 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 claude no 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 dotted claude-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 existing dropBareLegacyIds sibling-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 /continue as 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 PATH and 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 _npx cache in addition to resolving every agents entry on PATH. Dev installs are no longer hidden: a copy without the atomic app-bundle-install module is labelled unsafe legacy helper installer — remove this copy, because invoking it can still replace a live .app with 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() read agents.yaml twice per call — once up front, then again inside getActiveResourceProfileName() — and listResources() calls it once per resolved resource, so a listing paid two memoized readMeta() round-trips (ensureAgentsDir() plus four stats each) for every entry. Reading it only after the profile name is known drops one of them. Measured on yosemite-s1 against the real ~/.agents: one pass over all eight resource kinds (135 entries) went 10.52 ms → 6.23 ms, and agents doctor --json spends ~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 the if (!name) return null; guard now skips. The ensureAgentsDir() side effect is unchanged because getActiveResourceProfileName() always reaches readMeta(), via brand.ts listBrands() when a brand is set and via resource-profiles.ts otherwise. Source: apps/cli/src/lib/resource-profiles.ts.

  • Routine session discovery now supports an interactive picker and fuzzy names (RUSH-1998). agents sessions --routine opens 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 --routines is 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 doctor diagnoses Windows OpenSSH public-key enrollment (RUSH-2266). On Windows it reads the effective AuthorizedKeysFile, selects the ProgramData administrator file or per-user profile file, verifies a public-key record is present, and checks that the administrator file grants FullControl to only SYSTEM and Administrators. 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 setup a re-runnable onboarding hub with live capability status and direct access to browser, computer, secrets, fleet, share, watchdog, and preference wizards.

  • agents apply --provision-secrets pushes 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 — apply said "recreate manually" and nothing else — so they hand-exported the file store's master key across the fleet. The provisioning primitive now exists, and apply runs it as a fifth reconcile phase, last, because it is the most sensitive mutation apply performs.

    It is off by default and is a flag, not a manifest field: agents.yaml is shared, so a file-level default would mean someone else's apply -y silently ships credential values. Three gates, and every refusal still prints a needs-secret reminder 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 bar agents exec --copy-creds sets, EXEC-34).

    Backend follows the platform: file on Linux, keychain on 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, apply runs one extra agents secrets list --json per 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 — --force re-pushes regardless. The --plan matrix gains a secrets column, 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 the export --host action so a lib no longer needs a command module), apps/cli/src/lib/fleet/apply.ts, apps/cli/src/commands/apply.ts.

  • agents teams auto-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 --devices pool 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 start now probes the pool once (reachability + load from the same snapshot agents devices shows, plus whether the teammate's agent is installed there) and: excludes unreachable / overloaded (loaded headroom) / 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 start fails 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; --force downgrades 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 add is 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 during agents run --device <box> and the peer's tmux pane was gone by the time the link came back, the reattach ran agents sessions focus <id> --local --attach-only, which hard-failed with No live session matching … and dropped the user at a bare shell with the id scrolled off screen. Two fixes: the auto-reconnect reattach now runs agents 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 new agents reconnect (also agents 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 exact agents 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 add now rejects an agent the local daemon can't fire, at add time (RUSH-2102). --agent opencode (or any real, installable agent outside the daemon's AGENT_COMMANDS table — currently claude, codex, gemini, cursor, kimi, droid, muse) used to pass validateJob because 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). validateJob now rejects it immediately for the default local placement, with an error naming the supported agents. Routines placed with hostStrategy: host/fleet/cloud are unaffected — those dispatch through agents 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: true with zero hooks ever installed (RUSH-2122). registerHooksToSettings has no openclaw branch and silently returned { registered: [], errors: [] } for it, so agents sync openclaw reported success while installing nothing, and agents doctor treated 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 runs BOOT.md on gateway restart) — there is no general event->shell-command registration surface an agents-cli hooks.yaml manifest could target — so capabilities.hooks and supportsHooks now read false, matching what the CLI can actually do. A new completeness test (hooks-capability-completeness.test.ts) pins every hooks: true agent to a real branch in registerHooksToSettings so 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 stream keeps 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 fresh agents process or IPC connection per action. --task (or AGENTS_BROWSER_TASK) supplies the default task, a start response becomes the default for later lines, and malformed input returns an error response without ending the stream. Fleet-remote start requests enforce the same device-local browser remote-control consent 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 tools reads 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. Every ensureToolIndex pass 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 to tool_scan_ledger (parsed_offset, the byte just past the last complete record consumed, and parser_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_text rows are addressed by the rowid of the call they describe instead of the UNINDEXED call_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 running agents sessions optimize by 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-opened opencode.db once 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 its session row, 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 opens opencode.db once instead of once per session. The stamp deliberately does not rely on session.time_updated alone, which real databases leave hours behind the session's newest part. A side effect: sessions.file_size for 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 sessions listing sort (ORDER BY IFNULL(last_activity, timestamp) DESC) wrapped the sort column in IFNULL(), which defeats idx_sessions_last_activity and forces a full table sort on every list/resume query; a new migration backfills last_activity so it's unconditionally NOT NULL and the sort now runs on the bare column (EXPLAIN QUERY PLAN confirms USING INDEX idx_sessions_last_activity). The post-query existence check now batches fs.existsSync per 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 of readdirSync calls with the same result. Interactive label search (ftsSearch) no longer runs a leading-wildcard LOWER(label) LIKE '%q%' scan of the whole sessions table on every keystroke; it now queries the already-indexed FTS5 label column. Source: apps/cli/src/lib/session/db.ts.

  • The standalone browser binary now routes --host/--device (RUSH-2214). browser start --host <box> dispatches to the remote over SSH, exactly like agents browser start --host <box> already did — previously the standalone bin dropped the flag with unknown 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 --device no longer reports a host as unreachable when the SSH probe times out (RUSH-2249). The ready probe (readyProbe in hosts/ready.ts) now disables SSH multiplexing so a stale control socket cannot hang the local client, and it checks r.timedOut before 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 suggests agents ssh <host> agents view to confirm manually, rather than the misleading "not reachable over SSH" message. Source: apps/cli/src/lib/hosts/ready.ts.

  • agents teams start nudges the operator toward feed milestones (RUSH-2250). After launching teammates, teams start now prints a one-line tip — teammates are briefed to post IMPORTANT milestones to the feed (watch them with agents feed timeline), and team progress is watched with agents teams status <team>. Print-only in both the single-wave and --watch paths (suppressed under --json); no engine behavior changes. Pairs with the .agents-system guidance that instructs teammates to post those milestones. Source: apps/cli/src/commands/teams.ts.

  • secrets list no longer skips every biometry-ACL'd item, so hold/always-policy bundles are readable again (RUSH-2251). A regression first shipped in v1.22.10 added kSecUseAuthenticationUI: kSecUseAuthenticationUISkip to the keychain helper's list data-protection pass. UISkip makes SecItemCopyMatching silently omit every item protected by a biometry access control — which is exactly the value items set writes — so enumeration returned only the no-ACL metadata and never-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 as stored item '…' not found, and unlock could not even warm the broker to work around it. The DP pass is now attributes-only with no kSecUseAuthenticationUI key: kSecReturnAttributes without kSecReturnData never 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's EXIT trap, so a hard kill (SIGKILL, OOM, reboot) that skipped the trap orphaned the dir and every future refresh's mkdir failed — 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 doctor now 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 has rush on 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-fact owner failed: … line. agents doctor had 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 reports owner → unreachable: rush CLI not on this box's PATH / rush has no usable session here with 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-run is not this check (it short-circuits before the which rush preflight 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 insights owns counter mix; agents trends is a deprecated alias. The former top-level trends tree (harness/model mix, tools-per-session, token ratios, secrets/browser recipes, raw usage query) now lives under agents insights mix and agents insights <recipe> / query / recipes. Bare agents insights remains the behavioural report (transcript content, account split). agents trends still 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 on agents perf; quota on agents usage; skill/slash popularity on agents 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-lock watcher — 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 the watch-lock verb, so only the short-lived keychain reads/writes a wedged coreauthd can 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 doctor now 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.ts scans 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:8 gets a clean rc-secret-export while every shell, editor and agent started before the edit still carries the key and hands it to everything they spawn. Confirmed on yosemite-s1, which has zero rc exports and still had the value in its environment. The new env-secret-export warning 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.sh sets 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 --devices now 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 own doctor --json but parsed only .fleet, discarding the findings the remote had already computed — so a leaking box read as clean from anywhere but itself. The remote's rc-secret-export / env-secret-export rows now ride back, but the remote contributes exactly one thing: the kind. Severity, message and remediation are generated locally, and device is 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 run agents doctor there 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 --json measures 57s on yosemite-m0 and 136s on an idle box — so every slow device silently contributed nothing at all: no inventory, no sign-in, no divergence. Raised to 180s, matching ChildProcess.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 --active lifecycle model (RUSH-1993). Feed outcome headers count distinct mailbox agents in every state, so repeated open blocks from one agent cannot make needs you exceed agents. Feed rows replace generic terminal with a known host app and label routine-spawned sessions as routine:<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 --host said the opposite of the truth (RUSH-2022). The --host/--device router runs before commander parses, so agents session resume --host <box> (one letter off sessions, 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 to unknown 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 publish is in the lazy command table. commands/packages.ts registers 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, so agents docto --host <box> corrects to doctor and actually routes to <box> — previously the corrected command re-parsed locally with a --host it 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 plan headless and interactive (RUSH-2101). The capability registry previously listed only edit and skip, so agents run cursor --mode plan and routine jobs silently degraded to writable edit. cursor-agent has supported --plan since the 2026-01-16 CLI release; the registry, AGENT_COMMANDS flag 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 resume picker now filters in-memory on every keystroke instead of re-querying the database (RUSH-2212). The filter callback in the multi-select picker was calling filterSessionsByQuery, which runs a full-text search scan against sessions.db on 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 coreauthd can no longer hang secrets list forever (RUSH-2233). The keychain helper's list runs two passes, and the data-protection pass omits kSecUseAuthenticationUI: …Fail on 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 wedged coreauthd left SecItemCopyMatching blocked 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-locked errSecInteractionNotAllowed case already got. Set AGENTS_KEYCHAIN_LIST_TIMEOUT_MS to override the deadline; AGENTS_KEYCHAIN_BOUNDED_TEST=1 runs 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 --json remains 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-local watchdog.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). warp is now a first-class harness: agents add warp, agents run warp, agents teams add <team> warp, resource sync, and agents view all work. The CLI command is oz (the shared Warp binary); install is self-updating via brew install --cask oz (macOS) / oz-stable apt|yum|pacman (Linux), config lives under ~/.warp/, the rules/context file is AGENTS.md, and auth is oz login (browser OAuth) or a WARP_API_KEY token for headless/CI. Capabilities are truthful and gated by supports(): MCP (Claude .mcp.json schema 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 via oz run conversation get <id>), so there is no local transcript for agents 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). A plugins/<name>/ directory missing (or with a malformed) manifest was invisible to agents plugins list/info/sync, agents doctor, and the marketplace materialize-into-version-homes step, with zero diagnostic anywhere in the chain — no agents sync/agents repo pull could surface it, since discovery dropped it before any of those ran. Found via a real case: the work plugin merged to phnx-labs/.agents-system without its manifest and sat invisible for a full merge cycle; the same fix run against this box's own ~/.agents/plugins/duck found it missing one too. discoverPluginsInDir now writes one agents-cli: warning to stderr naming the directory and the missing manifest path, matching the existing advisory-not-fatal pattern syncMarketplaceManifest already 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). Live muse login writes ~/.config/muse/auth.json as { schema_version, providers: { meta: { access_token, user_email, … } } }. The presence detector only walked one object level, so it never saw the token under providers.meta and reported signed-out after a successful OAuth — agents run muse under balanced then failed with "excluded: 0.1.0 (signed_out)". Detection now recurses into nested provider slots and surfaces user_email when 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_PASSPHRASE or tampered encrypted value now reports the affected file-store item and stops before launching a consumer with an empty secret such as HCLOUD_TOKEN. Source: apps/cli/src/lib/secrets/filestore.ts, apps/cli/src/lib/secrets/bundles.ts.

1.22.24

  • agents run --lease now shares one warm pool across repositories by default (RUSH-2225). Repo sandbox/CI profile: labels no longer split lease reuse into one idle box per repo; a dedicated lease pool is explicit with .crabbox.yaml leaseProfile:. An empty pool keeps its newly warmed box for later callers. Concurrent runs attach with crabbox --reclaim and 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 insights turns multi-harness session history into an action list (RUSH-2280). The existing agents insights command is now also nested under the sessions noun, accepts repeatable --agent filters, 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-insights is a thin agent entry over the same CLI implementation; --narrative remains 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 focus recovers dead panes and shares the sessions browser's selectors (GH-2108). A retained tmux remain-on-exit pane is probed through #{pane_dead} immediately before attach, so dead or missing panes no longer open a Pane is dead screen. focus accepts session ids, topic/path searches, agent@version selectors (including per-device latest/oldest), device, project/time, team/routine, skill/plugin, favorites, and the complete live-state union. Focus, resume, attach, and run --resume now 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, and attach routes 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 setup no longer tells you to set AGENTS_SECRETS_PASSPHRASE, and docs/secrets.md stops recommending the shell-rc export it flags as a leak (RUSH-1968). The file-backend note read set 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 ~/.zshenv on 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 step getPassphrase no longer has. A new docs-hygiene.test.ts pins 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, and agents secrets export --to-file / import --from-file read AGENTS_SYNC_PASSPHRASE now; AGENTS_SECRETS_PASSPHRASE is 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 headless push/pull still 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 sets AGENTS_SYNC_PASSPHRASE and 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 a push --all over 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.), and agents sync's skip line with it — it previously read no 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 corrects SEC-29a, which claimed the variable applied "exclusively" to the file and age-vault backends: the age-vault backend never reads it (it is gated by agents login), and sync plus the portable --to-file envelope were two more consumers — the new SEC-29b states 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 --team as an alias for agents 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 feed no longer crashes when the session index is locked (RUSH-2006). Outcome enrichment calls discoverSessions against sessions.db; under concurrent scanner/daemon pressure that open can throw SQLITE_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. one pr.opened under twenty file.edited) was silently dropped. Event types are now passed into readRecentActivity so the cap counts matching rows; a non-activity --module skips the activity half entirely. Source: apps/cli/src/lib/event-stream.ts.

  • agents mcp add no longer strips comments from agents.yaml. The write path (writeManifest / serializeManifest in src/lib/manifest.ts) used plain yaml.stringify, which dropped every hand-written comment on every add. It now round-trips via yaml.parseDocument and edits only the keys that changed — the same approach serializeCentral already 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 mkdir lock, 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 a hook.cache.refresh event carrying the subshell's real exit code instead of a hardcoded 0. Source: apps/cli/src/lib/hooks/cache.ts.

  • agents sync --host all no longer fails every peer with unknown option '--json'. Fleet fan-out injects --json on each remote so the roster can parse per-device results, but sync never registered the flag. Register --json on agents sync and 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 agents processes (RUSH-2231, RUSH-2232). A stalled macOS coreauthd used to hang the signed keychain helper's XPC receive forever, so every secrets-touching agents command blocked and dozens of helper processes plus their <defunct> zombies accumulated and made the machine sluggish. Two fixes: (Layer 1) every keychain-helper spawnSync is 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 an agents proc 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 inspect and agents doctor now report harness-scoped hook inventory as capable, on-disk, wired, and unmanaged state. Their JSON output carries the same state sets from getResourceInventory, so a harness with hook files or native wiring no longer collapses to a misleading Hooks (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 macOS ps rejects with a non-zero exit — so execFileSync threw on every tick and reapOrphanedKeychainProcesses returned reaped: 0 on its only supported platform, leaving orphaned/wedged Agents CLI helper processes to pile up. Switched to the portable BSD etime keyword ([[dd-]hh:]mm:ss) with a parser to seconds. This also un-quarantines the darwin integration test that #2153 had to it.skip to unblock releases: that test's symlink fixture was correct all along — it read reaped: 0 only because the reaper's own ps etimes call 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 of agents models: for Claude / Codex / Cursor / …, shows which --mode plan|edit|auto|skip values work, the native CLI flags, the native default (*), any configured run.defaults mode, and degrade notes (e.g. plan→edit on Antigravity). agents inspect also prints the mode list next to capabilities, and agents run --help / agents models point at the new command. Source: apps/cli/src/commands/modes.ts, apps/cli/src/lib/agent-modes.ts.

  • agents insights --all now works as an alias for --since all. It previously exited with unknown 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 --since still wins, so --all --since 7d resolves 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_ENTRY record 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, launchd KeepAlive restarts 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 while agents menubar status still reported running: yes (a pid always existed). The plist's AGENTS_ENTRY is now treated as the owner and only the owner reinstalls freely; a same-install upgrade keeps its entry path, so npm update still 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, and agents menubar setup bypasses 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 status no longer promises that a stale helper "runs on next agents startup", 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), but getBinaryPath still resolved a version-home node_modules/.bin/muse that never exists. After agents 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/muse and $XDG_DATA_HOME/muse. After agents import muse, ~/.config/muse is a symlink into the version home — Muse refuses that path with Agent Definition filesystem source failed: SymlinkOrReparse and exits 1 (the Zion agents run [email protected] failure). Managed launches now pin XDG_CONFIG_HOME + XDG_DATA_HOME into the version home from buildExecEnv, the main shim, and versioned aliases ([email protected]), the same way Claude pins CLAUDE_CONFIG_DIR. Muse is also listed in CONFIG_ENV_ISOLATED_AGENTS so --isolated installs are honest. Source: apps/cli/src/lib/{exec,shims}.ts.

  • Scheduled routines now run on a per-account claude setup-token when one is provisioned, instead of throwing it away. buildRoutineSpawnEnv unconditionally deleted CLAUDE_CODE_OAUTH_TOKEN, so even after buildExecEnv injected a long-lived, non-rotating setup-token from the reserved file-backed auth bundle (resolveClaudeSetupToken), a routine still fell back to the version home's rotating .credentials.json login. 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 with auth_failed: OAuth access token has been revoked / Please run /login, even though agents view shows 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 ps for tmux-launched agents (RUSH-2100). An interactive agents run wraps the agent in tmux via exec 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 carried AGENTS_SECRETS_PASSPHRASE, the key that decrypts every file-backed bundle on that machine. The pane now sources a 0600 env file and unlinks it before exec, 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 --active gather (~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.ts is 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 with await Promise.all and 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 / --devices are 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 focus gains device + live-state scoping and multi-select tabs (RUSH-2206). focus now honors --device/--host <name> (scope the picker to a device's live sessions) and the same live-state filters sessions --active defines (--orphan/--crashed/--waiting/--idle/--working/--closed/--abandoned/ --queued/--unknown) — they compose, so agents sessions focus --orphan --device yosemite-s0 lists 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), reusing resume'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 (old go) behavior are unchanged. Source: apps/cli/src/commands/focus.ts, apps/cli/src/commands/go.ts.

  • agents harness edit is now wizard-capable, on a shared step engine (RUSH-2219). Run agents 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. Both add/fork and edit now drive one shared step engine: every step is skippable by the matching flag, so scripting stays non-interactive and a flagless non-interactive edit still 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 --mode selects a managed workspace profile with network access, on-request approvals, ~/.agents, and regenerable build caches. Explicit --mode plan remains filesystem-read-only but now retains network access. The same policy covers agents run, fallback attempts, resumes, routines, direct codex shims, versioned aliases, and Windows shim passthrough; only explicit skip disables 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, and agents send --to owner all 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-process channel:, spawned command: via {message}), which a per-command shell hook cannot. Source: apps/cli/src/lib/feed-broadcast.ts (truncateBroadcastBody).

  • agents run --lease self-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-idle agents lease gc window 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. muse is a first-class agent: install via curl -fsSL https://dev.meta.ai/install.sh | sh, run with agents run muse, and use in teams. Modes map to Muse safety flags (--disable-write / --disable-approval / --yolo); --model and --reasoning-effort are forwarded; headless is muse exec with --json; interactive resume is muse 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-contributor with 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-plugin manifests. 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 expensive ci.yml matrix (ubuntu + macOS + Windows × Node 22/24) still runs on release/** branch pushes and manual workflow_dispatch, but not when a version tag is pushed. release.sh tags 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-5 and claude-sonnet-5 were unpriced, so every session using them cost $0. The pricing table carried claude-opus-4, claude-sonnet-4, claude-fable-5 and claude-mythos-5 but not the Opus/Sonnet 5 line. Matching is dash-bounded (getModelPricing), so claude-opus-5 cannot fall back to the claude-opus-4 entry — 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, understating agents cost, agents output and agents insights alike. 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_usd is 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 stores token_count and output_tokens but not the uncached-input / cache-read / cache-write split the price table needs. So v34 flushes scan_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 headless ps-scan recognized only 6 agent executables, so a bare-headless grok/kimi/antigravity/openclaw/hermes/rush run was silently dropped from the live views; the comm→kind map is now derived from SESSION_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 --check verdict lines now carry a check: 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 reads never-synced (its real status) instead of the unrelated cold label, 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 file no longer requires AGENTS_SECRETS_PASSPHRASE. Since the file store became passphrase-free (auto-provisioning a 0600 machine-local key under ~/.agents/.secrets-key/), the remote import --backend file reads 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 no AGENTS_SECRETS_PASSPHRASE read/export prologue and the .env is the only stdin, so the remote keys the bundle under its own machine-local key (headless reads); set AGENTS_SECRETS_PASSPHRASE locally 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 provision apple.com on 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) and exec (1.22.21) interactive-unlock changes shipped in code + CHANGELOG only; the reference docs still implied every value command behaves the same. docs/secrets.md and the secrets skill now carry an explicit Touch-ID matrix (deliberate view --reveal/exec at a real terminal → one sheet on a locked bundle; get/export automation primitives → never prompt, fail closed to agents secrets unlock; anything an agent launches → broker-only), specifications.md adds SEC-13b + a Prompts? 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 fork is now agents sessions fork. Forking is a session operation, so it lives under the sessions group next to resume/focus — agents sessions fork <id> branches a conversation into a new, independent copy you can continue separately, leaving the original untouched. The old top-level agents fork keeps 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), but getBinaryPath still resolved a version-home node_modules/.bin/muse that never exists. After agents 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, while balanced rotation 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. --narrative is opt-in and adds a written read by piping the aggregate — never raw transcripts, unlike /insights — through a headless claude -p. Facets are cached per session in a new session_insights table 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 interrupted text was dropped outright, so the signal was unrecoverable downstream. parseSession(..., { includeInterrupts: true }) now emits a dedicated interrupt event. It stays OFF by default because the event array is a versioned consumer contract: agents sessions <id> --json serializes it verbatim, computeSummaryStats folds every timestamp into the session duration, and the live-state reader inspects a fixed window of trailing events. agents insights is the only caller that opts in. Source: apps/cli/src/lib/session/parse.ts.

  • digest.ts now classifies droid's Create as 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 view derives 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's auth_time (the OIDC authentication time) count, and a signed-out home reports no usage. auth_time is used rather than the auth.json file mtime because Codex rewrites auth.json on every token refresh, but a refresh does not re-authenticate, so auth_time stays 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>, and agents sessions --active open 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 computed availablePreviewRows went 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 of PREVIEW_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 across itemPicker, dynamicPicker, and multiItemPicker, so the bare browser, the query picker, and the --active browser 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 hardcoded agentOnly: true (commands/secrets.ts), so running exec on a locked bundle at a terminal failed closed with "run agents secrets unlock first" instead of raising the one Touch ID sheet the human just implied by asking to use the values. It now gates agentOnly on isHeadlessSecretsContext() || !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 for view --reveal. --host remote resolves and export/get are 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 org usageKey the same way agents run's balanced rotation keys quota. Sessions whose account cannot be established are reported as unattributed:<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 ~/.claude symlink 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 no account_key and rolls up under unattributed:<agent>.

  • account_key / account_org on indexed sessions, and --by account rollups. Schema v33 adds both columns plus idx_sessions_account_key, and queryUsageRollup accepts groupBy: 'account' — surfaced by agents cost --by account and agents output --by account, which render the org and email rather than the raw uuid. The migration repairs existing rows in place from file_path and version and deliberately does not flush scan_ledger: attribution needs no transcript re-parse, so a 2,736-row index migrates in ~200ms with every ledger entry still warm. getDB also runs a guarded self-healing repair for rows an older CLI left unattributed. Source: apps/cli/src/lib/session/db.ts.

  • upsertSessionsBatch bound 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 against SessionRow, so the compiler rejects the next omission. Source: apps/cli/src/lib/session/db.ts.

  • readClaudeHomeConfig() in apps/cli/src/lib/agents.ts — the single place a Claude home's oauthAccount identity is read. getAccountInfo now uses it, so identity extraction is no longer duplicated. Unlike getAccountInfo it does not apply the credential floor, because a revoked token does not change which org produced a past transcript.

  • agents routines add accepts --project <name> (repeatable) and --all-projects to tag a routine to one or more projects. Project names are validated against agents projects list at creation time; unknown names are rejected with a suggested fix command. The flag sets the new projects?: string[] field in the job config YAML — metadata-only, no effect on scheduling or execution. --all-projects sets projects: ["*"] (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 list now 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 (no projects: field), or Unknown projects (project names not found in agents projects). Pass --group-by device to restore the previous device-placement grouping. The --json payload gains projects (array) and projectGroup (string) fields. Source: apps/cli/src/commands/routines.ts (groupRoutineJobsByProject).

1.22.19

Fixed

  • agents sync --local -y refreshes 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 loops listInstalledVersions for 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 sync re-copies nested system hooks after content changes. listResources('hooks') treated event-group directories (pre-tool-use/) as resource names, so system:* pattern expansion never included nested scripts like git-guard.sh. Force sync then left stale flat copies in version homes forever. Hooks discovery now expands one-level group dirs the same way as getAvailableResources / 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 --active now carries terminalId on tmux-hosted rows (RUSH-2192). Grok/Codex (and every ag-* tmux pane) get their AGENT_TERMINAL_ID from the launch registry's by-pid entry. The ps-scan path already set terminalId; 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.json into the new version home, so agents view reported 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; run codex login (or agents 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 post now resolves owner delivery and session identity from their canonical indexes (RUSH-2193). agents notify and channel: owner consume the normal addressable channel from humans.yaml, so the phone destination is no longer duplicated in agents.yaml. When a harness leaves AGENT_SESSION_ID empty, feed posting joins AGENT_LAUNCH_ID to recent activity before requiring --session. agents feed post --help now states that the default milestone is recorded but a sink gated at minLevel: important only texts for --level important; --blocked remains 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 NSMenu tracking session instead of closing the dropdown and attempting a synthetic reopen. Cmd-Shift-O prebuilds 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 doctor now accepts symbolic version qualifiers; agents view no longer prompts to sync resources. agents doctor claude@latest, @oldest, @pinned, @all, and exact semver qualifiers are resolved through the shared resolveAgentTargets engine instead of falling through to a "not installed" error. Bare agents doctor <agent> still sweeps all installed versions with versionExplicit: false (so --fix excludes isolated copies). Separately, the implicit resource-drift scan and interactive sync prompt that ran at the end of agents view <agent> have been removed — view is 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. --cloud dispatches 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 as agents cloud run --agent <agent>, tracked by agents cloud list/status/logs/cancel/message. It sits alongside --host/--device/--lease as the third placement (local, machine, cloud) and is mutually exclusive with them; --where cloud[:provider] is the one-door spelling. --provider overrides routing; --repo (repeatable), --branch, and --cloud-env refine the task (run's --env stays 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 --provider is 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 run and agents run --cloud both call src/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> and agents 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 --resume can select another healthy harness/account and continue through /continue when 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. Manifest script: may be a relative path under hooks/. 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 when notify.owner exists in agents.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.owner in agents.yaml is migrated into it on first run and the notify.owner key is removed from agents.yaml; unrelated keys are preserved. agents send --to owner / agents notify prefer humans.yaml with a fallback to agents.yaml during the migration window. Source: apps/cli/src/lib/humans.ts, apps/cli/src/commands/humans.ts, apps/cli/src/lib/migrate.ts.

  • agents memory ignores AGENTS.md, CLAUDE.md, GEMINI.md, and MEMORY.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, and savePermissionSet now all write to the groups/ subdirectory (matching discoverPermissionGroups() which already reads from groups/). 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 blocked terminals/ 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 the headroom() "loaded" threshold (≥75%), and a red ✕ when critical. The local machine is probed natively via getloadavg (zero subprocess); fleet peers come from the daemon-warmed .fleet-stats.json cache with a freshness guard — never the slow agents doctor path. 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 / --all are gone; import is --from-linear only, and ~/.agents/factory/projects.json is never read or migrated. agents projects list --json returns definitions only (zero session scan / SSH); --with-agents is an explicit opt-in for local active counts. New agents projects save --json reads one complete ProjectDef from stdin, validates, writes atomically under ~/.agents/projects/, and prints the saved def. agents projects rm <name> --json returns machine-readable success/error. Factory's managedProjects.ts shells only through agents 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.

  • ProjectDef YAML gains dispatch block and linear.name. ~/.agents/projects/<name>.yaml now accepts a dispatch: block (enabled, maxAgents, provider, host) that opts a project into auto-dispatch and is read directly by agents __auto-dispatch — previously these fields lived only in Factory's own registry. linear.name stores the Linear project display name alongside the existing projectId and url. Both fields are optional; existing YAMLs are unchanged. Source: apps/cli/src/lib/projects.ts, apps/cli/src/lib/auto-dispatch.ts.

  • agents secrets no 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, every agents 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 broad agents-cli. keychain scan that also matched the ACL'd secret value items. On machines where a bundle carries a biometric ACL (e.g. a hold-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. Neither kSecUseAuthenticationUIFail nor LAContext.interactionNotAllowed can 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 makes secrets list cosmetically 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 and watchdog on|off now 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 list no longer shows the "Leased boxes" section by default — it moves behind a new --all flag (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-system SessionStart 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 --all restores the section; --no-stats remains 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 as agents plugins install (name@url, local path, --allow-exec-surfaces). Specialized verbs still work; agents install is 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> --reveal now resolves a locked keychain bundle interactively at a real terminal. The command hardcoded agentOnly: true on both reveal call sites (commands/secrets.ts), so an explicit human --reveal on 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. The agentOnly flag is now isHeadlessSecretsContext() || !isInteractiveTerminal() — under an agent (AGENTS_RUNTIME) or with no TTY it stays broker-only and never prompts, but a deliberate --reveal typed at an interactive terminal resolves the value with a single biometric sheet. This mirrors the existing reveal && !isInteractiveTerminal() guard a few lines up. export --plaintext and exec are 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 the tool_call_text / session_text full-text indexes on every rescan, and FTS5 never merges the resulting segments on its own — so over thousands of sessions the %_data shadow 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) and agents sessions slows 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 sessions accepts direct live-state flags and remains fleet-wide by default. --working, --idle, --waiting, --orphan/--orphaned, --crashed, --closed, --abandoned, --queued, and --unknown each imply the live scan; multiple flags form a union. --working is narrower than --active: it excludes idle, waiting, and lifecycle-failure rows. Cross-device collection was already the default and stays that way; --local opts out, while --all continues to widen historical directory and time scope. Source: apps/cli/src/commands/sessions.ts, apps/cli/src/commands/sessions.test.ts.

  • Workflows: name@source disambiguation (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-tools or agents 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 audit use the agents events --audit reader.

  • agents cli renamed to agents clis; resource directory cli/ renamed to clis/. The CLI resource kind and its subdirectory are now plural throughout: ResourceKind changes from 'cli' to 'clis', manifests live at clis/<name>.yaml, agents clis is the only command surface (no agents cli alias), and agents view --clis replaces --cli. A startup migration renames any existing cli/ directory to clis/ in the user, system, and project .agents/ layers; if both cli/ and clis/ 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.yaml no longer silently loses top-level keys across a version-skewed fleet. serializeCentral (lib/state.ts) rewrote the synced agents.yaml with a delete-any-key-not-in-the-in-memory-object pass. An older CLI version whose Meta type predated a key (beta:, notify.owner, feed:, imported projects) 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 commit 04295e3). The delete pass now consults a Record<keyof Meta, 'central' | 'device'> scope map (compile-time exhaustive — a new Meta field 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 per WATCHDOG_COOLDOWN_MS window) 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

  • --blocked iMessage notifications are now phone-actionable. The forwarded message dropped the block's --options, --default, and timeout and instead showed agents 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 the agents focus line, so a --blocked post that carries a --default self-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 projects is out of beta — no agents beta enable projects needed. The command tree (list / add / import / status / link / …) is always registered now; projects is dropped from the beta registry (ALL_BETA_FEATURES, BetaFeatureName) and the preAction beta gate is removed. Any lingering beta.enabled: [projects] entry is harmlessly ignored, and agents beta enable/disable projects prints 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 status shows every project across the whole fleet by default; scope it with --device/--devices. The old --fleet flag 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,c narrows the fan-out to a subset; with no filter the whole fleet is dialled. Reuses the shared --host/--device target resolution. Source: apps/cli/src/commands/projects.ts.

  • scripts/release.sh home-base hop: pass a single remote argv to agents ssh. Multi-arg forms (bash -lc '…') are joined without re-quoting by wrapRemoteCommand, so the remote cd never ran and publish failed with fatal: 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.md is discovered and resolved by agents run <name> with precedence project > user > plugin > extra > system — no separate install into ~/.agents/workflows/ required. Plugin inventory / resource groups list workflows. Source: apps/cli/src/lib/workflows.ts, apps/cli/src/lib/plugins.ts, apps/cli/src/lib/resources/workflows.ts.

  • scripts/release.sh routes the home-base publish hop via agents ssh. Plain ssh mac-mini fails host-key checks on headless Linux workers; agents ssh uses the devices registry and brokered credentials. Falls back to plain ssh only when agents is 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 "run agents secrets unlock <bundle>" hint instead of prompting. The AGENTS_SECRETS_NO_PROMPT environment 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 keychain list/list-synced enumeration queries now pass kSecUseAuthenticationUISkip (enumeration itself was evaluating the biometry ACL, so agents secrets list prompted and silently dropped keychain bundles when the sheet was cancelled), and the one-time hash-rekey + metadata-ACL heal run only inside the single unlock sheet 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 raw ssh. Device health, reachability, and sync route every remote command through agents 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 auto and agents teams add --device auto no longer reject with "Unknown device 'auto'" (RUSH-2185). The auto affinity sentinel was a run-only preprocessing step (applyDeviceAutoToOptions in smart-launch.ts, wired only from agents run's exec path) — every other --host/--device caller went straight to the shared resolver, which had no idea what auto meant and reported it as an unregistered device. matchHost (the one core every --host/--device caller shares) now resolves auto directly via the same resolveDeviceAffinity engine run uses, so agents ssh, agents teams add, and anything else routed through matchHost/resolveHost (including the generic --host/--device passthrough) pick a device the same way. agents teams add --device auto landing on the local machine now just runs the teammate locally, matching run's "null pick = local" outcome; agents ssh auto refuses a local pick with a clear message instead of self-SSHing, since agents ssh exists 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 edit gains --auth-provider, --fallback-model, and --from-secrets; add/fork gain 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 — like add/fork — accepts --from-secrets <bundle>[:<key>] to copy a value out of an existing agents secrets bundle 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/fork now 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 --lease is reuse-first against the crabbox profile pool, with a --fresh opt-out. A bare --lease used to always lease a brand-new box, so bursts of runs (e.g. resumed sessions) stacked up idle keep=true boxes at full monthly cost. Now, before warming a new box, the run looks for a warm box carrying the same profile label the warmup would use (read from the repo's .crabbox.yaml, matching scripts/sandbox.sh's pick_ready_box) and the same network mode — a tailnet box is never handed to a public run or vice versa — and reuses the first one crabbox status reports SSH-ready, keeping it after the run. A not-ready pool box is skipped, never stopped. --fresh forces 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 inside runDaemon() — so it could never fire while the daemon itself was down, the exact outage it exists to report. MenubarHelper is 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 own NSUserNotificationCenter delivery — 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 audit stays 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 view background refresh cannot tear or drop rows. The daemon path loads Claude credentials with fileOnly (setup-token / no-ACL cache / .credentials.json only) 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 browser now 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-local browser.remote-control setting (off by default, never synced) fixes that: a fleet-remote browser --host <this-machine> start is refused with an actionable message until the owner runs agents browser remote-control on here. Local starts (no --host) are never gated. The fleet passthrough marks every --host dispatch with AGENTS_FLEET_REMOTE so 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; --json supported). 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 browser tasks are now attributed to the caller that ran start, not to the browser daemon. Task.owner (RUSH-2020) was resolved with resolveActor() 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) puts actor (resolveActor().id) and launchId ($AGENT_LAUNCH_ID, the per-run id exec.ts injects for every harness) on the start request, and the daemon stamps exactly those. Adds Task.launchId — which run created a task — the scope a later browser status --mine and 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 edit and agents harness rename are now real commands. editProfile and renameProfile already existed in lib/profiles.ts but 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 --description in 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 its name field, and rewrites forkedFrom on every harness that pointed at the old name so the fork graph stays accurate. There is deliberately no --label: the header agents view prints 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 with agents harness add), the CLI now says Resolved 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 --strategy and account-picker notices on a custom-harness run are aligned too. Behavior is unchanged; the legacy agents profiles alias 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) in src/lib/placement.ts. agents run --where device:<name>|auto|lease[:backend]|local expands into the existing --host / --lease paths; mixing doors fails loud. Docs (00-concepts.md § Placement, hosts.md) and help on run / routines / monitors teach the matrix — including that monitors --device is owner, not body placement. Old flags remain aliases. Source: apps/cli/src/lib/placement.ts, apps/cli/src/commands/exec.ts.

  • agents harness fork no longer accepts --label (breaking change). The --label flag was used to set a human-facing display name for a custom harness. Display names are now always derived from the profile's name via a curated vendor/brand table (deepseek-flash → DeepSeek Flash, spark → Spark), so the flag is superfluous. Any script that passes --label to agents harness fork will receive a CLI error; remove the flag to migrate.

  • agents run no 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, so balanced/available/run auto could route into a revoked account and die at spawn ("session expired"). Eligibility now also reads the daemon's live auth-health probe (auth-health.ts): a revoked (401/403) account is excluded from the pick, reported as revoked by 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 cached revoked keeps 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 timeout when 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 forking view --json × N harnesses plus sessions --active --json (and sometimes feed) on every tick. agents snapshot --json returns the same shapes in one invocation: inventory (view), sessions (active rows), optional --with-feed / --with-sync. Default sessions scope is this machine; --all-hosts matches full sessions --active fan-out. Does not redefine agents 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 as agents 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 --project still 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-publish hook derives project from the session cwd, and agents feed post --blocked stamps it on the declared block. Live-session enrichment backfills project onto 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 activity is removed. The standalone milestone timeline is gone; its stream is now read through agents feed --filter all (blocks + updates) or agents feed --filter updates (updates only). activity --project <name> is replaced by feed --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 start no longer fails with "Custom binary not found" when the default profile came from another OS. ~/.agents/agents.yaml syncs across the fleet, so a default profile auto-created on macOS carried a /Applications/Google Chrome.app/... binary path that doesn't exist on a Linux box — a bare browser start there died with Custom binary not found, the top browser roadblock (one session burned six commands working around it). ensureDefaultBrowserProfile now validates that the resolved default can actually launch on THIS machine and, if its browser/binary is missing, regenerates the default from 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.hmackey item — 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 into maybeAutoRekey, which is bypassed for the hmackey and hashed-name lookups themselves (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey runs). So the exact hot paths that read the key — the agents devices list stats probe a SessionStart hook runs, and every background hashed read — never triggered the heal, and an already-migrated machine prompted forever. The documented agents secrets rekey remedy 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 by healedNoAcl, 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, binary omp) under id pi. Pi is a Bun-based, terminal-first, multi-provider coding agent; its cross-provider model catalog (OpenRouter, OpenAI, Anthropic, xAI, DeepSeek, …) surfaces in agents view and agents models pi via omp 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 the claude host binary) resolved a tier token (cheap|default|best|ultra) by calling resolveTier(options.agent, ...) with options.agent already overwritten to the HOST agent's id — so best resolved against Claude's own catalog and could push a real Claude model id as the --model flag, clobbering the profile's own ANTHROPIC_MODEL env value. Profiles can now declare a models: 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 --model value before exec.ts's native (host-catalog) tier logic ever runs. A profile with no models: 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 status card: host-grouped agents, focus units, and a warnings footer. Live agents render under @host rows so the same harness on two machines is not collapsed into one cell. Focus counts are labeled file-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 status and view/show share 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 --help and 05-sessions.md now teach one session-lifecycle matrix. focus / focus --attach-only / detach / attach / resume are listed as distinct intents (not synonyms), so operators stop guessing among go / 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|ultra to 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 view harness rows now lead with the version number. Custom harness rows previously showed via <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. The tracks default label 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 show custom · 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 auto with 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 when auto picked a harness like cursor-agent that exits immediately without a prompt: (F1) the auto-picker had no gate for whether a harness can open a bare interactive REPL — cursor-agent was a valid candidate even though its CLI requires a prompt and exits on argv = []; (F2) surfacePaneFailure was guarded by status !== 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 on paneExitStatus returning {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 new interactiveRepl capability bit in AgentConfig.capabilities marks every harness; auto now 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 via killSession instead. 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 auto can pick the Pi (omp) harness for prompt-less interactive runs again, and main builds green. The interactiveRepl capability 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 — so pi was the one agent in AGENTS that never declared the bit, and the completeness test that pins the registry to the capability list went red on main (pi missing interactiveRepl). Pi now declares interactiveRepl: true: bare omp runs the TUI, and omp -p is the one-shot form that answers a prompt and exits.

  • projects status --fleet no longer labels this box @local. Local sessions from getActiveSessions() lacked machine; host-grouped agents stamped remotes only. Locals are now filled with machineId() 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 of session_resource_usage (the skill/Skill-tool + slash-command tallies already recorded at index time), joined to sessions for attribution so --agent/--project/--since/--machine narrow the window and --kind/--plugin narrow the resources. A both-ends view: the most-invoked resources (--bottom flips to least-invoked, --top <n> caps), and the installed-but-never-invoked ones (cross-referenced against listResources/discoverPlugins) — the productized form of a manual transcript-scan audit. --json emits a versioned sessions-stats envelope. The signal captures EXPLICIT invocations only (slash commands + Skill tool 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 new agents sessions backfill resources folds historical sessions (indexed before the signal shipped) into the usage index, re-parsing each transcript from byte 0, gated by a new resource_scan_ledger (schema v31) so reruns skip completed transcripts — mirroring agents 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 share serves 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 matching content-type instead of application/octet-stream. GitHub's image proxy (camo) only renders an inline ![](url) when 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 via agents 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-session now counts every scanned session, not just agents teams runs. The recipe read sessions.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 by WHERE tool_call_count IS NOT NULL, pinning the fleet-wide p50 at 0 however many tools ran and leaving only claude in the table. It now reads tool_scan_ledger.call_count, the per-session count the tool indexer writes for every session it scans — the same index behind agents 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 surfaced grok, rush, codex, kimi, droid and antigravity, none of which had ever appeared. Run agents sessions backfill tools once if historical sessions were never indexed. Source: apps/cli/src/lib/analytics/recipes.ts.

1.22.5

  • agents events can now filter by --session <id> and --bundle <name> — trace which agent/session triggered a secret access. Every event already carries the provenance sessionId, and secrets events carry the bundle in their payload, but neither was queryable: you could see that the share bundle was read, not which session read it. --session (wired to the engine's existing sessionId filter) 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 --blocked records now survive the agent's next Stop. The feed-publish hook cleared the per-session block file on every Stop/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 in agents feed until they are actually answered (a terminal reply or recordAnswer); question/notification/approval blocks still clear as before. Source: apps/cli/src/lib/feed.ts.

  • agents add grok@latest no 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-disk versions/grok/<version>/ directory. The second account's credentials would overwrite the first's in that shared directory, even though agents view grok still listed both accounts as separately installed. installVersion now 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 a healedNoAcl flag 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 with agents secrets rekey. Source: apps/cli/src/lib/secrets/index.ts.

  • agents inspect and view --json now report isolation honestly. Found by diffing every command's output between an isolated-only and a normal install. inspect printed 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) and shim: null in JSON. inspect also showed only default: false for an isolated copy, hiding that it was the selected one; it now carries isolated and isolatedDefault, and the header reads [isolated default]. view --json had 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 gain isolated and isIsolatedDefault. Source: apps/cli/src/commands/inspect.ts, apps/cli/src/commands/view.ts.

  • agents models is 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 under model.tiers in agents.yaml (same selector shape as run.defaults) and resolve most-specific-first — <agent>:<version> → <agent>:* → auto; an overridden id a version doesn't ship falls back to auto, and agents models marks 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 like claude-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 --secrets injection read gated on isHeadlessSecretsContext(), which is FALSE for an interactive run — so an agents run … --interactive launch (the watchdog fires agents run auto --interactive every ~2 min via routine + menu-bar tick) could pop Touch ID for a keychain hold bundle, piling up helper sheets. An agent launch must never prompt regardless of tty (SEC-13): the read is now always agentOnly — it resolves from the broker (or a no-ACL bundle) and otherwise fails fast naming agents secrets unlock <bundle>, matching the behavior the code's own comment already described. Unchanged: the explicit agents share / agents share setup commands (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 locked hold/always keychain bundle, fails fast naming agents 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 --secrets injection. 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 --lease crabbox 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), the webhook serve receiver, and the get_secret MCP tool (throws as the tool error). Unchanged and still interactive-gated: the user-initiated agents secrets get/export, agents browser type, agents exec --secrets, the ssh askpass, and the explicit agents share / agents share setup provisioning 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 behind agents view, any getKeychainToken caller — 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's agents view poll), 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 every crabboxEnv call (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 required executablePath to end in exactly MenubarHelper, but lipo production builds name the binary MenubarHelper-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 spawned doctor child can never inherit it and hold the menubar.lock flock after the helper crashes; and SingleInstance.acquire now self-heals — when the flock is held but no live MenubarHelper owns 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.
  • agents projects definitions can now carry goals — the OKR-shaped "why". A project serves one or more goals[], each an objective (the outcome) plus an optional measure (the key result). Set them at scaffold time with agents projects add <name> --goal "objective:measure" (repeatable), replace them later with agents projects set <name> --goal …, or hand-edit the YAML. Goals show on the status card (compact) and in projects view (in full), and survive a --from-linear re-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. getAccountInfo falls back to the active/global HOME credential so agents view still 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 no auth.json looking 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 made agents secrets policy <b> never a 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 under hold/always kept its biometry ACL and kept popping Touch ID forever after the switch. policy now reconciles the value items to the new tier (reAclBundleItems): tightening to never re-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 every agents-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 a never bundle 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. The never tier's durability and attribution guarantees are now written into the §Secrets spec (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 run no longer pops a sheet to auto-inject the share token. On every agents run, shareRuntimeEnv auto-reads the share bundle's R2 write token to hand it to the spawned agent — and because share is 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 always agentOnly — 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 explicit agents share). And a NEW share bundle now defaults to the never tier (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 with agents 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 auto makes (collectHarnessCandidates + pickHarnessWeighted — zero healthy logs one rotate skip event per cooldown window to watchdog.log and leaves the terminal untouched), injects the harness's exit sequence (claude: Esc, Ctrl+C, Ctrl+C; codex/gemini/cursor/opencode: Ctrl+C twice), relaunches agents 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 manual agents run auto and 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>.json and spans ticks via a post-loop sweep. Config: agents watchdog rotate on|off writes watchdog.rotate in ~/.agents/agents.yaml (default on; rotate-only, nudging is unaffected), honored per tick; agents watchdog status / --json report 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 doctor de-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 .app bundles 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 ordinary agents invocations, and both did a non-atomic rm -rf dest + cp -R src dest straight 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 shared withFileLock with 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 --json no longer stampedes into dozens of concurrent runs — the overview is singleflighted and cached, with a new --refresh to force a live recompute. The bare doctor --json overview 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 a doctor --json that 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 --refresh bypasses the cache. Source: apps/cli/src/lib/devices/doctor-overview-cache.ts, apps/cli/src/commands/doctor.ts.

  • doctor --json releases 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@latest no 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, installVersion silently fell back to the literal string 'latest' as the resolved version — creating a bogus versions/grok/latest/ directory and defeating relocateGrokBinaryToVersionHome's exact-filename match (its regex could never match grok-latest-..., since the real file is named grok-<semver>-<platform>). The real multi-hundred-MB binary was left behind in the PREVIOUS default's downloads dir, and agents view grok never listed the new version as installed even though agents add reported 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 ~/.grok symlink 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 whose root is the monorepo and whose defaultPath is a subdir collapsed onto the same path as its umbrella — the longest-match tiebreak had nothing to separate them, and work in rush/apps/cli counted toward whichever definition happened to be listed first. A defaultPath nested under root now takes precedence over that root (the root says where the checkout is; defaultPath says which work is this project's), and each bound repo's checkout and subpath anchor too. A narrowed root still covers the rest of its checkout as a fallback, so a lone project defined with --path keeps 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 than status, 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 that status had shown all along. view and status now gather through one function and render through one card; view adds 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 match status. Source: apps/cli/src/commands/projects.ts.

  • The agents roster on the card lists live sessions only. It included every matched session, so a card headed 23 live went on to print claude · crashed ×25 — the corpses the dead row already reports, counted twice and contradicting the headline. Both now derive from one isDeadStatus predicate, pinned by a test across every ActiveStatus. 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 --host target 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'd doctor --json orphaned on timeout and piled up until the host was crushed. A new isSelfHost() matches every identity the box answers to (short id, loopback, tailscale dnsName + its short form) and gates all four self-checks — the generic --host passthrough (maybeRunOnHost), the --devices-all fan-out (runFleetPassthrough), remoteFleetTargets, and runFleet — 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 auto composes all three routing layers: host (14d launch affinity, unless --host is given), harness (installed CLIs weighted by best-account headroom), and account (the configured strategy). balanced/available now exit nonzero when every installed account is unhealthy — naming each excluded account, the earliest window reset, and the --strategy pinned escape 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 (behind agents sessions / agents activity summaries) tokenized the entire command — every pipeline segment, multi-KB heredoc bodies included — just to read the leading executable, costing up to ~1ms on a big cat <<HEREDOC …. It now tokenizes only the head of the first simple command. Coverage gaps that dumped commands into a raw other pile are closed too: a cd prefix 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, plus rmdir) is recognized — agents was the single top unrecognized token. ag stays the silver searcher, not an agents alias. Source: apps/cli/src/lib/session/bash-command.ts.

  • agents computer describe now counts toward usedComputer. Every other verb (click, type, key, screenshot, run, …) fires the computer.action event via emitComputerAction; describe never did, so a session that only ran agents computer describe read back usedComputer=false — a false-negative in the sessions preview. A new completeness-guard test pins every registered agents computer verb command to a matching emitComputerAction call 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 — on agents run and agents 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 ~$/Mtok where priced) and emits tiers in --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 refreshes prices.json with 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 status says what was worked on and what the dates prove. Two new lines. focus ranks the directories the window's commits landed in, read from the local checkout with git 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 .changelog otherwise ranked second and presented PR count as an area of focus. schedule states what the milestone dates prove — overdue by N days, due in N days, N milestones, no issues filed against any, or none 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/targetDate null, scopeHistory and completedScopeHistory empty), 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/--host auto-reconnect loop no longer trusts a remote-origin exit code of 255 as "the SSH link dropped." reattachRemoteSession's connected flag 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 in bash -lc so 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 peer agents version 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 sessions can query distinct tool calls and count static Bash program occurrences locally or across the fleet. Use --include tools, repeat --query with tool:, program:, input:, output:, status:, exit:, or error: fields, and add --fleet for live SSH fan-out. --count reports exact occurrence, containing-call, and session totals from ordered wrapper/effective rows without reparsing; synced mirrors are partitioned by origin so fleet evidence and totals do not duplicate sessions. Historical parsing is explicit and resumable through agents sessions backfill tools; normal scans index new and changed sessions once. Codex orchestration wrappers are parsed statically so only literal tools.exec_command commands 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>, not HEAD. createWorktree (and agents worktree provision for new branches) now git fetch origin then worktree add -b … origin/<default>, matching createRemoteWorktree. Previously local teammates forked from the orchestrator's current HEAD, 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-factory stops printing raw git errors. Reading each checkout's real remote is done per registry row, and a checkout with no origin makes git write error: 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 new computer.action event fire on every agents browser/agents computer action, carrying session identity for free. The sessions index persists usedBrowser/usedComputer (from a scoped events-log read, not a transcript re-scan) and a new session_resource_usage table records every skill and slash-command invocation with its owning plugin, source repo, and git commit — resolved against resolveResource()/discoverPlugins() at scan time. The sessions picker preview surfaces both as browser/computer and Skills: 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.

  • ResolvedResource and DiscoveredPlugin carry provenance: repoRoot and a lazily-resolved snapshotSha. 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 reads snapshotSha. Source: apps/cli/src/lib/resources.ts, apps/cli/src/lib/plugins.ts, apps/cli/src/lib/git.ts.

  • SessionEvent.slashCommand captures a typed or model-invoked slash command (both the <command-name> wrapper and the SlashCommand tool call), and agents sessions's perf sample for command.end now 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 status no longer reports "stopped" for a live scheduler, and agents routines start can'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 — status read only the pid file and reported stopped for a scheduler that was in fact running and firing jobs, while claimDaemonInstance() would start a concurrent JobScheduler that 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) read sessions.db plus a new value-free warehouse at ~/.agents/.history/analytics/usage.db. Secrets usage migrates once from secrets.db; agent run and browser launch/close emit into the warehouse. Quota stays on agents usage, latency on agents 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 MenubarHelper because the bundle had no CFBundleDisplayName. The bundle now ships CFBundleName / CFBundleDisplayName = AGI Menu, and agents menubar status/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 next agents run, 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 view reads Cursor's dashboard get-current-period-usage first for the Auto + Composer (A) / API (API) / Total (T) percent breakdown, falls back to usage-summary for accounts without a usable planUsage, and only drops to the legacy monthly request bar (M) for request-capped free/legacy plans. agents sessions also now folds a Cursor session's TodoWrite calls into SessionMeta.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.broadcast gains an in-process channel: sink and an implicit owner fallback (RUSH-2123). A feed.broadcast sink can now declare channel: <name> (plus to: for a non-owner destination) instead of command: [argv...] — it delivers through the same channel-provider registry agents send/agents notify use (deliverEnvelope()), no spawn. channel: owner is the address alias, expanding to notify.owner.{channel,to}. When an operator has notify.owner configured but never wrote a feed.broadcast block at all, an important-level post (--level important, or any --blocked post) now falls back to that owner address automatically instead of reaching nobody — previously a feed post --blocked with notify.owner set and no feed.broadcast looked recorded but delivered to no one. A routine milestone post still stays record-only even with the fallback available, and an operator-declared feed.broadcast always 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 run no 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). deriveUsageHeadroom projects 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 status gathers peers cache-first, ssh-reading a stale/missing peer via agents devices status --local --json through 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 --json is no longer a ~136-second stall (RUSH-2136). The overview probed every host-CLI manifest with a blocking spawnSync (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 hooks now reports them. agents perf gains --project <key> (scope to one repo), a P95 column alongside P50/P99, and an ERR/TIMEOUT rate column. New agents perf friction surfaces sessions stuck repeatedly hitting the same guard block instead of adapting. New agents routines stats [name] reports run count/failed/missed/avg/p50/p95 duration per routine; agents routines runs --json now includes duration. 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 status is 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 running status in 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 repeated status spends 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 rule mergeAuthHealthEntries already keeps for account health. A 429 records its x-ratelimit-requests-reset so 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 status card shows the milestone it calls next. 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 the next label on the wrong row). Source: apps/cli/src/commands/projects.ts.

  • agents projects stops reading the wrong GitHub repository. Factory derives a project's owner/repo from the checkout path's last two segments, so a repo cloned to ~/src/github.com/<you>/agents-cli whose origin is phnx-labs/agents-cli imported 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-factory now reads the checkout's actual origin and only falls back to the path guess when there is no remote to ask, and status/show print 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 $EDITOR on raw YAML or add --force, which rebuilds the definition from flags alone and silently drops linear, contexts, and description. set loads, 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 gh fetch caps at 100, and a busy repo where all 100 land inside the window has more — the count now renders 100+ rather than presenting the cap as a total, matching the existing Linear 2500+ contract. Source: apps/cli/src/lib/project-status.ts.

  • agents projects view <name> replaces show (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 silent 0%s hid that. Sixteen other command groups already use view <name>; projects was the only one that did not. Source: apps/cli/src/commands/projects.ts.

  • The status headline counts live agents, not corpses. It read 39 agents on a project where 19 had crashed. It now reads 19 live, with a separate dead row breaking down what finished or was lost — 19 crashed sessions is a thing to go fix, not throughput. orphaned counts as live: session/active.ts defines it as "alive, but no client is attached", and the repo's own dead rule is closed + crashed only. Source: apps/cli/src/lib/project-status.ts.

  • planPct is 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 project 0% 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. live and dead counts 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 / --host session-query paths, wired into CI (#1866). RUSH-2118 fixed a --local query dialing remote-host teammates over real ssh, but nothing bench-guarded the fix's latency, and the --host cross-fleet fan-out had no bench at all. bench/sessions-active-perf.ts times AgentManager(..., 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 the gatherActiveSessions({ hosts }) fan-out against N synthetic peers (asserting it stays parallel, not sequential). Wired into .github/workflows/bench.yml as the one gating step in that workflow — every other bench step stays continue-on-error. Documented with measured baselines in apps/cli/docs/05-sessions.md#benchmarks. Source: apps/cli/bench/sessions-active-perf.ts, .github/workflows/bench.yml.

  • agents view columns 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 so rate-limited and last-active drifted under the version column. Overview now caps compact meters to two windows (+N for the rest), always emits fixed account/usage/status/lastActive columns (empty cells space-padded), and measures padding with stringWidth so 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, then Sent 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 timeout in agents.yaml now accepts duration strings, not just bare seconds (#1555). A hook can be written timeout: 5s / timeout: 2m / timeout: 1h30m instead of only timeout: 30 — self-documenting at the call site. A bare number still means seconds, so every existing manifest keeps working. parseHookManifest normalizes 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 notify action now send through the registered channel provider (lookupTransport → ChannelProvider.send) instead of shelling out to openclaw directly. The recipient comes from notify.owner in agents.yaml — the hardcoded owner chat id is gone, so changing notify.owner is honoured by every path. A bare --notify on a monitor now targets notify.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 in notify.owner.channel, or --notify <channel>) fails that one send with a clean error — it does not exit the monitor daemon or abort the agents feed --dispatch loop. 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 ~/.agents to the canonical ~/src/github.com/<you>/.agents path (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's rules/subrules/* then silently shadowed the live user rules by filename, and the compile planted an out-of-date AGENTS.md in 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 --local no longer dials remote-host teammates over ssh, in the default listing or --active (RUSH-2118). --local is supposed to mean this-machine-only, but the underlying AgentManager poll still fired a real ssh round-trip for every teammate dispatched via agents teams add --device — even a teammate that had already finished. On a box with 30 completed remote-host teammates that measured out to 180 real ssh execve calls (6 per teammate: two ssh calls in syncRemoteMirror, run three times per poll) and a ~4.3s --active --local call. A --local query now reads a remote-host teammate's last-persisted meta.json state instead of dialing it, and a teammate that has already reached a terminal status (completed/failed/ stopped) is never re-dialed by ANY --active query, 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 --local surface: the bare default listing's live-glyph enrichment (maybeLiveIndex) and --preview (renderSessionPreview, freely combinable with --local) both called the local-only getActiveSessions() with no localOnly threaded through, despite the --local help 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 run time, not only after agents rules switch (RUSH-2128). setActiveRulesPreset used to take effect only on the next explicit agents 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 run now 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 events no 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 view no 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 — so getModelCatalog re-ran a full readFileSync scan 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 every agents 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 under agents devices (no new top-level noun): agents devices set-interactive <name> records the one device agents show YOU artifacts on (browser opens, dashboards) as config.interactiveHost in the central, synced agents.yaml — skills no longer guess "the online macOS box", and the host is marked ★ interactive in agents devices list. agents devices configure <name> --max-agents N --scheduler on|off and agents devices note <name> "…" (repeat to append, --clear to empty) write device-scope keys under config: 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 as browser.profile, routed to the existing device-local defaultBrowserProfile field (no duplicate key, resolution order unchanged). Unset keys always mean today's behavior; everything is scriptable with --json, and devices list --json now carries each row's config and an interactive flag. agents.yaml files the CLI writes now carry a yaml-language-server hint pointing at the new apps/cli/schema/agents-yaml.schema.json.

    The keys are live inputs, not just stored values. --scheduler off stops the routines scheduler from starting on that device — routines add skips the auto-start with the reason, a manual routines start refuses, and the daemon re-evaluates the gate on every SIGHUP reload (boot it again with agents devices configure <host> --scheduler on + any reload, no daemon restart). --max-agents feeds 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: bare agents setup ends with a skippable preferences step (which machine you sit at → interactive host; which browser agents drive here → device default), agents setup fleet offers the interactive host after a sync, and the agents setup browser picker 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_TTL overrides the default; agents message … --ttl 2h sets it per message). When a message expires, a live-but-idle box archives it with a dropped: expired receipt. The watchdog tick also runs a liveness sweep using the same live-session set as agents sessions --active, archiving pending mail in dead boxes as dropped: dead and 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 with agents mailboxes gc (--json supported). 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 --json through an unbounded Process + readDataToEndOfFile(). Two properties composed badly: the call had no deadline (doctor --json measures 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 the node -e version probes that child had forked. Both fire together, because the helper crashes under exactly the conditions that make the CLI slow: NSApplication.shared segfaults inside SLSNewConnection when WindowServer is too starved to hand out a connection, launchd's KeepAlive restarts 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 for doctor --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 gains ThrottleInterval 30 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 launch spam (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 every agents invocation — 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's agents secrets exec apple.com context), the prepack gate refuses to pack an un-notarized bundle (scripts/verify-menubar-helper.sh now 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 just codesign --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 import gains Linear as a source, and gates the Factory guess. import --from-linear turns the workspace's Linear projects into definitions via the linear CLI, binding a local checkout only on an exact name match so it never silently points a project at the wrong repo. --from-factory now imports only high-confidence rows by default (--min-confidence low|medium|high, --all to 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 list columns 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 status shows the next Linear milestone. A new next line 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 printed raw.githubusercontent.com/.../<b>/skills-index.json URL, but the commit still landed on the checked-out branch — so --branch dev from a main checkout published the index to main while advertising a dev URL that didn't resolve. commitAndPush now takes an optional target branch and pushes <current>:<target>, reporting back the branch the index actually landed on so the URL references it. Omitting --branch still 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 hq command. agents hq floor --json was a machine-readable bridge for an interactive Agents HQ floor UI that was never built — apps/factory has zero references to it and it had no other consumer. Typing agents hq now 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 (removed apps/cli/src/commands/hq.ts, apps/cli/src/lib/hq/).

  • Removed agents drive and the R2/CRDT background session-sync beta. Both predate agents sessions export/import, which now cover the same ground without a daemon: agents drive (rsync-based session/config mirroring) and the opt-in session-sync beta (agents sessions sync, the daemon's ~90s R2 push/pull loop, agents sync --sessions) are gone. If you had session-sync or drive enabled, re-enable is no longer possible — use --host for live cross-machine reads or agents sessions export --encrypt / agents sessions import for 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 the r2.backups bundle's shared R2_SYNC_ENC_KEY for 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 missed record, 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. detectOverdueJobs never checked endAt, and the scheduler only auto-disables lazily inside a live cron tick — so a routine whose endAt elapsed 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 runOnce flag while the scheduler uses isOneShotRoutine, 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. routineEffectiveStart resolved a routine's file through a user-layer-only lookup, but listJobs reads the system layer too — so a built-in shipped in the system repo had neither a createdAt stamp nor a resolvable path, the floor was skipped, and it read as instantly overdue on first daemon start. Added resolveJobFilePath, which resolves across every layer the loader reads.

  • A createdAt in 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-sweep ran 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. Omitting devices still means fleet-wide, which is what watchdog and check-updates want.

  • agents routines add --devices a,b and devices --set a,b are 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 doctor lists 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 in doctor --json as ambiguousDevicePins. 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 send is a real delivery envelope; notify is just --to owner (RUSH-2123). Flag-first form: --to, --text, --channel, --attach, --url. --to owner expands from notify.owner in agents.yaml. Positional text still works. Help names the three planes (deliver / record / control) so send is not confused with feed post, activity, or message/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 calls emit() 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. --source is stamped as module, so agents events --module factory filters to one producer. Routing is forced by the stores rather than chosen: a milestone kind requires a sessionId and lands in that session's activity log, everything else lands in the operational log. A milestone with no sessionId is 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-run validates 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.uri and factory.launch describe what a user did in the Factory VS Code extension. factory.launch is a milestone — it carries the sessionId and terminalId that later events join through — and factory.uri is 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 --since boundaries. ts stays reserved against payload injection — only the explicit override can set it. Source: apps/cli/src/lib/events.ts.

  • Fixed: agents _internal friction recorded its own invocation. The command exists precisely because shell guards run before any agents process exists and so cannot emit in-process, but it still fired the command.start / command.end audit 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 contentImage on 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 (CL claude, CX codex, GK grok, …), two letters because four harnesses start with c and two with g. agents run --notify and 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 --notify gains --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; --fleet adds a fleet line 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 hidden agents projects probe --json <path...> subcommand that is the peer half of the fan-out. One parallel SSH call per device (12s timeout), no git 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; probe itself is not beta-gated so peers answer whenever their binary carries it. The schema gains repos[].path (home-relative local checkout) to opt additional repos into probing beyond the primary root, and --json gains per-project workspaces[] with the host-tagged probe rows. The card's live line also counts agents on every box under --fleet via 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 projects outcomes on the card — agent×project members, releases, Linear counts, and projects link --linear (beta). The status card gains an agents line under live naming WHICH harness is on each project (claude · running · RUSH-2107 @zion, sorted running-first, capped at 6 with a +N more tail; under --fleet remote agents carry their peer's hostname), a latest-release tag on the ships line (primary repo only, best-effort gh release list), and a linear line 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 no linear.projectId. The new agents 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's linearProjects.ts), ambiguous/none prints the candidate list and exits 1. --json gains members[], latestRelease, and linear. 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 wrapped Skipping project resource target …: already exists and is user-owned line per file — six hand-authored commands meant twelve lines of terminal noise in the middle of agents 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 on SyncResult.projectSkipped for callers that want it. Source: apps/cli/src/lib/project-resources.ts.

  • agents secrets now 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 one emitSecretAudit chokepoint, which writes to BOTH the append-only ~/.agents/events.jsonl audit 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 at create time). agents secrets list gains --sort uses (most frequently accessed) alongside the existing --sort used, and the --json payloads carry uses, usage, and heldExpiresAt. A new agents 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 and AGENTS_NO_USAGE_TRACK=1 disables 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 normalized SessionEvent[] 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-redact is local-only. Reasoning defaults to omitted and can be folded or included explicitly. One or several selected sessions can be written to a mode-0600 Markdown file, and --json exposes the rendered documents for machine callers. The repo session skill and transcript-sharing guidance now require rendering .md before 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 sessions rows 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 reads 3d → 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 as created 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). sshExecAsync now uses a direct ssh connection whenever a timeoutMs is set, because a control-master outlives the local client and keeps the remote command running after we kill it. agents doctor also normalizes host names before excluding the local machine, so zion.local can 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. ensureValidSignature now checks spctl --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 via MENUBAR_HELPER_NOTARIZE and MENUBAR_HELPER_NOTARIZE_KEYCHAIN_PROFILE. Source: apps/cli/src/lib/menubar/install-menubar.ts, apps/cli/menubar/scripts/build.sh.

  • agents activity now 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 fans activity --json out to each reachable device and merges the peers' streams host-tagged, then buckets them by project, one level, no sub-grouping. --local scopes back to this machine, -H/--host to named boxes, and --flat (or --group-by none) restores the single stream; --devices-all/--hosts-all remain 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 +N tail, 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/cli files under <repo> instead of cli, 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 $HOME is not treated as a project. The agents sessions overview and agents feed post now share this one resolver (lib/project-key.ts), so a project reads identically everywhere instead of each view folding cwds its own way.

  • --limit is spent on milestones, not on collapsed churn. The default view rolls routine file.edited work up to a count, so a plain slice let one busy machine's 40 file edits hide every other device's PRs behind a single file edited ×40 line. The cap now bounds the milestones shown, with the routine events inside that window riding along for the counts. --all still 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-id script, someone else's half-installed plugin — printing five wrapped yellow lines above the timeline on every run. Those are agents 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 app agents sessions --active already 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 interactive agents run) resolves to the app its attached tmux client is in, via the same resolver behind agents 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> --terminal opens 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 where osascript cannot reach the GUI login. It has no scriptable split, so a split request opens a tab, and agents sessions resume --splits now says so instead of quietly producing tabs. detectCurrentBackend also recognizes TERM_PROGRAM=Apple_Terminal. Source: apps/cli/src/lib/terminal/backends/terminal-app.ts.

  • agents sessions resume / sessions focus reach 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-app forces it (named apart from run --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 in agents 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, described contexts[] starting points, external integrations[], Linear link) and agents run --project <name> resolves the definition before the old <root>/<slug> convention — undefined slugs behave exactly as before. The headline is agents 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 via gh). --window <days> and --no-remote tune the PR/artifact lookup. Also list / add (infers root + origin slug) / show / edit / import --from-factory (absorbs the Factory projects.json registry) / rm. Enable with agents 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 sessions fan-out (and therefore --resolve, cross-machine resume, and --active) picked peers with a strict tailscale.online === true test. A device registered with address.via: "manual" never gets a Tailscale peer entry at all, so its online stayed undefined and 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 now isDialableDevice, a union of both liveness signals: a device with no Tailscale block is unknown-not-offline (the rule ssh.ts renderDeviceTable and Factory's isDeviceOnline already 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 writes status.blocked to 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 at important, so passing --level too is a usage error rather than a silent override. Pair it with --option for an answerable choice or --default for a safe fallback policy may apply. Source: apps/cli/src/commands/feed.ts, apps/cli/src/lib/feed.ts.

  • Feed blocks are actually delivered. publishBlock wrote every "needs you" record to the ledger and stopped there — broadcastPostedEvent ran only for feed post, so a block was durable and invisible at the same time. Blocks now reach the configured feed.broadcast sinks, carrying the ask and the literal agents 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 desktop channel provider. agents send --channel desktop (and notify.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 for notify-send instead 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, and agent.run timings 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 profile reads 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 manual agents routines catchup. Observed cost: zion's daemon was down from 02:03Z to 08:23Z while the laptop slept, weekly-fleet-retro was armed for exactly 04:00Z, never ran, and the restart logged 2 routine(s) overdue and did nothing. The daemon now re-scans every 5 minutes as well as at startup and runs each missed routine via the same detached path catchup already used. Source: apps/cli/src/lib/catchup.ts, apps/cli/src/lib/daemon.ts.

  • New catchup: routine field, and agents routines add --no-catchup. Defaults to true — a routine you scheduled is one you expect to have run. Set catchup: false for 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 --json reports the effective value as catchup.

  • New missed run status. A missed fire previously left no trace anywhere — no run record, no log line in the routine's history — so agents routines list kept showing the previous run's completed as though it were current, sometimes for weeks. A miss is now written as a real run stamped at the moment the fire was due, so agents routines runs <name> shows the gap, and the listing renders it distinctly from failed (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-recursive mkdir — an atomic claim, so if the daemon's timer and a manual agents routines catchup overlap, 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. detectOverdueJobs walks back a week for the most recent expected occurrence, and a routine with no runs is overdue by definition — so before this, agents routines add on 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 a createdAt stamp (written once, like actor), 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 list can be filtered. It had no filtering at all — --host picks a machine and --json picks 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 through grep or 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 --sort and -n/--limit. Every axis narrows independently, so they compose. Following the agents sessions house style, an unknown value is a loud error naming the valid set rather than an empty list, filters apply before --json so the payload is the exact twin of the table, and they are forwarded over --host so a remote list narrows the same way. --held/--not-held read 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. countExpiringSoon counted only keys due in the next 30 days — the guard is d >= 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 were agents secrets view and 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, and secrets list --json gains an expired count alongside the existing expiringSoon. Source: apps/cli/src/commands/secrets.ts.

  • agents secrets list now states the hold window instead of the bare word hold. The hold tier is a duration — prompt once, then stay silent for secrets.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 running agents secrets status. The column now reads hold 7d, and hold 7d · held 6d while the broker is actually caching the bundle. It follows the configured window, so a 24-hour hold reads hold 1d. always and never are unchanged — neither has a window, and annotating one would repeat the mistake the daily rename fixed. Two adjacent bugs go with it: agents secrets view printed "7d by default" as a string literal and so misstated the window for anyone who had configured holdMs, and a stale broker entry past its expiry rendered as hold · held expired because the column tested the entry for presence rather than liveness. secrets list --json and secrets view --json gain an additive holdMs field (null on always/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 sessions quick 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|reports and other *.md/*.html creations), the repos it worked in (via .git walk-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, $VAR paths), 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.py hook tokenizes every Bash tool call and emits a structured bash.executed activity record with category, bashTool, and bashAction. High-signal commands also raise milestones: video.rendered/video.converted for ffmpeg, image.upscaled for realesrgan/waifu2x/swin2sr, and metadata.edited for exiftool/id3v2/metaflac/vorbiscomment. The session renderer and digest use the shared lib/session/bash-command.ts classifier. 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 --active now 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 ×N badge, and agents sessions focus <id> could not find them. The scanner now recovers the session id straight from the ag-<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 a runTmux timeout so a wedged tmux server can't hang the scan. Source: apps/cli/src/lib/session/active.ts.

  • agents view now shows live usage bars for Antigravity. The agy account row renders one bar per model quota bucket (3.1P: ███░░ 42% (1d) style), sourced from the same Google Code Assist :retrieveUserQuota endpoint agy itself talks to. Auth reuses the stored agy OAuth credential (macOS Keychain item gemini/antigravity, Linux Secret Service, or the ~/.gemini/antigravity-cli/antigravity-oauth-token file 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, and agents view --json (whose usage windows now carry a label so 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 with agents harness add (or agents profiles add) used to render as an indented profile row 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, and via <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> --json emits 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 --model because 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: and forkedFrom:. label sets the name agents view prints for the harness (defaults to the file name); forkedFrom records the parent as display-only lineage. Existing profiles keep working untouched. Source: apps/cli/src/lib/profiles.ts.

  • Breaking (--json): in agents view <agent> --json, the per-agent profiles key is now harnesses, and each entry carries new label, hostVersion, description, and forkedFrom fields 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 warm sessions --active cache. Accordion reopen rebuilds from cache only (no teams walk / no CLI schedule). Local/remote uses the same host normalize as CLI machineId() 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>.json for 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 /status inside 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_ID now rides the SSH hop. agents run --host forwarded actor provenance but not the launching tab's terminal id, so the remote pid registry recorded no terminal — leaving agents 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 --json now carries terminalId. 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 available as well as balanced — both route on the same cache, and available'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 list no 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-prs and hetzner-lease-gc all read failed from late July while yosemite-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 red exit 1 rows 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 --json returns null for lastStatus, exitCode, failureReason, lastRunStartedAt and lastRunCompletedAt (runsHere: false already 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 with agents routines list --device <name>; the local history is untouched and still readable via agents routines runs <name>. Source: apps/cli/src/commands/routines.ts (localLatestRun, groupRoutineJobsByDevice), apps/cli/docs/03-routines.md.

  • agents watchdog now 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, deriving connected / disconnected by diffing consecutive ticks. A session that was tracked but is now absent (its SSH link dropped or the peer went unreachable) flips to disconnected, and the flip is surfaced in agents watchdog --json under presence.transitions — an interactive drop as a reconnect-nudge candidate, a headless remote as keep-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 hold no longer fails, and agents secrets status stops naming the retired daily policy. The 1.20.79 daily → hold rename swept the help, docs, and the secrets list POLICY column, but two surfaces were never migrated. The worse one was functional: the onboarding wizard carried its own copy of the policy vocabulary, so agents setup secrets --policy hold — the canonical name every other secrets command prints — exited with Invalid --policy 'hold'. Use daily, always, or never., and its interactive prompt still offered daily as the default choice. It now shares parsePolicyOpt with agents secrets policy, so the two commands can't disagree about what a policy is called; daily/session stay accepted as aliases and the wizard's default is unchanged (the hold tier). The second was cosmetic: agents secrets status printed "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 say hold and 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 in agents sessions and f filters the list to the starred ones; outside a TTY, agents sessions favorite <id> (--remove / --list / --json) and agents sessions --favorites do the same. Stars live in ~/.agents/.history/favorites.json keyed 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, crashed and orphaned. A session whose editor window or connection went down hard used to just VANISH from agents sessions --active (its dead-pid registry entry was filtered out), and one still running in tmux with nobody attached reported a plain idle. Both now say so: ✗ crashed when the host window stopped republishing and the agent died with it, ◍ orphan when the agent is alive with zero clients attached. Derived from tmux's #{session_attached} and the IDE window's registry heartbeat — never from a deliberate agents 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 --favorites now 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 --waiting no longer counts a dead session. activity is 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> --json uses 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/usage every three minutes — roughly 100/hour — before the usage refresh added its own. Nothing read Retry-After. Measured on yosemite-s1: the endpoint answered 429 rate_limit_error with retry-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-shot agents 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 unparseable Retry-After still 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 on yosemite-s1: every Claude account's stored access token had expired (one of them eleven days earlier), so no read could succeed, and agents view claude --refresh printed 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 view marks bars the live read could not confirm. A row whose snapshot came from the cache after a failed live read renders the reading plus unverified, 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 --refresh reports 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/*.yml layer can also run one-off actions: run.agent, run.workflow, run.command, or delegate to an existing routine. Handlers support the same source/event/ action/label/repo/branch filters as routine triggers, plus Linear stateTo/stateFrom state-change filters. Prompts and commands can use {{issue.identifier}}, {{updatedFrom.state.name}}, etc. The receiver emits webhook.received, webhook.authorized, webhook.rejected, webhook.matched, webhook.fired, webhook.handler.start, and webhook.handler.end events. 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 add gains --state-to and --state-from filters 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.command are shell-quoted. A webhook context is built from an external payload, and fields like issue.title or a GitHub pull_request title 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 (POSIX sh), so a payload stays one inert argument while the operator's own template keeps its pipes, redirects, and &&. On Windows, where exec runs through cmd.exe and these quoting rules do not hold, a run.command containing {{…}} is refused with a clear error rather than run. run.prompt is 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-O quick-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 against linear projects reduced to lowercase alphanumerics, so agents-cli finds "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 linear processes were left wedged by a single ticket fetch. Both monitored paths (the ticket agent and linear 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 dynamic import() rooted at the launch entry (getAgentsBinPath → process.argv[1]). When that directory is later removed — a /tmp cleanup, 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. anchorDaemonCwd already rescues the cwd, but nothing can re-root a deleted module tree. runDaemon now calls warnEphemeralDaemonRoot at startup, so the risk is logged the moment the daemon comes up — including a direct agents __daemon-run that never passes through the launch-time validateDaemonBinary check. That launch-time check is also broadened from git-worktree-only to any ephemeral root via the shared describeEphemeralDaemonRoot predicate. 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.md sitting in a resource directory is no longer installed as a resource. listResources skipped only dotfiles, so every .md beside the actual resources was materialized as one: commands/README.md — which the system repo has shipped for months — installed a bogus /README slash command into every agent home, and adding per-directory AGENTS.md docs would have added /AGENTS, /CLAUDE, and /GEMINI alongside it. README, AGENTS, CLAUDE, and GEMINI are now filtered from both listResources and resolveResource for every kind except rules, where AGENTS.md is the resource (the composed ruleset that syncs as each agent's memory file). The check tests !entry.isDirectory() rather than isFile(), because a Dirent for a symlink reports isFile() === false and CLAUDE.md/GEMINI.md are symlinks to AGENTS.md by convention — a resource directory named agents/ is still a real resource. Verified against the real installed layers: 30 commands with README leaking before, 29 with none after.

  • agents commands list and the command picker no longer offer a name that cannot be opened. listCentralCommands and discoverCommands (src/lib/commands.ts) run their own readdirSync scans rather than going through listResources, so they kept offering README while agents commands view README answered "not found" — a listed-but-unopenable name. Both now share the one exported isDirectoryDoc predicate, so every enumerator agrees. Verified: 27 names with README before, 26 with none after.

  • agents commands add/remove/view no longer suggest README as the example command name. With README reserved 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 use plan, which actually ships.

  • File-backed secrets bundles no longer require AGENTS_SECRETS_PASSPHRASE on 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 unless AGENTS_SECRETS_PASSPHRASE was exported, which blocked headless reads (e.g. the auth bundle the usage/auth reader consults) and frequently hung. Setting AGENTS_SECRETS_PASSPHRASE still 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.png gradient "A" — outdated, and blank in the notification's left-hand app-icon slot. MenubarHelper.app's AppIcon.icns is now generated from the current brand mark (assets/app-icon.svg → app-icon.png: the lime-tile lowercase a shared with the agi-cli web favicon and the menu-bar glyph), which drives both the notification's right-hand contentImage and its left-hand app icon. The installer also registers the bundle with LaunchServices (lsregister -f) at its ~/Library/Application Support path 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 KeepAlive service plus a LaunchServices/open launch of the same .app — putting two agents marks in the menu bar, and the second copy could hold Cmd-Shift-V/Cmd-Shift-O (RegisterEventHotKey is first-come). The helper now takes an flock on ~/.agents/.cache/state/menubar.lock at 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". An flock rather than a pid file: the kernel releases it when the holder dies, so a SIGKILLed 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 setup configures 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 previous agents 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. --check reports without changing; --json emits the step list. Source: apps/cli/src/commands/menubar.ts, apps/cli/src/lib/menubar/install-menubar.ts.

  • agents menubar status now shows a duplicate. Live helper processes were collapsed to a boolean running, so two copies of the installed bundle — the duplicate a user actually sees — reported as healthy. --json now carries an instances array (copies of the installed bundle) beside the existing foreignInstances, and the text readout names every extra pid and points at agents 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.sh now 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 new scripts/release-lease.sh holds mutual exclusion on origin as an orphan commit at refs/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.sh now refuses to cut a new version while an older v* 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 down disables 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 in agents secrets, bind the receiver to 127.0.0.1, expose it with agents funnel up, rotate one source secret at a time, and turn the public port off with agents funnel down before 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-passphrase re-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 — rekey only renames macOS keychain service names and rotate <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>.enc under 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 later secrets set contaminated 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 a secrets set or 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 (--commit to 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 while AGENTS_SECRETS_PASSPHRASE is 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 --json now reports who is watching each session. The viewingIn field carries the same string the table prints — codium tab 3, ghostty tab 2, or detached for a live tmux pane with no client attached (its terminal was closed or crashed). It is null both for a session that isn't tmux-hosted and for one whose pane the locator could not resolve — detached is 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's Agents: Resume picker 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 as ghostty without 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.env and host placement. A handler can now inject environment variables into the process it spawns (run.env), and choose where that run executes (host). host takes a device name (yosemite-s0), or fleet to pick any eligible online worker, or fleet/<platform> / <platform>/fleet (also a bare linux / 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, so fleet/linux can never land on a macOS box. Omitting host runs locally, as before. Source: apps/cli/src/lib/triggers/handlers.ts (resolveHandlerHost), apps/cli/src/lib/routines-placement.ts (pickFleetDevice platform filter), apps/cli/src/lib/routines.ts (JobConfig.env), apps/cli/src/lib/runner.ts.

1.20.88

  • agents doctor redesigned 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-machine agents doctor collapses 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 new credentialPresence), 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 as claude (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 blocks agents.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 a host CLIs warning naming agents cli install <name>. Remediations reach every version in their row: a row collapsed across versions uses agents sync <agent>@all --yes (a bare agents sync <agent> would fix only the default version), a cross-device resource gap says agents repo pull rather than the central-to-home agents doctor --fix, and a diverged config repo names its own alias instead of always saying user. agents doctor --json adds a findings array and a per-version fleet.signIn map; the existing clis/sync/orphans/fleet/signIn/repos fields 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: cursor now 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 (including loop: jobs), and record successful runs correctly. Source: apps/cli/src/lib/runner.ts.

  • agents sessions now discovers, indexes, and renders Cursor agent transcripts (RUSH-2081). Cursor writes its conversation to projects/<encoded-cwd>/agent-transcripts/<uuid>/<uuid>.jsonl and metadata to chats/<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 --unmanaged to 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 usage now derives support from the usage library so Cursor, Grok, and future usage sources cannot drift from the command, and agents 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 agents on ~/.local/bin installs. The generated daemon service (systemd + launchd) now puts the agents shim's own directory on PATH, not only the Node runtime dir. On a box where the shim lives outside the Node bin dir (a ~/.local/bin global install, a separate npm prefix), the daemon's PATH previously carried only the Node dir, so every scheduled command routine — the always-on watchdog included — shelled out to a bare agents that resolved to nothing and died with exit 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 by New <Agent> and the balanced launch, but stays available through New <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 teams failures. The CLI's die() chokepoints in teams now emit a structured friction event (surface, failureId, error) before exiting, so the nightly factory-metrics routine can rank recurring failures without re-parsing transcripts. A hidden agents _internal friction recorder 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 view no longer reports a working Claude install as "logged out". Claude's signedIn is !!email read from a version home's .claude.json (lib/agents.ts), so a version that authenticates from an ambient CLAUDE_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 run now 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 sessions now shows which session spawned which team. The link already existed on disk and was discarded twice. SessionMeta.spawnedTeam — the team name read off the agents teams create/add command at scan time — had no column in sessions.db, so the writer dropped it and no consumer had ever seen a non-undefined value; a new spawned_team column (schema v21, which forces one full rescan) persists it, and orchestrator rows now carry a green team:<name> badge. Separately, classifyTeamSession was already opening each teammate's meta.json and throwing away its task_name and parent_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 a Team: line from either end of the lineage. New --in-team <name> (and a t hotkey 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, and agents teams list gains a by <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 local process.cwd() — so the filter dropped all of them. A host scope now implies all-directories (and the p hotkey is a no-op under one). Three more scope bugs on the same path: --device <this machine> fanned out to the whole tailnet, because gatherRemoteList reads 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> --cloud fell 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 as remote_session_id. classifyTeamSession looked only under the directory name, so most teammates were unreachable — on a live box, 14 of 16 records resolved only via remote_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 the existsSync + readFileSync the old path paid for every row in the pool. Source: apps/cli/src/lib/session/team-filter.ts.

  • detectSpawnedTeam no 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 read 2 or t. It matched documentation and echoed output rather than only executed commands; its flag-skip used \s and 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 auto left auto looking 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-team returns 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 --all does. 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 plan and directory text. A remote row's metadata is JSON the peer sent and parseRemoteList hands over verbatim; sanitizeMeta covered topic/label/cwd/todos but 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). directoriesTouched now reads the real recentDirectoriesTouched field instead of a dirsTouched that nothing in the repo ever wrote. Source: apps/cli/src/commands/sessions-picker.ts.

1.20.86

  • agents sessions now shows a Kimi session's todo list and its file-touching tool calls. Kimi writes its checklist with TodoList (items shaped {title, status}, where finished is done) rather than Claude's TodoWrite ({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 --active fan-out that carries progress off remote devices. Kimi also names the file argument path where Claude names it file_path, so Read/Write/Edit calls summarized as a bare Read with 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 view now 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 from locateModelSource, so resolveConfiguredModel returned null and the column stayed blank. Grok has no settings.json model field (its config is config.toml + models_cache.json); the authoritative default is grok models → Default model: <id>. The catalog extractor now spawns that command against the version-home binary (skipping failed-download stubs) and flags the default, so agents 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 0 now reads the whole stream, and a capped read says so. --limit parsed as Math.max(1, parseInt(raw) || 50), so --limit 0 collapsed back to 50 (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 --json silently 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 0 now means no cap (29,649 records on a 30-day stream here, against 50 before), a truncated read prints Showing the newest 50 — more events matched. Pass --limit 0 for all. (on stderr under --json, so a | jq pipeline still receives clean JSON), and a non-numeric, negative, or empty --limit exits 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 --notify finish notice) — was generated from the retired gradient "A" logo, so notifications carried stale branding while the menu-bar status item already used the new lowercase a. The shared master logo (assets/logo.png) is now the current a mark, so the menu-bar helper, the agents computer helper, and the keychain helper all regenerate their AppIcon.icns from it on the next build. Source: assets/logo.png, apps/cli/menubar/scripts/build.sh.

1.20.85

  • agents feed post can 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 under feed.broadcast in agents.yaml — argv templates, not built-in integrations — and each post is fanned out to them. --level important marks a post worth interrupting someone over, so a sink with minLevel: important never 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 updates now shows the progress posts agents actually wrote, across the fleet. The view read the most recent N activity events and then kept status.posted, so routine file.edited churn filled the whole slice — a box with six real posts rendered "0 posts" (and --json returned one of six). readRecentActivity gained events / tier filters that apply before the limit, so the limit counts posts; the same fix restores the milestone lane under agents feed. The updates view also fans out over SSH like the block view (-H/--host, --device, --local to 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 --notify posts 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 own exit, so it covers local, --host and --lease dispatch alike and survives anything that happens to the launcher. The helper's click actions also accept url:<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.md as a one-page on-ramp for agents, wired it from apps/cli/AGENTS.md and apps/cli/docs/README.md, and added scripts/verify-docs.sh (plus a verify-docs npm 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-write sandbox blocks $HOME, so cargo build, go build, npm/bun install, pip install etc. 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's config.toml ([sandbox_workspace_write].writable_roots) on permission sync — ~/.cargo, ~/.rustup, ~/.npm, ~/.bun, ~/go, ~/.deno, ~/.gradle, ~/.m2, ~/.gem, plus ~/Library/Caches

    • ~/Library/pnpm on macOS or ~/.cache + ~/.local/{share,state} on Linux. Credential dirs (~/.ssh, ~/.aws, ~/.gnupg, ~/.config, ~/.netrc) are deliberately excluded, so --mode auto stays a real sandbox — far narrower than danger-full-access. Any writable_roots you set yourself are preserved (unioned, never clobbered). Source: apps/cli/src/lib/permissions.ts (codexDefaultWritableRoots, mergeCodexSandboxWrite).
  • agents sessions team 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 --json carries assignedTask. 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, persists runOnce: true, marks the routine as one-shot in routines list, and agents routines cleanup removes 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 list groups 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; --flat keeps the legacy single table and --json remains a flat payload. Source: apps/cli/src/commands/routines.ts.

  • agents.yaml no longer churns on every meta write. writeMetaUnlocked wrote the central config with yaml.stringify, which strips all comments — so the freshly-serialized bytes never matched the comment-annotated file on disk, writeIfChanged rewrote it on every meta write, and the perpetually-dirty tree wedged agents sync ("Blocked by local changes") across the fleet. It now serializes via a yaml.Document round-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 leaves agents.yaml untouched. Source: apps/cli/src/lib/state.ts.

  • agents run codex can now reach the fleet from inside its sandbox. Codex's workspace-write sandbox blocks $HOME (verified against the live CLI and OpenAI's sandbox docs), but the model routinely shells out to agents ..., 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 hit EROFS (agents ssh died before connecting, so a remote agents run codex could not SSH or self-tune), and the fix was previously left to the caller (teams pass --add-dir ~/.agents explicitly; a plain agents run never did). buildExecCommand now grants ~/.agents as an extra writable root whenever Codex runs workspace-write (--mode edit/auto) — via --add-dir on fresh runs (deduped against user --add-dirs) and via -c sandbox_workspace_write.writable_roots on 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) and skip (sandbox already dropped) are unaffected. Source: apps/cli/src/lib/exec.ts (buildExecCommand, codexWritableRootsConfig).

  • Fix headless release signing (errSecInternalComponent). headless-sign-context.sh now runs security set-key-partition-list right after unlocking rush-signing.keychain-db, authorizing codesign/apple-tool to 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, so codesign fails 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). RegisterEventHotKey is 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 stray MenubarHelper --self-test from 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 status now names a second helper process instead of reporting a healthy running: yes. The check was pgrep -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=), reports running only for the installed bundle, and lists every other live copy with its pid under foreignInstances (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 RegisterEventHotKey conflict only wrote a line to a launchd log, and a missing Accessibility grant made Clip.inject return silently — both looked exactly like a dead hotkey. A stolen chord now posts a notification naming it, and a denied grant copies the host:path reference 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.sh re-validated CI and then aborted with line 933: RELEASE_COMMIT: unbound variable, so the retry never reached npm. The catch-up block that runs when main sits exactly at the release merge commit never set HISTORICAL_CATCHUP, so phase 4 took the normal-release branch and read RELEASE_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 resources now forward to their replacements instead of erroring (RUSH-1234). After the command consolidation, running the removed names produced a bare unknown command (their edit-distance to doctor was 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 … runs agents doctor --check … (so --json / --quiet / --devices and the CI drift-gate exit code carry through), and agents resources … runs agents view --merged … (with agents inspect <target> pointed to for per-agent/per-repo detail). The notice goes to stderr so a --json consumer'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 --host query, a render/filter flag, --json, or a non-interactive caller keep the streamed output. Source: apps/cli/src/commands/sessions.ts.

  • agents sessions now shows which orchestrator spawned each team. A teams teammate row was keyed off its orchestrator's session id (captured from AGENTS_SESSION_ID at 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 as orchestratorSessionId (+ a resolved orchestratorLabel) 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 as 0.142.0 cannot leave dead PreToolUse/Stop handlers that exit 127. It also writes SessionEnd hook timeouts at Codex's 3-second limit instead of emitting timeout: 5 and making Codex warn that it is clamping the value on every startup. Source: apps/cli/src/lib/hooks.ts.

  • agents secrets export/list/view now accept --device/--devices as aliases for --host/--hosts, and a keychain-backed export --host push is verified. --device mac-mini used 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 offloads apple.com provisioning 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 confusing Bundle '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 resume shows 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; tab still 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 --active now distinguishes dead and abandoned sessions (RUSH-2066). The active-session engine computes lifecycle from PID liveness and transcript mtime: a dead process reports closed, a transcript stale for ABANDONED_STALE_MS reports abandoned, and a live opaque harness still reports running as its honest floor. The default list, grouped active tallies, and agents hq floor render closed / abandoned distinctly, and Factory maps closed to done and abandoned to 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 view now 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) from cursor.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 $HOME on 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 — spawnJobAttempt and command runs pass no explicit cwd. Bun then failed getcwd() at startup and every routine died at 0 seconds with ENOENT: Bun could not find a file (or The current working directory was deleted) before the agent ran — a fleet-wide routine outage from a single removed worktree. runDaemon now 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 refresh is deprecated in favor of agents 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) or agents sync <agent> (one agent). It still runs for now so existing scripts and muscle memory don't break — refresh was a partial variant of sync (it only ever materialized the single global-default version, and silently no-op'd for an agent with installed versions but no global default), whereas sync covers every installed version. Internal callers (crabbox bootstrap, the agents pull redirect, agents setup help) now use agents sync --local. The underlying refresh() function stays — it is the reconcile stage behind agents sync. Source: apps/cli/src/commands/repo.ts, apps/cli/src/lib/crabbox/.

  • agents view now shows Grok usage limits. Grok's network usage endpoints 404, so usage is parsed from the local ~/.grok/logs/unified.jsonl log instead — the latest billing-period config and subscription tier render as a W window, matching the other agents' live-usage display. Source: apps/cli/src/lib/usage.ts.

  • agents run --host now starts in the same project you launched it from, not the remote $HOME. A host run with neither --cwd nor --remote-cwd sent no cd at 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 manual cd. 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-cwd is 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/thing says 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 $HOME dropped, and the pick is passed as --cwd to 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 --name is seeded from a slug of your task text instead of an opaque quick-<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 exec deploys all run at once. The short-id is derived from the AGENT_SESSION_ID the exec env already exports; no keychain-helper re-sign is required (the enriched string flows through the existing AGENTS_KEYCHAIN_PROMPT env). Source: apps/cli/src/lib/secrets/index.ts, apps/cli/src/lib/secrets/bundles.ts.

  • agents teams list renders from cached team metadata instead of full status probes (RUSH-1996). The list and picker rows now read the team registry plus teammate meta.json snapshots, 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 runs agents teams status <team>. Source: apps/cli/src/commands/teams.ts, apps/cli/src/commands/teams.test.ts, apps/cli/docs/teams.md.

  • agents setup secrets now guides first-run secrets onboarding (RUSH-1999). The setup command registers a new secrets capability wizard that chooses a default storage backend (keychain, encrypted file, or synced vault), sets the existing default prompt policy (daily/always, with never gated for explicit automation use), persists secrets.backend so future agents secrets create/import commands use the selected backend when no backend flag is passed, optionally delegates imports to agents 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 fleet now guides Tailscale device onboarding (RUSH-2000). The setup command registers a new fleet capability wizard that verifies Tailscale, syncs discovered devices through the existing agents devices sync path, applies SSH auth with agents devices set, optionally writes the managed SSH config include, tests connectivity with agents ssh <device> uname, and can run agents fleet update after registration. Source: apps/cli/src/commands/setup-fleet.ts, apps/cli/src/commands/setup.ts.

  • agents feed post carries artifacts and a project chip, and progress posts render rich (RUSH-2013 / RUSH-2014). feed post gains --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. A status.posted event renders as a multi-line update — agent · session · host · project chips, the message, an attachment row with per-kind glyphs, and a ↳ ag focus/sessions hint — wherever it appears (feed post echo, the feed activity lane, agents feed --filter updates, and agents activity).

  • agents feed --filter needs|updates|all (RUSH-2015). needs (default) is the open-blocks inbox as before; updates shows only deliberate progress posts over the local activity timeline (no block pipeline, no remote fan-out); all renders the blocks then appends the updates view. --json under --filter updates emits the raw status.posted events. 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 sessions and --active for real runs (RUSH-2018/2019). Two bugs, found by driving a real agents run end-to-end: (1) the session index's actor/initiated_by were kept out of the upsert ON CONFLICT entirely, so any row indexed before its actor sidecar landed (an older scanner, or a scan racing the spawn-time write) was locked to NULL forever — now COALESCE(existing, incoming) backfills a null while still never clobbering a stored owner; (2) the live --active owner read only the per-pid registry entry, which the SessionStart hook rewrites without an actor, so real runs showed no owner — --active now falls back to the durable per-session actor sidecar. Verified with a real agents run: the actor reaches sessions.db and the --active owner 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 --json adds an always-present prLink key. Existing session indexes migrate to schema v20 and rescan transcripts to backfill model data.

1.20.81

  • Generic --device all / --host all fleet fan-out for every fleet-aware command (RUSH-1969). The passthrough now treats all as a sentinel value on --host, --device, --hosts, and --devices. For any routable command (view, output, sync, doctor, list, …) it runs agents <cmd> --json on 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 --json to 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 apply no 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 apply now excludes droid — and any future harness added to the shared SINGLE_USE_ROTATING_REFRESH_AGENTS set in src/lib/fleet/auth-sync.ts — from credential propagation. The plan surfaces these as manual 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 --active and the agents sessions focus picker dropped every non-Claude tmux session (codex/gemini/kimi/grok/…) that lacked a launch-minted id, so a live agents run --device <host> <agent> was invisible and un-refocusable after an SSH drop. listTmuxAgentSessions now 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.sh refused 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). The wait_for_ci_green gate is unchanged, so the published tarball is always a tree the full matrix validated. The tree-comparison decision is extracted into scripts/select-publish-commit.sh and 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 check and resources (RUSH-1234). Two overlapping command clusters had grown ambiguous. agents check (the CI drift gate) is folded into agents doctor --check — same drift engine, now with a scriptable exit code. --check --quiet, --check --json (backward-compatible payload: every field the old check --json emitted, plus additive unwiredHookVersions/sourceBehind), and --check --devices all carry over; the standalone check command is removed. agents resources (the merged first-wins cross-layer resource table) is folded into agents view --merged; the standalone resources command is removed. The observability surfaces (events, feed, activity, output, sessions) now have a documented one-role-each taxonomy — events is the raw unified audit stream, feed the cross-agent decisions/status inbox, activity the human milestone timeline, output productivity accounting, sessions the live roster + transcripts. Running the removed agents check / agents resources now reports error: 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 --active shows who launched each run (RUSH-2018). New owner column on the active-sessions table (and an owner field 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-once actor / initiated_by columns, kept out of the upsert ON CONFLICT set 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 sessions attributes historical sessions to a person, and teams carry spawn lineage (RUSH-2019). Each run now writes a durable sessionId -> actor sidecar at spawn (~/.agents/.history/by-session/, unlike the pruned pid registry), and the session scanner joins it while indexing — so the write-once actor / initiated_by columns added in RUSH-2018 populate automatically and the durable agents sessions listing (not just --active) shows who launched each session. Teammate spawns inherit the orchestrator's frozen actor and now record a parent_session_id (the orchestrator's own AGENTS_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 + kind through the audit origin (so agents events stats carry a byActor breakdown in agents 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 of UNRESOLVED@<host> — its run records gain actor (creator) and triggeredBy (who kicked off that run); a browser task records the owner who 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 sessions now 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 blanket unknown because findSessionFileForKind / computeLiveSignals only resolved and parsed Claude and Codex transcripts — a running Codex or grok session displayed unknown. Every tracked harness whose transcript is locatable + parseable is now wired into the same state engine: findSessionFileForKind resolves each kind's transcript through the session index (latestSessionFileForCwd), and computeLiveSignals parses it with that harness's own parser and runs it through the same inferSessionState, so it gets a real working / waiting_input / idle the principled way Claude/Codex do. For a genuinely opaque kind (cursor) or an unreadable transcript, resolveFallbackStatus now reports running for any live process — a running agent never displays unknown (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 fabricated idle. (2) The default agents sessions list (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 --active column shows, with waiting the unmistakable "needs you" case. The single-session preview (agents sessions <id> --preview) leads with the same live status line, flagging ← needs you when 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 inject now 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 treats idle as its territory while leaving waiting prompts 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 activity goes 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 same activity --json payload out to every reachable device (feed-style, via gatherRemoteAgentsJson) and merges each peer's stream host-tagged; -H/--host / --device scope to specific boxes; --local forces local-only (still the default). --group-by project|device|agent buckets 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 --json stays 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-5 pins the default model (and/or --mode) that agents run uses for that agent version. It reads and writes the same store as agents defaults run set (agents.yaml -> run.defaults), so the two stay consistent. Bare agents set lists every default; agents set <selector> shows one. Source: apps/cli/src/commands/set.ts.

  • agents doctor now 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] --fix
    

    A 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/unparseable settings.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 report agents doctor <agent>@<version> and the bare agents doctor overview, which now opens with a Health banner aggregated across every installed version. The existing per-resource detail rows are kept — the health block layers on top of them as the verdict. --json gains a verdict field (target mode) and a health field (overview), each carrying severity/category/subject/ impact/fix per issue; the existing summary/kinds/hookWiring/ sourceBehind/sync/orphans fields are unchanged. Source: apps/cli/src/commands/doctor.ts (computeVerdict, computeOverviewHealth, healthBlockLines, renderHealthBlock, verdictIsAutoFixable).

  • The daemon's MenubarHelper --notify one-shots can no longer pile up in the menu bar. Each routine notification (start/finish/overdue/heal) spawned a fresh, detached, unsupervised MenubarHelper --notify process; 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: runOneShot arms 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 commitOwnDeviceMeta auto-commit from the pull path. It committed this machine's devices/<host>/agents.yaml pin snapshot to the user repo's main on nearly every pullRepo, without pushing — so main diverged N-ahead per machine and wedged agents sync across the fleet. Now that per-device pins are gitignored (they are local runtime state — written by writeMetaUnlocked, read on-disk by pinned-strategy resolution and the shim), the function only ever no-ops, so it and its sole pullRepo call are deleted along with their tests. --strategy balanced never 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, and agents sync gemini now 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 under apps/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 secrets now surfaces two typed, value-free events for the complete access picture. secrets.get records every path that resolves a secret VALUE out of a bundle (run --secrets, secrets exec/export, view --reveal, raw get <item>, sync push, remote bundle@host), and a new secrets.unlocked records the deliberate agents secrets unlock grant 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 resolving agent scope and lands at audit level in the append-only ~/.agents/events.jsonl audit trail, but is non-milestone so it does not clutter agents 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 canonical emitSecretAudit helper (apps/cli/src/lib/secrets/audit.ts), wired into lib/secrets/bundles.ts, commands/secrets.ts (reveal / raw get / unlock), lib/secrets/sync.ts, and lib/secrets/remote.ts; the new event type is registered in lib/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 a setTimeout, 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 made isDaemonRunning() report false, so startDaemon() 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: lsof showed two processes on one socket path at two different kernel socket addresses. stopDaemon now waits for the process to actually stop serving before clearing the pid file (and escalates to a tree-kill only if it does not), and bindBrokerSocket probes 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 dedicated agent.owner file rather than the standalone service's agent.pid single-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 and teardownStaleBroker also 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 doctor now 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 that settings.json actually references each hook in the right event array — so a hook whose script was byte-identical to source but never wired into PreToolUse/Stop/… reported ok and silently never fired (reproduced on yosemite-s1: [email protected] printed hooks 32 items 32 ok while its settings.json PreToolUse array omitted ask-user-question-guard.sh). Doctor now inspects the version's native settings.json (Claude-family: claude, droid), verifying each hook per (event, matcher) group, and reports a present-but-not-wired hook as UNWIRED <hook> event=<event> matcher=<matcher>, counted against the verdict; a missing/unparseable settings.json is surfaced too. --fix re-wires via the same registerHooksToSettings path agents sync uses. (2) A source layer behind origin/main means 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 the agents repo pull remediation. Both checks run in every mode, not just agents doctor <agent>@<version>: bare agents doctor (overview) and the CI gate agents check now flag a present-but-unwired hook and a behind-origin source layer, and agents check exits 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 pull reconciles a diverged repo instead of wedging on it. It ran git 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. Since pullRepo itself auto-commits the machine's own devices/<host>/agents.yaml before 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 / push exit non-zero when a repo fails. Both printed a failure line and returned 0, so agents fleet run "agents repo pull user" reported 11 ok across a fleet that pulled nothing. Any automation gating on the exit code read a total no-op as success. Matches agents sync <repo>, which already did this.

  • agents repo status reports across the fleet. New --devices-all (alias --hosts-all) fans repo status/repo list out to every reachable device and renders one aggregated table (device · repo · sync · changes); --devices <who> (alias --hosts) takes all or a comma-separated device list. Unreachable peers are skipped with a clear marker, never failing the command, and a single --device/--host still 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: buildExecEnv spreads the ambient process.env, and sanitizeProcessEnv only strips loader/interpreter variables, never credentials. So on any box whose daemon environment happened to carry CLAUDE_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). buildRoutineSpawnEnv now drops the variable, and the routine still authenticates exactly as before — CLAUDE_CONFIG_DIR is 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 daily secrets policy is now called hold, because it was never daily. The default prompt policy holds a bundle for secrets.agent.holdMs — 7 days out of the box — yet it was named daily, so agents secrets policy --help read as "you will be asked once a day" while the code comment beside it said "one Touch ID per ~7d". Both the CLI help and docs/secrets.md had 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. daily and the wire token session remain accepted everywhere (agents secrets policy <bundle> daily, secrets.policy: daily in agents.yaml, and the tier: session key 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 --json and agents secrets view --json now report "policy": "hold" where a default-tier bundle previously reported "daily". Anything matching on that string needs updating — the CLI keeps accepting daily as 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 degrades hold to prompt-every-read; only never is prompt-free independently of the broker. Source: apps/cli/src/lib/secrets/bundles.ts (SecretsPolicy, parsePolicy, secretsDefaultPolicy), apps/cli/src/commands/secrets.ts (parsePolicyOpt, policy command 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), so ag view and the background warm both prompted. loadClaudeOauth now first resolves a per-account claude setup-token from the reserved file-based auth secrets bundle (keyed by account email as CLAUDE_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 existing gatherRemoteList SSH sweep), and when exactly one machine holds it, renders that session's summary from the owning peer via runOnPeer (instead of Session 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 --json answer id-resolves too, so a content mentioner can never masquerade as the session. --local still 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 / shouldFanOutForId in apps/cli/src/commands/sessions.ts (wired into renderOneSession), and the id-only --json resolution at the sessions listing seam. (RUSH-2024)

  • agents doctor --devices now 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/.system config-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 like swarm installed on zion but absent on yosemite-s0 now surfaces as a clear warning (yosemite-s0 is missing plugin 'swarm' (present on zion)) instead of only being discovered at runtime as Unknown 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; --json carries a stable fleet divergence block for the VS Code extension to consume. agents fleet status gained the same per-device divergence warning in its rollup. Every device's top-level doctor --json now emits a fleet inventory 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, --json fleet field), apps/cli/src/lib/devices/health-report.ts (buildFleetHealthReport divergence warning), apps/cli/src/lib/git.ts (readRepoState).

  • agents doctor now 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 by readRepoBehindMarkers(), which returns the same data without printing. agents doctor reads these markers and renders a "Repo updates" section showing which repos are behind and the agents repo pull <alias> fix command. agents doctor --json emits a repos array 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 can GROUP BY machine. queryAffinityRollup returns launch counts by device (and harness/joint for analytics). Host affinity sampling lives in smart-launch.ts as resolveDeviceAffinity / 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 auto in RUSH-2059 (not a public --smart flag and not harness auto-pick). Source: apps/cli/src/lib/session/db.ts, origin-machine.ts, smart-launch.ts.

  • agents sessions --all now widens every non-status filter, not just the directory (RUSH-2055). --all used 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. --active still composes as a status filter, and -a / --device / --since still narrow their own axis (an explicit --since overrides 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 / harness auto) (RUSH-2059). Host pick from 14d usage affinity is a special value on the existing host flags: agents run claude --device auto or --host auto. The harness is always the agent you type — never auto-selected. Deprecated hidden --smart maps to --device auto for 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 --active no longer hides most of your running sessions. On a TTY, --active opens the interactive browser, and the browser resolved "running" two ways that both dropped live sessions: its live scan called the local-only getActiveSessions() 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, or machine:pid so 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, or tmux→ghostty when a tmux session is being watched through another app (a bare tmux means 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 claude on the same machine: through the rotation-pinned account's own CLAUDE_CONFIG_DIR login (.credentials.json), which Claude Code refreshes per-device. The daemon previously read a token from the claude secrets bundle and injected it into every routine spawn — first as one ambient CLAUDE_CODE_OAUTH_TOKEN (RUSH-1759), then also as per-account CLAUDE_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 sandbox ENV_ALLOWLIST entry that forwarded them; a sandboxed routine now strips CLAUDE_CODE_OAUTH_TOKEN from 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 a re-login required hint 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), resolveAccountSetupToken and apps/cli/src/lib/secrets/account-token.ts, claudeHomeHasOwnCredential (agents.ts). Source: apps/cli/src/lib/daemon.ts, runner.ts, sandbox.ts, agents.ts.

  • agents sessions no 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 a 442 passwords generated log, a git status: 442 files line, or a 442 passes/sec benchmark was reported as Tests ✓ tests 442 pass. It also treated any command merely containing a runner token as a test run, so npm-script sub-targets like bun test:setup, npm run test:watch, or pnpm test:ci were counted. Test-run classification now matches only real invocations (bun/npm/yarn/pnpm test bare, vitest, jest, mocha, pytest, go test, cargo test, tsc) and rejects :sub-target scripts, and pass/fail counts are read only from each runner's authoritative summary construct — vitest's Tests N passed / Tests N failed | M passed row, jest's Tests: line, pytest's === N passed[, M failed] in Xs === rule, bun's N pass/ N fail block closed by Ran N tests, and mocha's N 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_RUNNERS classification with (?![:\w-]) guard, new parseSummaryLine, parseTestOutput); consumed by apps/cli/src/lib/session/render.ts (renderTestsLine) and apps/cli/src/commands/sessions-picker.ts.

  • Readable Dirs: line in agents 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.sh is 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, the PREFERRED_SIGN_HOSTS list, the zion fallback, and the agents devices fleet 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.sh selects an available Hetzner VM for the repo's .crabbox.yaml profile or warms one — never a hardcoded instance), and build + sign + notarize + npm publish + computer-helper on the mac-mini home 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 the v<version> tag into a throwaway worktree and running that worktree's release.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 shared scripts/headless-sign-context.sh factors the headless keychain-unlock + AGENTS_SECRETS_PASSPHRASE preamble (no Touch ID) used by both the on-home-base publish and remote-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 unlock now grants globally, so one Touch ID actually covers everything. An unlock was silently scoped to the ambient AGENTS_AGENT_NAME: typed in a plain shell it was stored under a literal cli harness, 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 --for is 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 old cli scope 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 (get handler), 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 way sessions <id> does — by id only, never fuzzy content. The id-only fix landed for the sessions view but sessions export still gated its index lookup on isCompleteSessionId, so a bare hex short-id like d3470b57 absent 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 beside isCompleteSessionId in lib/session/discover.ts and is shared: sessions export resolves 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 ~/.claude symlink (which repoints to the newest installed version) meant that installing a new version silently hid every still-running older-version session — no sessionFile, so agents sessions rendered it unknown and the watchdog skipped it as "no activity timestamp". findClaudeSessionFile now searches all version-home project roots via getAgentSessionDirs, newest mtime winning. Source: apps/cli/src/lib/session/active.ts.

1.20.77

  • Interactive agents run --host now 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 sessions couldn't surface it and a dropped link couldn't auto-reconnect it (RUSH-2033 fixed only the Claude --session-id path). The launcher now forwards one correlation key it controls (AGENT_LAUNCH_ID); the remote agents run adopts 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, resolveLaunchId in apps/cli/src/lib/exec.ts, and the interactive --host branch in apps/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/*.yml stays inspection-only until agents routines enable-project (with interactive / --yes approval) records the project on meta.routines.projects and materialises copies into ~/.agents/routines/ with a source: block (projectPath, git repo/branch/commit). agents routines sync (and daemon start/SIGHUP reload) refreshes those copies when project YAML changes; disable-project / projects manage the allowlist. New hostStrategy: local|host|fleet|cloud (CLI --placement) chooses where the job body runs: local, a named --run-on host, one online fleet device per fire (no cross-device double-fire — off-box strategies auto-pin devices), or the agent's native cloud provider. --host remains 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's plan default for interactive terminal launches. Codex, Claude, Gemini, Cursor, OpenCode, and Antigravity now start in auto (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's Notification), which the feed-publish hook now handles: it publishes an approval-class block with a high cost-of-delay and a deny safe-default, so the blocked agent surfaces on agents feed and agents feed --dispatch pages the phone as urgent. A Codex approval card clears once the approved tool runs, via a matcher-less PostToolUse clear hook registered for Codex only — so Claude's card lifetime (its permission_prompt/idle_prompt/elicitation_dialog notification blocks persist until Stop/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 / ensureFeedPublishHook in apps/cli/src/lib/feed.ts, apps/factory/src/core/waitingNotifier.ts. (RUSH-2039)

  • agents fleet ping now 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 the fleet status version-probe budget, which is enough for the ~8 s provider-fetch inside the local auth probe). fanOutDevices gained an optional perDeviceTimeoutMs that races each probe against a deadline and records it as failed: timed out instead of hanging. runFleetPing now 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 sessions surfaces checklist progress in every list/preview (RUSH-2045). The picker preview, --active rows (local + cross-machine), flat doing cell, and metadata-only previews now show compact ✓done/total · current step from SessionMeta.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.py hook now appends a task.completed milestone to the session activity log (and checklist.created the first time a checklist appears), so completions show in agents feed and the unified agents events stream with the item subject and running N/M. Detection folds the transcript across harnesses — Claude TaskUpdate/TodoWrite, Grok todo_write, Codex update_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 remote agents teams supervisor, or any --host passthrough) used to drop the resolved actor at the SSH boundary, so the remote re-resolved it from the originating box's SSH_CONNECTION and mis-credited the work to the shared machine or UNRESOLVED@<host>. The dispatch layer now forwards AGENTS_ACTOR* / GIT_* across the wire (POSIX export and Windows $env: alike), so the remote inherits the origin identity instead of re-resolving. A caller-supplied env value still wins on collision (mirrors buildExecEnv). Source: withActorEnv in apps/cli/src/lib/hosts/dispatch.ts, wired into launchDetached / runInteractiveOnHost and the --host passthrough. (RUSH-2028)

  • A Linux-driven release now auto-discovers its macOS sign host instead of hardcoding mac-mini. scripts/remote-sign-mac.sh previously defaulted SIGN_HOST to mac-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. With SIGN_HOST unset the script now reads agents devices list --json, keeps the reachable/online macOS devices, and picks the first that answers ssh in preference order mac-mini → zion → any other online Mac. mac-mini stays first because it signs headlessly (no Touch ID); zion (the interactive Mac) is the fallback. An explicit SIGN_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: interactiveUnlock defaulted to true whenever an agent name was present, which let an agent-initiated read fall through the agentOnly guard; and isHeadlessSecretsContext recognized the headless and teams runtimes but not terminal, which is what an interactive run sets. Agent launches now resolve broker-only and a locked bundle fails fast naming agents 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.sh now 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 → local npmjs.com bundle → 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 tries SECRET_HOST first, then zion, then mac-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.app companion instead of raw AppleScript, so they carry the agents-cli mark rather than the generic Script Editor icon; they degrade to osascript/notify-send only 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 status now suggests which bundles to unlock. It reads the existing secrets.get audit 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 ready agents secrets unlock <name> command. never/no-ACL bundles (which never prompt) are excluded, and the hint is best-effort so it never breaks status. 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 like d3470b57 was not caught by isCompleteSessionId, 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 both resolveSessionQuery and the renderOneSession content-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 --active attributes the initiating device for SSH-launched sessions. A session started by ssh'ing into a box (common for tmux-hosted runs) used to render as local with no origin, because the tmux discovery path stamped a transport:'local' placeholder that made provenance enrichment skip it. Enrichment now probes the pane process's env and upgrades the row to ssh with the real origin, then resolves the SSH client IP against the device registry into provenance.origin ({ device, user? }). Both the flat listing and the interactive browser read ssh←<device> (e.g. ssh←zion); an unregistered IP stays bare ssh. Answers "which box launched this session" without scraping ps/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 / --last now 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> as role=user records, and <system-reminder>/<task-notification>/<command-*>/hook-feedback/skill bodies land the same way — so --include user --first 3 returned the jump command and its shell output before the real ask, and every consumer of --include user (the verify-work-complete Stop hook's "original request" self-audit, session-recall) inherited the noise. parseSession now flags these injected role=user events _synthetic at 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/--markdown stream for full fidelity. Claude-specific in practice — Codex/Gemini/OpenCode/Grok/Kimi/Rush route shell output to tool_result, never to role=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-token is a 1-year, non-rotating token that sidesteps this. The daemon now injects every CLAUDE_CODE_OAUTH_TOKEN_<account> present in the claude bundle (not just the one ambient token), and a routine spawn selects the token matching the account its version-home is pinned to (runner.ts buildRoutineSpawnEnv → 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-ACL claude bundle. Interactive agents run and remote --host dispatch 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's agentOnly branch) threw for every keychain-backed bundle absent from the broker, but a never/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-only claude bundle 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 exempts never/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.sh can 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 new patch-from-main case accepts the version one patch above package.json when 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 of release.sh into scripts/validate-bump.sh so it can be tested directly — release.sh itself 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 run re-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 carries actorEnv(resolveActor()) (process env < actor < --env overrides), so all teammates inherit the single frozen actor. Teammate records also carry an actor field, persisted to meta.json and 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 under Multiple sessions match "<id>" plus the advice Pass a longer ID to narrow it down — impossible to follow, since a full id is already the longest form. The same fallthrough made --preview render a different session's transcript, let an 8-char short id lose to a content hit, and made agents 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 reports No 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), and ses_ + 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), and apps/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 grok now writes native .md command files there instead of converting commands to skills, so agents view grok and agents commands list grok report 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 /missions or droid exec --mission (optional -f is a prompt file, not a named template). ~/.factory/missions/<sessionId>/ is runtime state only — no auto-discovery dir agents-cli can populate — so workflows stays false with 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 deadlocked agents repo pull (RUSH-1925). Registry presets from SEEDED_REGISTRIES (today skill.hermes) were seeded on the state read path, which wrote the registry entry plus a seededPresets marker into agents.yaml. That file is git-tracked in the user's DotAgents repo, so the write left the working tree dirty and every subsequent agents repo pull aborted with Working tree has uncommitted changes — naming neither the file nor the cause. Because every agents invocation reads state, the dirt reappeared the instant it was cleared: git checkout -- agents.yaml && agents repo pull re-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 raw git pull --rebase lost the race, and one machine sat 27 commits behind for weeks as a result. Seeded presets are now resolved in memory by getRegistries — the same way DEFAULT_REGISTRIES has always worked — so nothing is written and no later write can flush them into the file. seededPresets becomes a removal tombstone: agents registry remove skill hermes records 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 own registries: block, and the explicit entry still wins, so upgrading changes nothing for them. setRegistry also falls back to SEEDED_REGISTRIES when merging a partial update, so registry disable/enable/config on a never-materialized preset can no longer persist a stripped entry that drops url. 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 status no 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 expensive agents --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 an unreachable row with zero further SSH round-trips. Measured on a single blackholed target, fleet status dropped 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 devices and agents fleet status now 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 cached tailscale.online snapshot. A reachable box (including a via:"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 status output 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 · cold counts across every orphan version, an ●5 ·8 ◐3 auth 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) showing name · 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-line agents prune nudge in the footer; the freshness footer names the cache age and what --live / --verbose add. A healthy fleet reads in a few lines. The full per-device auth/CLI/sync/version grid moves behind the new --verbose flag; --json is unchanged. Source: apps/cli/src/lib/devices/health-report.ts (renderFleetSummary, buildFleetAttentionItems), apps/cli/src/commands/ssh.ts (runFleetStatus).

  • agents doctor now flags credentials exported from shell rc files (RUSH-1968). A secret exported from ~/.zshenv/~/.zshrc/~/.bashrc/~/.profile is inherited by every process the login shell spawns and is readable from /proc/<pid>/environ by any same-user process — .zshenv is sourced even by non-interactive ssh 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 a Secrets in shell config warning that names each credential-shaped export by file:line and variable name (never the value), with the file-store master key AGENTS_SECRETS_PASSPHRASE called out separately — its off-env home is ~/.agents/.secrets-key/passphrase (chmod 600), and other credentials should move to agents secrets and inject via agents 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 into apps/cli/src/commands/doctor.ts (renderRcHygieneAdvisory).

  • agents devices no longer forces a Touch ID prompt on a password-auth box (RUSH-1970). The read-only stats probe's live SSH to an uncached auth.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 (alias relocate) relocates a RUNNING session onto another machine, then stops the source here (RUSH-1977). --auto scores the fleet and picks a target, --host <name> names one explicitly, and --lease provisions a fresh ephemeral box; --mode resume|rehydrate chooses whether the target resumes the native transcript or replays it via /continue. Every migration is written to an append-only ledger, viewable with agents 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 with agents 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 --json now emits flat ticketId and project keys on every row. A supervising watcher joins active sessions on ticket + project, but the raw row nested the ticket under ticket.id and carried no project at all, so a naive join silently dropped every session. Each row now carries top-level ticketId (from the detected ticket) and project (the basename of the session's cwd — the same derivation the historical --json listing uses), both always present and null when 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 it tailscale whoises the client IP to the connecting tailnet identity (name + login email); locally it stays honest with UNRESOLVED@<host> and claims no identity. The resolved actor rides the agent's process env as AGENTS_ACTOR / AGENTS_ACTOR_KIND (inherited by the whole spawn tree, so it resolves once), and for a resolved human it also injects GIT_AUTHOR_* / GIT_COMMITTER_* — so the agent's own git commit is 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 into buildExecEnv (apps/cli/src/lib/exec.ts).

  • New optional actors: map in agents.yaml. Keyed by a short slug, each entry (kind / name / email / github / login) enriches or overrides what tailscale whois resolves — 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 --print flag was gated on the raw --headless flag, which defaults to false at the CLI layer — but headless is inferred from prompt presence. So a bare agents run antigravity "do X" built agy <prompt> with no --print, launching the interactive TUI and dying with bubbletea: could not open TTY: /dev/tty in any non-terminal shell (headless runs, teams, routines, --host). Print flags are now gated on the resolved headless state, matching the documented "--headless auto-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 -p print 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 failed with an auth_failed: / auth_preflight: reason instead of a generic non-zero exit. The login-error text is no longer written into report.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 a result event with is_error:true), which is the reliable signal — terminal_reason is "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 run now exits non-zero when a run doesn't complete. A run that ends in failed, timeout, or an auth failure returns exit code 1 and --json { "ok": false, … } with the reason, instead of exiting 0 with ok:true — so cron wrappers, && chains, and --json consumers actually see the failure. (Exit code is set via process.exitCode so 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 revoked auth verdict fails fast with auth_preflight: revoked without 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, reusing apps/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 WARN and fires a desktop notification instead of quietly spawning Claude routines that all fail auth. Source: apps/cli/src/lib/daemon.ts.

  • agents sessions indexes again from the standalone binary. Every session write went through a named-parameter bind (INSERT ... VALUES (@id, @short_id, ...) in apps/cli/src/lib/session/db.ts), and bun:sqlite matches such an object only when its keys carry the SQL sigil — bare keys bound nothing, so all columns landed NULL and sessions.short_id (NOT NULL) rejected the row. The shims exec the Bun-compiled dist/bin/agents, so no session reached the index from the CLI: agents sessions printed Warning: skipped unindexable session <id>: NOT NULL constraint failed: sessions.short_id per session and then listed only rows indexed earlier by the Node entrypoint. The suite runs under Node (vitest), where node:sqlite accepts bare keys, which is why CI stayed green. apps/cli/src/lib/sqlite.ts now opens the DB with strict: true under 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.ts covers both the bind and a full agents sessions scan in a real bun subprocess.

  • The compiled binary no longer reports itself as a phantom /$bunfs install, 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 __dirname is 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), and agents upgrade failed closed with /$bunfs is not an npm-managed install because no global prefix can be derived from a virtual path. A new resolveRunningPackageRoot() resolves the real on-disk root by walking up from process.execPath to the directory whose package.json names this package, and both sites use it. The PATH scan also recognizes <root>/dist/bin/agents as 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. getClaudeUsageInfo refreshed 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 by agents 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 pure claudeUsageAccessTokenNoRefresh uses the stored access token and, when it is within the refresh leeway, reports "no usage right now" instead of rotating — exactly mirroring probeClaudeStatus. The single legitimate refresh stays on the real claude run, 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 polls agents view --json every 60s per agent) and the daemon's every-3-min auth-health probe each read Claude's own ACL-bound Claude 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 mechanism secrets/session-store.ts uses 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 with allowDefaultSwitch: 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 installs latest and pins it. A background default switch installs a fresh version home, which for Claude is a fresh, empty credential scope (macOS keychain keyed off CLAUDE_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 a WARN naming 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-pinned agents run --resume path — 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 under sessions next to focus/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, /continue replay 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>.log so a crash after detach is debuggable. agents sessions --active --json now carries a presence field (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 old detach alias is renamed to relocate to free the detach name for the background/foreground verb — migrate and relocate both still work.)

  • agents devices sync no longer auto-registers tailnet nodes another user shared into the tailnet. tailscale status includes 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 in agents fleet ls. Parsing now carries a sharee flag, runDeviceSync filters those peers out of auto-registration and suggestions, and the interactive picker leaves them unchecked (labeled shared). Deliberate paths — devices register/add and a fleet: 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 sessions on large / unchanged session trees. Session discovery re-walked and re-stat'd every transcript directory on every agents sessions / output / view / teams call — for a heavy user the immutable version-home and backup roots dominated the cost yet never changed. A new dir_ledger (SQLite, schema v14) caches each leaf transcript directory's (mtime, entry_count); when both match, the per-file stat of 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. Set AGENTS_SESSIONS_NO_DIR_LEDGER=1 to 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 a status.posted milestone to the per-session activity log (same stream as agents activity and 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 export AGENT_SESSION_ID / AGENTS_AGENT_NAME / AGENTS_CWD so 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 — in pty-server.ts and teams/agents.ts — and neither copy had a Windows branch: both fell through to ps, which does not exist there, so the function always returned null and every caller silently skipped the guard. A dead session whose PID the OS had recycled read as alive, and agents teams stop could signal an unrelated process group. Both copies now delegate to a single implementation in platform/process.ts that reads CreationDate from Win32_Process as 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() ran path.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/me became D:\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.ts opened tasks.db and exported no closer, so nothing could release the handle — on Windows that leaves the file un-unlinkable. Adds closeStore(), the mirror of closeDB() in session/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 one sessions --active row 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 ago rows 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 carrying pidCount: N. Source: apps/cli/src/lib/session/active.ts.

  • sessions --active now stamps a start and last-activity time on every interactive session. Terminal, tmux, and headless agents discovered by the process scan carried no startedAtMs — so the Factory Floor rendered every running agent as "0s ago" even when its transcript, topic, and progress had resolved. The scan now stamps startedAtMs (the SessionStart hook's own timestamp, else the transcript's creation time) and a new lastActivityMs (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 sessions now 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 placeholder session_start event — parseGrok was a stub. It now reads the session's chat_history.jsonl and normalizes every line into the shared SessionEvent shape: user/assistant messages, reasoning → thinking, assistant.tool_calls[] → tool_use (with path and command surfaced), and tool_result correlated back to its call by tool_call_id (an Error:-prefixed result becomes an error event). The scanner records summary.json as the session path, so the parser resolves chat_history.jsonl beside it; per-line timestamps aren't stored, so each event carries the session's created_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.1 writes ~/.agents/profiles/spark.yml, and agents run spark then dispatches OpenCode pinned to that model; --model at run time still overrides it. A harness is a profile under the hood (same YAML, same run resolution, same agents repo push user device sync), so agents profiles is unchanged; harness adds the host+model one-shot (no preset required), owns its own --host (never remote-routed, unlike profiles --host), and agents harness list shows 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 the opencode preset help all named meta/claude-spark-1.1 — a model neither OpenRouter nor OpenCode serves; the live id is meta/muse-spark-1.1. Separately, an authOptional preset (opencode) still wrote a keychain auth block that resolveProfileEnv always read, so agents run opencode-spark died with "Keychain item not found" even though OpenCode uses its own login. resolveProfileEnv now 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 sessions now heals any pre-existing empty-shortId rows on upgrade. The prior fix stopped producing empty shortIds (bare-prefix ids like a session_ 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 the short_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 sessions for every agent, not just Claude. A --host dispatch 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 in agents sessions and 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-id flag the dispatch forwards); the launcher parses it out of the followed log and stamps it on the host task, so agents 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 run reconciles 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, and agents sessions learned 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 exec didn't print a parseable id, the provider minted a synthetic codex-<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 at agents cloud list rather 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.3 printed 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 makes agents 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.cpSync defaults to force: true, but Bun drops that default when a filter is supplied — so copies that strip symlinks left existing destination files untouched. dist/bin/agents is bun-compiled, so this affected a shipped path, not just tests. Now passed explicitly in config-transfer.ts and import.ts.

  • agents import --isolated no longer misdescribes itself, chokes on codex, or copies your session history. Three defects found by using it: (1) the confirmation summary printed config: ~/.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 reads will be COPIED — your original stays put. (2) Seeding failed outright for codex with Cannot overwrite non-directory, because its version home is a SUN_LEN-safe symlink to ~/.agents/.codex-homes/<version>/.codex rather 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 it sessions — so runtime state is now skipped and reported (33MB for the same install), with --all to 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 the scan_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 (a gh pr create in 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 sessions and every consumer that scans: output / view / teams / the watcher). Each persists a resumable continuation in the scan_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 (a gh pr create function_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 whole summary.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 @default like any other install. Two places still assumed a managed version is reachable on PATH — which an isolated install deliberately is not. (1) agents sessions resume 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 way agents run already 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 / @pinned read only the global default, so an isolated-only agent threw "No default version set" even after an explicit agents use — resolveVersion had gained the isolated-default fallback but this resolver had not. Both now consult it, and report isolated-default as the source rather than claiming a global default. opencode resume 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.attentionMarks now takes the caller's live-session set and unlinks sentinels whose sessionId is not alive — the 06-attention-sentinel.sh hook already clears on Stop/UserPromptSubmit, but leaks when a terminal is killed hard, a Claude version has no hook, or the sessionId doesn't round-trip; the reader is the only layer with pidAlive ground truth. Verified on mac-mini: 6 stale sentinels aged 1–22 days pruned to 0 on one dump run. addNeedsAttention groups 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 input filler when the Notification message is empty. addActive collapses the "other" bucket to a single clickable ACTIVE · 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 it code 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 launchd KeepAlive service it restarted forever, and its unstable identity made the Accessibility grant (needed for the clip→paste keystroke in Clip.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, and inspect surface the model an agent+version actually runs with, beside the version (the identity cluster reads agent · version · model · account). The model is resolved agents.yaml run.defaults → the native settings.json → the built-in default, and agents view --json gains a configuredModel { 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 pull now reloads the routines daemon so device pins refresh. The scheduler froze each routine's config — device pins included — in memory at daemon start. A repo pull rewrites 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 claude routines pick an account with the default balanced (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 set account: (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 over version:, which pins a version number that is GC'd on the next upgrade, silently dropping the routine back to balanced. 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_TOKEN into its environment so a token-less default account still authenticates (RUSH-1759). But Claude — and the Linux shim's own -z CLAUDE_CODE_OAUTH_TOKEN guard — both prefer that env var over a pinned account's CLAUDE_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 --value literals) is now stored WITHOUT the biometry ACL at every prompt-policy tier, not just never. Metadata is non-sensitive by contract — real secret values live in separate agents-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 runs agents 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 list is 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, under GROK_HOME) and .grok/workflows/ (repo-level). agents-cli now declares workflows: { since: 0.2.111 } for grok, projects central WORKFLOW.md bundles into managed Rhai scripts via transformWorkflowForGrok (with an // agents_workflow: <name> marker so user-authored scripts are never overwritten), and registers the writer + detector so agents 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 --image instead 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 runs linear create with --image <path> for each selected screenshot, so paths pass through Swift argv and survive spaces or @ in CleanShot filenames. Coordinates with the linear create --image support added in phnx-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.app previously had no CFBundleIconFile/.icns, so Notification Center and System Settings → Privacy & Security showed a blank square. Each build script now generates AppIcon.icns from assets/logo.png and adds CFBundleIconFile to the bundle Info.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/config stanza win and dialed its bare name, while sessions --host, session bundles, and agents ssh dialed the device Tailscale user@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 (so agents devices sync takes effect without re-enrolling, no more frozen route), an enrolled device keeps its presence and dispatchable flags, a password-auth device cannot be made dispatchable by shadowing it with an inline entry, and a host present only in ~/.ssh/config is now visible to the sessions --host fan-out.

  • agents run --device/--host now 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, and agents sessions focus by 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 own agents 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 --device path 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) spawned process.execPath -e <inline node program>, which is only correct when process.execPath is node. Since 1.20.53 the macOS agents is a bun-compiled Mach-O, so process.execPath is the CLI itself and the spawn became agents -e … — rejected with error: 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 the daily policy's one-prompt-per-7d never applied and every bundle read re-popped Touch ID — agents secrets status would report the broker running while holding nothing but explicitly unlocked 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-lock tokens built by the shared getCliLaunch primitive and intercepted in index.ts before commander — alongside __daemon-run and __vault-age-helper, and deliberately above the line where every normal command runs checkForUpdates() 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 both import --from icloud and import --from 1password all called readBundle() 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. delete now uses the new readBundleIfDecryptable() and proceeds without the plaintext, reporting that the bundle's keychain items cannot be enumerated for purging instead of claiming a clean purge. The view hint no longer points at import --from icloud for 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 no AGENTS_SECRETS_PASSPHRASE) still fails loudly and is left in place, so secrets delete <name> --yes from 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/upsertSessionsBatch wrote label = excluded.label unconditionally on conflict. The ON CONFLICT clause now preserves an existing non-empty label and only overwrites when a real label arrives, so a /rename, agent-generated title, or --name handle survives later rescans. Headless runs launched with --name now 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 --active no longer shows zombie sessions from recycled pids. Liveness was a bare process.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. isPidAlive now takes the session's recorded startedAtMs and, 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 via ps -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 sessions no longer corrupts the index with empty shortId rows. Session ids that are only a known prefix — a bare session_ Rush directory, an id of exactly api- (Hermes) or ses_ (OpenCode) — used to strip to '' ('session_'.replace(/^session_/, '').slice(0, 8) === ''). An empty shortId passes the short_id TEXT NOT NULL constraint (empty string is not NULL) yet matches nothing in the short_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 in discover.ts, session/cloud.ts, cloud/session-index.ts, hosts/session-index.ts, session/fork.ts, and commands/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 unknown instead of a fake idle (RUSH-1976). agents sessions --active now reports an explicit unknown status (◌) for a live agent whose activity it cannot introspect — a running gemini/droid/cursor/opencode whose transcript format is not parsed — instead of the misleading idle it showed before. Status resolution is standardized in one place (resolveFallbackStatus): a vanished transcript file no longer flips to a false running, an unanswered prose question with no mtime signal no longer sticks as "waiting on you" forever (the RUSH-1522 null-mtime hole), and the ps/lsof probes 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 (tab toggles 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) and PR# 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 sessions now 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 after agents add --isolated, where keeping the two apart was the whole point. Listing is now scoped to managed versions (isolated or not); --unmanaged brings 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 run agents add sees 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, --unmanaged needs 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 --active now 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 $TMUX is 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 weaker ps-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 --nudge routine 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 customizable watchdog workflow: drop a watchdog WORKFLOW.md in your project or user workflows/ to override the prompt and pick the model:; absent one, the improved built-in prompt runs via agents 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.log in 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 enable used to flip a private ~/.agents/.cache/state/watchdog/enabled sentinel that only meant anything to a manually-launched agents watchdog --watch loop — so the auto-nudge only ran while some shell was babysitting it. It now creates and enables a plain watchdog command 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 in agents routines list. disable pauses that routine; status reports whether it is enabled. The Swift menu-bar toggle and watchdog status --json are unchanged. Bare agents watchdog (dry) and agents 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.yaml entries 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 one claude@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@all expands 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-device agents view --json probe, 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.json recorded its paths with the host's native separator, so a sync run on Windows wrote entries like skills\myskill. That file lives in the version-controlled project .agents dir and travels between machines, and the cleanup pass matches manifest entries with path.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 plain agents import is 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). --isolated copies instead: your settings land in the isolated home, your real config stays exactly where it is, and the version is finalized the way agents add --isolated does — 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-auth opts in. Symlinks into ~/.agents are 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 bare agents run <agent> reaches your isolated copy. Isolated installs were unreachable by name: resolveVersion ended at the global default, and an isolated install deliberately never becomes one — so agents use refused, and an isolated-only user had to type the full agents run [email protected] every time while a bare agents run codex fell through to whatever codex meant on PATH. use now 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 in isolatedAgents: in agents.yaml, never in the global agents: map — that separation is what keeps getGlobalDefault incapable of returning an isolated version, and with it the launcher, bare shim, config symlink and self-heal shadowing check 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 view labels 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. --isolated was 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-cli so you can --diff it and take the parts you want. Your files are never modified. --replace promotes a sandbox wholesale (yours is moved to backups/<agent>/<ts>, and it is the only mode that asks for confirmation); --staged dumps the tree into ~/.<agent>/.agents-export-<ts>/ and activates nothing. Every mode strips symlinks pointing back into ~/.agents so the result keeps working after agents-cli is gone, keeps your own symlinks, and writes a receipt to ~/.<agent>/.agents-cli-export.json recording 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. --isolated used 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 .isolated markers 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 --isolated is 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 import and doctor --adopt refuse with guidance rather than a stack trace — import is 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 view no 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 single agents add <agent>@<v> --isolated made the user's globally-installed CLI disappear from the one command they'd run to confirm --isolated had 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 tagged 9.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 --active now 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 .jsonl in the cwd", which collapses co-located agents onto one row. ag run now mints a launch id and exports it as AGENT_LAUNCH_ID on 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 a ps-discovered process to the hook's authoritative session id by launchId (robust even when the hook runs under a different pid — a tmux pane leaf or cmd.exe wrapper), falling back to terminalId and pid. This also attributes agents ag run never launched (you typing claude in 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 with EACCES on Apple Silicon Macs where that directory is root-owned and not user-writable. A new resolveBinDir() picks the install directory instead: honor AGENTS_CLI_BIN_DIR if set, else prefer ~/.local/bin (created on demand — the same XDG user-bin dir shims already use), else fall back to /usr/local/bin with an actionable error pointing at AGENTS_CLI_BIN_DIR / ~/.local/bin instead of a bare EACCES. 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 --host and hosts logs -f now follow remote logs with a long-lived tail -f stream that reconnects from the saved byte offset and captures the remote .exit code 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 --json now 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 migrate now 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 setup now configures the R2 lifecycle rule and sets the Worker WRITE_TOKEN through 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) through publishToEndpoint, 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 setup now maps share.agents-cli.sh when the Cloudflare token can see the agents-cli.sh zone, while keeping the workers.dev endpoint when it cannot and honoring --domain overrides. Source: apps/cli/src/commands/share.ts, apps/cli/docs/share.md.

  • Expire shared artifacts end to end (RUSH-1797). agents share --expire now stores expires-at metadata for the Worker to enforce and agents share setup installs a managed R2 lifecycle rule so old share objects self-clean. Source: apps/cli/src/lib/share/{publish,provision,worker-template}.ts.

  • agents share central mode now follows synced config plus injected write tokens (RUSH-1798). agents share join can bind an existing synced endpoint without reprovisioning, publish reads SHARE_WRITE_TOKEN from runtime env before falling back to the local share bundle, 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> --json now 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 share regression 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 pull now fast-forwards the local checkout after fetch (--ff-only semantics) 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 share default 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 --slug values and existing links are unchanged. docs/share.md now states the security model explicitly (unlisted-not-secret; reads are public; use --expire for 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/view gain --json (RUSH-1834). Agents can now discover which secrets bundles and keys exist as machine-readable JSON before injecting one — list --json emits bundle metadata (name, key count, policy, backend, timestamps) and view <bundle> --json lists each key with its kind and stored/missing state. Values stay null unless --reveal (which keeps the same non-TTY --plaintext gate and audit event as the human view), so the discovery surface never leaks a secret. Gated on the explicit --json flag, not stdout.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 during agents share setup --analytics-token, and check status with agents 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_HOME overflows the Unix-socket SUN_LEN limit. 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 points CODEX_HOME at 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 — so codex app-server daemon start failed with path must be shorter than SUN_LEN and 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 and buildExecEnv now 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-set CODEX_HOME is 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_request and agents cloud run --on pr now preserve GitHub --action and --label filters, so a UX test routine can fire only when a PR receives ux-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 daemon command tree (RUSH-403). The legacy agents daemon start|stop|status|logs aliases are gone for v2.0; use agents routines start|stop|status|scheduler-logs for 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.account names such as GITHUB_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, and agents whoami plus agents secrets create --synced for age-encrypted synced secrets stored in ~/.agents/vault.age.

  • Protected synced secrets vaults from accidental replacement: agents login --create and agents login --join <path> now require --force before replacing an existing vault.age, and synced bundle writes batch metadata plus stored keys into one vault update.

  • Synced vault encryption now runs without re-executing the agents binary, 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 activity renders the stream newest-first (milestones individually, routine edits collapsed to a count); agents feed gains 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 ~/.agents setup onto the box, i.e. copySetup=false). Headless / --json never blocks — it provisions fresh unless --reuse/--box is given. New subcommands agents lease list (--json) and agents lease stop <slug>.
    • Step UI (F2). The box-side setup now renders as a live checklist — each ___PHASE_<name>___ step from the lib's onStep stream prints via renderStepLine (✔ Step — detail (elapsed)). Non-TTY prints one line per step; --json emits {phase:"setup",name,elapsedMs} events. Host-side warmup/ready/teardown phases are unchanged.
    • Devices (F4). agents devices gains a live "Leased boxes (ephemeral · via crabbox)" section computed from crabboxList() — never written into the device registry. agents ssh <slug> now resolves a leased-box slug and connects to crabbox@<tailnet-or-ip>:2222.
    • Net-mode (F5). New --tailscale / --no-tailscale on agents run. netMode = (--tailscale || reuse-context) && !--no-tailscale (a solo one-shot --lease stays public) is threaded into the lease so the lib leases onto the tailnet. agents lease setup now also captures a Tailscale auth key (EPHEMERAL, pre-authorized, tag:crabbox) into the tailscale.com secrets bundle as CRABBOX_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).

  • 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 --lease gains a library core the command layer wires up: copySetupToBox pushes the git-tracked subset of the local ~/.agents onto the box and refreshes it (never ~/.claude); the box bootstrap now echoes ___PHASE_<name>___ sentinels parsed into a structured LeaseStep stream (onStep + renderStepLine); and a netMode: 'tailscale' path leases boxes onto the tailnet (--network tailscale -tailscale-tags tag:crabbox, CRABBOX_TAILSCALE_AUTH_KEY from a secrets bundle) with CrabboxBox.tailscaleIPv4/tailscaleFQDN parsed 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 --json now emits machine-readable failures. die() — the shared fatal-exit path — always wrote red text to stderr and left stdout empty, so an agent parsing --json output 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 pure formatDie() makes the human/agent split unit-testable. Every failure path in cloud run now threads the resolved --json flag. 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 as S/W/M windows for Droid, matching Claude's live-usage display. Source: apps/cli/src/lib/usage.ts.

  • agents events is 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 activity shows agent events, --audit restricts to operational only, and all existing filters (--event, --agent, --since, --command) apply across both. New readUnifiedEvents (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 without libnotify-bin — surfaces as an asynchronous spawn 'error' event, not the synchronous throw the surrounding try/catch expected, 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 start failed 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 the fleet verb. The idempotent reconcile engine already shipped as top-level agents apply, but users who reach for fleet/devices as the noun (fleet capture, fleet login, fleet status) had no matching fleet apply. This surfaces the identical command as agents fleet apply (and agents devices apply) via a shared configurator, so the two can never drift — same flags, same engine, same --plan/--device/--only semantics. Pure discoverability alias; no behavior change to agents apply. Source: apps/cli/src/commands/apply.ts (configureApplyCommand, registerFleetApplyAlias), apps/cli/src/commands/ssh.ts.

  • agents fleet login now 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 — so kimi/droid/codex were "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-code Authorize deep-links and TTL countdowns — so you enter codes back-to-back instead of babysitting N terminals. Default mode requests all codes concurrently; --interactive walks 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 ping stops 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 yellow 0/N, and the --verbose per-account list painted expired red — lumped with a real revoked — even though expired is 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 for revoked (the only "re-login now"); unverified reads as neutral gray "signed in (unverifiable)"; expired/rate_limited/error are soft yellow; and the cell numerator counts signed-in accounts (live + present) so a logged-in codex fleet reads 1/1, not 0/1. Separately, fleet ping --verbose now actually emits the per-account breakdown: the root program's global --verbose was 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.yaml operation-family policies (read, write, command, url) for built-in tools; this file is active only when .forge.toml has restricted = true, and MCP tools bypass it. Hermes now receives command allow rules in ~/.hermes/config.yaml command_allowlist and deny globs in approvals.deny, preserving sibling YAML keys such as mcp_servers and hooks. 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/--box fail fast outside a git repo instead of billing a dead box. crabbox syncs the working directory to the leased box with git ls-files, so from a non-git directory the run died at build sync file list: exit status 128 — but only after provisioning (and billing) the box. agents run now 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 --lease setup-copy (and agents ssh <slug>) to actually reach the box. Both used a raw ssh crabbox@ip:2222, which fails publickey — 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 ~/.agents config really lands on a leased box (agents repo refresh then materializes it) and agents 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 --label trigger filter matching nothing. routines and monitors jobs triggered on linear:Issue with --label <name> never fired: the matcher read data.labels.nodes[].name (the GraphQL-query connection shape), but Linear webhook bodies flatten list relations — data.labels is a flat array of label objects. The .nodes read 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 menubar now works from the Bun single-file binary. When the CLI runs as the compiled Bun executable, import.meta.url points inside the virtual /$bunfs/ bundle, so the menu-bar helper couldn't find the shipped MenubarHelper.app on disk (bundle source: missing (cannot enable)) or read its own package.json (current version: unknown, and a perpetual "stale" warning). enable refused 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 the agents launcher symlink, so enable/disable/status behave 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 .lobster files under .openclaw/workflows/.

  • Fix the PTY sidecar (agents pty, interactive agents teams, agents fleet login) on the macOS standalone binary. Since the macOS release became a bun --compile standalone (#315), the sidecar was spawned AS that binary (process.execPath pty _server) — but a Bun standalone cannot require() a native addon, so node-pty's pty.node failed to load (Cannot require module ../build/Debug/pty.node) and every PTY-backed command died with "PTY server failed to start within 5 seconds." getServerSpawnArgs now detects the standalone case and runs the sidecar via a real node executing the dist/index.js that ships beside the binary (where the prebuilt pty.node loads 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 sessions now indexes routine-run transcripts from durable run history; use agents sessions --routine --all or agents sessions <run-id> to inspect a routine run with the existing summary view.

  • agents routines add, run, and runs now support --json. Previously only list/status emitted JSON, so an agent creating a routine or triggering a run had to scrape human strings for the job name / run id. add emits { ok, added, job }, run emits { ok, job, runId, logDir }, and runs emits 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 --interactive resolves 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>" or agents run <agent> --headless to read the prompt from stdin). An explicit --interactive is 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 exitCode and failureReason from agents routines list --json, show failed routine reasons inline, label healthy-but-overdue routines as overdue instead of exit 0, and open the concise logs summary instead of a raw Terminal dump.

  • Show Droid teammate activity in agents teams collect by 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 feed only 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-block timeoutMinutes is honored for approval defaults and decision parking, policy/default answers are tested against the real mailbox spool, block-specific allowedOperators restrict 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 feed ranks 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 with ag feed --pause / ag feed --kill actions. 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 tail now prints low-noise message/tool/result lines unless --json is passed, and agents logs -f <id> keeps raw transcript streaming behind --full.

  • Fix agents repo refresh for 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> --lease now 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 pty now 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 migrate so OpenClaw Keychain writes no longer expose secret values in process argv, and migration now fails closed when matching plaintext credentials disagree.

  • agents share setup and agents setup share now read Cloudflare provisioning credentials from the cloudflare secrets bundle and persist the Worker write token as WRITE_TOKEN in the share bundle, matching the setup/onboarding contract while keeping endpoint config in agents.yaml under share:.

  • Fix agents routines list and agents routines view so a project-layer routine with the same name no longer hides the user-layer devices allowlist written by agents routines devices --set.

  • Keep piped CLI output human-readable unless --json is passed.

  • Add --json to agents monitors view and agents monitors test, and send monitor errors to stderr so JSON stdout stays parseable.

  • Add --json output to agents routines add, agents routines run, and agents routines runs so scripts can capture routine and run ids without scraping human text.

  • Fix hook directory sync status so bundled hook directories such as hooks/tests copy correctly and no longer appear permanently drifted after sync.

  • Neutralize residual OSS scrub breadcrumbs by replacing browser/session test fixture hostnames with remote-host and 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 mine is the wizard; agents mine init/list/toggle/remove manage 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 run or agents 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 concurrent agents teams processes can't double-print), and skipped for non-TTY, CI, --json/--quiet, or AGENTS_NO_NUDGE=1. The agents teams call 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 doctor from reporting phantom drift and agents prune from 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-repo rules/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 that prune cleanup would delete; and command-as-skill wrappers (the agents_command marker) 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 with agents apply (#1305). New agents fleet capture (alias agents devices capture) snapshots the live environment into the portable fleet: block — the device roster (names only), the source's agents as defaults, secrets-bundle names, and routine names. It commits zero Tailscale IPs or usernames: agents apply reconstructs a fresh machine's roster by resolving each device name live from Tailscale (ensureDevicesRegistered), so git clone + agents apply replicates the fleet with nothing sensitive in the repo. apply now also passes declared sync: scopes through to agents sync <scope> (previously a bare sync) and surfaces declared secrets-bundle names to recreate on each device (values stay keychain-local, never pushed). Browser profiles are intentionally not duplicated into fleet: — they already sync via the central browser: 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 status no 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-written Host <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 during agents view, or an agents secrets add) via /usr/bin/security add-generic-password -w piped the value over stdin — but readpassphrase(3) reads the controlling terminal when one exists, so in an interactive shell security prompted the user ("password data for new item:") and hung to the timeout, ignoring the piped value. The write now runs detached (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> --json now reports the true final status of a host task. For a run that finished remotely between dispatch and the one-shot --json read, the payload emitted a stale status: "running" with no exitCode — even though the completed log was already present — because hostTaskLogJson discarded the reconciled record reconcileTask returns (it heals a new object rather than mutating in place). It now emits the reconciled task, so a polling agent sees completed/failed + exitCode + finishedAt. Source: apps/cli/src/lib/hosts/logs.ts.

  • Fix the macOS menu-bar auto-heal so upgrades actually restart the helper. agents has an on-startup self-heal that re-copies MenubarHelper.app when the CLI version changes, but on modern macOS launchctl bootstrap fails when the job is already bootstrapped, and the deprecated launchctl load -w fallback plus kickstart -k did not recover a job that launchd had stopped respawning after a WindowServer event port death. The helper would stay updated on disk but invisible in the menu bar. enableMenubarService now 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 pull no longer wedges on per-machine pin drift. The committed devices/<machineId>/agents.yaml (each box's agent version pins) is rewritten whenever a pin changes, leaving the working tree perpetually dirty — so agents repo pull, which refuses a dirty tree, kept failing until the file was hand-committed. pullRepo now durably commits just that one path (explicit pathspec) before pulling, via commitOwnDeviceMeta. 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 unlock now 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 set secrets.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 Windows unlock is 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 share cover capture finds Playwright's chrome-headless-shell packages. scanCaches() only knew the classic chrome-mac/Chromium.app and Google Chrome for Testing layouts, so on machines whose Playwright cache holds only the newer chromium_headless_shell-* packages (a raw chrome-headless-shell binary, not an .app bundle) — and no system Chrome/Brave/Edge — the OG cover capture silently returned null and shared plans published without a preview card. The scan now matches chrome-headless-shell-mac-arm64, chrome-headless-shell-mac-x64, and chrome-headless-shell-linux64 layouts 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 on PATH, 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 ~/.claude a dangling symlink. agents uninstall is the reverse of agents 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 by getConfigSymlinkVersion, the same check removeVersion uses); --dry-run prints the full plan without changing anything; and if any restore step errors, --purge self-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 ~/.agents handled). Source: apps/cli/src/lib/uninstall.ts, apps/cli/src/commands/uninstall.ts.

1.20.70

  • Fix agents setup computer / agents computer setup refusing to install a valid downloaded helper. The signature check read codesign -dv from stdout, but that command writes its details to stderr on success — so the Team-ID check saw an empty string, found no TeamIdentifier, and rejected every validly-signed, notarized helper with "signed by unexpected Team (none)". It now reads both streams via spawnSync. Verified end-to-end against the real published v1.20.69 release asset (download → sha256 → extract → codesign + Team 2HTP252L87 + spctl notarization → 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 release used swift 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-only MenubarHelper.app in the tarball that could not run on Intel Macs. It now builds each slice via --triple and lipos 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 say limits 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 computer helper now ships as a signed + notarized release asset, downloaded on demand. A fresh npm i -g @phnx-labs/agents-cli no longer needs to build the Swift helper from source: agents computer setup / agents setup computer fetch ComputerHelper.app.zip from the matching v<version> GitHub release, verify it against the published .sha256, and re-check the code signature (Developer ID Team 2HTP252L87) 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 view chip (#1285). New agents fleet ping completes 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 view now 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 the sessionId field 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. --name labels the fork. Source: apps/cli/src/lib/session/fork.ts, apps/cli/src/commands/fork.ts.

  • agents setup is now a capability hub with guided browser / computer / share subcommands. Bare agents setup still 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 browser detects an installed Chromium-family browser and creates/points the default profile; agents setup share provisions or joins a Cloudflare share endpoint (reusing agents share setup/join); agents setup computer installs the macOS helper and walks you through the Accessibility + Screen-Recording grants — opening the exact System Settings panes and polling until trust lands. The existing agents share setup / agents computer setup remain 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.sync previously 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.json for Claude, .codex/config.toml for 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 (getProjectMcpConfigPath exported), apps/cli/src/lib/resources/mcp.test.ts.

  • Production MCP sync path also writes project-level configs. installMcpServers now merges project-layer servers into the agent's project-level config file, using a shared writeMcpConfig serializer with overwrite/merge modes. OpenClaw serialization is corrected to nest under mcp.servers, matching the existing reader; Grok/OpenClaw user-level configs are written directly with merge mode so multiple servers don't clobber each other; and installMcpServers only reports applied for 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 from organizationType (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 compact S:/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 reads usage unavailable instead of a blank gauge, and a new agents 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 status now renders a fleet-wide warnings rollup plus a device matrix for reachability, resource headroom, sync drift, CLI readiness, and agents-cli version skew; --json emits the same report for scripts and --strict exits non-zero when any warning is present. agents check --devices now 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 doctor shows signed-in/logged-out, and agents run warns before launching into a logged-out account. agents doctor's Agent CLIs section now renders a ✓ signed in <account> / ✗ logged out badge per installed agent (also surfaced in --json under signIn), and an interactive agents 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 /login back. It is advisory (file-based getAccountInfo, 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 add adopts an existing checkout instead of dead-ending. When the target ~/.agents-<alias>/ already holds a git repo whose origin matches the requested source, repos add now registers it in place (no re-clone) rather than erroring Directory 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.

  • --json for repos list and plugins list. Both list commands now emit machine-readable JSON with --json, matching the repos view --json / plugins marketplaces --json that 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-cwd now fails loud instead of silently doing nothing. The flag rides the shared --host option family but teams add treats --host/--device as 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-cwd help 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 --active and a bare agents sessions now 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: s search, r running-only, c teams, a agent (cycles), d device (cycles), p this-repo↔all-dirs, w time 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 — y copies (and --print-cmd prints) the exact ag 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-interactive keep the existing static listing verbatim, so scripts and headless agents are unchanged. Adds -p as the short form of --project, --print-cmd, --preview (agents sessions <id> --preview prints the compact digest without the pager), and --no-interactive. Built on a new async-refetch dynamicPicker variant 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 plan now auto-downgrades to auto instead of crashing or stalling (RUSH-1810). kimi's headless -p refuses to combine with --plan (it hard-failed at spawn) and grok's --permission-mode plan silently stalls a headless run at its ExitPlanMode gate. Both now model this honestly with a capabilities.headlessPlan: false flag: a headless plan request degrades to auto (kimi -p auto-runs; grok maps auto→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 covers agents run, agents teams add teammates, 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-device no longer silently watches the local machine on a bad name. An unregistered or mistyped --watch-device name is now rejected at add time (same registry gate as --device/--devices), and the device source evaluator returns an explicit device not registered observation 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'), so agents monitors runs reflects the auto-pause that view's last fired already showed. Adds sources/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 of devices/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); --devices is the advanced allowlist; --run-on offloads 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. A rateLimit: {max, per} firehose guard auto-pauses a runaway monitor. The daemon hosts a MonitorEngine beside 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 setup provisions an R2 bucket + a ~30-line Worker on your Cloudflare (read from your cloudflare.com secrets bundle), enables the free *.workers.dev subdomain, and — if your token owns the zone — maps a custom domain like share.agents-cli.sh; agents share plan.html [--slug x] [--expire 30d] then does an authed PUT and 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 (a share secrets bundle) + synced share: config in agents.yaml (agents share join uses an existing endpoint with no provisioning). Expiry is per-object (x-share-expires-at metadata → 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.share in apps/cli/src/lib/types.ts).

  • agents share now auto-generates an OG cover so links unfurl (RUSH-1809). Publishing an HTML page screenshots its own hero at 1200×630 and attaches it as og:image + twitter:card, so a share.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-cover to 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 through publish.ts and the agents share command.

  • SSH host-key pinning for credential-copy and agents ssh (Security, RUSH-1767). --copy-creds now refuses to ship credentials to a --host whose SSH host key is not pinned, and agents ssh pins a host key (via ssh-keyscan into 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-creds gate and the agents ssh pin 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-only agents serve gains an opt-in control mode: a bearer-gated HTTP surface that adds POST /api/run (dispatch a headless agents run, local or --host <device>, returning a server-minted session id so the run is immediately addressable) and POST /api/session/:id/message (steer a running agent via agents message), on top of the existing GET /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 --control boot 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 (extracted handleServeGet), 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 --json and 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 pattern hosts/progress.ts uses — normalizing each line to {type, raw} (message / tool_use / tool_result / result / error) and emitting one SSE frame per event. Each frame's id: is the exact byte offset past its line, so a phone that drops mid-run reconnects with ?offset=<bytes> or the standard Last-Event-ID header and loses or duplicates nothing; the stream closes on the terminal result/error event. Scope: streams anchor-local runs; streaming a --host-offloaded run reuses pullRemoteLogDelta and is a follow-up. Source: apps/cli/src/lib/serve/stream.ts, apps/cli/src/lib/serve/control.ts (startSessionStream, defaultRunner capture, spawnDetached stdio), + stream.test.ts / control.test.ts.

  • Devices gain a control role + agents devices pair-ios for the iOS cockpit (RUSH-1733). A DeviceProfile now carries an optional role: '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 the agents sessions --active SSH fan-out (remote-list.ts now bails on isControlDevice(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. New agents devices pair-ios [name] (run on the anchor) mints a bearer token for agents serve --control (hash-only on disk, shown once), marks a matching registered device role=control so 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/*.yaml defines 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 with agents 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 → installMcpServers and 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 list now 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 view surfaces 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. getAccountInfo now reads organizationType/organizationName from each version home's .claude.json oauthAccount (file-only, no keychain access, so no macOS ACL prompts); agents view appends 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). The agents use version picker gets the same badge, and view --json emits the raw organizationType/organizationName fields. Companion fix: agents view --prune now keys duplicate detection on accountKey (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 view compact 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-model sonnet_week), matching the exact set deriveUsageStatusFromSnapshot already 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 the fleet: block of any -f file (default agents.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-run renders a device×dimension matrix (agents-cli · agents · config · login) without changing anything; -y/--yes skips the confirm; --device <name> scopes to one device; --only agents,config,login limits dimensions; --no-login skips 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 (sshExec stdin, never shell-interpolated); an internal --recv-auth receiver 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 the Meta schema; project agents: 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 new host cloud provider — tasks visible in both agents cloud ps and agents hosts ps (one sidecar store, two views); status reconciles from the remote .exit with a per-target reachability memo, never guessing failure. (2) agents run --host gains a forwarding contract (RUN_OPTION_FORWARDING): --effort --env --add-dir --timeout --strategy/--balanced/--fallback, the --loop family, --json --verbose --yes --acp and -- passthrough now forward to the remote; --secrets*, bare --resume, --resume-checkpoint reject loud before dispatch (all previously silently dropped) — locked by a commander-introspection test. (3) Devices join the host pool via a devices HostProvider: agents hosts list shows them, capability routing reaches them, agents hosts add <device> --cap enrolls from the device profile. (4) Routines placement: agents routines add --run-on <host> [--run-cwd <dir>] executes the job body on a machine (auto-pins devices: to the adding machine against duplicate fleet fires; daemon finalizes from the remote exit). Auto-dispatch projects can pin provider: 'host' + host:. Also fixes --no-auto-secrets being a local no-op (commander stores it as autoSecrets). 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 --host branch stripped @version and ignored --strategy, --effort, --add-dir, --json, --verbose, --timeout, --yes, and --acp, so the remote host applied its own defaults. The local CLI now parses agent@version verbatim, normalizes --strategy/--balanced, makes --add-dir paths remote-portable, and forwards all of these flags to the remote agents run invocation. --add-dir portability uses the same ~/$HOME re-rooting that --cwd already 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 ~/.agents stays clean and agents repo pull can sync routines across the fleet. writeJob previously re-emitted the entire document via yaml.stringify on every mutation (pause/resume, routines devices --set, add), restyling untouched scalars — unquoting schedule, re-wrapping the folded prompt block — which left the git-backed user repo perpetually dirty. That made cross-device agents repo pull refuse ("uncommitted changes"), so a devices: pin set on one machine never reached the others and Devices: all routines kept firing on every box. A new serializeJob edits 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 secrets gains a server-independent recovery path (RUSH-1414). Secrets recovery no longer hard-depends on api.prix.dev: secrets export --to-file <path> writes an encrypted offline bundle (AES-256-GCM via the existing encryptForFallback, mode 0600) and secrets import --from-file <path> restores it — both gated on AGENTS_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 the export --host push mechanics. Source: apps/cli/src/commands/secrets.ts (exportBundleToFile, importBundleFromFile, --from-ssh), apps/cli/src/commands/secrets.test.ts.

  • agents sessions <id> --json now carries session.todos — checklist progress from the state engine (RUSH-1503). The single-session JSON already surfaced session.plan (the last ExitPlanMode); it now also carries todos — 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 --include filter. 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 Codex update_plan (plan: [{ step, status }]), not just Claude TodoWrite, so --active --json (ActiveSession.todos) and the Factory Floor show live plan progress for Codex sessions too. TodoItem/TodoProgress moved from lib/session/state.ts to lib/session/types.ts (re-exported from state.ts) so SessionMeta can carry todos without 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.cpSync that 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-surfaces consent like the other agents. Source: apps/cli/src/lib/plugins.ts (stripEscapingSymlinks, installHermesPlugin, syncPluginToVersion), apps/cli/src/lib/plugin-marketplace.ts (copyPluginToMarketplace).

  • agents serve now rejects non-loopback Host headers (Security, RUSH-1766). The read-only viewer binds 127.0.0.1, but binding alone didn't stop DNS-rebinding: a remote page could point a hostname at 127.0.0.1 and drive the victim's browser to GET /api/state, exfiltrating uncommitted git diff HEAD of every worktree plus routine/cloud config. The server now serves only requests whose Host header is loopback (localhost/127.0.0.1/[::1], any port); a missing Host (raw non-browser client) is still allowed, and the authenticated --control server 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 daily hold is now reliable, configurable, and diagnosable. Three changes to the secrets-agent so a daily-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 new secrets.agent.holdMs key in agents.yaml caps how long an unlocked/auto-cached bundle is held before the next re-prompt (default 7 days; e.g. 86400000 for 24h), clamped to [1m, 30d] and applied consistently across the value read-path, the secrets list metadata cache, and unlock. (3) Diagnostic — agents secrets status now shows the hold window, a version-skew warning (a broker on an older build gets torn down on agents-cli-update, wiping held bundles — the top "why did daily re-prompt" cause), and clear held-vs-prompts-once guidance. Source: apps/cli/src/lib/secrets/agent.ts (secretsHoldMs/clampHoldMs, agentReachableSync, load-truthful runAgentLoadFromStdin), 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 export in a pipe) now resolves broker-only and fails with an actionable "run agents 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 by isHeadlessSecretsContext() (macOS-keychain-only; false off-darwin) across every readAndResolveBundleEnv call 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.md tools: list becomes Claude's --tools allowlist, which restricts the available built-ins. A workflow that ships a subagents/ dir — whose files agents run <workflow> copies into the shared agents dir specifically so the Task tool can dispatch them — but whose tools: omits Task had 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. the doc-gaps / blog-engine / iterate-until-good routines, which showed "failed" on schedule). agents run <workflow> now keeps Task in the restricted tool set whenever the run installs ≥1 dispatchable subagent, so a tools: list that forgets Task can'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

  • teams skill documents the fleet-comms surface (RUSH-1739). The Monitoring section now points teammates-of-teams at agents feed (what agents need from you), agents mailboxes / --watch / --graph / --between (what agents say to each other), and the agents message / agents teams message reply path — so an operator running a team can see and answer the whole conversation. Source: skills/teams/SKILL.md.
  • agents feed reskin to shared fleet-comms visual language (RUSH-1738). Presentation only: amber they need you masthead, shared GLYPH ask/answered markers (▲ / ✓), and ↳ ag message <id> "…" reply hints — same product face as agents mailboxes. Grouping, fan-out, policy, and JSON contracts are unchanged. Source: apps/cli/src/commands/feed.ts.
  • agents mailboxes grows into the fleet-comms surface (RUSH-1737). The overview now opens with the shared fleet comms masthead (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/-f streams cross-box messages live (HH:MM:SS from ─→ toLabel text, NDJSON with --json, clean Ctrl-C via an AbortController, --since backfills the window first, and mail addressed to the watching agent's own AGENTS_MAILBOX_DIR box renders as ▲ you so an orchestrator sees its replies); --between <a> <b> reads one relationship as a chronological thread in both directions under an a ⇄ b · N messages · span header; --graph renders who-talks-to-whom from └─▶ to ···· count adjacency, busiest first; --from/--to/--since filter the overview recency log, the watch stream, and the graph. The <id> detail view, --limit, and the mailbox alias 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 accepts command: <shell> as a third execution mode alongside agent: and workflow: (exactly one is required). A command routine runs the shell string directly via /bin/sh -c (cmd /c on Windows) in the real environment (no sandbox overlay — it can npm i -g / git pull), honoring timeout with the same SIGTERM→SIGKILL kill the agent path uses, and writes the identical run record (meta.json + stdout.log, status from exit code) so agents routines list/runs, overdue tracking, and device scoping are unchanged. agents routines add gains --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 mailboxes and agents 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 X runs the whole command on the remote box over SSH, so agents view executes on Linux there. Claude usage needs a live OAuth-token fetch, but loadClaudeOauth read the token only from the OS keychain — which on macOS falls through to /usr/bin/security and 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.json instead). The token load now falls back to that file when the keychain has no item — the same keychain-then-.credentials.json order readClaudeCredentialsBlob (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 under agents message / agents feed / agents teams message, but until now it had no inspection surface — agents mailboxes failed with unknown 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; --json for machine output, -n/--limit bounds the overview log; mailbox is an alias. Adds listBoxes() (enumerate boxes, consistent with the GC's validity contract) and readBox() (read all buckets, tagged by state, non-destructive — unlike peek, includes consumed/) 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 usage now reports live usage for Droid and Kimi, matching agents view. Both agents already render live usage bars in agents view — Droid via GET https://api.factory.ai/api/billing/limits (decrypted from ~/.factory/auth.v2.file; 5-hour/weekly/monthly rolling windows), Kimi via its /usages API — but the standalone agents usage command still marked them "does not publish usage data" because its supported-agent set had drifted from the live sources. agents usage droid / agents usage kimi now 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 list now 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). --full adds per-device core count and free/total memory; --no-stats restores the instant registry-only view; --json stays 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 export bundles selected sessions into a portable, self-describing archive and agents sessions import restores 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-redact to keep them). Selection reuses the agents sessions flags (--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 in agents sessions tagged with that machine and never overwrites your own local sessions; dedup is byte-exact (--overwrite to replace conflicts, --dry-run to preview). Multi-device transfer rides the existing SSH transport — agents sessions import --from-host <h> (and export --host <h>) run the export on the peer and stream the bundle back, equivalent to agents 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 in apps/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-tool tasks/*.json, Grok's <uuid>/events.jsonl — only a single file survived and the actual conversation was never synced. SyncAgentSpec gains dirShaped/exts/fileFilter/mergeableExts; listLocalTranscripts now 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's lastTs is 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 via gh:phnx-labs/.agents-system, which every install pulls at agents 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 with enabled: false disables it. The daemon (which loads with no cwd) sees user + system routines; writeJob still only ever writes to the user layer, so built-ins are never mutated in place. This is what lets a routine like check-updates ship 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 / --device work 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 raw unknown option. Remote routing now covers repos/repo, status/inspect groups, config/resource groups (plugins, skills, hooks, sync, …), teams, routines, and more via the central maybeRunOnHost allowlist. 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 of unknown 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 in subagents.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 single SUBAGENT_TARGETS table (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 the subagents capability gate — the writer, detector, and subagents.ts need no new arm, and a test pins Object.keys(SUBAGENT_TARGETS) to capableAgents('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 every subagents.ts function, so its subagents could not be listed, pruned, or removed), and copilot/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/github and /hooks/linear with raw-body HMAC verification, Linear timestamp checks, duplicate delivery suppression, and rate limiting. agents funnel status/up wraps 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.md covering the single-writer prefix layout, manifest + mirror model, CRDT convergence, client-side AES-256-GCM encryption, the opt-in beta gate, and the r2.backups credential 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 setup and agents sessions sync --setup (RUSH-1468). Joining a machine to the cross-machine session-sync fabric no longer requires hand-running four agents secrets add r2.backups … commands. A new interactive step mints the r2.backups bundle (R2 account/bucket/access-key/secret + a generated R2_SYNC_ENC_KEY), probes read+write connectivity with a throwaway object, and opts the machine into the session-sync beta 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 setup offers it opt-in (default No, never blocks setup); agents sessions sync --setup runs 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 fleet alias + fleet-wide rollout (RUSH-1632). fleet is an alias for devices. New subcommands update [version] and run <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> (alias kill) terminates a detached host run from the origin machine (RUSH-1360). Sends SIGTERM to the remote process group, writes exit 143 only when a live group was signaled (or no .exit existed), and keeps the remote log for agents 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 gcMailbox only incremented messagesDroppedExpired without moving the file to consumed/, so agents feed --dispatch could report drops while leaving expired messages in inbox//processing/. GC now reuses sweepExpired (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). agy uses 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-token may be absent even when the user is signed in. After the file check, getAccountInfo now probes secret-tool lookup service gemini username antigravity (exit 0 = present; stdout discarded), mirroring the macOS security find-generic-password probe from #506. Missing secret-tool, locked collections, and timeouts all read as signed out. Opt out with AGENTS_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 sync now 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 new R2_SYNC_ENC_KEY in the r2.backups bundle — 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 new R2_ENDPOINT override 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 sync now includes these four agents in its upload/download matrix. SyncAgentSpec gains an optional ext field so agents with non-.jsonl transcript files (e.g., Kimi state.json) are walked correctly. Droid .jsonl rollouts, Grok events.jsonl streams, and Kimi state.json metadata files round-trip through the R2 mirror; OpenCode is slotted in SYNC_AGENTS but 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 repos is canonical (repo alias); push/pull echo the resolved target; push no longer no-ops when clean-but-ahead; pull rebases on diverge (RUSH-1454). Help now prints Usage: agents repos …. Push/pull report user (~/.agents → origin/main): … instead of the bare alias. commitAndPush still git pushes when the tree is clean but local is ahead of origin (previously returned success without pushing). pullRepo uses git pull --rebase so 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-creds provisions runtime credentials on a persistent host (RUSH-1608). Reuses the --lease credential path (resolveClaudeCredentialsBlob + the ~/.claude/.credentials.json bootstrap) 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/, and sessions/<task>/ (screenshots, PDFs, recordings). Previously downloads had no configured destination — the CLI only set the download dir when the agent explicitly ran browser 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's downloads/ dir browser-global at connect time (both fresh launch and every attach path), so downloads always land somewhere agents-cli controls; browser download --path becomes an optional override (omit it to use the profile default) and reports the resolved path. Screenshots/PDFs/recordings moved from the old GLOBAL browser/sessions/<task>/ root to the per-profile browser/<profile>/sessions/<task>/, with a one-shot migration that folds existing captures into the owning profile (attributed via each profile's tasks.json; unattributable captures go to a _legacy bucket). New agents browser sessions [--profile <name>] [--open latest|<file>] [--json] lists a profile's captures + downloads, aliased as agents 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). agents now syncs slash commands to Goose as recipe YAML files under ~/.config/goose/commands/<name>.yaml, each registered in ~/.config/goose/config.yaml under a slash_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 other config.yaml key (mcp_servers, extensions, …) and other slash_commands entries, and removal soft-deletes the recipe + unregisters the entry. Flip Goose's commands capability and add goose branches to install/list/match/remove, the staleness commands writer, and doctor-diff, backed by a new goose-commands.ts module + a markdownToGooseRecipe converter. 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.enabled to the exec-surface trust verdict on every sync. An ordinary un-flagged background re-sync computes enable=false for a plugin with hooks/tools, so it stripped that plugin from the ~/.hermes/config.yaml allowlist — 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). agents now 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). transformSubagentForGoose emits the same recipe schema agents-cli already uses for Goose workflow recipes (version/title/description/instructions/prompt, plus optional settings.goose_model). Flip Goose's subagents capability 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). agents now syncs slash commands to ForgeCode as Markdown files under ~/.forge/commands/<name>.md (previously ForgeCode had commands: false and received commands only as skills), and named subagents as Markdown-with-frontmatter definitions under ~/.forge/agents/<name>.md (same color-less shape as Droid/Copilot/Cursor, so transformSubagentForForge aliases transformSubagentForDroid). Flip ForgeCode's commands/subagents capabilities, set commandsDir, 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.json tools.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 absolute tools.allow list is never touched, and all other keys (mcp, exec, agents, …) are preserved on read-modify-write. Flip OpenClaw's allowlist: true, add convertToOpenClawFormat + the openclaw branch in applyPermissionsToVersion, 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 the color field, gated at >= 2026.1.22 (cursor-agent's CalVer build tag for Cursor 2.4). Flip Cursor's subagents, add transformSubagentForCursor (alias of transformSubagentForDroid), 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). agents now registers central hooks into Hermes Agent's ~/.hermes/config.yaml under a hooks: block (YAML, ≥ 0.11.0). The registrar read-modify-writes that shared config so sibling keys like mcp_servers survive, 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 view stops mislabeling a logged-in install as "not signed in." getAccountInfo had no opencode case, so it fell through to signedIn: false and every row printed "(not signed in — run opencode to log in)" even with a live login. It now reads OpenCode's auth.json ($XDG_DATA_HOME/opencode/auth.json, defaulting to ~/.local/share/opencode/auth.json on every platform — xdg-basedir does not special-case macOS), validates each provider entry against its oauth/api/wellknown credential 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). agents now syncs workflows to Antigravity as markdown files with the required description frontmatter plus an agents_workflow ownership marker, invocable as /<name> slash commands. Antigravity workflows are the one non-version-isolated target: agy scans a single shared, HOME-global ~/.gemini/config/global_workflows/ at startup (a real home directory, never symlinked per version — verified via strace of agy), 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 auto and ACP skip semantics. The README and bundled run skill now distinguish Kimi's interactive --auto from its already-auto-approved headless -p path, document Droid's native --auto high, and explain that ACP skip prefers allow_always but 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.md with the >= 1.0.16 version gate enforced during sync. Kimi receives workflows as managed type: flow skills under .kimi-code/skills/<name>/SKILL.md, using the canonical slug as the flow name and an agents_workflow marker 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 main advances. A registry/auth failure after the release PR merged previously stranded that version because the catch-up guard required the release merge to remain current main. 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/*.md from 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.json as tools.core / tools.exclude entries with per-command ShellTool(...) patterns; non-Bash canonical permissions remain unsupported by Gemini's native tool grammar and are skipped. Flip allowlist: true, register the permission writer/detector through the capability table, and replace the dormant legacy tools.allowed serializer. 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 --host branch stripped @version and ignored --strategy, --effort, --add-dir, --json, --verbose, --timeout, --yes, and --acp, so the remote host applied its own defaults. The local CLI now parses agent@version verbatim, normalizes --strategy/--balanced, makes --add-dir paths remote-portable, and forwards all of these flags to the remote agents run invocation. --add-dir portability uses the same ~/$HOME re-rooting that --cwd already 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 ~/.agents stays clean and agents repo pull can sync routines across the fleet. writeJob previously re-emitted the entire document via yaml.stringify on every mutation (pause/resume, routines devices --set, add), restyling untouched scalars — unquoting schedule, re-wrapping the folded prompt block — which left the git-backed user repo perpetually dirty. That made cross-device agents repo pull refuse ("uncommitted changes"), so a devices: pin set on one machine never reached the others and Devices: all routines kept firing on every box. A new serializeJob edits 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 --json now carries live plan progress (RUSH-1380). The state engine parses the latest TodoWrite off the transcript tail into ActiveSession.todos ({ items: [{ content, status, activeForm? }], done, total, activeForm }), and the live preview verb reads Plan 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 --host name, not the resolved user@ip SSH 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 to bash -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). Flip skills: { 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). Flip skills: { 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). Flip allowlist: true since 0.57.5, add convertToDroidFormat, 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). Flip allowlist: true since 0.128.0, add convertToCodexFormat, 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 is opencode.jsonc at 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 main no longer counts as release validation by itself: release.sh resolves the merged release/v<version> PR, requires its merge commit to be current main, fetches the PR head that ran CI, requires that head tree to equal current main, 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's subagents: { since: '1.23.0' }, add transformSubagentForKiro, 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 view no 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/deny with Shell/Read/Write/WebFetch/Mcp). Flip allowlist: true, add convertToCursorFormat (Bash→Shell), and write via applyPermissionsToVersion + 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/view now surfaces synced Copilot agents and agents subagents remove soft-deletes their .agent.md files to trash — both previously skipped copilot entirely. 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-O capture 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-O already 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 existing linear 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 --json now 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 into attachments so 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-agent launchd 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 new retireLegacySecretsAgentService() and relies on the daemon (Path 0), with a one-off detached broker as the only fallback. The upgrade migration (scripts/postinstall.js → healLongRunningProcesses) now launchctl 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 start is now a thin alias that brings the daemon up (and waits for the broker to answer); agents secrets stop locks all bundles and retires any leftover legacy service while leaving the always-on daemon running; agents secrets status reports 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; removed installSecretsAgentService/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 bundled run skill now discourage skip, list its exact direct-exec per-harness flag mappings and ACP allow_always behavior, replace an older recommendation of unsafe full for ordinary writes, and distinguish Codex auto (sandboxed edit, which can still prompt) from Codex skip (--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.yaml as 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 --reveal no longer leaves a 60-second SSH control master behind. The interactive -tt reveal path now opts out of default SSH multiplexing (multiplex: false), matching the transport guidance for one-shot commands that must not keep a ControlPersist socket 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 --project shorthand jumps to a project by name). Previously --cwd was silently dropped for --host runs — only the separate --remote-cwd flag worked — so agents run claude --host s1 --cwd ~/src/foo landed in the remote login-shell's default directory with no warning. --cwd is 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 $HOME so it resolves correctly across machines with different home paths (/Users/me → /home/me). --remote-cwd remains 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 in agents.yaml, or set/shown with agents defaults project-root [path]; --project foo@fix targets the fix git worktree. Works for local and --host runs. Verified end-to-end: agents run claude --host yosemite-s1 --project agents-cli runs the remote agent with pwd = /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/--cwd host 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/bin on PATH, and launched the CLI entry bare when it was an extension-less shim or a bin/agents → dist/index.js symlink (an extension check on the link name missed it). The moment that exact nvm patch was upgraded away, the shim's #!/usr/bin/env node shebang 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. getDaemonLaunch now detects Node-script entries by resolving symlinks and sniffing the shebang (not just the .js/.cjs/.mjs extension), so it pins them to process.execPath; and the generated PATH now leads with path.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 itself process.argv[1], so its daemon self-heal could stamp node scripts/postinstall.js daemon _run into 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 message resolves 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 to running so teams status tracks it live. teams message is 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 to teams start. Works for every harness — the resume delegates to agents run --resume, inheriting native resume for Claude/Codex and the universal /continue replay 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 on teams stop's long-standing "can be restarted later" promise, which no code implemented. Source: apps/cli/src/commands/teams.ts (message/resume subcommands, decideTeamMessageRoute), apps/cli/src/lib/teams/agents.ts (AgentManager.resumeTeammate, resume-aware buildRunArgv/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 new startHostedBroker() before the scheduler and the heavy browser/session-sync services, so agents secrets resolves within ms of daemon start. It serves the same socket + wire protocol as the standalone broker (no PROTOCOL_VERSION bump — agentGetSync/agentPing/agentAutoLoadSync are unchanged), but is daemon-safe: no pid-guard, no process.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 standalone com.phnx-labs.agents-secrets-agent launchd 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 gated launchctl bootout migration) and child-spawning the heavy services are the follow-on (#417). Source: apps/cli/src/lib/secrets/agent.ts (startHostedBroker, ensureAgentRunning Path 0, agentPing exported), apps/cli/src/lib/daemon.ts (runDaemon broker host + shutdown).
  • Clarified agents secrets list POLICY 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 consistent policy · state form: daily, daily · held 7d, always · prompt, and never · 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/agents entry as existing at process.argv[1], while the real physical executable lives at process.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, and agents profiles check work again on the signed standalone binary (regression from #315). When agents resolves 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 executable process.argv[1] is the virtual entry /$bunfs/root/agents, so the child died with unknown 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 shared getAgentsInvocation(subArgs) (apps/cli/src/lib/daemon.ts) resolves the real on-disk binary — mapping the /$bunfs/root/… virtual path to process.execPath, running a .js entry under node, and a native binary directly — and teams/agents.ts, commands/message.ts, and commands/profiles.ts route through it. Verified end-to-end: a teammate spawned by the freshly-compiled binary runs to completed with no $bunfs error. 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 status distinguishes running, wedged, and stopped, 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 stale running records; 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 profiles now ships open-claude and claude-spark for Claude Code through OpenRouter, plus opencode, opencode-spark, and opencode-qwen presets 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 run tmux session reliably closes just that split (de-flakes CI #965). The guarded pane-died hook's else-branch was a bare kill-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 flaky session.test.ts pane-died tests reproduced in CI). An intermediate external tmux -S <socket> self-client still raced the server under Linux load. The else-branch now runs run-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 introduced run-shell -C. AGENT_HOOK_SCHEMA bumps 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 sync pins the login user on Windows too. os.userInfo().username returns COMPUTER\user / DOMAIN\user on 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. sanitizeLoginUser now strips the domain prefix to the bare ssh account before the guard. Also folds the duplicate user@host splitter (parseTarget in ssh.ts) into the canonical splitUserHost so 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) rendered ACTIVE · 1 running. The helper now feeds triage + ACTIVE from agents 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 on node:util.styleText) when the CLI was installed under Node 22/24. Native agents launchers 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. A devices: [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 list devices: [yosemite-s0] replaces the legacy singular device: pin — v12 migration converts any existing device: X YAML automatically to devices: [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 --host hint. routines add --devices yosemite-s0,mac-mini sets the list at creation (validated against the registered fleet); routines devices <name> opens a preselected multi-select picker; --set <csv> and --clear update it non-interactively and are mutually exclusive. routines list gains a Devices column; --json gains devices array and runsHere. --host <device> (alias: --device) routes any routines subcommand 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 auto instead of plan (RUSH-1595). A routine created without an explicit mode now runs under the smart classifier (auto) rather than read-only plan, so unattended jobs can create PRs, write files, and run tests end-to-end without every user opting in — auto maps 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 at plan). Opt down to mode: plan for read-only monitoring/reporting. JOB_DEFAULTS.mode, the agents routines add --mode flag default, and the file-add default all move to auto; writeJob now omits mode when it equals auto. 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 with agents search/agents install. Publish walks a git repo's skills/ directory, records a sha256 of every SKILL.md, and writes a flat skills-index.json (SkillIndexDocument shape) at the repo root, then commits + pushes it and prints the raw.githubusercontent.com URL plus the exact agents 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 existing fetchSkillIndex/searchSkillRegistries path. Targets your ~/.agents repo by default or an extra repo via --repo <alias> (--dry-run previews without pushing). Each index entry carries sha256, threaded through SkillEntry/normalizeSkillEntry, and agents install now verifies the freshly cloned SKILL.md against 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 (publish subcommand + 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 ssh now accepts the full target grammar the fan-out already used — a registered name, a user@device (same device, login user overridden, still dialed via its Tailscale route rather than raw LAN DNS), and an ad-hoc user@host/host literal — instead of only an exact device name (agents ssh muqsit@mac-mini no longer errors "Unknown device"). A bare unregistered alias still reports "Unknown device". sessions --host user@device now resolves the host part through the registry too, so it stops silently diverging onto the non-Tailscale route. New resolveDeviceTarget; resolveSshTarget shares one host-part matcher. Source: apps/cli/src/lib/devices/resolve-target.ts, apps/cli/src/commands/ssh.ts.
  • agents sessions --host searches 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, so sessions --host <box> read as empty (No sessions found for /home/<user>) even when the box's index was full. --host now defaults to whole-index (--all) scope; an explicit path query / --project / --since / --agent filter 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 sync pins 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>@latest resolves to a concrete version before installing (no install race). latest (like oldest) is now resolved via npm view up front and installed as a pinned spec directly into versions/<agent>/<version>/. Previously latest installed into a shared, well-known versions/<agent>/latest/ scratch dir and was renamed to the real version only after npm finished — so a concurrent agents view reconcile (reconcileStaleLatestForAgent) or a second latest install could rename that dir out from under npm mid-extraction, corrupting the install with ENOENT on the seeded package.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.json and only deletes those; user-authored *.md under the agent memory dir survive. Source: apps/cli/src/lib/memory.ts.

  • Feed high-consequence authz uses the canonical operator registry (RUSH-1618). recordAnswer no longer looks for operators.yaml under the feed root; it resolves operators from ~/.agents/ via loadOperators(). 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#N so two repos' PR #10 no longer collide under pr:#10. Source: apps/cli/src/lib/feed-outcome.ts.

  • Urgent OpenClaw notifications use --target and --message (RUSH-1620). openclaw message send requires a destination and the --message flag (not --text); without --target the 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_ID must match the claimed id (and the id must be in operators.yaml). Source: apps/cli/src/lib/operator.ts, apps/cli/src/commands/message.ts.

  • Hermes and ForgeCode are first-class install targets (RUSH-559). AgentId now includes hermes and forge, with resource capability metadata for skills, rules, and MCP. MCP sync writes Hermes mcp_servers YAML in ~/.hermes/config.yaml and ForgeCode mcpServers JSON 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 idle status (RUSH-601). Provider-side stopped states such as Rush idle/paused/needs_review, Codex paused/needs_review, and Antigravity idle/paused normalize to canonical idle; stream output, agents sessions, and agents cloud list/status render 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/.js files (RUSH-1617). Drop nested and .mjs/.cjs installs 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.log and rate-limit detection uses that file alone; prior attempts still append into stdout.log for 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|matcher instead 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_MAP so sync no longer installs dead entries. Source: apps/cli/src/lib/hooks.ts.

  • Mailbox delivery receipts are monotonic (RUSH-1614). recordMessageReceipt no longer lets a late queued write overwrite an already-recorded consumed/continued when 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.rateLimited flows through remote fan-out into Factory's FloorAgent.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 --v3 so 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.base now includes --v3 so agents run kiro opts 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 feed reports a digest (N stalls auto-resolved by policy); --all shows suppressed items; --json stamps each block with its ask classification. Agent-tagged blockClass: decision is 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 message no 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 via agents 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 frontmatter mode: subagent. Flip subagents: true, add transformSubagentForOpenCode, wire writer/detector/list/diff/remove. Source: apps/cli/src/lib/agents.ts, apps/cli/src/lib/subagents.ts. (RUSH-1386)

  • agents feed groups 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; --flat restores the per-agent list; --json stamps each block with its outcome ref. 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 (menubarDensity in UserDefaults, MENUBAR_DENSITY env 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". Session gained title/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.jsonc under permission (bash patterns etc.; present since ~1.1.1). Flip allowlist: { since: '1.1.1' } so the existing convertToOpenCodeFormat / 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 .md files under ~/.grok/agents/ (docs: user-guide/16-subagents.md). Flip subagents: 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/*.yaml with a sibling *.system.md referenced via system_prompt_path (Kimi has no inline system_prompt field) and a managed parent _agents-cli.yaml that declares agent.subagents for --agent-file. Flip subagents: true, add transformSubagentForKimi / 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 grok from registerHooksToSettings, 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 carry matcher so 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-O still 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 with agents 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,codex filters visible agents and AGENTS_QUICK_DISPATCH_AGENTS=claude,codex preselects 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-O quick-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 mapStatus in cloud/{rush,codex,antigravity}.ts — which had drifted (different vocabularies and defaults) — collapse into one exported normalizeProviderStatus(provider, wireStatus) in cloud/types.ts, with provider-specific defaults still explicit (rush default running, codex default running, antigravity undefined-safe default completed); factory's structurally-different mapResultStatus is left in place. getGitRoot moves to lib/git.ts and commands/worktree.ts now calls it instead of a private gitRoot copy; the two divergent isGitRepo variants (synchronous root-only in git.ts vs async worktree-correct in teams/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 agents binary — the primary fix for EDR (CrowdStrike Falcon) blocking the CLI. The resolved agents on 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 with bun 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 with notarytool, and ship it in the npm tarball at dist/bin/agents. postinstall points the alias shims and the ~/.local/bin/agents/ag links 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. A prepack gate (scripts/verify-cli-binary.sh) refuses to pack unless the binary matches its sign-run sha pin, embeds the release version, and (on macOS) passes codesign --verify with a Developer ID authority. Linux-driven releases build + sign the binary on the mac sign host via scripts/remote-sign-mac.sh. Intel Macs and non-mac platforms keep the JS entrypoint. (#315)

  • Codex multi-file apply_patch now surfaces every path. parseCodex used .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. Now applyPatchTargetPaths uses matchAll and emits one Edit event per file. Source: apps/cli/src/lib/session/parse.ts. (RUSH-1410)

  • memory is a first-class top-level resource (distinct from rules). agents memory list|add|remove|view|sync manages portable knowledge facts under ~/.agents/memory/ (project > user > system; MEMORY.md index + one <slug>.md per fact). The legacy agents memory → rules tombstone is gone. Capable agents (claude, codex, openclaw, grok) get facts fanned into version homes on syncResourcesToVersion / agents memory sync. Plugins can ship a memory/ dir (surfaced in pluginResourceGroups). Note: internal ResourceSelection.memory still means the composed rules file — rename to rules is 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 --json now carries tokPerSec, and agents sessions --roots --json emits the session-scan directories — one CLI contract the Factory extension consumes instead of re-implementing (issue #741). Every active row gained tokPerSec: live output-token throughput over a rolling 60s window from the transcript tail (Claude assistant output_tokens; Codex token_count output + reasoning; Gemini output + thoughts), absent when the session is idle or its format reports no usage. The new agents sessions --roots --json prints, per on-disk agent, the exact directories the CLI scans for transcripts (every version home + backup mirror), so an external watcher (the Factory Floor's fs.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 --active no longer attaches stale Codex transcripts to live processes. Codex session lookup now sorts by indexed last_activity and 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 as running. 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 --verbose diagnostics on stderr. The unified shim/shadow/PATH repair path no longer pollutes command stdout, including agents 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). Flip subagents: { since: '0.117.0' }, add transformSubagentForCodex, 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 bare claude/codex name under a sandbox HOME, which could surface as agents: no version of claude configured even when installs existed, and never walked past a credit-exhausted default. The runner now resolves a healthy install via the configured run strategy (default balanced), pins the absolute binary, injects per-version config dirs + the daemon's CLAUDE_CODE_OAUTH_TOKEN into sandboxed spawns, and on foreground agents routines run re-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). Flip plugins: true and 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). Flip plugins: true, set pluginManifestDir: '.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). Flip plugins: true, install modules from a plugin's opencode/ or plugins/ dir (or root) into the version home, and track install/remove via isPluginSynced / 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 into hooks.json while 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. Flip supportsHooks and gate hooks: { 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. Flip supportsHooks and gate hooks: { since: '0.10.0' }, map canonical events to Kiro PascalCase triggers, and write a single managed agents-cli-hooks.json on 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 declared hooks: false so 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 --lease now 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 (crabboxWarmup was a blocking spawnSync, 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; createLeaseOutputRouter splits 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. crabboxWarmup is now async so the event loop stays free to animate. The spinner is a purpose-built createSpinner (NOT ora): ora hooks stream.write and re-renders on every external write while spinning, which ballooned to multi-GB output when a lease streamed past a live spinner — createSpinner writes exactly one short line per fixed tick and nowhere else (a unit test asserts 100k update() 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, clean LOGIN_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 matcher and no longer run twice per event. The Grok hook registrar (registerHooksForGrok) dropped the manifest matcher when writing ~/.grok/hooks/, so a matcher-scoped hook fired on every tool call — the plan-presentation hook (meant for ExitPlanMode only) hard-blocked whole Grok sessions (exit 2 = explicit deny). It also double-registered every hook into BOTH hooks.json AND a per-event file (pretooluse.json, …); Grok merges all of ~/.grok/hooks/*.json, so each hook ran twice. Now the registrar emits matcher only 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 is exit_plan_mode), and writes a single hooks.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 message ties 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: queued when agents message enqueues, consumed when the mailbox drain archives the message, and continued once the agent continues past the block. A human answer typed directly in the terminal is reconciled via the UserPromptSubmit hook, 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 (blockId on MailboxMessage, 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, and costOfDelay; the feed-publish hook captures these from the agent's AskUserQuestion tool_input. High-consequence answers require a verified operator id known to ~/.agents/operators.yaml (agents message --as <operator>). agents feed --dispatch evaluates 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 (costOfDelay at 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). enqueue accepts an optional ttlSeconds; expired messages are archived to consumed/ with dropped: expired instead of being returned by drain/peek. agents feed --dispatch runs a liveness sweep against getActiveSessions(): boxes whose owning agent is no longer alive are treated as dead, their pending messages are archived with dropped: dead, and any feed block tied to that mailbox is removed so the operator never answers a ghost. Old consumed/ 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 (--dispatch liveness sweep).

  • Keychain service names are no longer silently enumerable — items are stored under opaque HMAC-hashed names (macOS). The helper's list never decrypts and never prompts (that's what keeps agents secrets list snappy), 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. Every agents-cli.* item now lives under an opaque name (agents-cli.h.<ns>.m for 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, so secrets exec/run --secrets still 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 new agents secrets rekey (--status to 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 a never-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 reachable bin/Agents CLI.app matched the pin. Rebuilt the helper from current source on the release machine (universal, signed, notarized: Gatekeeper Notarized 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. ensureAgentRunning tore down a reachable broker whenever the broker's running version differed from the caller's on-disk version — unconditionally, via launchctl kickstart -k, wiping every unlocked bundle. On a machine where installed versions churn (dev builds stamp a fresh 0.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 — rotate no longer serves the old secret for up to 7 days. Only secrets policy invalidated the secrets-agent after a write; add, rotate, remove, rename, delete, and import updated 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. writeBundle and deleteBundle now 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 honors AGENTS_SECRETS_NO_AGENT plus the test-backend override so suites never evict a user's real unlocks (shouldEvictAfterBundleWrite). The policy command'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 import gains 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 new icloud. Bundles created in the pre-biometry era were synced via iCloud Keychain; the device-local cutover pinned every query to kSecAttrSynchronizable: false, which orphaned those items — visible in Keychain Access under iCloud, invisible to secrets list and migrate-acl. agents secrets import --from icloud discovers 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 --purge deletes 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 as reserved, not importable instead 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 feed surfaces every top-level agent block that is waiting on the user across the fleet. AskUserQuestion and waiting Notification hooks 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 bare agents feed merges local blocks with every registered online device in parallel, --host/--device scopes the fleet, --local skips SSH, and --json returns 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 in apps/cli/src/lib/exec.ts and apps/cli/src/lib/teams/agents.ts. (RUSH-1473)
  • Browser-profile credentials: account identity, secrets get <bundle> <KEY>, and browser type --secret for leak-free login. agents browser profiles logins now shows, per profile, the account signed into each live service (plaintext username from Chromium Login Data — never decrypts the encrypted password) and whether login creds are declared in the profile's secrets bundle (columns SERVICE | ACCOUNT | CREDS); profiles show gains a Logins: block. agents secrets get <bundle> <KEY> prints one resolved value from a bundle (arg-count overload of the existing raw get <item>; ungated like it, and the secrets.get audit 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 composing profiles 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 --secrets bundle is the credential store, keyed by the <PREFIX>_USERNAME/<PREFIX>_PASSWORD convention (per-service prefixes in AUTH_SIGNATURES); profiles create --secrets now warns if the bundle doesn't exist yet. Cookie-persistence-first remains the headline (the browser skill'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 audit queries events with filters (--module, --command, --event, --agent, --caller, --level, --since, --limit, --json) and --follow for live tailing; agents logs stats shows aggregate breakdowns by level, event type, module, and user; agents logs rotate prunes old numbered archives (--days, default 7). Events carry level (audit/warn/info/debug) and an environment-derived caller (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 as audit. At 10 MB the active file rotates losslessly through events.1.jsonl.gz, events.2.jsonl.gz, and so on; query() reads every archive transparently. New instrumentation in cloud.ts, factory.ts, teams.ts, secrets.ts, mcp.ts, and rotate.ts. Source: apps/cli/src/lib/events.ts, apps/cli/src/commands/logs.ts, instrumentation call sites. (RUSH-460)
  • agents run claude --lease now 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.json file branch. buildCredentialScript writes that blob to ~/.claude/.credentials.json (0600) via the same quoted-heredoc that carries every other cred (the box's ~/.claude is a symlink into the versioned home, so it lands exactly where the shim's CLAUDE_CONFIG_DIR reads 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 ran agents run claude and 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 bare agents browser start uses — so agents stop opening a logged-out Chrome. With no --profile, start used to auto-detect the first installed Chromium-family browser (Chrome first on macOS) and save it as default, ignoring a profile you'd actually logged into. Now start resolves in order: (1) your configured default, (2) an existing default profile, (3) auto-detect. The configured default ALSO re-points an explicit --profile default, so an agent that hardcodes default still 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/show mark it; set-default --unset reverts 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 browser now warns when a task opens a login-gated site on a logged-out profile — grounded in real session state. New apps/cli/src/lib/browser/login-detection.ts reads 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 trip node: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 like profile "default" has no linkedin.com session. logged in elsewhere: comet-local. try: --profile comet-local when the chosen profile is logged out; it never blocks or slows start. agents browser profiles logins shows 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_required forever (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 as idle, not waiting_input — so agents sessions --active and 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 pending ExitPlanMode (plan review) or AskUserQuestion still classifies as waiting_input at 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 browser or computer shim. Daemon launch resolution now maps installed sibling shims to the agents launcher and compiled shims to index.js before 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 --lease bootstraps 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's npm install -g @phnx-labs/agents-cli ran with no node/npm on the box and swallowed the failure with || true, so every run died deep in the script with agents: command not found (exit 127) and no hint why; (2) even with the CLI installed, a fresh install refuses agents run with "agents-cli is not set up" until agents setup has run; (3) teardown called crabbox stop --id <slug>, but crabbox's stop takes a positional target (unlike status/run/ssh) and died with flag provided but not defined: -id — silently, because crabboxStop is best-effort — so every one-shot lease box was kept, billed, and left carrying the run's working data until someone noticed (Box … kept instead of destroyed). The bootstrap now: exports ~/.local/bin onto PATH, installs node user-level from the official latest-v22.x tarball when missing (arch-aware, satisfies engines.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 runs agents setup behind the same [ ! -d ~/.agents/.system ] first-run guard the hosts bootstrap uses; crabboxStop passes 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) with Box … kept; after the node/npm fix it reached agents-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 with Box <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 ~/.agents instead 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 ~/.agents is 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 pull just printed user: not a git repo, skipping and the machine silently fell out of config sync — no rules/, no agents sync of 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 .git in without deleting anything, materializes the tracked resources it was missing, and backs up any locally-modified tracked file (e.g. a machine-specific agents.yaml) to a sibling ~/.agents.pre-adopt-backup/ before overwriting it. Untracked runtime state (.cache/, .history/, .system/ — all gitignored) is never touched. Every subsequent agents repo pull / agents sync is plain (the remote is now origin). No new command; the URL is only needed the first time. SSH transport is preserved (a git@… 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 run now warns when a headless run leaves committed-but-unpushed work, instead of stranding it silently. A headless agents run in 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 an origin/* ref is not flagged) and prints a loud stderr warning naming the branch, the unpushed commits, and the exact git push / gh pr create commands. 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 by shouldWarnUnpushed(mode, interactive) so it stays silent for interactive runs (the human sees their shell) and read-only plan mode. 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 skip is yolo. --mode edit used 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 plan mapped to workspace-write (writable!) because the template predated Codex's read-only sandbox. 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 to codex --yolo). Same fix in routine jobs (runner.ts) and in headless codex 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; interactive codex resume now 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-dir is now forwarded to Codex (it was silently dropped). agents teams passes --add-dir ~/.agents so Codex teammates can run agents teams add, but buildExecCommand emitted --add-dir for Claude only — the grant never reached Codex, masked until now by edit mode's accidental sandbox bypass. Codex takes --add-dir natively (widens the workspace-write sandbox); it is now forwarded for fresh runs and skipped on resume (codex exec resume rejects 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 with too many arguments before 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 -- --yolo launches 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 "…" -- --yolo forwards --yolo and codex reports sandbox: 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/downloads whenever the installer runs with GROK_HOME set — via the shim, a correct agents add grok, or a grok self-update from within the shim. Both generated shims (the dispatcher in generateShimScript and the grok@<version> versioned alias in generateVersionedAliasScript) 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. getBinaryPath already checked the versioned home first, so agents view and the shims disagreed. Both shim blocks now check $VERSION_DIR/home/.grok/downloads first and fall back to the global $HOME/.grok/downloads for pre-fix installs, then the existing adopted-launcher/PATH last resort. Bumps SHIM_SCHEMA_VERSION 25→26 and VERSIONED_ALIAS_SCHEMA_VERSION 12→13 so existing on-disk grok shims regenerate. Supersedes the pre-monorepo #830. Source: apps/cli/src/lib/shims.ts (generateShimScript grok dispatcher block, generateVersionedAliasScript binaryResolution).

  • Fix: browser stop --host <windows> tree-kills the remote browser — relaunches never wedge on a stale SingletonLock. The kill script used Stop-Process on the CDP port owner only; orphaned Chromium child processes survived, kept the profile's SingletonLock held, and the next browser start --host against the same profile exited immediately as a second instance. The script now uses taskkill /PID <owner> /T /F to 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 WMI Win32_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/version probe hung forever and DevToolsActivePort was never written, so browser start --host failed 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 --host work. The CDP client rode the platform (undici) WebSocket, which enforces a non-configurable max decompressed message size; a Page.captureScreenshot response 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 the ws client (no permessage-deflate offer by default, explicit 256MB maxPayload); the local pipe transport is unchanged. Source: apps/cli/src/lib/browser/cdp.ts. (GitHub #561)

  • agents sessions <id> --json now 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 trailing ExitPlanMode tool call, but the plan markdown itself was dropped on the floor — forcing every consumer that wanted it (the Factory NEEDS-YOU panel via parsePlanFromClaudeJsonl, 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 surfaces state.plan alongside awaitingReason, the Claude scanner captures the plan text at scan time and persists it to sessions.db (schema v11, additive, rescan-on-migrate), it's exposed as plan on SessionMeta in every agents sessions --json row, and agents sessions <id> --json now emits { session: SessionMeta, events: SessionEvent[] } so the plan is one top-level output.session.plan read instead of a needle-in-haystack scan. Verified live against a real Claude session with an ExitPlanMode event: agents sessions 74464df7 --json prints 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 computer Windows parity: scoped screenshots, get-text --max-chars, status/reload --host, and honest --background/--require-frontmost handling (#548). Four params the CLI already sent were silently ignored by the Windows daemon. (1) Screenshots are now pid-scoped like macOS — screenshot --list enumerates the target pid's top-level windows (window_id is the Win32 HWND, the same id raise --window-id takes), the default capture crops to the pid's largest on-screen window, --window-id shoots one window, and --display captures 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-chars is honored (default stays 20k, ceiling 200k like macOS) — --max-chars 100 now returns exactly 100 chars. (3) status --host <device> and reload --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-frontmost is enforced on Windows — SendInput lands in the focused window, so type-text/key now report frontmost (feeding the existing CLI warning) and the flag hard-fails with not_frontmost when the target isn't foreground; --background is rejected with action_unsupported instead 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 never prompt-policy is now live — the signed keychain helper was rebuilt, re-notarized, and re-pinned. agents secrets create --policy never --i-understand stores bundle values with no biometry ACL (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, device-local, non-synchronizable) so headless automation can read them with zero Touch ID prompts. The Swift set-no-acl path 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 submission a2373c91-7fc2-4894-a801-b37c111597aa, status Accepted, stapled, Gatekeeper Notarized Developer ID), and scripts/Agents CLI.app.sha256 re-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, and termLink had 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 cloud task lists, agents sessions overflow, and session prompt/tool summaries previously showed ASCII ... or a bare .; (2) agents cloud relative timestamps switch from the long "5 minutes ago" form to the compact "5m ago" form already used by agents teams; (3) the agents teams picker 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 a visibleWidth regex missing its \x1b escape, so ANSI-colored cells were mis-measured and columns could misalign — the canonical visibleWidth strips the full SGR sequence. lib/events.ts's truncate (a distinct nullable, exported helper that truncates persisted event payloads) and the domain-specific statusColor copies (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.ts and consumers across apps/cli/src/commands/ and apps/cli/src/lib/. (GitHub #753 / RUSH-1515)

  • agents computer setup --host now works from a plain npm i -g install — the Windows helper exe downloads on demand from GitHub releases. The ~157MB computer-helper-win.exe never 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. On v* tags the computer-helper-win.yml workflow now builds the self-contained exe, smoke-tests it, and uploads it plus a .sha256 as GitHub release assets; setup --host resolves 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).

  • registerMcp HTTP 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 hardcoded agentId !== 'claude' && agentId !== 'codex' && agentId !== 'gemini' / agentId !== 'claude' checks in apps/cli/src/lib/agents.ts, bypassing the capabilities.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 the AgentConfig matrix — mcpHttp (Claude/Codex/Gemini today) and mcpHeaders (Claude only) — and both inline allowlists are replaced with supports(agentId, cap) calls. Pure refactor: capableAgents('mcpHttp') is ['claude','codex','gemini'] and capableAgents('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 declare matches: 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"), but shouldFire() (the evaluator in src/lib/hooks/match.ts) had zero runtime callers: the agent execs the registered command directly and nothing evaluated matches:, so any hook with a matches: block fired unconditionally. A hook that declares matches: (with or without cache:) 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 as cache:"skip"), a matching event runs it. Matches-only hooks (no cache:) get a gate-only pass-through shim; cached hooks apply the gate before the cache. The shim gate is a faithful port of shouldFire() (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 declares matches:, 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 a matches: { tool_name: Bash, tool_args_match: "rm -rf" } manifest and firing it: a Read event and a Bash+ls event were skipped, a Bash+rm -rf event ran the body. Source: apps/cli/src/lib/hooks/cache.ts (renderShim gate + 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-ssh spawns in the browser SSH driver (ensureRemoteBrowser, runSSHCommand) passed only -o BatchMode=yes, with no ConnectTimeout, 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 baseline SSH_OPTS from ssh-exec.ts (BatchMode + ConnectTimeout=10 + ServerAlive keepalive) rather than re-listing options — the same baseline sshExec and the -L tunnel already use. The options now also precede the target (matching sshExec); on macOS/BSD getopt an option placed after the target is swallowed into the remote command instead of applied. Verified against TEST-NET 203.0.113.1 (guaranteed unreachable): -o BatchMode=yes alone was still hanging at a 30s cap (en route to ~127s), while the SSH_OPTS set failed in 10.04s with connect 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 sqlite3 CLI 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 in agents sessions there. All three now read through the same runtime-aware node/bun Database wrapper 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 --json now carries the agent's actual decision, not a truncated status line. A session waiting on you used to collapse everything to a one-line preview — an AskUserQuestion became 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 structured question object ({ 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 assistant tail for context, and preview no 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 reports awaitingReason: question with 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's origin) says how each device gets the code, and teams 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 --device pin → single-device pool (whole team there) → multi-device pool (least-loaded auto-schedule) → local. Remote teammates dispatch over SSH via the existing agents devices/host machinery (a third teammate backend beside local and cloud), are monitored by offset-tailing the remote log + .exit sentinel, 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 logs show 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 --devices shows a cross-device agent-readiness matrix. agents doctor could already run on one remote machine via --host, but checking the whole fleet meant running the command once per box. --devices fans out agents teams doctor --json to 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 bootstraps PATH with 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 teams now honor your configured Codex model instead of silently defaulting to gpt-5.3-codex. Codex runs under a per-version CODEX_HOME, and your model preference (~/.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-level model, so Codex fell back to its built-in default — which a ChatGPT-tier account isn't entitled to use, so the run died with 400: The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account before doing any work, even though ag view reported Codex "signed in". When no explicit --model is passed, the model is now defaulted (for Codex) to the top-level model in your active ~/.codex/config.toml and 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 changed gpt-5.3-codex → gpt-5.5 and [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." installVersion runs npm 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-code is a required step: the package ships a ~500-byte stub at bin/claude.exe plus per-arch native binaries as optional deps, and its postinstall (node install.cjs) is what copies the correct ~231 MB native binary over the stub. Skipped, every launch died with Error: 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 raw ENOENT, 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-scripts install, so it never copied the binary either. installVersion now runs the first-party package's declared postinstall after the npm install (scoped to that one package — never the dependency tree, never claude-code's exit 1 prepare guard), best-effort, before the integrity gate. Because installVersion is the single choke point for agents add, config refresh, run-time heal, and the daemon's proactive heal, this also revives the repair path for the whole class. isMissingBinarySignature was 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), and claude.exe --version returns 2.1.186 (Claude Code) — with no manual install.cjs step. Source: apps/cli/src/lib/versions.ts (installVersion, isMissingBinarySignature).

1.20.49 — 2026-07-08

  • agents run --mode plan no 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 plan used to abort with does not support 'plan' mode — breaking multi-agent scripts that pass a uniform plan flag, and diverging from agents teams add (default mode edit). resolveMode now degrades unsupported plan to the agent's safest native mode (capabilities.modes[0], typically edit), matching the existing auto → edit degrade. The CLI prints a yellow warning when the user explicitly asked for plan (gray for the implicit default) so the elevation is never silent. skip still hard-fails when unsupported. Source: apps/cli/src/lib/exec.ts, apps/cli/src/commands/exec.ts.
  • agents cloud cancel now actually cancels paused runs. RushProvider.cancel() issued DELETE /api/v1/cloud-runs/{id}, which the backend doesn't implement — it 404s — so agents cloud cancel (and the Factory Floor's cancel affordance) silently failed on any run that wasn't actively running: queued, needs_review, and input_required runs 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 endpoint POST /api/v1/cloud-runs/{id}/cancel, which the backend implements and which cancels paused runs too. Verified live against api.prix.dev (the POST returned {"ok":true,"status":"cancelled"} and the stuck run transitioned needs_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-O bar 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-O bar. (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's URL: line, the helper parses it, and clicking the notification (or its Open button) opens the ticket in Linear (via an NSUserNotificationCenterDelegate; 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 find agents without 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 after agents upgrade. The startup self-heal now also re-points when the plist's baked AGENTS_ENTRY/AGENTS_NODE no longer match the install currently running agents (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 .app helpers a Linux runner can't build — bin/Agents CLI.app (the keychain helper: swiftc universal → codesign with entitlements + embedded provisioning profile → notarytool → staple) and bin/MenubarHelper.app (the menu-bar status item: swift build → codesign, no notarization) — which is the only reason publishing was macOS-pinned. New scripts/remote-sign-mac.sh (invoked automatically by release.sh when it runs on a non-macOS host and the signed apps are absent, or on any host with FORCE_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 (unlocks rush-signing.keychain-db, injects Apple notary creds via the apple.com secrets bundle), then pulls the signed bin/*.app back and re-verifies the keychain sha locally. The build script now copies the helpers into dist/ on a presence gate ([ -d 'bin/…' ]) instead of [ "$(uname)" = 'Darwin' ], so a Linux box that pulled the pre-signed bundles packages them, and prepack's sha gate uses shasum or sha256sum (whichever is present) so it works on Linux too. Override the sign host with SIGN_HOST and its checkout with SIGN_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 the shims self-heal check (daemon + interactive startup): (1) drift repair — an agent shim whose baked AGENTS_BIN names 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 with exit 127 or shadow the real package bin on PATH, are removed when their baked install is gone (pruneOrphanedCommandShim); user agents alias shims 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-V now 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 .accessory window 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.performKeyEquivalent now 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-keyring migrates 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/has now 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 at import-keyring; once a locked/1312 error is seen the store is marked unreachable so it stops re-probing a known-dead store. (2) NEW agents secrets import-keyring — the Linux/Windows analogue of the macOS migrate-acl/orphan sweep — enumerates agents-cli items in the native store and copies them into the encrypted file store (the durable, passwordless headless backend). Dry-run by default; --commit writes; existing file-store items are never overwritten; Windows enumeration is floored to the agents-cli. namespace since Credential Manager targets have no service scoping. macOS is unaffected (it has no file fallback and keeps migrate-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 gave agents run an 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 — verifyInstalledBinaryLaunches returned healthy on win32 unconditionally, 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 native claude.exe to claude.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 .cmd wrapper agents run actually execs (getBinaryPath + '.cmd', resolved via cmd.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 same ensureAgentRunnable machinery; a missing .cmd (a non-npm/global agent like droid.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 next agents run hits the ENOENT, not at spawn time (the run-time ensureAgentRunnable only fires once a run is already starting). Verified end-to-end on a real Windows host: renaming claude.exe to .old makes the .cmd probe emit is not recognized; restoring it returns 2.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-O opens 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 with Cmd-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 one AgentsCLI.dispatchTicketAgent call so a cloud pod is a later swap) that reads the screenshots, runs agents sessions to identify which repo/project this concerns, does a brief investigation for real context, and files the ticket via ~/.agents/skills/linear/scripts/linear create with an honest priority + a repo:<name> label — no preview step, the panel closes immediately and a notification reports the created RUSH-####. Focus is handled for a no-Dock .accessory app (NSApp.activate → makeKeyAndOrderFront → makeFirstResponder, with a borderless NSPanel overriding canBecomeKey; click-outside dismissal is armed only after the summon settles so the activation race can't self-dismiss the panel). The Cmd-Shift-V clip hotkey is unchanged — the Carbon hotkey manager now demultiplexes both chords by EventHotKeyID.id through one installed handler. Self-test: MENUBAR_ISSUE_TEST=1 MenubarHelper exercises screenshot selection, ticket-id parsing, and the meta-prompt contract; MENUBAR_PROMPT_PREVIEW=1 renders 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 a process.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 single HealCheck registry (lib/self-heal/) with one runner (runSelfHeal) driven by two front doors — the daemon (on its existing ~30s-after-start + ~6h safe-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), and resources (the existing heal() engine, wrapped unchanged). The daemon's heal cycle now runs all four in safe mode (low-risk fixes silently; risky ones reported), replacing the resource-only heal() 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 (isShimCurrent exported).

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 --host runs 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, runs agents run <agent> on the host, and lets the remote machine's agents start 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 so agents sessions can surface and resolve the remote run. --no-follow is rejected for interactive host runs (it is meaningless for an attached TTY), and --mode, --model, --name, passthrough args after --, and --raw/--no-tmux are 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 --host now works against Windows targets, and a new agents secrets unlock --host unlocks 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 import now accepts --from - (read the .env from stdin, replacing the POSIX-only /dev/stdin), and the push is platform-aware — bash -lc on POSIX, powershell -EncodedCommand on Windows, with the target's OS taken from the device registry. Because the npm agents.ps1 shim does not forward ssh-piped stdin to the underlying node process (a raw --from - read hangs), the Windows keychain push bridges the piped .env through 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-mini imported all 13 keys. Separately, agents secrets unlock --host <machine> <bundle> runs the unlock ON the remote over ssh -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 --host is 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. --name seeds the session label instead of a parallel column. Shipping agents run --name (1.20.43) as a separate immutable name column created two look-alike fields — an unshown, frozen name and the shown, searchable label — that both resolved agents sessions <ref> and forced tie-break bookkeeping nobody could keep straight. They unify into one field. --name is now the universal way to seed the label at launch — the same field an agent-generated title (Claude's /rename) later refines and agents sessions displays 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 --name seed, else the listing falls back to topic. So a Claude run's --name shows until Claude titles it (your seed, then refined); a non-Claude run keeps its --name as the label (it has no auto-title). The seeded name is now fuzzy-searchable in FTS (the old name column was not). agents hosts logs <name> is unchanged — it resolves against the host-task sidecar, not the session column. Schema v10 folds any existing name into label (where the label was empty), mirrors it into the FTS row, then drops the name column; the run-name sidecars re-seed every scan (seedLabelsFromNames), so no rescan is needed. Reworks the 1.20.43 --name design (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/start warns when a version-pinned teammate is on a throttled or signed-out account. The 1.20.43 balanced-default fix keeps bare teammates off rate-limited accounts (they route through bare agents run, which rotates), but a version-pinned ([email protected]) or profile teammate spawns agents 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) and agents teams start (per staged teammate, deduped by agent@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 the agents view badge 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); --force silences 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 logs command 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, and agents hosts logs / agents teams logs / agents routines logs each cat'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/--full for the raw log: agents logs <session> renders the same summary digest as agents 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> / --full for 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. renderSessionLog now takes a mode and defaults to 'summary'; agents sessions <id> was already summary-by-default and is unchanged. Regression-tested: tailLines truncation/elision math (hosts/logs.test.ts) and formatRunDuration human-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, so logs can't yet resolve them to the full renderSummary — 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-died hook on already-running agents run sessions. 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, unconditional detach-client hook 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 runs reconcileSessionHooks() ~20s after startup and every ~5 min: it walks the managed ag- 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-hook only, never a kill-pane or detach-client — so it is safe to run against sessions you're attached to; a per-session @ag_hook_schema marker 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 run self-heals a gutted install instead of crashing with ENOENT. 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's package.json lands, its vendor/<triple>/…/codex binary does not (an interrupted or concurrently-raced agents add into the same version dir). The CLI's wrapper require.resolves the platform package, finds the package.json, and sails straight past its own "missing optional dependency" guard into a spawn(binaryPath) that dies with a raw ENOENT. agents run now probes the version it's about to launch and, if the binary can't run, repairs it in place (a clean reinstall — the partial node_modules is 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 installing latest — only erroring if nothing can be made runnable. installVersion gained 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 interactive ag run <agent> wrapped the agent in tmux and the agent died the instant it spawned (e.g. a gutted install crashing with spawn … ENOENT, a bad flag, a startup crash), the pane-died hook detached the client before you could read anything — you got a bare [detached (from session …)] with zero indication of why. runInTmux now recaps the dead pane's last output (read from scrollback via capture-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-tmux on agents run. The interactive tmux wrapper (which gives %pane addressing + 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 --raw and AGENTS_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 at node_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. installVersion now 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 --version is 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.ts carried its own private redactSecrets (used by every log() write to logs.jsonl) that predated and diverged from the canonical redact.ts — it caught sk-, eyJ…, Bearer …, and a narrow NAME=value list, but not ghp_ (GitHub PAT), AKIA… (AWS access key), or npm_ (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 canonical redactSecrets in redact.ts, which covers all of those classes with a stronger quote-aware NAME=value pattern. The one pattern the daemon copy had and the canonical lacked — Bearer <token> — is added to redact.ts, so the shared redactor (also used by session-transcript export in session/render.ts) is now a strict superset. New redact.test.ts pins 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 doctor tells 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 the agents run self-heal above. (1) agents teams doctor lied — it reported installed: true whenever a shim file existed, never checking the real binary, so a stub or gutted-native install (the exact codex/kimi failure) showed "ready" and then ENOENT'd at spawn. checkCliAvailable now 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 as argv[0], which is not on PATH, so it died with spawn [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 baked AGENTS_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 whatever agents resolves to on PATH before erroring (SHIM_SCHEMA_VERSION → 25). Also drops a stale, npm-unrecoverable codex 0.116.0 pin from the repo's own agents.yaml so 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-follow tip 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 to agents logs, which dumps the raw, token-heavy transcript. --name is chosen at launch, agent-agnostic, and stored on the structures that already back these views: a first-class name column on sessions.db (schema v9, additive, no rescan) parallel to label — agents sessions <ref> resolves against both name and label; the HostTask sidecar (forwarded to the remote run, so agents hosts ps gains a NAME column and agents 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 via syncNames — the same idempotent pattern as /rename label sync. The name column is deliberately left out of the upsert ON CONFLICT … SET clause, so a discovery rescan can never null an existing name (regression-tested in db.names.test.ts). Omitting --name is a strict no-op: name stays unset and every id-based path is unchanged. The --no-follow dispatch tip now prints the real handle and steers to the compact agents sessions digest 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; balanced is now the default run strategy. Two coupled fixes. (1) A bare agents run <agent> — every new agent terminal the extension spawns, and every non-version-pinned agents teams add/start teammate, since both route through bare agents run — used to default to the available strategy, which prefers the pinned default version when it looks healthy. But "healthy" was judged by the router's getRoutingUsedPercent, 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 — while agents view showed it "rate-limited" (its badge, deriveUsageStatusFromSnapshot, counts the session window). The router and the badge disagreed. Now hasUsageAvailable shares the badge's exact signal: an account maxed on any blocking window (session or weekly) is ineligible and skipped by both available and balanced — 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 now balanced (was available): a bare run spreads load across all healthy accounts by remaining headroom instead of sticking to the pinned default. Override per-workspace with run.<agent>.strategy in agents.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 pins session.restore_on_startup: 1 ("continue where you left off") in the profile's Default/Preferences, which is the switch Chromium's cookie purge actually keys off — and pairs it with --no-startup-window so 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). Bare agents 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.ts carried its own copy of assertValidSshTarget that omitted the host.startsWith('-') guard every other SSH path enforces, so a bare flag like -l or -F/path — which passes the character allowlist — was handed straight to ssh as an argument (-oProxyCommand=…-class injection) before any connection. The duplicate validator (and its SSH_TARGET_RE) is deleted; runRemoteSessions now routes through the canonical assertValidSshTarget in ssh-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 run session kicked you out of tmux entirely. When you split the window of an interactive agent session (ag run claude) with Ctrl-b "/% and then exited 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: runInTmux installed a session-wide pane-died hook (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 through readAndResolveBundleEnv (the only place that emitted secrets.get), so they were invisible: secrets push (which reads the whole bundle to upload it — the most sensitive silent read), secrets view --reveal, the raw secrets get <item>, secrets set <item> (a raw write, no secrets.set), and the initiating side of secrets exec --host / run --secrets bundle@host (only the remote host logged it). Each now emits with a source telling you HOW it was read — keychain, agent (served from the unlocked broker), reveal, raw-item, sync-push, or remote (with the target host) — alongside the bundle, caller, keyCount, and OS-user/host/transport. The resolved value is never written to the log, only names and counts. All secrets.* events are now tagged module: 'secrets' so --module secrets actually 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 --active showed 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. findClaudeSessionFile fell back to the newest .jsonl in the cwd whenever a session's <id>.jsonl wasn't found, so every distinct session collapsed onto ONE file's preview/topic. The stale-id trigger: an editor caches the launch uuid in live-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 as idle rather than running. Source: apps/cli/src/lib/session/active.ts.
  • Fix: one malformed Kimi session blanked the WHOLE agents sessions listing. A Kimi state.json with neither createdAt nor updatedAt made readKimiMeta return an undefined timestamp, which binds NULL into the timestamp TEXT NOT NULL column and aborts the entire batch index — so a single bad session took down the listing for every session, not just itself. Two layers: readKimiMeta now coerces the timestamp to never-null, falling back to the state.json mtime (matching how the listing already ranks Kimi via last_activity, like every other parser); and upsertSessionsBatch wraps 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 (tmux switch-client/attach-session, a remote tmux over ssh -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 folds go's attach paths in. Source: src/commands/focus.ts, src/commands/go.ts.
  • --device is now a first-class alias of --host on every host-routable command (sessions, run, …), registered centrally on addHostOption so a local fall-through no longer errors. Source: src/lib/hosts/.
  • agents computer steers 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 match normalizeHost(), and shim resolution honors the per-device default pin (not just the central agents.yaml).
  • agents sessions go is retired as a deprecated alias for agents sessions focus --attach-only. go was already a strict subset of focus — its only unique behavior was "attach the live terminal or refuse, never fork/resume." That behavior is now a first-class --attach-only flag on focus (focus.ts: selectFallback() picks refuseFallback under --attach-only, else the resume-in-a-new-tab fallback). go now prints a one-line deprecation notice and delegates to focusAction(id, { attachOnly: true }); the shared reach engine (jumpTo/gatherLiveTargets/pickLiveTarget/refuseFallback) still lives in go.ts and is imported by focus.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 --json paths; runRemoteSessionsJson() reuses the existing gatherRemoteList SSH fan-out. The non-JSON banner path and --active are 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 -A and per-pid lsof — both POSIX-only, both failing silently into "No active agent sessions" with a dozen live claude.exe processes running. The process table now comes from one CIM query on win32 (powershell.exe Get-CimInstance Win32_Process; wmic is removed on current Windows 11) parsed into the same pid/ppid/comm rows, agent-kind matching strips the .exe image suffix case-insensitively (POSIX comms stay exact-match — macOS's Claude desktop app process is named Claude and 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 (no lsof on Windows), same-kind child agent processes — Claude runs subagents and its bundled ripgrep as child claude processes — 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 root claude.exe processes render as 6 rows (previously zero). Source: src/lib/session/active.ts.

  • Those Windows rows grouped under unknown with no topic because only ag run recorded a pid → session/cwd registry entry. The transparent shim delegate (execShimPassthrough) — the path every claude/codex typed into a terminal actually takes — now writes the same by-pid registry 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 .cmd shell 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 of unknown. (POSIX bash shims exec the 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 create no longer hands out a port an already-running browser is listening on

  • findFreeProfilePort probed candidate ports by shelling out to lsof, 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 --endpoint was assigned cdp://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 through isPortInUse (chrome.ts, newly exported), the same platform-aware probe the launcher already used: lsof on POSIX, netstat -ano on 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: true without windowsHide. On Windows detached maps to DETACHED_PROCESS, under which CreateProcess ignores CREATE_NO_WINDOW and the child runs console-less, so every console-subsystem descendant (powershell.exe for a Credential Manager read, git, node, a .cmd shim'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 through powershell.exe on every session-sync cycle (90s). The new backgroundSpawnOptions() (src/lib/platform/process.ts) is the single place that decides the pattern: POSIX keeps detached: true (own process group, group-kill still works); Windows switches to windowsHide: true with 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 yields VISIBLE=True, the new pattern allocates no console window at all. Leaf spawns of console tools reachable from a console-less parent (powershell in secrets/windows.ts + platform/winpath.ts, tasklist/netstat/taskkill in the browser runtime probes, tailscale status, ssh, ffmpeg) now pass windowsHide: true as 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 set read 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 as caf├⌐ Γÿò). The static PS script now reads stdin as raw bytes ([Console]::OpenStandardInput() → MemoryStream) and pins [Console]::OutputEncoding to 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): windowsHide is inert whenever a stdio slot is redirected to an fd — libuv skips CREATE_NO_WINDOW if 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 moment agents daemon start returned (the #556 failure, reproduced live: child alive-after-launcher-exit=false under {detached:false, windowsHide:true} with fd stdio, true under {detached:true, …}). backgroundSpawnOptions({ fdStdio: true }) now keeps DETACHED_PROCESS for these roots — the child runs console-less and windowless, and its console-tool spawns stay invisible via the leaf windowsHide fixes 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-watch watches them and reacts without a human in the loop. Each poll it resolves the PRs the team opened (from each teammate's pr_url, else the gh pr create detected in the session it ran), snapshots CI + review comments via the gh CLI (gh pr checks --json, gh api …/pulls/{n}/comments), and decides follow-ups: RED CI spawns a fix teammate --after the 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 a bugfix teammate (the existing TaskType) --after the source, with the comment body injected. Both slot into the team DAG the supervisor already drains — the loop calls startReady each pass so staged fixers launch when their source completes — and every reaction is visible in agents teams status. Dedupe is by check-run id / comment id, persisted to pr-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 in src/lib/teams/pr-watch.test.ts, no network); the gh collectors and the handleSpawn-backed reactor sit on top. Deferred (documented follow-up): the event-driven path from #331's webhook receiver — pollPrSnapshot is the seam where a check_run / pull_request_review_comment payload plugs in, producing the same PrSnapshot the 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, plus teams add --host, and every other --host consumer via the shared resolver) now resolves in order: the agents hosts registry (unchanged), then the agents devices registry, then an ad-hoc user@host. A machine registered once with agents devices sync is reachable immediately — previously it errored Unknown host unless you also ran agents 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 by assertValidSshTarget). A device that authenticates by password can't offload over the BatchMode ssh path, so it throws a typed, actionable DeviceOffloadUnsupportedError (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 concrete 2HTP252L87.com.phnx-labs.agents-keychain that every query now pins (keychain-helper.swift dpBase). Those items are intact and the wildcard entitlement authorizes reading them, but the pinned queries never ask for that group, so has/get/list reported them missing and whole bundles vanished from secrets 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/has add an un-pinned data-protection fallback pass after the pinned miss, so an orphan reads and reports present instead of missing; (2) get/get-batch re-home an orphan inline the first time it's read — reusing the read's Touch ID, add-before-delete, deleting the exact orphan by kSecValuePersistentRef — mirroring the existing file-based migrateInline; (3) a new migrate-orphans helper verb bulk re-homes every orphan behind a single Touch ID. list is now un-pinned so orphaned bundle metadata reappears, and set/delete clear across all groups so a rotate/delete can't leave a shadow copy. New list-orphans verb enumerates orphans prompt-free. Source: src/lib/secrets/keychain-helper.swift, src/lib/secrets/index.ts.
  • agents secrets migrate-acl now sweeps orphaned-access-group items in addition to legacy-ACL stragglers: the dry-run lists both classes, --commit re-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 team 2HTP252L87 and 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 on release/** + v*) had both build (windows-latest, …) legs red: tests/events-audit.test.ts and tests/teams-events.test.ts spawn the CLI with a redirected HOME and then read the audit trail under it, but the events writer rooted its log dir at a bare os.homedir() (src/lib/events.ts:24). On Windows os.homedir() resolves from USERPROFILE and ignores a HOME override, so every command.start/command.end record was silently written to the real profile instead of the test's temp home — the events array came back empty and the log file ENOENT'd (macOS/Ubuntu were green because os.homedir() honors $HOME on POSIX). The writer now roots its log dir through state.getLogsDir(), the single canonical home anchor (process.env.HOME ?? os.homedir()), which honors an explicit HOME on every platform and still resolves to USERPROFILE in production on Windows (where HOME is unset), so real users are unaffected. One events-audit case also reconstructed its log filename from a UTC toISOString() while the writer names files from the local date, so it ENOENT'd whenever a runner's local and UTC dates straddled midnight; it now globs the log dir like the other assertions. scripts/release.sh restores both build (windows-latest, 22|24) entries to EXPECTED_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. parseAntigravity read its conversation SQLite DBs by shelling out to the sqlite3 CLI (src/lib/session/parse.ts:893), which is absent on Windows — so execFileSync('sqlite3', …) threw spawnSync sqlite3 ENOENT and the parser silently returned [] for every real Antigravity session on Windows. It now reads the step_payload BLOBs through the runtime-agnostic src/lib/sqlite.ts wrapper (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.ts builds its fixture DB through the same wrapper instead of the CLI. (2) parse-droid.test.ts derived its testdata dir from new URL(import.meta.url).pathname, which on Windows yields /C:/… — so path.join produced a doubled-drive C:\C:\… that ENOENT'd; it now uses fileURLToPath(import.meta.url). (3) git.test.ts's syncRepoGit "pull-only" case failed because the Windows runner's core.autocrlf=true converted the freshly-cloned README.md to CRLF during git clone — before configIdentity() could set autocrlf=false on the clone — so status.isClean() saw a phantom modification and syncRepoGit refused with "Working tree has uncommitted changes." The test seed now commits a .gitattributes (* -text), which wins over autocrlf at checkout time so every clone lands byte-identical LF content. (parseOpenCode shells out to sqlite3 the same way and has the same latent Windows gap, but its test mocks execFileSync so 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 (was agents cloud message); a live local/teams/loop agent gets the text enqueued into a per-agent file-spool mailbox that a PreToolUse hook 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 (via REMOTE_PASSTHROUGH) to the box that owns the agent, and message registers as a lazy SQLite-backed command like cloud/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 by msgId). Every message stamps a to field and a monotonic FIFO msgId; 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-time to stamp so a traversal-bearing id fails loud instead of silently misrouting. At spawn, buildExecEnv points each agent at its own box via AGENTS_MAILBOX_DIR (keyed by session id); a loop overrides it to the run-level box so every iteration shares one inbox, and prints agents 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 tolerant parseWatchdogResponse. summarizeWatchdogTail extracts 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 from sessionId + agent by reusing getAgentSessionDirs() rather than hardcoding paths — including the recursive walkForFiles walk that reaches Codex's deep sessions/YYYY/MM/DD/rollout-…jsonl layout and Gemini's tmp layout, driven per-agent by WATCHDOG_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]

  • injectIntoTerminal extends 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 a LaunchSpec run through the same runSpec transport, so injection inherits local/remote (--host over SSH) execution for free. Backends: tmux send-keys -t <pane> (socket-addressed), iterm tell session id "<uuid>" to write text (no activate, so it addresses the exact split without stealing focus), vscodium (VSCodium/Cursor/VS Code) over the editor CLI's --open-url into the extension's /inject verb, and pty via the agents-pty sidecar (local-only). Ink-TUI Enter semantics: text and Enter are two separate writes by default (a fused text\r is swallowed by Claude's Ink TUI), and combined opts into the single fused write for plain shells. Source: src/lib/terminal/inject.ts, src/lib/terminal/index.ts.
  • resolveInjectTarget is the single resolver the watchdog calls: sessionId → a precise InjectTarget or an honest { addressable: false, reason }, with precedence tmux > iterm > vscodium > pty and a deliberate safe skip for Ghostty (no addressable split API). deriveProvenance now captures $ITERM_SESSION_ID and, absent tmux, exposes an iterm reply 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/--pty to target a backend directly, --combined to toggle the Ink-safe two-write default, --no-enter to send without submitting, and --host to 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]

  • runWatchdogTick ties the pure pieces together into one pass over getActiveSessions(): classifyTerminal() finds stalls, readWatchdogTail() reads the transcript, isLikelyTrulyBlocked() gates on the promise-without-toolcall heuristic (deterministic v1) or an optional --smart LLM decider, resolveInjectTargetForSession() is the absolute safety gate, and injectIntoTerminal() delivers Continue. into the EXACT split. A nudge fires ONLY on addressable:true; an addressable:false stall is flagged to a tray-readable state file and skipped — never a guessed target. Per-session policy is off|keep|handsoff (handsoff detects and flags but never injects); cooldown and un-addressable flags persist under ~/.agents/.cache/state/watchdog/. The agents watchdog command runs it without the menu-bar: bare = one dry tick (reports would-nudge/skip + why), --nudge injects for real, --watch is a daemon loop (--interval, default 30s), --json is machine-readable, and --stall/--cooldown/--dormant override thresholds. runner.test.ts drives 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 via agents watchdog enable|disable and shows N stalled · M nudged, and AgentsCLI gains watchdogStatus()/watchdogTick(nudge:)/watchdogSetEnabled() mirroring the doctorOverview() 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-agent terminal backend opens each resumed session as an agent-terminal tab in a running VSCodium / Cursor / VS Code window — via the swarm-ext extension's /spawn URI 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 no zsh -ilc wrap 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-decodes uri.query once before the handler parses it (a bare echo a && touch b was otherwise truncated at the &). Wired into sessions resume as --vscodium; auto-detect is intentionally omitted (TERM_PROGRAM=vscode can'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 (--tabs still 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 new syncRepoGit refuses on a dirty working tree (commit or discard first), otherwise git fetch origin + git pull --rebase origin <branch> against the repo's own HEAD branch (falling back to main), reinstalls the git hooks, and reports the resulting short commit. The user repo and enabled extra-repo aliases also git push local commits up; system is a pull-only mirror of the npm-shipped upstream (push: false). project and 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 like system/user would otherwise fail parseAgentSpec. Source: src/lib/git.ts, src/commands/sync.ts.
  • Bare agents sync no 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 via agents sessions --host <machine>). Interactive bare agents 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 via mergeRepoScopedSelections / 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.yaml used to carry machine-specific fields and was held back with a git skip-worktree band-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 central agents.yaml is left portable. writeMetaUnlocked writes the device and history files BEFORE stripping and rewriting central, so a crash mid-write never drops pins/versions before they persist; readMeta overlays the machine-local files back on via overlayMachineLocal (device pins win and self-heal a pre-migration central). Source: src/lib/state.ts, src/lib/machine-id.ts.
  • Migration migrateSplitDeviceLocalMeta (sentinel bumped to v11) performs the one-time split on raw YAML, merging into any existing device/history files (existing entries win) via atomicWriteFileSync, and only rewrites central when it actually carries machine-local fields — a portable-only agents.yaml is left byte-untouched — while always clearing the skip-worktree bit 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-level state.ts can 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 on SessionMeta.machine by discoverSessions. The picker row, previously stuck on shortId · 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 so yosemite-s0/yosemite-s1 read as s0/s1), a blue PR#/ticket column (only when some row carries a ref), and a magenta wt:<slug> worktree badge. Column flags are computed once over the whole pool via pickerColumnsFor and 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 subtitle hint line renders between the header and the rows (new subtitle field on PickerConfig/SessionPickerConfig), rotating a Tip: 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 but formatPickerLabel reserved 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 through PickerColumns and 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 --host operations can target Windows remotes, where ssh lands in cmd.exe/PowerShell and bash -lc does not exist. remoteShellFor(os) routes windows → powershell and everything else (including unknown/absent) → posix, so linux/macOS never regress; buildWindowsAgentsCommand emits powershell -NoProfile -EncodedCommand <base64-utf16le>, which survives cmd.exe re-parsing with zero quoting hazards. The peer OS is resolved from the tailscale-synced device registry (fleet fan-out) or the enrolled HostEntry.os (explicit --host). This fixes agents sessions --host / --active and remote secrets reads (browse + use-a-remote-bundle), which previously wrapped the remote invocation in bash -lc and got 'bash' is not recognized from a Windows peer. The secrets export/import --host write 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 … resume no longer crashes on Windows with spawn EFTYPE: resumeSessionInPlace spawned the version-pinned launcher ([email protected]) with shell: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 via needsWindowsShell and reports a synchronous launch failure truthfully. The generated hook-cache shim also hardcoded python3 for its hash/timer/mtime, but on Windows python3 is often the Microsoft Store execution-alias stub (prints to stderr, exits non-zero, 0 bytes) — silently emptying mtime so every call missed the cache and re-ran the hook; it now probes for a runnable interpreter (python3, then python) 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-windows job runs the suite on windows-latest for changes under hooks/platform/shims (the required test gate runs on ubuntu-latest, where path.sep is /, so a backslash-path bug is invisible), and toPortableCommand is now pure/exported with injectable home + separator so a unit test can assert Windows C:\… → ~/… folding on any host. Separately, a prepare: npm run build hook rebuilds the gitignored dist/ on every install/link (and before npm publish), so a dev-linked checkout can't silently run a stale dist/ 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, and package.json carry the new license, and the human-facing docs (the AGENTS.md brand lines, the CONTRIBUTING.md CLA clause, DESIGN.md) were aligned so the stated license is consistent everywhere.

Security hardening batch (#474–#478)

  • Shell / option injection. agents inspect no longer builds its git call as a shell string: a crafted repo path could inject via $(…) or other shell syntax through execSync(\git -C ${…} ${args}`). It now uses argv-form execFileSync('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 an agents version 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 on release/** + v*) installed deps with bun install --ignore-scripts, so pty.node from @homebridge/node-pty-prebuilt-multiarch was never fetched/built. That package ships prebuilt binaries only for Linux; macOS/Windows obtain pty.node via 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-scripts in 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 fake HOME under os.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-byte sun_path limit — so bind() failed with EADDRINUSE. The test now roots HOME at a short base on POSIX (Windows uses length-unlimited named pipes); real users with a normal HOME were 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> and agents daemon logs existed, and agents hosts wasn'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]/--session load directly (host task tried first, then session); with no id, --host/--agent/--version filter a merged candidate list (one match shows, several open a fuzzy picker, non-TTY prints the list). Additive: agents hosts logs and agents sessions tail are 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 (raw Buffer via sshExecRaw). 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 renderWhatsNew so 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), --active could only map a discovered agent process to a session by guessing the newest .jsonl in its cwd, so several agents in the same repo collapsed onto one session row (observed live: a single id listed 28 times), and /restore couldn't tell them apart. agents run now records each launch to ~/.agents/.cache/terminals/by-pid/<pid>.json ({agent, cwd, tmuxPane, sessionId, startedAtMs}) — the headless equivalent of the terminal extension's live-terminals.json — so --active and /restore attribute 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 runs bun install && bun run build && bun run test on a leased crabbox VM via scripts/sandbox.sh instead of freezing the local machine, matching CI's Build→Test order (crabbox's sync honors .gitignore, so the gitignored dist/ is built on the box). A new bun run test:remote offloads 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.sh box acquisition is now robust: secrets load via agents secrets export --plaintext (the bare form now hard-errors), a missing .crabbox.yaml no longer aborts the script under set -e, and the agents-cli/claude install is gated to PR mode so test-mode runs match GitHub CI. Box selection gates on crabbox status … ready=true — skipping failed-bootstrap duds (which still report status=running) and warming a fresh box if none are ready — keyed on the stable profile label rather than an ephemeral slug. A dedicated agents-cli crabbox 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 > Claude ai-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 -N summary) instead of the old flat "Modified" list, a Tools histogram (per-tool call counts), and a Tests verdict parsed from the last vitest / jest / pytest / go test / cargo test / tsc run. The same signals are folded into the interactive picker preview.
  • agents sessions --active now collapses the many subagent/fork PIDs of one session into a single row with a ×N count 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 --active infers real activity from each transcript's tail — working / waiting / idle — rather than the old mtime-only running/idle guess, using structural signals (Claude ExitPlanMode / 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.
  • --waiting filters --active to only sessions blocked on your input and exits non-zero (a scriptable gate); --tree groups 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 $COLUMNS first, so it no longer wraps or drifts under tmux or over --host SSH (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 setup agents is /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-set AGENTS_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-cli package — an unrelated ancestor repo no longer counts. Extracted to src/lib/startup/dev-build.ts with 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) to daily (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 daily bundle on first read with no secrets.agent.auto: true needed. Opt out with secrets.agent.auto: false.
  • Configurable, still flexible. Set the global default in agents.yaml (secrets.policy: always to restore prompt-every-time), or override per bundle with agents secrets policy <bundle> always for high-value keys (signing, SSH) you want to confirm on every read.
  • Explicit always now persists under the legacy tier: biometry token (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 becomes daily on 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, teams meta.json, and the cloud tasks.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 with agents menubar enable | disable | status. Opt out with agents 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 update actually moves users onto the new helper binary + plist rather than leaving the old one running (#442). agents menubar status shows 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 as agents repos view, now a first-class alias of the repo command) 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 the inspect repo renderer, so output matches agents inspect <repo>; supports --brief and --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/sync only reconcile each agent's default version, so a non-default installed version keeps serving stale/invalid resources; (2) a plugin.json with a bare-name skills/commands field 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 --fix turns the read-only diagnosis into a heal: installs missing resources, repairs Claude-invalid plugin manifests (strips the bare skills/commands field — 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> --fix scopes to one.
  • Daemon safety check: the routines daemon now runs the same heal in conservative safe mode (~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.
  • .source now 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 doctor overview 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 --fix heals) 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/.rst doc sibling (e.g. git-guard.md next to git-guard.sh) was wrongly treated as the hook's runtime data file, so the installer's correct omission of docs showed as perpetual drift in doctor (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> --fix or agents sync <agent>@<version>.

Secrets prompt policy: human-readable always / daily, and secrets list now shows it

  • Renamed the secrets-agent tier to 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 name session was 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 with agents secrets policy <bundle> [always|daily].
  • Disclosure fixed. agents secrets list now has a POLICY column — previously there was no way to tell which bundles would Touch-ID-prompt you. daily bundles currently held by the agent show daily · Nh left. agents secrets view and create now 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/session token, so bundles stay readable across mixed CLI versions on synced machines. agents secrets tier, --tier, and the biometry/session values keep working as aliases.
  • A third never policy (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 -g swaps 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: postinstall now 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 / with AGENTS_NO_HEAL=1.
  • Broker version-skew self-heal: the broker's ping reports 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. New getCliVersionFresh() re-reads package.json to 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. New agents secrets start installs the broker as a launchd user service (RunAtLoad + KeepAlive, ProcessType: Interactive for 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 stop removes it; agents secrets status shows whether it's installed.
  • unlock and the auto-cache worker now install/kickstart this service automatically via ensureAgentRunning, 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 a session-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 detached secrets _agent-load worker that reuses the robust ensureAgentRunning path (spawn-then-ping, 20s budget) and loads synchronously, so it reliably populates even when the broker is slow to start. Manual agents secrets unlock was 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, parallel agents run --secrets) re-prompts once per process. New agents 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 lock wipes it; agents secrets status shows 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 single agentSocketExists() stat. The single integration point is readAndResolveBundleEnv, 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, and lock.
  • Snapshot semantics: unlock freezes a bundle's dynamic exec:/env:/file: refs at unlock time; keychain and literal values are unaffected.
  • Release note: auto-lock on screen-lock/sleep adds a watch-lock subcommand to keychain-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 --tier on create). biometry (default) is today's behavior — only an explicit unlock puts it in the agent. session makes a bundle agent-eligible.
  • New secrets.agent.auto: true in agents.yaml (default off): the first real keychain read of a session-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 manual unlock. A biometry-tier bundle is never auto-held.
  • A none tier (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_PASSPHRASE was set, leaving agents secrets silently 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_PASSPHRASE still takes precedence (off-disk key), an existing .passphrase is reused for stable interactive/headless behavior, and interactive TTY sessions are still prompted. Security model + resolution order documented in docs/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) or secret-tool (Linux-only). get prints 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 a SessionStart hook needs to probe-and-fallback quietly. Routing goes through the existing cross-platform keychain layer: macOS via /usr/bin/security, Linux via secret-tool with the encrypted-file fallback.
  • setKeychainToken now 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-key silently on every launch — routing it through the Touch ID helper would attach an ACL the /usr/bin/security read can't satisfy without popping the legacy password sheet. The change is purely additive: every existing caller passes an agents-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 --brief are unchanged; --json gains 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

  • discoverPluginHooks read the top-level keys of a plugin's hooks/hooks.json, so the official { description, hooks: { SessionStart: [...] } } format surfaced as description, hooks instead of the real events. It now reads the hooks wrapper when present (falling back to top-level keys for the flat format), so agents 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.yaml entry 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 — meaning agents prune cleanup could have deleted live hooks. Doctor's Orphans section and prune cleanup hooks now share this single manifest-based definition. parseHookManifest gained 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 via agents repo add (~/.agents-<alias>/): a top-level commands/<name>.md is written into the agent's version home on agents sync, and plugins under plugins/<name>/ are synthesized into a registered agents-<alias> marketplace on launch. Both already work in main; 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.ps1 live) 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 then npm i -g @phnx-labs/agents-cli succeeds yet agents is "not recognized". The shims dir (claude/codex/…) is still left to agents setup, which the user can now run because agents resolves.
  • Postinstall also detects a Restricted/AllSigned PowerShell execution policy (which blocks the generated .ps1 launchers, so even an on-PATH agents fails 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.ts into a new src/lib/platform/winpath.ts leaf module (prependToWindowsUserPath, getEffectiveExecutionPolicy, blocksLocalScripts, npmGlobalBinFromEntry); addShimsToWindowsUserPath now delegates to it. Pure helpers are unit-tested.

Factory AI Droid (first-class support)

  • Add droid as a first-class supported agent (AgentId + full registry entry for Factory AI's droid CLI, 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 ~/.factory config symlink (Droid has no *_HOME override).
  • Resource sync wired for the four resource types Droid supports natively: MCP (~/.factory/mcp.json), rules (native AGENTS.md), subagents (custom droids flattened to ~/.factory/droids/*.md, with the unsupported color frontmatter key stripped), and commands (~/.factory/commands/). Skills/plugins/workflows have no Droid equivalent and are disabled; hooks/permissions are deferred.
  • agents run droid and agents teams add … droid work end-to-end: headless droid exec with mode mapping (plan → read-only, edit → --auto low, auto → --auto high, skip → --skip-permissions-unsafe), -o stream-json output, -m model selection, and -r reasoning effort. Routine/daemon jobs (buildJobCommand) support Droid too.
  • Known limitation: agents teams renders Droid events through the generic normalizer pending a verified droid exec -o stream-json event 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 failed SecItemAdd with errSecMissingEntitlement -34018) kept the broken helper until the lazy staleness check in getKeychainHelperPath() happened to repair it on their next secret operation. installResolvedPackage now force-refreshes the helper (ensureKeychainHelperInstalled({ forceReinstall: true })) on darwin after the install, so both the explicit agents upgrade and 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, and agents helper install --force remains 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.yaml is parsed for its run.<agent>.strategy and any agents.<agent> version pins, shown under manifests instead 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. --json carries all of it (git.lastCommit, git.ahead/behind, manifest, size, and per-kind {count, bytes, files, names}); --brief still 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 --porcelain path; 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-level agents.yaml version-pin or an unrelated source skills/ dir at the repo root was mistaken for a DotAgents root, so inspect . reported the wrong directory's resources (e.g. plugins 0 while 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> --plugins now 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 cladue used to print Unknown agent 'cladue' even though agents run cladue auto-corrected. resolveAgentName — the canonical resolver behind view, usage, inspect, doctor, sync, models, skills, hooks, import, sessions --agent, and every agent@version spec (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 both kiro and kimi) and inputs under 3 characters still error. agents run keeps its existing exact -> profile -> workflow -> fuzzy precedence, so a profile named claud still beats the typo correction.
  • Fixes kimi being listed as a valid agent but missing from the alias map — agents view kimi previously errored. Added kimi / kimi-code entries.

1.20.7 — 2026-07-07

agents inspect — DotAgents repo targets (#256)

  • agents inspect now accepts a DotAgents repo as the target, not just an installed agent: user (/.agents/), system (/.agents/.system/), project (nearest .agents/ from cwd), any extra-repo alias registered via agents 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 -u plus "${BUILD_ARGS[@]}" on an empty array aborted the dev install with BUILD_ARGS[@]: unbound variable under 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 awkward agents view <agent>@<version> deep-detail mode as a dedicated verb; view itself 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). --json works 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 status alias for view @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 x failed with secret-tool: Cannot create an item in a locked collection. Diagnosis in the issue: gnome-keyring-daemon is running and D-Bus is reachable, but the default login collection is locked because no graphical login has fed the daemon the passphrase, and secret-tool from libsecret-tools has no --collection flag so it can't target the unlocked session collection. This made agents secrets effectively macOS-only on any headless box.
  • src/lib/secrets/linux.ts now 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 from AGENTS_SECRETS_PASSPHRASE (preferred) or a TTY prompt via /dev/tty with stty -echo for non-echoing input. The fallback also activates when libsecret-tools is not installed at all but AGENTS_SECRETS_PASSPHRASE is 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>. The KeychainBackend interface in src/lib/secrets/index.ts is unchanged — has/get/set/delete/list work identically against either backend, so bundles.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)

  • copyPluginToMarketplace used fs.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's app -> ../../../rush/app, web -> rush/web, widgets -> rush/widgets). Those targets resolve to the rush monorepo (~8.7 GB of app/ including node_modules + .next builds, 782 MB of web/, 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 realpath escapes 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 rm against ~/.claude/plugins/marketplaces/agents-cli/plugins/*/{app,web,widgets,*-symlinks-that-escaped} then re-run agents pull or 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 run strategy is available, which calls getUsageInfoForIdentity to skip rate-limited accounts. With a 2-minute cache, every cold invocation past that window made a blocking fetch to api.anthropic.com/api/oauth/usage (5 s timeout, plus an optional 15 s OAuth token refresh) before spawn(claude) — so agents run claude regularly 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 setImmediate so the synchronous Keychain CLI call (security find-generic-password, invoked by loadClaudeOauth) 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 a metaCache module global plus writeMetaUnlocked cache-invalidation logic wired in years ago — but no read path ever consulted the cache. So every call did 2x fs.readFileSync + 2x yaml.parse on system + user agents.yaml, and hot callers (getConfiguredRunStrategy, getGlobalDefault, getVersionResources, ensureVersionResourcePatterns) fire it multiple times per agents 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 (via config.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 agy uses the latest version. Documented the canonical config path ~/.gemini/antigravity-cli/ and its mcp_config.json.
  • Documentation: Updated 02-resource-sync.md to 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 run agents add first.
  • --agents claude@all and the bare all literal now work across every callsite that takes --agents — previously agents install gh:..., mcp register, mcp remove, and inline mcp add had 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> add with no --agents and no --yes used 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).
  • --agents parsing in <resource> add understands @all and the bare all literal; promptAgentVersionSelection's picker surfaces version counts when there's more than one installed, mirroring what @all would 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 --types per kind. Pass --types skills,workflows to 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> add accepts --names and @all uniformly 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 create command — interactive wizard to assemble a profile from gateway or self-hosted presets (OpenRouter, OpenAI-compatible) without hand-writing YAML.
  • --smoke-test exercises the resolved env block against the configured endpoint before writing the profile.

Feedback (in-CLI bug / idea / question routing)

  • New agents feedback command — 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)

  • monitorRunningJobs used to hardcode status: 'failed' whenever it detected that a detached child had exited — executeJobDetached fires-and-forgets, so the real exit code was unreachable. Every scheduler-driven routine ended up labeled failed/exitCode: null, even when the agent completed cleanly.
  • Fix: when finalizing a vanished child, scan the tail of its stream-json stdout.log for Claude's type: result terminator (which carries is_error). If found, set status and exitCode from it. Only fall back to failed when 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 failed until their stream tail parsers are added.

Security

  • security(cli): eliminated shell: true from manifest-driven installs — closes a command-injection vector in install/add paths that took git URLs or shell-interpolated metadata.
  • security(logs): prompts and tokens are redacted before events.jsonl is 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 — only localhost and 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-cli is 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 prepack and prepare hooks 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 (osascript on macOS, notify-send on Linux).
  • agents routines list annotates overdue rows with (overdue) and prints a footer pointing at the catchup command.
  • New agents routines catchup command: lists overdue routines and fires them in the background under the scheduler. --dry-run lists without triggering.
  • JobScheduler.schedule now sets croner's catch: true and forwards timezone defensively, 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 codex for Codex >= 0.117.0. getActuallySyncedResources now detects converted command-skills via the agents_command marker in ~/.codex/skills/<name>/SKILL.md instead of only scanning the empty legacy prompts/ directory.
  • Summary and selection prompts are version-aware: the static COMMANDS_CAPABLE_AGENTS gate is replaced by supports(agent, 'commands', version) so the "X commands" line only appears for versions that can actually take them.
  • Generalize shouldInstallCommandAsSkill beyond 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 grok as a first-class supported agent (AgentId + full registry entry using official ~/.grok/README.md paths).
  • Implement proper binary resolution from ~/.grok/downloads/.
  • Add GROK_HOME isolation to generated shims for true versioned config (skills, hooks, plugins, agents/, MCP, memory, etc.).
  • Extend installVersion to 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 --record convenience flag for one-shot recording sessions.
  • Auto-discover per-site SKILL.md on browser start so skills appear under the active task without manual wiring.
  • Auto-pick a Chromium-family browser when --profile is omitted; the limitation is surfaced in --help and 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-chromium devdependency.

Secrets / Keychain

  • agents secrets list and agents 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 groupActiveSessions into a tested helper for --active window grouping.
  • Propagate windowid from live-terminals into the active session record.

Copilot

  • Emit COPILOT_HOME in the shim and exec env builder for versioned isolation.
  • Wire the Copilot session dir and .jsonl extension into the sessions reader.

OpenClaw

  • Carry OpenClaw user data forward on version switch.

Teams

  • Warn loudly when --after teammates 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/prompts 7.10.1 → 8.5.1, diff 8.0.4 → 9.0.0, tsx 4.22.2 → 4.22.3, actions/setup-node 4.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 via AGENTS_BROWSER_TASK, or pass --task <name> for a per-call override:
    export AGENTS_BROWSER_TASK=$(agents browser start --profile work)
    agents browser navigate --url https://example.com
    agents browser click 42
    agents browser screenshot
    
    Env vars are per-process, so parallel agents in different shells never collide.
  • Breaking: URL/text/expression/scroll arguments are now flag-only — positional forms removed:
    • navigate --url <url> (was navigate <url>)
    • tab add --url <url> (was tab add <url>)
    • type <ref> --text "..." (was type <ref> "...")
    • evaluate --expression "..." or --file <path> (was evaluate "...")
    • scroll --dx <n> --dy <n> (was scroll <dx> <dy> — fixes negative-value parser collision)
  • screenshot prints a one-line auto-save tip on stderr when --output is not passed, so agents see the directory without having to dirname() the path.

1.18.4 — 2026-07-07

Browser

  • agents browser start writes 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 makes T=$(agents browser start --profile X) Just Work — no --quiet flag 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 list is now agents browser tabs (top-level), pairing cleanly with agents browser tab focus <id>. The old tab list form is removed.
  • agents browser --help is 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 prime and agents browser profiles launch are removed. Both were thin duplicates of start. For first-run onboarding, just agents browser start --profile <name> and complete the interactive screens in the browser; the user-data-dir persists across runs. The daemon's launch-profile IPC 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: local
    
    agents browser start --profile rush --endpoint mac-mini picks a specific preset; --endpoint falls back to defaultEndpoint or the first preset. Pre-validated client-side so a typo doesn't waste an IPC round-trip. Per-endpoint binary and targetFilter override the profile-level fields. agents browser profiles show lists 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 status and tasks show the composite name, so you can tell at a glance which variant a task is using.
  • agents browser screenshot --quality raw captures pixel-faithful PNG (no downscale) for archived QA evidence. Default stays compressed (JPEG, capped near 100 KB) for chat-injected screenshots.
  • New agents browser record start / agents browser record stop recording verbs. Captures via CDP Page.startScreencast, pipes frames into ffmpeg (image2pipe → libvpx-vp9) and writes a webm under sessions/<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 sync now 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 /plugins UI under Installed and respond to /plugin enable, /plugin disable.
  • A synthetic agents-cli marketplace is materialized per version: .claude-plugin/marketplace.json is synthesized from discovered plugins, an entry is added to <versionHome>/.claude/plugins/known_marketplaces.json, and settings.json#enabledPlugins["<plugin>@agents-cli"] is flipped to true. 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 into settings.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 namespaced mcpServers["<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. The agents view <agent>@<version> Plugins section gains MCP/LSP/Monitor/Bin/Settings counts. New discoverPluginMcpServers, discoverPluginLspServers, discoverPluginMonitors helpers parse .mcp.json, .lsp.json, and monitors/monitors.json.

1.18.2 — 2026-07-07

Teams

  • Dropped ~/.agents/teams/config.json entirely. 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 via listInstalledVersions() (the same source agents view uses) and invoke them via the canonical agents run subcommand. One spawn path, one canonical exec module (src/lib/exec.ts). The deprecated AGENT_COMMANDS, applyEditMode, applyFullMode, readConfig, writeConfig, setAgentEnabled, AgentConfig, SwarmConfig, ProviderConfig, ModelOverrides, ReadConfigResult, and EffortLevel (the persistence-module copy) exports are removed from @phnx-labs/agents-cli/teams. Migration deletes both ~/.agents/teams/config.json and the legacy ~/.agents/config.json.
  • ~/.agents/teams/registry.json moves to ~/.agents/.history/teams/registry.json — it's per-machine runtime state (timestamps + absolute worktree paths) and shouldn't be synced across machines via agents repo push.
  • New agents run --quiet flag suppresses the rotation banner and Running: … 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> by scripts/install.sh, or invoked from a working tree where <cli-dir>/../.git/ exists) and defaults AGENTS_NO_AUTOPULL=1, AGENTS_SKIP_MIGRATION=1, and AGENTS_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.sh now sets mode 0o755 on every file declared in package.json#bin after tsc emits dist/. Newer npm versions preserve file mode from the published tarball and do NOT auto-chmod the bin target during npm install -g, so 1.18.0 shipped with mode-644 entrypoints. Users hit zsh: permission denied: agents after auto-update. Re-install to recover: npm install -g @phnx-labs/agents-cli@latest.
  • New scripts/install.sh builds 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 --version shows 0.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, alongside skills/, commands/, hooks/, etc. — git-tracked as source of truth. Previously, migrateRuntimeToCache moved ~/.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 a Plugins section 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

  • getAvailableResources and the version-home sync now treat only executable files in hooks/ 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 workflows skill — author-and-run guide for workflow bundles (WORKFLOW.md frontmatter, subagents/ directory for multi-agent pipelines, scoped skills/ and plugins/, sharing via agents repo push or GitHub install). Calls out the --mode plan deadlock that bites workflows which need to post comments or edit files.
  • agents workflows --help rewritten with a structure diagram, project > user > system resolution order, and an explicit note that workflows mutating state need --mode edit or --mode full to avoid a headless deadlock at ExitPlanMode.
  • README gains a Workflows section between Teams and Browser covering the bundle layout, frontmatter, subagents/skills/plugins, and the --mode requirement.

1.17.4 — 2026-07-07

Browser

  • agents browser type now 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 WHATWG beforeinput dispatch (InputEvent('beforeinput', { inputType: 'insertText', ... })) for Lexical/ProseMirror/Slate/Quill/CKEditor5/Draft and el.editor.insertString() for Trix. agents browser refs --json surfaces the new editor field, and type --clear prepends a select-all + deleteContentBackward dispatch before inserting.
  • Plain-input reliability also improved: typeText now issues a single CDP Input.insertText instead of per-character dispatchKeyEvent, so framework-controlled inputs (React, Vue, Solid, MUI/Chakra/Mantine TextField, masked-number fields, Canva-style pickers) actually receive beforeinput/input/textInput events. focusNode falls back to the first focusable descendant when DOM.focus throws "Element is not focusable" — fixes wrapper-ref UIs like Slack composer, Linear comments, Notion blocks, and every MUI/Chakra/Mantine TextField. (#12)

1.17.3 — 2026-07-07

Browser

  • agents browser profiles create gains --electron, --binary, and --target-filter for driving Electron desktop apps (Canva, Slack, etc.) that expose multiple CDP page targets. The picker matches by url:<substring> or title:<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.evaluate now uses awaitPromise: true and surfaces exceptionDetails so async script errors propagate as thrown errors. (#14)

Secrets

  • agents secrets list rework — drop the misleading SENSITIVE column and add SYNC (iCloud yes/no) plus CREATED / UPDATED / USED relative-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. Set AGENTS_NO_USAGE_TRACK=1 to disable the usage stamp. agents secrets view shows 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=1 forces 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 freshly agents 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 optional subagents/, 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, copies subagents/* into ~/.claude/agents/ for Agent-tool discovery, and syncs workflow-scoped skills/ and plugins/ at run time.
  • agents view now 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 scroll plus new profiles launch, profiles doctor, profiles prime, viewport position, and port diagnostics commands.
  • agents browser profiles list now shows a description column when any profile has one.
  • isProcessRunning treats EPERM as process-alive (fixes false-negative on sandboxed processes).

Cloud dispatch

  • --balanced strategy and --upload-account-tokens flag on cloud dispatch.
  • Remote account API client; --balanced skips the client manifest path.

Plugin system extension

  • Plugins now ship with commands/, agents/, bin/, MCP configs, settings, and install / update hooks. Discovery and sync extended end-to-end.

Secrets

  • agents secrets import <bundle> --from-1password / export <bundle> --to-1password with vault picker, skip-empty-fields on import, overwrite-only-with---force on 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 --linear and --post-file post run output to Linear tickets.
  • Dynamic GitHub App token, gh CLI installed, stale git credentials cleaned.

Sessions / SQLite concurrency

  • Scan coordinator prevents concurrent session indexing.
  • SQLite concurrency hardened with BEGIN IMMEDIATE and ledger recheck on contention.
  • Session discovery uses getHistoryDir for 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_TOKEN from the per-version .oauth_token file when unset.

Resource UI

  • agents view replaces 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; start renamed to open; 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 sessions now 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.sh documented 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 secrets now works on Linux backed by libsecret/GNOME Keyring with the same UX as macOS Keychain. Headless workarounds documented.
  • New agents password generate subcommand.
  • 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 prune learned trash, sessions, and runs cleanup targets.

Fixes

  • Command-injection hole in daemon + secrets closed.
  • Layered permission resolution corrected; daemon tests isolated from real user state.
  • .tmp-bun gitignore pattern fixed.
  • codex interactive mode no longer routes through exec subcommand.

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, and expiresAt are 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 binary field to browser profiles for specifying custom executable paths (e.g., Electron apps like Rush)
  • Added electron field to browser profiles — when true, uses existing windows instead of creating new ones (Electron doesn't support Target.createTarget)
  • New custom browser 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 --json flag to agents sessions list and agents sessions for 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 openclaw so 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 ~/.openclaw instead of leaving cwd empty 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 view now opens a live-search picker by default in interactive terminals
  • agents sessions --agent ... and agents sessions --project ... now open the same live-search picker before falling back to the table view
  • agents sessions view <query> now resolves prompt text, not just exact session IDs
  • Fixed --project search 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 Agent column
  • 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 pty command 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 directory
  • agents 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-headless
  • agents pty write <id> <input> -- send keystrokes with escape sequence support (\n, \t, \e, \xHH)
  • agents pty read <id> -- read raw PTY output with configurable timeout
  • agents pty signal <id> [INT|TERM|KILL] -- send signals to the PTY process
  • agents pty list -- show active sessions with status, PID, age, and active command
  • agents pty server start|stop|status -- manage the sidecar server directly
  • Session idle cleanup (30 min) and server auto-exit (1 hour with no sessions)
  • --json output 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 drive command 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 ~/.claude symlinks to the drive, so Claude reads/writes there
  • agents drive detach -- restore symlinks to the version home
  • agents 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@Version combined column (e.g., [email protected])
  • Added Topic column 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 cron is now agents routines -- aligns with Claude Code Routines naming
  • agents cron and agents jobs still work as deprecated aliases
  • ~/.agents/cron/ directory renamed to ~/.agents/routines/

Sessions:

  • Sessions list now shows Agent@Version in a combined column (e.g., [email protected])
  • Added Topic column 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 agents
  • agents rules add -- install and sync rule files from a repo or local path
  • agents rules view -- view rule file content for any agent
  • agents 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 commands capability flag to agent configs
  • agents use openclaw and agents view openclaw no longer show or sync slash commands or memory files
  • Fixed hasNewResources to 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 agents filter 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 jobs command to cron (jobs still 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: resume shows only paused jobs, pause shows only enabled jobs
  • Effort-based model mapping: --effort fast|default|detailed maps to agent-specific models

Resource command cleanup

  • Added view command to commands, mcp, hooks, and permissions
  • Removed push commands from all resources (commands, skills, mcp, memory, hooks)
  • Deprecated perms alias for permissions (shows warning but still works)
  • Deprecated info alias for skills view, show alias for memory 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 view command replaces list and status
  • agents view / agents view claude shows installed versions
  • agents 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 pull now 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, press q to 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 from info)
  • Fixed --version showing 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 claude syncs only Claude resources
  • Agent aliases: cc, cx, gx, cr, oc for quick filtering
  • Overview display: Shows NEW vs EXISTING resources before installation
  • Per-resource prompts: Choose overwrite/skip/cancel for each conflict
  • -y flag: Auto-confirm and skip conflicts
  • -f flag: 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)