@phnx-labs/agents-cli 1.22.26 → 1.22.27

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 (131) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/README.md +18 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/bench.d.ts +2 -0
  5. package/dist/commands/bench.js +101 -0
  6. package/dist/commands/doctor.js +24 -10
  7. package/dist/commands/exec.js +17 -16
  8. package/dist/commands/insights.js +39 -5
  9. package/dist/commands/output.js +100 -25
  10. package/dist/commands/resume.d.ts +11 -0
  11. package/dist/commands/resume.js +51 -0
  12. package/dist/commands/sessions-picker.js +12 -7
  13. package/dist/commands/sessions.d.ts +47 -1
  14. package/dist/commands/sessions.js +345 -37
  15. package/dist/commands/ssh.js +184 -11
  16. package/dist/commands/teams.js +3 -3
  17. package/dist/index.js +16 -20
  18. package/dist/lib/auth-health.d.ts +47 -3
  19. package/dist/lib/auth-health.js +74 -15
  20. package/dist/lib/bench/index.d.ts +4 -0
  21. package/dist/lib/bench/index.js +4 -0
  22. package/dist/lib/bench/runner.d.ts +16 -0
  23. package/dist/lib/bench/runner.js +111 -0
  24. package/dist/lib/bench/schema.d.ts +5 -0
  25. package/dist/lib/bench/schema.js +91 -0
  26. package/dist/lib/bench/storage.d.ts +5 -0
  27. package/dist/lib/bench/storage.js +32 -0
  28. package/dist/lib/bench/types.d.ts +40 -0
  29. package/dist/lib/bench/types.js +1 -0
  30. package/dist/lib/claude-account-token.d.ts +2 -0
  31. package/dist/lib/claude-account-token.js +27 -1
  32. package/dist/lib/crabbox/cli.d.ts +2 -0
  33. package/dist/lib/crabbox/cli.js +2 -0
  34. package/dist/lib/crabbox/lease.js +7 -1
  35. package/dist/lib/daemon.js +20 -0
  36. package/dist/lib/devices/connect.d.ts +2 -0
  37. package/dist/lib/devices/connect.js +7 -0
  38. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  39. package/dist/lib/devices/doctor-findings.js +15 -1
  40. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  41. package/dist/lib/devices/harness-inventory.js +0 -0
  42. package/dist/lib/devices/registry.d.ts +2 -0
  43. package/dist/lib/devices/ssh-config.js +3 -0
  44. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  45. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  46. package/dist/lib/exec.bench.d.ts +1 -0
  47. package/dist/lib/exec.bench.js +186 -0
  48. package/dist/lib/exec.js +18 -8
  49. package/dist/lib/fleet/apply.js +9 -7
  50. package/dist/lib/fleet/remote-login.d.ts +4 -3
  51. package/dist/lib/fleet/remote-login.js +11 -9
  52. package/dist/lib/gemini-settings.d.ts +0 -1
  53. package/dist/lib/gemini-settings.js +12 -7
  54. package/dist/lib/hooks.d.ts +8 -5
  55. package/dist/lib/hooks.js +10 -6
  56. package/dist/lib/hosts/dispatch.js +7 -3
  57. package/dist/lib/hosts/passthrough.d.ts +22 -0
  58. package/dist/lib/hosts/passthrough.js +14 -8
  59. package/dist/lib/hosts/progress.d.ts +2 -1
  60. package/dist/lib/hosts/progress.js +4 -3
  61. package/dist/lib/hosts/providers/devices.js +1 -0
  62. package/dist/lib/hosts/ready.d.ts +4 -2
  63. package/dist/lib/hosts/ready.js +24 -8
  64. package/dist/lib/hosts/reconcile.d.ts +1 -1
  65. package/dist/lib/hosts/reconcile.js +16 -7
  66. package/dist/lib/hosts/reconnect.d.ts +45 -12
  67. package/dist/lib/hosts/reconnect.js +90 -34
  68. package/dist/lib/hosts/registry.d.ts +2 -2
  69. package/dist/lib/hosts/registry.js +3 -5
  70. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  71. package/dist/lib/hosts/remote-cmd.js +29 -0
  72. package/dist/lib/hosts/tasks.d.ts +2 -0
  73. package/dist/lib/hosts/types.d.ts +1 -0
  74. package/dist/lib/hosts/types.js +3 -0
  75. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  76. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  77. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  78. package/dist/lib/menubar/install-menubar.js +20 -9
  79. package/dist/lib/models.d.ts +30 -1
  80. package/dist/lib/models.js +42 -144
  81. package/dist/lib/pricing/cost.d.ts +9 -0
  82. package/dist/lib/pricing/cost.js +24 -0
  83. package/dist/lib/pricing/index.d.ts +1 -1
  84. package/dist/lib/pricing/index.js +1 -1
  85. package/dist/lib/redact.js +8 -3
  86. package/dist/lib/remote-agents-json.d.ts +32 -0
  87. package/dist/lib/remote-agents-json.js +47 -16
  88. package/dist/lib/resource-profiles.js +1 -2
  89. package/dist/lib/routine-notify-owner.d.ts +102 -0
  90. package/dist/lib/routine-notify-owner.js +232 -0
  91. package/dist/lib/routines.d.ts +10 -0
  92. package/dist/lib/runner.d.ts +9 -1
  93. package/dist/lib/runner.js +182 -17
  94. package/dist/lib/sandbox.d.ts +0 -2
  95. package/dist/lib/sandbox.js +2 -19
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.js +3 -2
  99. package/dist/lib/secrets/lease.d.ts +25 -0
  100. package/dist/lib/secrets/lease.js +44 -0
  101. package/dist/lib/self-update.d.ts +20 -5
  102. package/dist/lib/self-update.js +93 -16
  103. package/dist/lib/session/db.d.ts +19 -2
  104. package/dist/lib/session/db.js +54 -5
  105. package/dist/lib/session/discover.d.ts +21 -1
  106. package/dist/lib/session/discover.js +124 -32
  107. package/dist/lib/session/insights.d.ts +19 -0
  108. package/dist/lib/session/insights.js +78 -10
  109. package/dist/lib/session/recovery.d.ts +17 -1
  110. package/dist/lib/session/recovery.js +111 -4
  111. package/dist/lib/session/remote-list.d.ts +10 -9
  112. package/dist/lib/session/remote-list.js +25 -23
  113. package/dist/lib/session/resume-owner.d.ts +55 -0
  114. package/dist/lib/session/resume-owner.js +69 -0
  115. package/dist/lib/session/team-filter.d.ts +65 -0
  116. package/dist/lib/session/team-filter.js +98 -3
  117. package/dist/lib/session/types.d.ts +33 -0
  118. package/dist/lib/smart-launch.d.ts +31 -5
  119. package/dist/lib/smart-launch.js +43 -7
  120. package/dist/lib/ssh-exec.d.ts +27 -0
  121. package/dist/lib/ssh-exec.js +34 -1
  122. package/dist/lib/ssh-tunnel.d.ts +3 -2
  123. package/dist/lib/ssh-tunnel.js +25 -16
  124. package/dist/lib/startup/command-registry.d.ts +1 -0
  125. package/dist/lib/startup/command-registry.js +2 -0
  126. package/dist/lib/teams/placement-probe.d.ts +1 -1
  127. package/dist/lib/teams/placement-probe.js +24 -24
  128. package/dist/lib/tmux/session.d.ts +8 -0
  129. package/dist/lib/tmux/session.js +22 -0
  130. package/dist/lib/types.d.ts +2 -0
  131. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,249 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.27
4
+
5
+ - **`agents sessions --teams` groups sessions by team (RUSH-1997).** Instead of one flat list with a `[team/handle]` tag, `--teams` now prints a report grouped by team: each team names its spawner (the orchestrator session that created it) and spawn time, and every teammate row shows its mode + handle. Team-flagged spawns that carry no teammate `meta.json` — headless `agents run` sessions, or teammates whose team record aged out — sink into a trailing `(no team)` bucket, so a real `agents teams` teammate and a bare SDK spawn are never shown as the same thing. `--teams --flat`/`--tree` keep the plain inline table, and a search query keeps the interactive picker. `TeamOrigin` (also on `--teams --json`) now carries the teammate's `startedAt` spawn time and a `source` (`meta` = teammate, `entrypoint` = bare spawn). Source: `apps/cli/src/lib/session/team-filter.ts`, `apps/cli/src/commands/sessions.ts`.
6
+
7
+ - **`agents run --device auto` and `agents teams add --device auto` now choose from live fleet health instead of 14-day launch affinity (RUSH-2001).** The picker probes candidates in parallel, excludes unreachable, overloaded, and missing-agent machines, prefers signed-in candidates with the lowest normalized load, and keeps execution local when no remote is better. Remote readiness now uses `agents view --json`, preserving the installed/sign-in split instead of treating every remote sign-in state as unknown. Source: `apps/cli/src/lib/smart-launch.ts`, `apps/cli/src/lib/teams/placement-probe.ts`, `apps/cli/src/lib/hosts/ready.ts`, `apps/cli/src/commands/exec.ts`, `apps/cli/src/commands/teams.ts`.
8
+
9
+ - **`agents devices harnesses` / `agents devices accounts` — per-device harness + account readiness (RUSH-2003).** Two new fleet lenses: `harnesses` lists every installed `agent@version` across the fleet with its account, signed-in state, quota (highest usage-window utilization; `*` = cached snapshot), and a single `ready` verdict (signed in AND not rate-limited); `accounts` collapses that to one row per account, naming which harnesses share it. Both SSH-probe each online device (bounded like `fleet ping`, so one unreachable box can't stall the glance) and read the daemon-warmed usage cache — `--refresh` (`--live`) forces a live quota read. Scope with `--agents <csv>` / `--device <csv>`; `--json` emits per-host rows (harnesses) or account groups (accounts). Source: `apps/cli/src/lib/devices/harness-inventory.ts`, `apps/cli/src/commands/ssh.ts`.
10
+
11
+ - **A session that ran on another device now resumes ON that device, and a typo'd
12
+ command with `--host` says `unknown command` (RUSH-2022).** Two bugs found while
13
+ recovering ~15 sessions after a machine crash, both of which sent recovery down the
14
+ wrong path.
15
+
16
+ (1) **`agents resume <id>` restarted a remote session locally.** The harness keeps its
17
+ conversation state on the machine that produced the session, but nothing checked which
18
+ machine that was — so a peer-owned session started the agent here, against state this
19
+ box had never seen (`sessions-resume.ts` even swapped in `process.cwd()` when the
20
+ recorded directory did not exist locally). `agents resume` now re-runs itself on the
21
+ owning device over SSH; `--here` overrides. The bare `agents sessions` picker routes
22
+ the same way, and `agents sessions attach` hops as an **attach** (its detach record and
23
+ the headless process it stops both live on the owner). The multi-select
24
+ `agents sessions resume` inherits it: each tab it opens runs the canonical
25
+ `agents resume <id>`, which now routes itself, and its no-tab-backend path routes
26
+ explicitly. Root cause of
27
+ the population that made this common: a run dispatched with `agents run --device
28
+ <box>` was indexed with no origin machine at all, so the index claimed the dispatching
29
+ box; it now records `<box>`, which also means such a run finally shows up under
30
+ `agents sessions --host <box>`. Source: `apps/cli/src/lib/session/resume-owner.ts`,
31
+ `apps/cli/src/lib/hosts/session-index.ts`, `apps/cli/src/commands/resume.ts`.
32
+
33
+ The hop carries its "don't route again" pin as an exported env var, not a flag, so it
34
+ works against a peer still on an older CLI. Sessions indexed *before* this release keep
35
+ their old machine tag — re-dispatch or a fresh scan corrects them.
36
+
37
+ (2) **The `--host`/`--device` router answered for commands that do not exist.** It runs
38
+ before commander parses, so `agents session resume --host <box>` (one letter off
39
+ `sessions`, which *does* accept `--host`) reported `` `agents session` does not support
40
+ --host/--device `` — a true statement about a command nobody typed and the opposite of
41
+ the truth for the one they meant. Unknown names now fall through to `unknown command
42
+ '<name>'` with a did-you-mean, and the spellcheck can suggest the lazily-registered
43
+ groups (`sessions`/`teams`/`cloud`/…) it previously could not see. A real command with
44
+ no remote semantics still gets the flag-support error. Source:
45
+ `apps/cli/src/lib/hosts/passthrough.ts`, `apps/cli/src/lib/startup/command-registry.ts`.
46
+
47
+ - **`agents publish` is in the lazy command table.** `commands/packages.ts` registers it at
48
+ top level but the registry did not list it, so it only resolved through the
49
+ unknown-command fallback that loads the whole command tree. Found by the new test that
50
+ pins the command-name set against the real tree.
51
+
52
+ - **A mistyped command keeps its `--host`.** The distance-1 auto-correct now runs *before*
53
+ the router instead of after commander gave up, so `agents docto --host <box>` corrects to
54
+ `doctor` **and** runs on `<box>` — previously the corrected command re-parsed locally with
55
+ a `--host` it did not accept. Four routing-table entries naming commands that do not exist
56
+ (`cli`, `packages`, `versions`, `daemon`) were removed; a test now keeps both routing
57
+ tables to real command names.
58
+
59
+ - **Cross-machine `agents … --json` fan-out caps each peer's stdout at 16 MiB
60
+ instead of buffering it unbounded (RUSH-2065).** The shared fan-out
61
+ (`gatherRemoteAgentsJson`, behind `agents sessions --active`, `agents feed`, and
62
+ every other fleet-wide JSON sweep) streamed each peer's output into memory with
63
+ no ceiling, under one `Promise.all` — so a single peer returning a corrupt or
64
+ pathologically large payload could retain ~170 MB and OOM the whole sweep. Each
65
+ peer's capture now stops and SIGKILLs the connection once it would exceed the
66
+ ceiling, treating that box as unreachable (reported in `skipped`) so the rest of
67
+ the fleet still renders. The bound and the UTF-8-safe accumulator now live once
68
+ in `apps/cli/src/lib/ssh-exec.ts`, shared with the `agents sessions` browse
69
+ fan-out that already had the guard. Source: `apps/cli/src/lib/remote-agents-json.ts`,
70
+ `apps/cli/src/lib/ssh-exec.ts`, `apps/cli/src/lib/session/remote-list.ts`.
71
+
72
+ - **Auth-health probes once per account, not once per version home (RUSH-2111).**
73
+ The daemon's every-3-minute auth-health refresh fanned `probeLocalFleetAuth`
74
+ over *every* installed version home at once, so a box with several Claude homes
75
+ signed into one account fired that many concurrent requests at the same
76
+ provider OAuth endpoint — racing its rate limit into a `429` that then parked
77
+ the whole box's usage reads behind a `Retry-After` penalty (`usage-backoff.ts`
78
+ survives that penalty; this removes its cause). Installs are now grouped by
79
+ account and the live probe runs once per (agent, account), fanning the one
80
+ verdict out to each home's per-version cache row. Homes with no resolvable
81
+ account are still probed individually. Source: `apps/cli/src/lib/auth-health.ts`.
82
+
83
+ - **Fallback-chain agents now receive their own active rules preset before dispatch (RUSH-2129).** `runWithFallback` resolves each attempted entry's harness/version home and runs the same skip-fast preset synchronization as the primary `agents run` path, so a rate-limit handoff cannot launch against stale rules. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/rules/run-sync.ts`.
84
+
85
+ - **Gemini hard-deprecation: routines daemon can no longer execute a legacy Gemini routine (RUSH-2202).** RUSH-2060 gated `agents add`/`import`/`sync`/`run`/`routines add` against a hard-deprecated harness, but the routines daemon's own executor (`runner.ts`) had no equivalent gate — a Gemini routine written before RUSH-2060, or synced/edited on disk directly, would still fire and try to build a `gemini …` command against a backend Google retired. `executeJob`/`executeJobDetached` now reject a hard-deprecated agent up front, before any version/account resolution or sandbox prep, and record a `failed` run with the same deprecation message every other entry point already shows. Also removed the now-unreachable Gemini model-catalog extractor (`models.ts`), Factory's Gemini model-catalog fetch (`agentModels.ts`), and the dead Gemini sandbox-config writer (`sandbox.ts`/`gemini-settings.ts` — its generic JSON helpers stay, since Antigravity's permission writer reuses them), plus a stale `profiles.md` doc line still listing `gemini` as a live profile agent. Source: `apps/cli/src/lib/runner.ts`, `apps/cli/src/lib/models.ts`, `apps/cli/src/lib/sandbox.ts`, `apps/cli/src/lib/gemini-settings.ts`, `apps/factory/src/core/agentModels.ts`.
86
+
87
+ - **Routine transcripts now archive as `origin='routine'` sessions (RUSH-2271).** A
88
+ Claude (and Codex) routine writes its transcript to the per-version `CLAUDE_CONFIG_DIR`
89
+ / `CODEX_HOME` home, not the sandbox overlay the archiver scanned — so routine runs
90
+ were indexed as ordinary `origin='cli'` sessions and never linked to their routine or
91
+ run. `archiveRoutineTranscripts` now reads the same per-version home `buildExecEnv`
92
+ writes to (re-pointed to each failover attempt's account as the chain advances), scoped
93
+ by a pre-spawn baseline so it copies only that run's transcript out of the shared home,
94
+ and `agents sessions --routine` shows them again. Kimi relocates too but its
95
+ routine-archive discovery reader is a separate follow-up. Source:
96
+ `apps/cli/src/lib/runner.ts`, `apps/cli/src/lib/routines.ts`.
97
+
98
+ - **Release lease detects a holder killed from outside (RUSH-2274).** An externally
99
+ killed release (SIGKILL, a severed ssh, a rebooted box) left its lease on `origin`
100
+ and `scripts/release-lease.sh status` read `held` for up to the 30-minute TTL with
101
+ nothing actually releasing. The lease now records the holding `host`, `pid`, and
102
+ that pid's start time, and `status` reports `holder-alive=yes|no|unknown`. A holder
103
+ that is provably gone is reclaimed by the next `claim` immediately instead of
104
+ waiting out the TTL, and a new `release-lease.sh clear` drops such a lease without
105
+ starting a release. A live holder is never taken at any age, an unprobeable one
106
+ (another box, or a lease from an older release) still falls back to the TTL, and a
107
+ reused pid or an unreaped zombie counts as dead rather than as a live release.
108
+ Source: `apps/cli/scripts/release-lease.sh`, `apps/cli/scripts/release.sh`.
109
+
110
+ - **`agents output` and the session index no longer under-count Windows hosts
111
+ (RUSH-2286).** A Windows box could report zero token burn / zero sessions even
112
+ when it was actively used, because two per-harness scanners in
113
+ `session/discover.ts` failed on Windows: the OpenClaw scan gated on `which
114
+ openclaw`, which is POSIX-only (`which` throws ENOENT on Windows, so the whole
115
+ OpenClaw scan silently returned before indexing anything), and the Grok scanner
116
+ recovered a session's version from `summary.grok_home` with a `/`-only regex
117
+ that never matched a backslash-separated Windows path. The OpenClaw presence
118
+ check now uses the cross-platform `hasCommand`, its `openclaw` invocations route
119
+ through `execFileShellSpec` so a Windows `.cmd`/`.ps1` shim actually launches,
120
+ and the Grok version regex normalizes separators first. Separately, JSON relayed
121
+ from a Windows peer over SSH (`agents output --host <win> --json`,
122
+ `agents sessions … --json`) is now stripped of any PowerShell `#< CLIXML`
123
+ banner before parsing (`stripClixml` in `hosts/remote-cmd.ts`), so a fleet-wide
124
+ rollup that folds in a Windows box no longer drops it on a `JSON.parse` failure.
125
+ The banner strip is applied at every remote-`--json` boundary a Windows peer's
126
+ output flows through: the `remote-agents-json` fan-out, the session
127
+ `remote-list` list/payload/tool-search parsers, the `--host` fleet passthrough
128
+ (`agents view --host all`), and `agents output`'s per-device fetch.
129
+ Source: `apps/cli/src/lib/session/discover.ts`,
130
+ `apps/cli/src/lib/hosts/remote-cmd.ts`, `apps/cli/src/lib/hosts/passthrough.ts`,
131
+ `apps/cli/src/lib/remote-agents-json.ts`,
132
+ `apps/cli/src/lib/session/remote-list.ts`, `apps/cli/src/commands/output.ts`.
133
+
134
+ - **`agents output` now reports the burn split and a `--pricing no-cache` scenario (RUSH-2287).**
135
+ The productivity rollup collapsed token burn into a single counter. It now
136
+ breaks the burn into uncached **input** / **cache-read** / **cache-write** tokens
137
+ wherever the harness records a per-message cache split (Claude, Codex, Gemini,
138
+ Droid) — a `burn split:` line in the text report and the three counts on `burn`
139
+ and every `breakdown` row in `--json`. New `--pricing no-cache` reprices cached
140
+ tokens at the model's full input rate to model "what would this cost with caching
141
+ off?"; the text report leads with that figure (breakdown column `burn(nc)`) while
142
+ `--json` always carries **both** `costUsd` and `costUsdNoCache` so a dashboard can
143
+ choose. The saving is surfaced in actual mode too (`caching: actual $X vs no-cache
144
+ $Y`). Backed by four new session columns (`input_tokens`, `cache_read_tokens`,
145
+ `cache_write_tokens`, `cost_usd_nocache`, schema v37) populated at scan time;
146
+ pre-upgrade sessions show total-only until re-scanned. Source:
147
+ `apps/cli/src/commands/output.ts`, `apps/cli/src/lib/session/{db,discover}.ts`,
148
+ `apps/cli/src/lib/pricing/cost.ts`.
149
+
150
+ - **Routine failures now reach the owner's phone, not just the local desktop (RUSH-2288).**
151
+ A `failed`/`timeout` routine finish, or a pre-spawn failure such as `auth_failed`, now
152
+ also pings the owner over the same channel stack `agents notify` uses (the
153
+ `owner.channels` in `humans.yaml`, or the legacy `notify.owner`), delivered in-process by
154
+ the daemon — no shelling out to `ssh mac-mini agents notify`. This closes the gap where a
155
+ failed scheduled routine on a headless fleet box was invisible, and specifically covers
156
+ the `auth_failed` case the per-routine `agents notify` prompt can never send (its agent
157
+ never spawned). If the primary owner channel cannot deliver from the box, the daemon walks
158
+ the remaining configured channels as fallbacks (Telegram and intrusive/voice channels are
159
+ excluded). Green routines of any kind stay silent, the existing desktop thresholds are
160
+ unchanged, and delivery is deduped per job+runId. Source:
161
+ `apps/cli/src/lib/routine-notify-owner.ts`, `apps/cli/src/lib/daemon.ts`.
162
+
163
+ - Add `agents bench list`, `agents bench run`, and `agents bench results`: benchmark cells fan out through the existing `agents run` path with isolated fixture copies, bounded concurrency, custom harness names, wall-time/exit/token capture, and durable JSON results under `~/.agents/.history/bench/` (RUSH-2302, RUSH-2303).
164
+
165
+ - **New user quickstart: install, harnesses, teams, fleet.** `apps/cli/docs/QUICKSTART.md` walks a fresh install through `agents setup`, adding and logging into harnesses, a minimal `agents teams` smoke test, and setting up a fleet (`agents devices sync`, `agents devices set-interactive`, `agents apply`) — linked from `docs/README.md` and the root README's Quickstart. Docs only — no runtime change. Source: `apps/cli/docs/QUICKSTART.md`, `apps/cli/docs/README.md`, `README.md`.
166
+
167
+ - **`agents insights` detects agent silent stalls (model goes idle until you resume).**
168
+ When the assistant is last to speak and the next user message is ≥5 minutes later,
169
+ facets count duration-bucketed `silent stall: 5-15m` / `15-60m` / `1h+` friction
170
+ signals; resume nudges (`continue`, `keep going`, …) after that silence also count
171
+ as `resume after silent stall`. Report, actions, `--narrative`, and
172
+ `/sessions-insights` instruct models to call these out (not reframe as "user was
173
+ slow"). Extractor version bumped to 5 so cached facets recompute. Source:
174
+ `apps/cli/src/lib/session/insights.ts`, `commands/insights.ts`,
175
+ `docs/06-observability.md`.
176
+
177
+ - **`agents insights` splits silent stalls by harness and model.** The By-agent/account
178
+ table now shows per-group stall and resume-nudge counts (so laziness is visible without
179
+ `--json`). Stalls are also attributed to the model that last spoke
180
+ (`silentStallsByModel`, "Silent stalls by model" section). Extractor version 6.
181
+ Source: `apps/cli/src/lib/session/insights.ts`, `commands/insights.ts`.
182
+
183
+ - **Redact Claude OAuth setup-tokens (`sk-ant-oat01-…`) from logs and exports, and reject a malformed one before it reaches the auth header (#1767).** The log redactor masked API keys (`sk-ant-api03-…`) but not OAuth setup-tokens — the generic `sk-` rule can't reach an `oat01` token because the hyphen after `ant` breaks its run — so a captured setup-token could leak verbatim into a run log or session export. `resolveClaudeSetupToken` also now validates the stored value: a corrupt `auth` bundle entry (e.g. a captured `claude setup-token` TTY banner+ANSI blob, the exact #1767 shape) is refused instead of being injected as `CLAUDE_CODE_OAUTH_TOKEN` → an invalid `Authorization: Bearer` header that crashes the run; the caller falls back to the normal login. Source: `apps/cli/src/lib/redact.ts`, `apps/cli/src/lib/claude-account-token.ts`.
184
+
185
+ - **Stop the interactive host auto-reconnect spinning forever on a flapping link
186
+ (#1884).** A reattach only refills the retry budget now if it reached the host
187
+ **and** held the remote pane for at least 10 seconds. Before, the budget refilled
188
+ on the preflight probe alone, so a link that reconnected and dropped the user
189
+ straight back out — or an attach that died at TTY negotiation every time — printed
190
+ `Reconnecting … (attempt 1/6)` on every cycle forever and `MAX_ATTEMPTS` bounded
191
+ nothing. A link that keeps dropping now spends the budget and gives up with a
192
+ message that says so ("kept dropping again within 10 seconds of getting back in"),
193
+ distinct from the unreachable-host "couldn't reconnect". A session that blinks all
194
+ day and reconnects into a working pane each time is unaffected. Source:
195
+ `apps/cli/src/lib/hosts/reconnect.ts`, `docs/hosts.md`.
196
+
197
+ - **`agents models claude` no longer lists bare legacy ids that 404 (#1892).** The
198
+ native-binary id-scan fallback (`scanClaudeCatalogIds`, used when the curated maps
199
+ come up empty) is now word-boundary anchored and matches the id body atomically, so
200
+ it can't scrape a bare-major prefix (`claude-sonnet-4`) out of the binary's own dotted
201
+ `claude-sonnet-4.6` "Typo in model ID" troubleshooting string, out of a suffix-glued
202
+ token (`claude-opus-4-1x`), or out of a token glued to a preceding identifier char. The
203
+ existing `dropBareLegacyIds` sibling-drop still removes the standalone
204
+ `.includes("claude-opus-4")` prefix-check artifacts; genuine bare currents
205
+ (`claude-sonnet-5`) are kept. Catalog output is unchanged across all shipped Claude
206
+ binaries. Source: `apps/cli/src/lib/models.ts`.
207
+
208
+ - **Make ended-session focus open the correct recovery target (#2108).** `agents sessions focus <id>` now reaps metadata-less dead tmux panes, proves the indexed transcript belongs to the exact active version home before native resume, launches Claude from the transcript's original project directory, and passes replacement-version `/continue` as an interactive positional prompt. Source: `apps/cli/src/lib/session/recovery.ts`, `apps/cli/src/lib/tmux/session.ts`, `apps/cli/src/lib/exec.ts`.
209
+
210
+ - **The multi-install warning now inventories copies outside `PATH` and flags
211
+ legacy installs that can corrupt the shared macOS helper bundle (#2147).**
212
+ Discovery covers NVM, fnm, Volta, Bun, common npm global prefixes, and npm's
213
+ `_npx` cache in addition to resolving every `agents` entry on `PATH`. Dev
214
+ installs are no longer hidden: a copy without the atomic
215
+ `app-bundle-install` module is labelled `unsafe legacy helper installer —
216
+ remove this copy`, because invoking it can still replace a live `.app` with a
217
+ partial bundle. Source: `apps/cli/src/lib/self-update.ts`,
218
+ `apps/cli/src/index.ts`.
219
+
220
+ - **Newest signed agents-cli install owns the menu-bar helper (#2210).** On multi-install Macs (e.g. Homebrew + nvm), a newer release now takes over the helper immediately and an older install can no longer reclaim or downgrade it. Equal-version foreign installs keep the existing owner; missing-helper, Developer-ID repair, and unversioned legacy cooldown behavior are unchanged. Source: `apps/cli/src/lib/menubar/install-menubar.ts`.
221
+
222
+ - **Layered resource listing is ~40% faster.** `getActiveResourceProfile()` read
223
+ `agents.yaml` twice per call — once up front, then again inside
224
+ `getActiveResourceProfileName()` — and `listResources()` calls it once per
225
+ resolved resource, so a listing paid two memoized `readMeta()` round-trips
226
+ (`ensureAgentsDir()` plus four `stat`s each) for every entry. Reading it only
227
+ after the profile name is known drops one of them. Measured on `yosemite-s1`
228
+ against the real `~/.agents`: one pass over all eight resource kinds (135
229
+ entries) went 10.52 ms → 6.23 ms, and `agents doctor --json` spends ~243 ms in
230
+ this path across 95 listings. No behavior change: the read count is never
231
+ higher on any path and is unchanged whenever a profile name resolves — the one
232
+ saved read is the up-front one that the `if (!name) return null;` guard now
233
+ skips. The `ensureAgentsDir()` side effect is unchanged because
234
+ `getActiveResourceProfileName()` always reaches `readMeta()`, via
235
+ `brand.ts` `listBrands()` when a brand is set and via
236
+ `resource-profiles.ts` otherwise. Source:
237
+ `apps/cli/src/lib/resource-profiles.ts`.
238
+
239
+ - **Routine session discovery now supports an interactive picker and fuzzy names (RUSH-1998).** `agents sessions --routine` opens a routine picker on a TTY with each routine's last run, run count, and latest-run session count; the selected sessions are grouped by run ID and timestamp. `--routine <name>` accepts exact, substring, or unambiguous typo matches, and `--routines` is an alias for the same session filter.
240
+
241
+ - **Secret leases now have one scoped, time-boxed domain model (RUSH-2254).** A lease names one bundle, an explicit validated key subset, an absolute expiry, harness scope, and sleep-persistence posture. Durations use the broker's 1-minute to 30-day safety bounds, duplicate keys normalize once, unknown keys fail closed, and expired leases cannot project values. Source: `apps/cli/src/lib/secrets/lease.ts`.
242
+
243
+ - **Fleet devices accept an explicit SSH private-key path (RUSH-2265).** `agents devices set <name> --auth key --identity-file <path>` stores the path on that device and every shared SSH invocation passes it to OpenSSH with `-i`, so devices no longer depend on whichever key the ambient ssh agent happens to offer. Switching a device from password to key auth also removes stale password-bundle metadata from the registry.
244
+
245
+ - **`agents doctor` diagnoses Windows OpenSSH public-key enrollment (RUSH-2266).** On Windows it reads the effective `AuthorizedKeysFile`, selects the ProgramData administrator file or per-user profile file, verifies a public-key record is present, and checks that the administrator file grants `FullControl` to only `SYSTEM` and `Administrators`. The audit is read-only and never reads or prints private keys or passwords; fleet doctor forwards a locally authored summary and directs operators to run doctor on that box for details.
246
+
3
247
  ## 1.22.26
4
248
 
5
249
  - Make bare `agents setup` a re-runnable onboarding hub with live capability status and direct access to browser, computer, secrets, fleet, share, watchdog, and preference wizards.
package/README.md CHANGED
@@ -52,6 +52,8 @@ agents run claude "explain this repo" # run any agent on your existing subscrip
52
52
 
53
53
  `agents setup` is interactive and idempotent -- safe to re-run on any machine. Once core setup exists, it opens a status-aware menu for browser, computer, secrets, fleet, share, watchdog, and device preferences; each choice delegates to the same wizard available under `agents setup <capability>`. In CI or another non-TTY, bare setup prints the checklist without prompting. The `agi-cli.sh` one-liner installs this same canonical `@phnx-labs/agents-cli` package. Prefer bun? `bun install -g @phnx-labs/agents-cli` works too.
54
54
 
55
+ Full path -- installing harnesses, logging in, smoke-testing `agents teams`, and setting up your own fleet: [`apps/cli/docs/QUICKSTART.md`](apps/cli/docs/QUICKSTART.md).
56
+
55
57
  Already installed? `agents upgrade` updates agents-cli itself to the latest version (`agents upgrade 1.2.3` for a specific version or dist-tag, `-y` to skip the confirm prompt). The command is `upgrade` on every platform -- there is no `agents update` (on macOS, `agents helper update` is a different command that reinstalls the keychain helper, not agents-cli).
56
58
 
57
59
  Source: [github.com/phnx-labs/agents-cli](https://github.com/phnx-labs/agents-cli)
@@ -370,7 +372,7 @@ agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # original harness/version/d
370
372
  agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its account is unavailable
371
373
  ```
372
374
 
373
- `agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and recover on the source device. If the origin version is installed, signed in, and healthy, its isolated home performs native resume. Otherwise a healthy version of the **same harness** starts with `/continue <id>`, which reads the indexed transcript even when the old version home is retained under version trash. It never native-resumes from a different isolated home. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
375
+ `agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and recover on the source device. If the origin version is installed, signed in, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories. Otherwise a healthy version of the **same harness** starts with `/continue <id>`, which reads the indexed transcript even when the old version home is retained under version trash or the same version number was reinstalled into a new home. It never native-resumes from a different isolated home. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
374
376
 
375
377
  ### Send an agent to the background — and bring it back
376
378
 
@@ -546,6 +548,9 @@ agents fleet status # online/offline rollup + NEEDS ATTENTIO
546
548
  agents fleet status --verbose # full per-device auth/CLI/sync/version grid
547
549
  agents fleet status --live # force a live resource probe (alias of --refresh)
548
550
  agents fleet status --json --strict # scriptable fleet health gate
551
+ agents devices harnesses # per device: agent@version · account · signed · quota · ready
552
+ agents devices accounts # same, one row per account (which harnesses share it)
553
+ agents devices harnesses --agents claude,codex --json # scoped, machine-readable
549
554
  agents doctor --check --devices # CI drift gate across every registered device
550
555
 
551
556
  # Your Tailscale fleet, auto-discovered
@@ -594,6 +599,18 @@ endpoint — e.g. codex/grok — benign), `◐degraded` (soft/self-healing: expi
594
599
  rate-limited), and `○revoked` (server rejected — re-login now). Only `○` means a real
595
600
  re-login is needed. Run `agents fleet ping` to force a live re-verification across the fleet.
596
601
 
602
+ `agents devices harnesses` answers "what can each box actually run right now?" — one row
603
+ per installed `agent@version` across the fleet with its **account**, **signed-in**,
604
+ **quota** (highest usage-window utilization; `*` = from the cached snapshot), and a single
605
+ **ready** verdict (signed in AND not rate-limited). It SSH-probes each online device
606
+ (bounded, so one unreachable box can't stall the glance) and reuses the daemon-warmed usage
607
+ cache, so it never blocks on a per-account network fetch — pass `--refresh` (`--live`) for a
608
+ live quota read. `agents devices accounts` is the same data through the **identity lens**:
609
+ one row per account, collapsing the installs that share it (e.g. five claude versions on one
610
+ email) and naming which harnesses use it — the fast way to see which accounts are logged in
611
+ and healthy across every machine. Scope either with `--agents <csv>` / `--device <csv>`, and
612
+ add `--json` for the machine-readable per-host rows.
613
+
597
614
  **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).
598
615
 
599
616
  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>`.
package/dist/bin/agents CHANGED
Binary file
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerBenchCommand(program: Command): void;
@@ -0,0 +1,101 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { detectSignedInRuntimes } from "../lib/crabbox/runtimes.js";
5
+ import { listRuns, loadRun, loadTask, runCells, saveRun, } from "../lib/bench/index.js";
6
+ import { setHelpSections } from "../lib/help.js";
7
+ const TASKS_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../bench/tasks");
8
+ function csv(value) {
9
+ return (value
10
+ ?.split(",")
11
+ .map((item) => item.trim())
12
+ .filter(Boolean) ?? []);
13
+ }
14
+ function taskIds(root = TASKS_ROOT) {
15
+ if (!fs.existsSync(root))
16
+ return [];
17
+ return fs
18
+ .readdirSync(root, { withFileTypes: true })
19
+ .filter((entry) => entry.isDirectory() &&
20
+ fs.existsSync(path.join(root, entry.name, "task.json")))
21
+ .map((entry) => entry.name)
22
+ .sort();
23
+ }
24
+ function renderResult(result) {
25
+ console.log(`Run ${result.run_id}${result.task_id ? ` · ${result.task_id}` : ""}`);
26
+ for (const cell of result.cells)
27
+ console.log(`${cell.status === "passed" ? "PASS" : "FAIL"} ${cell.agent}${cell.model ? `/${cell.model}` : ""} ${cell.wall_ms} ms exit ${cell.exit ?? "spawn-error"}`);
28
+ }
29
+ export function registerBenchCommand(program) {
30
+ const bench = program
31
+ .command("bench")
32
+ .description("Run the same task across agent and model cells, with isolated fixtures and durable JSON results.");
33
+ setHelpSections(bench, {
34
+ examples: `agents bench list\nagents bench run hello-repo --agent claude,codex --model cheap,default\nagents bench results --json`,
35
+ notes: `Task definitions live under apps/cli/bench/tasks/<id>/task.json. Custom harness names accepted by agents run are valid --agent values.`,
36
+ });
37
+ bench
38
+ .command("list")
39
+ .description("List available benchmark tasks.")
40
+ .option("--json", "Emit JSON.")
41
+ .action((options) => {
42
+ const tasks = taskIds();
43
+ if (options.json)
44
+ console.log(JSON.stringify(tasks, null, 2));
45
+ else if (tasks.length === 0)
46
+ console.log("No benchmark tasks installed.");
47
+ else
48
+ tasks.forEach((id) => console.log(id));
49
+ });
50
+ bench
51
+ .command("results [run-id]")
52
+ .description("Show one saved run, or list saved runs newest first.")
53
+ .option("--json", "Emit JSON.")
54
+ .action((runId, options) => {
55
+ const value = runId ? loadRun(runId) : listRuns();
56
+ if (options.json)
57
+ console.log(JSON.stringify(value, null, 2));
58
+ else if (Array.isArray(value)) {
59
+ if (value.length === 0)
60
+ console.log("No benchmark results yet.");
61
+ else
62
+ value.forEach(renderResult);
63
+ }
64
+ else
65
+ renderResult(value);
66
+ });
67
+ bench
68
+ .command("run [task-id]")
69
+ .description("Run one task or prompt across an agent × model matrix.")
70
+ .option("--prompt <text>", "Prompt to benchmark instead of a house task.")
71
+ .option("--agent <names>", "Comma-separated native agents or custom harness names. Defaults to signed-in native agents.")
72
+ .option("--model <models>", "Comma-separated model tiers or concrete model ids.")
73
+ .option("--concurrency <n>", "Maximum cells running at once.", "3")
74
+ .option("--json", "Emit the saved JSON result.")
75
+ .action(async (taskId, options) => {
76
+ if (!!taskId === !!options.prompt)
77
+ throw new Error("Pass exactly one of <task-id> or --prompt.");
78
+ const task = taskId ? loadTask(taskId, TASKS_ROOT) : undefined;
79
+ const prompt = options.prompt ?? task.prompt;
80
+ let agents = csv(options.agent);
81
+ if (agents.length === 0)
82
+ agents = (await detectSignedInRuntimes())
83
+ .filter((runtime) => runtime.signedIn)
84
+ .map((runtime) => runtime.id);
85
+ if (agents.length === 0)
86
+ throw new Error("No signed-in native agents found. Pass --agent <name>.");
87
+ const models = csv(options.model);
88
+ const cells = agents.flatMap((agent) => models.length > 0
89
+ ? models.map((model) => ({ agent, model }))
90
+ : [{ agent }]);
91
+ const concurrency = Number(options.concurrency);
92
+ const result = await runCells({ task, prompt, cells, concurrency });
93
+ saveRun(result);
94
+ if (options.json)
95
+ console.log(JSON.stringify(result, null, 2));
96
+ else
97
+ renderResult(result);
98
+ if (result.cells.some((cell) => cell.status === "failed"))
99
+ process.exitCode = 1;
100
+ });
101
+ }
@@ -12,7 +12,8 @@ import { buildLocalFindings, fleetDivergenceToFindings, signInToFindings, render
12
12
  import { getCliVersion } from '../lib/version.js';
13
13
  import { resolveHost } from '../lib/hosts/registry.js';
14
14
  import { sshExecAsync } from '../lib/ssh-exec.js';
15
- import { sshTargetFor } from '../lib/hosts/types.js';
15
+ import { hostIdentityArgs, sshTargetFor } from '../lib/hosts/types.js';
16
+ import { deviceIdentityArgs } from '../lib/devices/connect.js';
16
17
  import { machineId, normalizeHost } from '../lib/session/sync/config.js';
17
18
  import { findAmbiguousDevicePins } from '../lib/routines.js';
18
19
  import chalk from 'chalk';
@@ -33,6 +34,7 @@ import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
33
34
  import { setHelpSections } from '../lib/help.js';
34
35
  import { heal, healChangedAnything } from '../lib/heal.js';
35
36
  import { getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
37
+ import { auditWindowsSshEnrollment, diagnoseWindowsSshFailure } from '../lib/devices/windows-ssh-enrollment.js';
36
38
  import { scanUserRcFiles, masterPassphraseInEnv } from '../lib/secrets/rc-hygiene.js';
37
39
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
38
40
  import { readRepoBehindMarkers } from '../lib/auto-pull.js';
@@ -83,14 +85,14 @@ function printWrappedLine(prefix, text) {
83
85
  console.log(chalk.gray(line));
84
86
  }
85
87
  /**
86
- * Narrow a remote `findings` array to the secret-hygiene rows, or `[]`.
88
+ * Narrow a remote `findings` array to rows only that device can observe.
87
89
  *
88
90
  * Deliberately NOT "forward every remote finding". The aggregator already
89
91
  * rebuilds a remote's sign-in rows from its inventory and its divergence rows
90
92
  * from the comparator, so forwarding wholesale would double them; and pulling a
91
93
  * remote's orphan/drift rows into a fleet readout is a much larger UX change
92
- * than this fix. The two secret kinds are the ones that are BOTH unrecomputable
93
- * centrally and security-relevant, which is exactly why they were being lost.
94
+ * than this fix. These kinds are unrecomputable centrally: shell/process secret
95
+ * hygiene and the Windows host's effective OpenSSH key path/content/ACL.
94
96
  *
95
97
  * **The remote contributes exactly one thing: the KIND.** Severity, message and
96
98
  * remediation are all generated HERE. That is not defensiveness for its own
@@ -111,13 +113,15 @@ function printWrappedLine(prefix, text) {
111
113
  * file and line. Run `agents doctor` on that box for the specifics — the
112
114
  * message says so.
113
115
  */
114
- const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export'];
116
+ const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export', 'ssh-key-enrollment'];
115
117
  /** Canonical, locally-authored text for a forwarded kind. Never the remote's. */
116
118
  const REMOTE_SECRET_MESSAGE = {
117
119
  'rc-secret-export': 'a credential-shaped export was found in this box\'s shell rc files'
118
120
  + ' — run `agents doctor` there for the file and line',
119
121
  'env-secret-export': 'AGENTS_SECRETS_PASSPHRASE is set in this box\'s process environment'
120
122
  + ' — run `agents doctor` there for detail',
123
+ 'ssh-key-enrollment': 'Windows OpenSSH key enrollment is invalid'
124
+ + ' — run `agents doctor` on this box for the effective path or ACL failure',
121
125
  };
122
126
  export function asRemoteSecretFindings(raw, device) {
123
127
  if (!Array.isArray(raw))
@@ -158,11 +162,12 @@ async function resolveFleetTargets(opts) {
158
162
  name: deviceProfile.name,
159
163
  sshTarget: deviceProfile.name,
160
164
  os: deviceProfile.platform !== 'unknown' ? deviceProfile.platform : undefined,
165
+ extraSshArgs: deviceIdentityArgs(deviceProfile),
161
166
  }];
162
167
  }
163
168
  const host = await resolveHost(singleName);
164
169
  if (host) {
165
- return [{ name: singleName, sshTarget: sshTargetFor(host), os: host.os }];
170
+ return [{ name: singleName, sshTarget: sshTargetFor(host), os: host.os, extraSshArgs: hostIdentityArgs(host) }];
166
171
  }
167
172
  console.error(chalk.red(`Unknown host or device '${singleName}'.`));
168
173
  process.exit(1);
@@ -180,6 +185,7 @@ async function resolveFleetTargets(opts) {
180
185
  name: d.name,
181
186
  sshTarget: d.name,
182
187
  os: d.platform !== 'unknown' ? d.platform : undefined,
188
+ extraSshArgs: deviceIdentityArgs(d),
183
189
  }));
184
190
  }
185
191
  async function probeFleetTarget(target) {
@@ -190,12 +196,14 @@ async function probeFleetTarget(target) {
190
196
  // has it via the install profile, and our single-quote escaping would
191
197
  // prevent $HOME expansion there, so skip the bootstrap on Windows.
192
198
  isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
193
- const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
199
+ const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs: target.extraSshArgs });
194
200
  if (res.code !== 0) {
195
201
  return {
196
202
  name: target.name,
197
203
  online: false,
198
- error: res.timedOut ? 'timed out' : (res.stderr || `exit ${res.code ?? 'unknown'}`),
204
+ error: isWin
205
+ ? diagnoseWindowsSshFailure(res.stderr, res.timedOut)
206
+ : res.timedOut ? 'timed out' : (res.stderr || `exit ${res.code ?? 'unknown'}`),
199
207
  agents: {},
200
208
  };
201
209
  }
@@ -238,7 +246,11 @@ export const FLEET_INVENTORY_TIMEOUT_MS = 180_000;
238
246
  async function probeFleetInventory(target) {
239
247
  const isWin = /^win/i.test((target.os ?? '').trim());
240
248
  const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
241
- const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: FLEET_INVENTORY_TIMEOUT_MS, multiplex: true });
249
+ const res = await sshExecAsync(target.sshTarget, remoteCmd, {
250
+ timeoutMs: FLEET_INVENTORY_TIMEOUT_MS,
251
+ multiplex: true,
252
+ extraSshArgs: target.extraSshArgs,
253
+ });
242
254
  if (res.code !== 0)
243
255
  return null;
244
256
  try {
@@ -404,6 +416,7 @@ async function runDevicesDoctor(opts) {
404
416
  execPolicy: process.platform === 'win32'
405
417
  ? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
406
418
  : undefined,
419
+ windowsSshEnrollment: auditWindowsSshEnrollment(),
407
420
  isolatedVersions: localReports
408
421
  .filter((rep) => isVersionIsolated(rep.agent, rep.version))
409
422
  .map((rep) => `${rep.agent}@${rep.version}`),
@@ -1182,7 +1195,7 @@ function checkPayload(device, drift) {
1182
1195
  async function probeDeviceCheck(target) {
1183
1196
  const isWin = /^win/i.test((target.platform ?? '').trim());
1184
1197
  const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--check', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
1185
- const res = await sshExecAsync(target.dialTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
1198
+ const res = await sshExecAsync(target.dialTarget, remoteCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs: target.extraSshArgs });
1186
1199
  if (res.code !== 0 && !res.stdout.trim()) {
1187
1200
  throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
1188
1201
  }
@@ -1211,6 +1224,7 @@ async function runDevicesCheck(opts, cwd) {
1211
1224
  platform: t.device.platform,
1212
1225
  skip: t.skip,
1213
1226
  dialTarget: fleetDialTarget(t.device),
1227
+ extraSshArgs: deviceIdentityArgs(t.device),
1214
1228
  }));
1215
1229
  const remote = await fanOutDevices(remoteTargets, probeDeviceCheck);
1216
1230
  const devices = [local];
@@ -554,7 +554,7 @@ export function registerRunCommand(program) {
554
554
  .option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
555
555
  .option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
556
556
  .option('--where <spec>', 'Where this run\'s body executes (one placement door): local | device:<name> | auto | lease[:backend] | cloud[:provider]. Expands to --host/--lease/--cloud. Do not combine with those flags. See docs/00-concepts.md#placement.')
557
- .option('--host <name>', 'Offload this run onto another machine over SSH — a device name, registered host, or user@host. Pass "auto" to pick from 14d usage affinity (most-used online device has highest probability). Same as --where device:<name>. See `agents devices`.')
557
+ .option('--host <name>', 'Offload this run onto another machine over SSH — a device name, registered host, or user@host. Pass "auto" to pick the least-loaded reachable device where the requested agent is installed and signed in, keeping the run local when no remote is better. Same as --where device:<name>. See `agents devices`.')
558
558
  .option('--device <name>', 'Alias of --host. Pass "auto" for affinity-based device pick (same as --where auto).')
559
559
  .option('--remote-cwd <dir>', "Explicit host working directory for --host runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
560
560
  .option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
@@ -947,13 +947,18 @@ export function registerRunCommand(program) {
947
947
  if (!resolvedResumeSource && runAutoDefaultsToAffinity(options))
948
948
  options.device = 'auto';
949
949
  }
950
- // --device auto / --host auto (and deprecated --smart): affinity-pick host.
950
+ // --device auto / --host auto (and deprecated --smart): live fleet pick.
951
951
  // Harness is always the agent the user typed — never auto-picked.
952
952
  // Affinity failure degrades to local (does not kill the run).
953
953
  {
954
954
  const { applyDeviceAutoToOptions } = await import('../lib/smart-launch.js');
955
- const result = applyDeviceAutoToOptions(options, {
955
+ const result = await applyDeviceAutoToOptions(options, {
956
956
  accountPickerRequested,
957
+ // `run auto` selects its harness after placement, so do not filter
958
+ // candidates against an arbitrary proxy harness at this stage.
959
+ agent: normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD
960
+ ? undefined
961
+ : (resolveAgentName(normalizedAgentSpec.split('@')[0]) ?? undefined),
957
962
  });
958
963
  if (!options.quiet && result.deprecationSmart) {
959
964
  process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
@@ -964,7 +969,7 @@ export function registerRunCommand(program) {
964
969
  if (!options.quiet && result.banner) {
965
970
  const { hostLabel, deviceHint, acctNote } = result.banner;
966
971
  process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
967
- (deviceHint ? ` (affinity ${deviceHint})` : '') +
972
+ (deviceHint ? ` (load ${deviceHint})` : '') +
968
973
  ` · ${acctNote}\n`));
969
974
  }
970
975
  }
@@ -2268,22 +2273,18 @@ export function registerRunCommand(program) {
2268
2273
  process.exit(1);
2269
2274
  }
2270
2275
  version = resolvedRecoveryTarget.version;
2271
- const canResumeNatively = resolvedRecoveryTarget.mode === 'native';
2272
- if (canResumeNatively) {
2276
+ if (resolvedRecoveryTarget.mode === 'native') {
2273
2277
  version = session.version;
2274
2278
  resumeNative = true;
2275
2279
  resumeSessionId = session.id;
2276
- // Native `--resume` (claude/codex) resolves the transcript relative to the
2277
- // working directory (projects/<cwd-hash>/). The session may have been started
2278
- // in a different directory than we're standing in now — most importantly when a
2279
- // routine daemon fires `agents run --resume` from its own cwd. Spawn from the
2280
- // session's ORIGIN cwd so the resume actually finds it; otherwise the agent
2281
- // exits "No conversation found with session ID". Honor an explicit --cwd only if
2282
- // the caller passed one (they're overriding on purpose).
2283
- if (!options.cwd && session.cwd)
2284
- options.cwd = session.cwd;
2280
+ // The centralized recovery decision proves the transcript belongs to
2281
+ // this exact isolated home and resolves any harness-specific launch cwd.
2282
+ // Claude's indexed `session.cwd` is the first user-turn cwd, which may
2283
+ // differ from the earlier cwd that selected projects/<cwd-key>.
2284
+ if (!options.cwd && resolvedRecoveryTarget.cwd)
2285
+ options.cwd = resolvedRecoveryTarget.cwd;
2285
2286
  if (!options.quiet)
2286
- process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''}${!options.cwd || options.cwd === session.cwd ? ` in ${session.cwd ?? cwd}` : ''}\n`));
2287
+ process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''} in ${options.cwd ?? cwd}\n`));
2287
2288
  }
2288
2289
  else {
2289
2290
  // Tier-2: launch fresh with a /continue <id> first message; the agent