@phnx-labs/agents-cli 1.20.63 → 1.20.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,12 +1,60 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.20.65
4
+
5
+ - **`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`.
6
+
7
+ - **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`.
8
+
9
+ - **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`.
10
+
11
+ - **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`.
12
+
13
+ - **`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`.
14
+
15
+ - **`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`).
16
+
17
+ - **`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`).
18
+
19
+ - **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`.
20
+ - **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`.
21
+ - **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`.
22
+
23
+ - **`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`.
24
+
25
+ - **`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`.
26
+
27
+ - **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`).
28
+
29
+ - **`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`).
30
+
31
+ - **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`).
32
+
33
+ - **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`.
34
+
35
+ - **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`.
36
+
37
+ ## 1.20.64
38
+
39
+ - **`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`.
40
+ - **`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`.
41
+ - **`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`.
42
+ - **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`.
43
+ - **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`.
44
+ - **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`.
45
+ - **`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`.
46
+ - **`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`.
47
+ - **`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`.
48
+ - **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`.
49
+ - **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`.
50
+ - **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`.
4
51
 
5
52
  ## 1.20.63
6
53
 
7
54
  - **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`.
8
55
 
9
56
  - **`--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`.
57
+ - **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`.
10
58
  - **Signed webhook ingress for routines via Tailscale Funnel (RUSH-1456, RUSH-1459, RUSH-1460, RUSH-1461).**
11
59
  Routine triggers now understand both GitHub and Linear event sources, including
12
60
  Linear action/team/label filters. `agents webhook serve --secrets-bundle <name>`
@@ -18,6 +66,7 @@
18
66
  `apps/cli/src/lib/routines.ts`, `apps/cli/src/lib/triggers/webhook.ts`,
19
67
  `apps/cli/src/commands/routines.ts`, `apps/cli/src/commands/webhook.ts`,
20
68
  `apps/cli/src/commands/funnel.ts`, `apps/cli/src/lib/funnel.ts`.
69
+ - **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`.
21
70
  - **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`.
22
71
  - **`agents fleet` alias + fleet-wide rollout (RUSH-1632).** `fleet` is an alias
23
72
  for `devices`. New subcommands `update [version]` and `run <cmd…>` roll out
@@ -64,6 +113,7 @@
64
113
  - **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`.
65
114
 
66
115
  ## 1.20.60
116
+
67
117
  - **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`.
68
118
  - **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`.
69
119
  - **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`.
@@ -115,6 +165,7 @@
115
165
 
116
166
  - **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`.
117
167
  - **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`.
168
+
118
169
  ## 1.20.55
119
170
 
120
171
  - **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`.
@@ -247,25 +298,27 @@
247
298
  - **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`.
248
299
 
249
300
  - **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`.
301
+
250
302
  ## 1.20.47
251
303
 
252
304
  - **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`.
253
305
  - **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`.
254
306
  - **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`.
255
307
 
256
- ## 1.20.45
257
-
258
308
  ## 1.20.46
259
309
 
260
310
  - **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`.
261
311
  - **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).
312
+
262
313
  ## 1.20.45
314
+
263
315
  - **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`.
264
316
  - **`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`.
265
317
  - **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`.
266
318
  - **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`.
267
319
 
268
320
  ## 1.20.44
321
+
269
322
  - **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.
270
323
  - **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`.
271
324
  - **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`.
package/README.md CHANGED
@@ -52,6 +52,7 @@ Also available as `ag` -- all commands work with both `agents` and `ag`.
52
52
  - [Run any agent](#run-any-agent)
53
53
  - [Sessions across agents](#sessions-across-agents)
54
54
  - [Control the fleet](#control-the-fleet)
55
+ - [Sync the fleet](#sync-the-fleet)
55
56
  - [Run open models through Claude Code](#run-open-models-through-claude-code)
56
57
  - [Teams](#teams)
57
58
  - [Cloud](#cloud)
@@ -295,6 +296,34 @@ agents watchdog --watch # daemon loop: a tick every --interval
295
296
 
296
297
  ---
297
298
 
299
+ ## Sync the fleet
300
+
301
+ One machine is set up the way you like it. Make every other machine match -- same agents installed, same config, logins seeded -- in one command.
302
+
303
+ ```yaml
304
+ # agents.yaml -- add a fleet: block
305
+ fleet:
306
+ devices: all # every online registered device (minus this one)
307
+ defaults:
308
+ agents: [claude@latest, codex@latest, gemini@latest]
309
+ sync: [user] # config scopes to reconcile
310
+ login: sync # propagate logins where the token is portable
311
+ ```
312
+
313
+ ```bash
314
+ agents apply --plan # device x dimension matrix; changes nothing
315
+ agents apply # reconcile the fleet (confirms first; -y to skip)
316
+ agents apply --device yosemite-s0 # scope to one device
317
+ agents apply --only agents,config # limit dimensions (agents, config, login)
318
+ agents apply --no-login # skip login propagation
319
+ ```
320
+
321
+ `agents apply` (`ag apply`) probes every target over the existing SSH transport, then reconciles it to the profile: installs missing agents, upgrades `agents-cli`, syncs the named config scopes, and **propagates logins** so a host signed in once seeds the fleet -- turning "6 hosts x 8 harnesses = 48 OAuth flows" into one. Portable credential files (claude, codex, gemini, grok, kimi, opencode, droid, antigravity) stream to each target over encrypted SSH stdin, never shell-interpolated, and land at `0600`. **Honest boundary:** macOS keychain-bound tokens (claude, antigravity on a Mac target) can't be extracted -- those surface as a one-time manual login, never faked. `--plan` / `--dry-run` shows the full matrix without touching anything.
322
+
323
+ See [docs/fleet.md](apps/cli/docs/fleet.md) for the manifest schema and reconcile semantics.
324
+
325
+ ---
326
+
298
327
  ## Run open models through Claude Code (experimental)
299
328
 
300
329
  > **Note:** Profiles are experimental, but available by default — no enable step needed.
@@ -361,11 +390,26 @@ agents doctor --device mac-mini # same matrix, scoped to one device
361
390
 
362
391
  # Your Tailscale fleet, auto-discovered
363
392
  agents devices sync # ingest `tailscale status`
393
+ agents devices list # fleet + live headroom: load, mem, idle/busy — which box has room
394
+ agents devices list --full # add per-device cores and free/total RAM
395
+ agents devices list --no-stats # instant: names/addresses only, skip the probe
364
396
  agents ssh mac-mini # hardened SSH: fails fast if offline,
365
397
  # PowerShell on Windows, password-from-Keychain
398
+ agents hosts list # devices show up here too (one host pool)
399
+ agents hosts add mac-mini --cap gpu # tag a device for capability routing (--host gpu)
400
+
401
+ # Hosts as a task backend + scheduled placement
402
+ agents cloud run "nightly benchmark" --host gpu-box --agent claude # task in cloud ps AND hosts ps
403
+ agents routines add nightly -s "0 2 * * *" -a claude -p "run the sweep" --run-on gpu-box
366
404
  ```
367
405
 
368
- **Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH. See [docs/00-concepts.md](apps/cli/docs/00-concepts.md#devices--hosts).
406
+ `agents devices list` probes every reachable box in parallel (bounded timeout, so a
407
+ slow node degrades to `—` instead of hanging) and shows normalized load, memory
408
+ pressure, and an idle/light/busy/loaded headroom badge, plus a fleet-capacity summary
409
+ (`164 cores · 421G free / 518G RAM`). It answers "which machine has room right now?" —
410
+ the utilization signal the teammate scheduler doesn't yet see.
411
+
412
+ **Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH and feed one host pool: devices appear in `agents hosts list` and capability routing without a second enrollment. On `--host` runs every `agents run` option is either forwarded (`--effort --env --timeout --loop …`), rejected loud (`--secrets` never crosses SSH implicitly), or consumed locally — nothing silently drops. See [docs/00-concepts.md](apps/cli/docs/00-concepts.md#devices--hosts).
369
413
 
370
414
  Every `--host` command rides one multiplexed SSH engine, tuned for driving a fleet from a small laptop: the first call to a machine opens a control socket and every later call reuses it (no repeat TCP+auth handshake), connections carry keepalive so a dropped link dies in ~45 s instead of zombying, and following a remote run polls in a single round-trip per cycle. Measured against a Tailscale-relayed host: repeated calls **~6–7× faster**, dispatch readiness **~2×**, and the follow loop **~21× faster with 50% fewer local ssh spawns**. Design: [docs/09-ssh-transport.md](apps/cli/docs/09-ssh-transport.md) · reproduce: `node scripts/bench-ssh.mjs <host>`.
371
415
 
@@ -393,7 +437,7 @@ Team state is observable via `agents teams list --json` / `agents teams status -
393
437
 
394
438
  ## Cloud
395
439
 
396
- Some work shouldn't tie up your laptop. `agents cloud run` hands a task to a managed provider that clones the repo, plans, implements, tests, and opens a PR -- while your terminal stays free.
440
+ Some work shouldn't tie up your laptop. `agents cloud run` hands a task to a managed provider that clones the repo, plans, implements, tests, and opens a PR -- while your terminal stays free. A fifth provider, `host`, dispatches the same way onto machines you own: `agents cloud run "…" --host gpu-box` (tasks track in `agents cloud ps` and `agents hosts ps` alike).
397
441
 
398
442
  <p align="center">
399
443
  <img src="assets/cloud.svg" alt="agents cloud run dispatches one prompt to a managed provider (Rush, Codex, Factory, or Antigravity) that clones, plans, tests, and opens a pull request while you keep working" width="100%" />
package/dist/bin/agents CHANGED
Binary file
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `agents apply` (alias `ag apply`) — reconcile the whole fleet to a declared
3
+ * profile in one command: install agents-cli + agents, sync config, and
4
+ * propagate login so a machine that's signed in once seeds every device. Kills
5
+ * the "6 hosts x ~8 harnesses = ~48 OAuth flows" slog.
6
+ *
7
+ * The manifest is the `fleet:` block of any `-f` file (default `agents.yaml`).
8
+ */
9
+ import { Command } from 'commander';
10
+ /** padEnd on the visible width, ignoring chalk color codes. Exported for tests. */
11
+ export declare function stripPad(s: string, width: number): string;
12
+ export declare function registerApplyCommand(program: Command): void;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `agents apply` (alias `ag apply`) — reconcile the whole fleet to a declared
3
+ * profile in one command: install agents-cli + agents, sync config, and
4
+ * propagate login so a machine that's signed in once seeds every device. Kills
5
+ * the "6 hosts x ~8 harnesses = ~48 OAuth flows" slog.
6
+ *
7
+ * The manifest is the `fleet:` block of any `-f` file (default `agents.yaml`).
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as os from 'os';
11
+ import * as path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { Option } from 'commander';
14
+ import chalk from 'chalk';
15
+ import { setHelpSections } from '../lib/help.js';
16
+ import { machineId } from '../lib/session/sync/config.js';
17
+ import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
18
+ import { readFleetFile, resolveDesired } from '../lib/fleet/manifest.js';
19
+ import { snapshotAuth, materializeAuth, parseAuthBundle, KEYCHAIN_BOUND_ON_MAC } from '../lib/fleet/auth-sync.js';
20
+ import { agentIdOf, diffFleet, probeDevice, runFleetApply, pool, sourceHome, } from '../lib/fleet/apply.js';
21
+ /** Version of the running agents-cli — the fleet target version. */
22
+ function localCliVersion() {
23
+ try {
24
+ const here = path.dirname(fileURLToPath(import.meta.url));
25
+ const pkg = JSON.parse(fs.readFileSync(path.join(here, '..', '..', 'package.json'), 'utf-8'));
26
+ return String(pkg.version ?? '');
27
+ }
28
+ catch {
29
+ return '';
30
+ }
31
+ }
32
+ async function readStdin() {
33
+ const chunks = [];
34
+ for await (const c of process.stdin)
35
+ chunks.push(c);
36
+ return Buffer.concat(chunks).toString('utf-8');
37
+ }
38
+ /** Hidden path: receive an auth bundle on stdin and materialize it locally. */
39
+ async function runRecvAuth() {
40
+ const raw = await readStdin();
41
+ const bundle = parseAuthBundle(raw);
42
+ const res = materializeAuth(bundle, { home: os.homedir() });
43
+ if (res.errors.length > 0) {
44
+ console.error(`recv-auth: ${res.errors.length} error(s): ${res.errors.join('; ')}`);
45
+ process.exit(1);
46
+ }
47
+ console.log(`recv-auth: wrote login for ${res.written.join(', ') || '(nothing)'}`);
48
+ }
49
+ function confirm(question) {
50
+ return new Promise((resolve) => {
51
+ process.stdout.write(question);
52
+ const onData = (d) => {
53
+ process.stdin.pause();
54
+ process.stdin.off('data', onData);
55
+ resolve(/^y(es)?$/i.test(d.toString().trim()));
56
+ };
57
+ process.stdin.resume();
58
+ process.stdin.once('data', onData);
59
+ });
60
+ }
61
+ const ONLY_KINDS = {
62
+ agents: new Set(['install-cli', 'upgrade-cli', 'add-agent']),
63
+ config: new Set(['sync-config']),
64
+ login: new Set(['push-login', 'needs-login']),
65
+ };
66
+ /** Render the device x dimension matrix (cribbed from `doctor --devices`). */
67
+ function renderPlan(plan) {
68
+ const rows = plan.devices;
69
+ const nameWidth = Math.max('device'.length, ...rows.map((r) => r.device.length));
70
+ const cell = (row, kinds, okLabel) => {
71
+ if (!row.probe.reachable)
72
+ return chalk.gray('- offline');
73
+ const acts = row.actions.filter((a) => kinds.includes(a.kind));
74
+ if (acts.length === 0)
75
+ return chalk.green(`ok ${okLabel}`);
76
+ if (acts.some((a) => a.kind === 'needs-login')) {
77
+ const push = acts.filter((a) => a.kind === 'push-login').length;
78
+ const need = acts.filter((a) => a.kind === 'needs-login').length;
79
+ return chalk.yellow(`${push} push · ${need} manual`);
80
+ }
81
+ return chalk.cyan('↑ ' + acts.map((a) => a.agent ?? a.kind.replace('-cli', '')).join(','));
82
+ };
83
+ const header = ` ${'device'.padEnd(nameWidth)} ${'agents-cli'.padEnd(12)}${'agents'.padEnd(20)}${'config'.padEnd(10)}login`;
84
+ console.log(chalk.gray(header));
85
+ for (const row of rows) {
86
+ const cli = row.probe.reachable
87
+ ? (row.actions.find((a) => a.kind === 'install-cli') ? chalk.cyan('install')
88
+ : row.actions.find((a) => a.kind === 'upgrade-cli') ? chalk.cyan('upgrade')
89
+ : chalk.green(`ok ${row.probe.cliVersion ?? ''}`))
90
+ : chalk.gray('- offline');
91
+ const agentsCell = row.probe.reachable
92
+ ? (() => {
93
+ const add = row.actions.filter((a) => a.kind === 'add-agent');
94
+ return add.length === 0 ? chalk.green(`ok ${row.desired.agents.length}/${row.desired.agents.length}`) : chalk.cyan('+ ' + add.map((a) => a.agent).join(','));
95
+ })()
96
+ : chalk.gray('-');
97
+ const configCell = row.probe.reachable
98
+ ? (row.actions.some((a) => a.kind === 'sync-config') ? chalk.cyan('↑ sync') : chalk.green('ok'))
99
+ : chalk.gray('-');
100
+ const loginCell = cell(row, ['push-login', 'needs-login'], `${row.desired.agents.length}/${row.desired.agents.length}`);
101
+ console.log(` ${row.device.padEnd(nameWidth)} ${stripPad(cli, 12)}${stripPad(agentsCell, 20)}${stripPad(configCell, 10)}${loginCell}`);
102
+ }
103
+ console.log();
104
+ console.log(chalk.gray(` ${plan.actions.length} action(s) across ${rows.filter((r) => r.probe.reachable).length} reachable device(s)`));
105
+ // Distinguish *why* a login can't be propagated: a macOS keychain-bound token
106
+ // vs. the source simply not being signed in to that agent (no portable file).
107
+ const bound = [];
108
+ const noToken = [];
109
+ for (const r of rows) {
110
+ for (const a of r.loginBlocked) {
111
+ const tag = `${a}@${r.device}`;
112
+ if (r.probe.platform === 'macos' && KEYCHAIN_BOUND_ON_MAC.has(a))
113
+ bound.push(tag);
114
+ else
115
+ noToken.push(tag);
116
+ }
117
+ }
118
+ if (bound.length > 0) {
119
+ console.log(chalk.yellow(` manual login needed (macOS keychain-bound): ${bound.join(', ')}`));
120
+ }
121
+ if (noToken.length > 0) {
122
+ console.log(chalk.yellow(` manual login needed (no portable token on source): ${noToken.join(', ')}`));
123
+ }
124
+ }
125
+ /** padEnd on the visible width, ignoring chalk color codes. Exported for tests. */
126
+ export function stripPad(s, width) {
127
+ // Strip the whole SGR sequence (ESC `[` ... `m`). Matching only the `[...m`
128
+ // tail leaves each leading ESC byte counted as visible, so every colored
129
+ // cell over-pads and the plan table misaligns in a real TTY.
130
+ // eslint-disable-next-line no-control-regex
131
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '').length;
132
+ return s + ' '.repeat(Math.max(1, width - visible));
133
+ }
134
+ async function runApply(opts) {
135
+ const file = opts.file ?? 'agents.yaml';
136
+ const manifest = readFleetFile(path.resolve(file));
137
+ const source = machineId();
138
+ const registry = await loadDevices();
139
+ // Control devices (a cockpit) never run agents — exclude them from the
140
+ // reconcile set entirely so `agents apply` doesn't try to install/sync/login
141
+ // onto a paired phone.
142
+ const all = Object.values(registry).filter((d) => !isControlDevice(d));
143
+ const online = all.filter((d) => d.tailscale?.online === true).map((d) => d.name);
144
+ const registered = all.map((d) => d.name);
145
+ let desired = resolveDesired(manifest, { onlineDevices: online, registeredDevices: registered, source });
146
+ if (opts.device) {
147
+ desired = desired.filter((d) => d.device === opts.device);
148
+ if (desired.length === 0)
149
+ throw new Error(`Device '${opts.device}' is not a target in this manifest.`);
150
+ }
151
+ if (opts.login === false)
152
+ desired = desired.map((d) => ({ ...d, login: 'skip' }));
153
+ if (desired.length === 0) {
154
+ console.log(chalk.gray('No target devices — nothing to apply.'));
155
+ return;
156
+ }
157
+ // Snapshot source auth once for every agent named anywhere in the profile.
158
+ const allAgents = [...new Set(desired.flatMap((d) => d.agents.map(agentIdOf)))];
159
+ const snap = snapshotAuth(allAgents, { home: sourceHome(), platform: process.platform });
160
+ const filesByAgent = new Map();
161
+ for (const f of snap.files) {
162
+ const arr = filesByAgent.get(f.agent) ?? [];
163
+ arr.push(f);
164
+ filesByAgent.set(f.agent, arr);
165
+ }
166
+ const sourceAuth = {
167
+ available: new Set(snap.files.map((f) => f.agent)),
168
+ bound: new Set(snap.bound),
169
+ filesByAgent,
170
+ };
171
+ // Probe every target device in parallel.
172
+ const nameToProfile = new Map(desired.map((d) => [d.device, registry[d.device]]));
173
+ console.log(chalk.gray(`Probing ${desired.length} device(s)…`));
174
+ const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device)));
175
+ const probes = new Map(probeList.map((p) => [p.device, p]));
176
+ const targetCliVersion = localCliVersion();
177
+ let plan = diffFleet(desired, probes, { targetCliVersion, sourceAuth });
178
+ // --only filter.
179
+ if (opts.only) {
180
+ const keep = new Set();
181
+ for (const k of opts.only.split(',').map((s) => s.trim()))
182
+ for (const x of ONLY_KINDS[k] ?? [])
183
+ keep.add(x);
184
+ plan = {
185
+ devices: plan.devices.map((r) => ({ ...r, actions: r.actions.filter((a) => keep.has(a.kind)) })),
186
+ actions: plan.actions.filter((a) => keep.has(a.kind)),
187
+ };
188
+ }
189
+ console.log();
190
+ console.log(chalk.bold(`Fleet profile · ${desired.length} device(s) · ${allAgents.length} agent(s) (${allAgents.join(', ')})`));
191
+ renderPlan(plan);
192
+ const isDry = opts.plan || opts.dryRun;
193
+ if (isDry)
194
+ return;
195
+ if (plan.actions.filter((a) => a.kind !== 'needs-login').length === 0) {
196
+ console.log(chalk.green('\nNothing to do — fleet already matches the profile.'));
197
+ return;
198
+ }
199
+ if (!opts.yes) {
200
+ const ok = await confirm(chalk.bold(`\nApply this plan? (${plan.actions.length} action(s)) [y/N] `));
201
+ if (!ok) {
202
+ console.log(chalk.gray('Aborted.'));
203
+ return;
204
+ }
205
+ }
206
+ console.log();
207
+ const results = await runFleetApply(plan.devices, nameToProfile, {
208
+ targetCliVersion,
209
+ source,
210
+ sourceAuth,
211
+ });
212
+ reportResults(results);
213
+ }
214
+ function reportResults(results) {
215
+ let failures = 0;
216
+ for (const r of results) {
217
+ if (r.note && r.steps.length === 0) {
218
+ console.log(` ${chalk.gray(r.device.padEnd(16))} ${chalk.yellow('skipped')} ${chalk.gray(r.note)}`);
219
+ continue;
220
+ }
221
+ const badge = r.ok ? chalk.green('ok ') : chalk.red('fail');
222
+ if (!r.ok)
223
+ failures++;
224
+ const summary = r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.kind}`).join(' ');
225
+ console.log(` ${chalk.bold(r.device.padEnd(16))} ${badge} ${chalk.gray(summary)}`);
226
+ for (const s of r.steps.filter((x) => x.kind === 'needs-login')) {
227
+ console.log(` ${chalk.yellow('→')} ${chalk.gray(s.detail)}`);
228
+ }
229
+ }
230
+ console.log();
231
+ if (failures > 0) {
232
+ console.error(chalk.red(`${failures} device(s) had failures.`));
233
+ process.exit(1);
234
+ }
235
+ console.log(chalk.green('Fleet reconciled.'));
236
+ }
237
+ export function registerApplyCommand(program) {
238
+ const applyCmd = program
239
+ .command('apply')
240
+ .description('Reconcile the fleet to a declared profile: install agents, sync config, propagate login.')
241
+ .option('-f, --file <path>', 'Manifest file carrying a fleet: block (default: agents.yaml)')
242
+ .option('--plan', 'Show the reconcile plan and exit (no changes)')
243
+ .option('--dry-run', 'Alias for --plan')
244
+ .option('-y, --yes', 'Skip the confirmation prompt')
245
+ .option('--device <name>', 'Scope the apply to a single device')
246
+ .option('--only <dims>', 'Limit to dimensions: comma list of agents,config,login')
247
+ .option('--no-login', 'Do not propagate logins')
248
+ .addOption(new Option('--recv-auth', 'internal: receive an auth bundle on stdin').hideHelp())
249
+ .action(async (opts) => {
250
+ try {
251
+ if (opts.recvAuth) {
252
+ await runRecvAuth();
253
+ return;
254
+ }
255
+ await runApply(opts);
256
+ }
257
+ catch (e) {
258
+ console.error(chalk.red(e.message));
259
+ process.exit(1);
260
+ }
261
+ });
262
+ setHelpSections(applyCmd, {
263
+ examples: `
264
+ # Preview what would change across the fleet
265
+ agents apply --plan -f agents.yaml
266
+
267
+ # Bring every device to the profile (installs, syncs, propagates login)
268
+ ag apply -f agents.yaml
269
+
270
+ # One device only
271
+ agents apply --device yosemite-s1 -y
272
+ `,
273
+ });
274
+ }