# Changelog

## 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 `FailurePattern`s 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 `.app`s, 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 `PeriodicService`s (`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 `SIGKILL`s 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/node-pty-prebuilt-multiarch@0.13.1` 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`.
</content>
</invoke>

## 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
  **302**s 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-client`s 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 `spawnSync`ed 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 `.app`s
  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 `PeriodicService`s 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
  404. 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 (`muqsitnawaz@gmail.com` → `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/agents-cli@1.22.47` 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 you@example.com` — 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 codex@0.146.0 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 <box>" 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-client`s as before (so `resolveAfterAttach` reads the exit status, EXEC-23b unchanged); with **no** client attached it `kill-session`s 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 <device> 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 claude@2.1.220 --model opus-5` moves to `agents models set claude@2.1.220 --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.rename`s 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 `setInterval`s). 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](https://github.com/phnx-labs/.agents-system/pull/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 '<name>' 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 claude@2.1.181` 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 claude@2.1.220` 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 `codex@0.145.0` 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 `stat`s 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 claude@2.1.112. 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 muse@0.1.0` failure). Managed launches now pin
  `XDG_CONFIG_HOME` + `XDG_DATA_HOME` into the version home from `buildExecEnv`,
  the main shim, and versioned aliases (`muse@0.1.0`), 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 `--option`s, `--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](https://omp.sh) (`@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: `grok@0.2.118` 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
  `muqsit@getrush.ai` 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 `SIGKILL`ed 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-dir`s) 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 claude@2.1.220 --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):
  ```
  Claude@2.1.220
    ✗ 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 claude@2.1.220 --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 claude@2.1.220 --fix

    heal what's auto-fixable:  agents doctor claude@2.1.220 --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`: `Claude@2.1.207` 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 claude@2.1.220`) 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 whois`es 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 `unlock`ed 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 (`claude@2.1.170`,
  `claude@2.1.207`, …) and installs each missing one on the target; a pinned
  `claude@2.1.207` 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 codex@0.144.6` 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 `lipo`s 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 — `taylor@modsquad.com (ModSquad · Team)`, `taylor@turingsaas.com (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

- **`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

- **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 push`es 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

- **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

- **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

- **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

- **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

- **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 bootout`s 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

- **`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

- **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

- **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

- **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

- **`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

- **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

- **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 git@github.com: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@<version> 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

- **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 codex@0.142.0 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

- **`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

- **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

- **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

- **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

- **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** (`claude@2.1.112`) 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

- **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.resolve`s 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 kimi@0.19.2`, 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 kimi@0.19.2 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

- **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

- **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 `exit`ed *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

- **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

**[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 (`claude@2.1.196`) 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

**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

**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

**`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

**`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 `realpath`s 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](docs/menubar.md). 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 codx@2.1.170`) — 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

**`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

**`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

**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

**`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

**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

**Agents selector (auto-install missing versions + unified `@all` everywhere)**

- `--agents claude@2.1.999` 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 claude@2.1.141` (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

**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

**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

**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:
  ```bash
  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

**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:
  ```yaml
  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

**Plugins** ([#22](https://github.com/phnx-labs/agents-cli/issues/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

**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

**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

**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](https://github.com/phnx-labs/agents-cli/issues/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

**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

**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](https://github.com/phnx-labs/agents-cli/pull/12))

## 1.17.3

**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](https://github.com/phnx-labs/agents-cli/pull/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](https://github.com/phnx-labs/agents-cli/pull/18))

## 1.17.2

**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](https://github.com/phnx-labs/agents-cli/issues/15))

## 1.17.1

**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

**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

**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-<alias>/ 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

**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

**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

**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

**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

**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 `codex@0.113.0`)
- 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

**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

**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

**Better sessions**

- Sessions list and picker show `Agent@Version` combined column (e.g., `claude@2.1.85`)
- Added `Topic` column showing first user message of each session
- Account shows email instead of display name

## 1.9.0

**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., `claude@2.1.85`)
- 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

**"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

**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

**Bug fix**

- Fixed memory file detection counting symlinks as separate files (CLAUDE.md/GEMINI.md -> AGENTS.md)

## 1.6.4

**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

**Bug fix**

- Fixed infinite "new resources available" loop in `agents view`
- Partial resource syncs no longer wipe out previously synced resources

## 1.5.82

**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

**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

- Upgrade prompt now shows on ALL command flows (--version, --help, bare `agents`)

## 1.5.67

**Unified view command**

- New `agents view` command replaces `list` and `status`
- `agents view` / `agents view claude` shows installed versions
- `agents view claude@2.0.65` shows full resources (commands, skills, mcp, hooks, memory)
- Old commands show deprecation warning but continue to work

## 1.5.48

**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

**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

- Remove trailing newlines from command output

## 1.5.5

- Update prompt: Interactive menu before command runs (Upgrade now / Later)

## 1.5.4

- `cli list`: Shows spinner while checking installed CLIs

## 1.5.3

- `skills view`: Opens in pager (less) for scrolling, press `q` to quit

## 1.5.2

- `skills view`: Truncate descriptions to fit on one line

## 1.5.1

- 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

**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

- Conflict detection for pull command
- Bulk conflict handling (overwrite all / skip all / cancel)

## 1.3.13

- Enabled skills support for Cursor and OpenCode
- Fixed Cursor MCP config path (now uses mcp.json)

## 1.3.12

- 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

- Status command shows resource names instead of counts
- Better formatting for installed commands, skills, and MCPs

## 1.3.0

- Added Agent Skills support (SKILL.md + rules/)
- Skills validation with metadata requirements
- Central skills directory at ~/.agents/skills/

## 1.2.0

- Added hooks support for Claude and Gemini
- Hook discovery from hooks/ directory
- Project-scope hooks support

## 1.1.0

- Added MCP server registration
- Support for stdio and http transports
- Per-agent MCP configuration

## 1.0.0

- Initial release
- Pull/push commands for syncing agent configurations
- Slash command management
- Multi-agent support (Claude, Codex, Gemini, Cursor, OpenCode)
