@phnx-labs/agents-cli 1.22.46 → 1.22.49

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 (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -14,6 +14,7 @@ import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection }
14
14
  import { getUserAgentsDir, readMeta } from '../lib/state.js';
15
15
  import { parseLoopInterval } from '../lib/loop.js';
16
16
  import { AGENTS, resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
17
+ import { parseAgentVersionSpec } from '../lib/agent-spec/agents.js';
17
18
  import { recordDispatchedRun } from '../lib/audit/log.js';
18
19
  import { maybeShowStarNudge } from '../lib/star-nudge.js';
19
20
  import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
@@ -416,7 +417,7 @@ async function handleTerminalHandoff(agentSpec, options, prompt) {
416
417
  // isValidAgent / profileExists / resolveWorkflowRef chain below), so this must
417
418
  // accept all three. Gating on the agent table alone rejected every profile —
418
419
  // the whole Kimi/DeepSeek/Qwen/GLM path — for `--terminal` runs only.
419
- const rawTarget = parseRunAccountPickerRequest(agentSpec).normalizedAgentSpec.split('@')[0];
420
+ const rawTarget = parseRunAccountPickerRequest(agentSpec).normalizedAgentSpec.split('#')[0].split('@')[0];
420
421
  const knownAgent = resolveAgentName(rawTarget);
421
422
  const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
422
423
  import('../lib/profiles.js'),
@@ -490,7 +491,7 @@ export function registerRunCommand(program) {
490
491
  const runCmd = program
491
492
  .command('run [agent] [prompt]')
492
493
  .description('Execute an agent. Pass a prompt for headless runs; omit it to launch the agent interactively. With --broadcast, run the same prompt/task across an agent × model matrix.')
493
- .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (smart classifier auto-approves safe ops, prompts for risky), skip (bypass all permission prompts). Omitted Codex mode defaults to safe writable edit; other harnesses default to plan. \'full\' accepted as alias for skip.', 'plan')
494
+ .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (more autonomous than edit, mechanism per-harness: smart classifier auto-approves safe ops and still prompts for risky ones on Claude/Copilot; approval_policy=never over the edit sandbox on Codex, which never prompts), skip (bypass all permission prompts). Omitted Codex mode defaults to safe writable edit; other harnesses default to plan. \'full\' accepted as alias for skip.', 'plan')
494
495
  .option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto (claude and codex only)', 'auto')
495
496
  .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete model id; tiers resolve per harness+version to a supported model')
496
497
  .option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
@@ -507,7 +508,7 @@ export function registerRunCommand(program) {
507
508
  .option('--no-auth-check', 'Skip the pre-launch "looks logged out" warning on an interactive run (advisory; never blocks anyway). Also silenced by AGENTS_NO_AUTH_CHECK=1.')
508
509
  .option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
509
510
  .option('--broadcast', 'Run the same prompt or --task across multiple agents (comma-separated [agent]) × --model cells. Replaces the former `agents bench` group.')
510
- .option('--task <id>', 'With --broadcast: house benchmark task id under apps/cli/bench/tasks/')
511
+ .option('--task <id>', 'With --broadcast: house benchmark task id under cli/bench/tasks/')
511
512
  .option('--list-tasks', 'With --broadcast: list available broadcast task ids')
512
513
  .option('--results [run-id]', 'With --broadcast: show one saved matrix run, or list saved runs newest first')
513
514
  .option('--concurrency <n>', 'With --broadcast: maximum cells running at once', '3')
@@ -517,14 +518,14 @@ export function registerRunCommand(program) {
517
518
  .option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
518
519
  .option('--terminal [backend]', "Open this run in a real terminal tab instead of here. Without a value the terminal is detected from your live sessions (`agents sessions --active` host), so it lands where you already work — Ghostty for a Ghostty user, iTerm for an iTerm user. Name one to force it: iterm | ghostty | terminal | tmux | vscodium-agent. This is how the menu bar's New Session opens.")
519
520
  .option('--verbose', 'Show detailed execution logs')
520
- .option('--raw', 'Interactive runs on macOS/Linux launch inside a shared tmux session (for %pane addressing + re-attach). Pass --raw to spawn the agent directly instead. Also disabled by AGENTS_NO_TMUX=1.')
521
- .option('--no-tmux', 'Spawn the agent directly instead of wrapping it in the shared tmux session. Same effect as --raw / AGENTS_NO_TMUX=1. Use this to see the agent\'s full startup output when a launch is failing; to turn the wrap off for every run on this machine, set `agents config set devices.<name>.tmux off`.')
522
- .option('--disable-tmux', 'Alias for --no-tmux.')
521
+ .option('--raw', 'Keep this interactive run direct when the device has opted into tmux wrapping. A no-op under the default tmux-off configuration; equivalent to AGENTS_NO_TMUX=1.')
522
+ .option('--no-tmux', 'Keep this run direct when tmux wrapping is enabled for the device. Same effect as --raw / AGENTS_NO_TMUX=1; it is a no-op under the default tmux-off configuration.')
523
+ .option('--disable-tmux', 'Compatibility alias for --no-tmux; a no-op when tmux wrapping is already off.')
523
524
  .option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
524
525
  .option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,antigravity). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
525
526
  .option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
526
527
  .option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then balanced (spreads load across healthy accounts and skips any that are rate-limited). (Legacy `rotate` accepted as alias for `balanced`.)')
527
- .option('--account <name>', 'Use this durable provider credential for the run')
528
+ .option('--account <label>', 'Use this labeled native login or durable provider credential for the run')
528
529
  .option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for claude via @zed-industries/claude-code-acp adapter. Unified event stream; emits ndjson when --json.')
529
530
  .option('-y, --yes', 'Skip the interactive budget-confirm prompt (require_confirm_over). Never skips a hard budget block.', false)
530
531
  .option('--loop', 'Re-inject the prompt/entrypoint each iteration until a stop condition (issue #332). Guards (--max-iterations, --budget, --until) are enforced outside the agent. Writes a checkpoint after every iteration for --resume-checkpoint.')
@@ -534,7 +535,7 @@ export function registerRunCommand(program) {
534
535
  .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.')
535
536
  .option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
536
537
  .option('--where <spec>', 'Where this run\'s body executes (one placement door): local | device:<name> | auto | lease[:backend] | cloud[:provider]. Expands to --device/--lease/--cloud. Do not combine with those flags. See docs/00-concepts.md#placement.')
537
- .option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, 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`.')
538
+ .option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, 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, or "interactive" for the machine pinned as interactive.host (the box a human is sitting at). Same as --where device:<name>. See `agents devices`.')
538
539
  .option('--remote-cwd <dir>', "Explicit device working directory for --device 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).")
539
540
  .option('--no-follow', 'With --device, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
540
541
  .option('--any', 'With --device <cap> (a capability tag), pick any matching device instead of erroring when several match.')
@@ -636,7 +637,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
636
637
  Modes (not every agent supports every mode — run \`agents modes <agent>\`):
637
638
  plan read-only investigation; no writes, no shell side-effects
638
639
  edit may edit files; prompts for shell / risky operations
639
- auto smart classifier auto-approves safe ops, prompts for risky (claude, copilot)
640
+ auto more autonomous than edit; the mechanism is per-harness --
641
+ claude, copilot: smart classifier auto-approves safe ops and
642
+ STILL PROMPTS for risky ones
643
+ codex: approval_policy=never over the edit sandbox; never
644
+ prompts at all, and a denied command fails instead
640
645
  skip bypass every permission prompt (dangerously-skip-permissions)
641
646
  Legacy 'full' is silently rewritten to 'skip'.
642
647
  List per-harness support + native flags: agents modes · agents modes claude
@@ -829,7 +834,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
829
834
  process.exit(1);
830
835
  }
831
836
  // Hard-deprecated harnesses cannot be run — point the user at the successor.
832
- const runBaseAgentName = normalizedAgentSpec.split('@')[0];
837
+ const runBaseAgentName = normalizedAgentSpec.split('#')[0].split('@')[0];
833
838
  const runBaseAgentId = resolveAgentName(runBaseAgentName);
834
839
  const { profileExists: runProfileExists } = await import('../lib/profiles.js');
835
840
  if (runBaseAgentId && !runProfileExists(runBaseAgentName) && isAgentHardDeprecated(runBaseAgentId)) {
@@ -849,7 +854,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
849
854
  }
850
855
  // `agents run auto`: the reserved harness keyword — full-auto dispatch
851
856
  // (host affinity → cross-harness balance → account balance, RUSH-2132).
852
- if (normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD && normalizedAgentSpec !== RUN_AUTO_KEYWORD) {
857
+ if (normalizedAgentSpec.split('#')[0].split('@')[0] === RUN_AUTO_KEYWORD && normalizedAgentSpec !== RUN_AUTO_KEYWORD) {
853
858
  console.error(chalk.red(`agents run auto picks the harness itself — a @version pin does not apply. ` +
854
859
  `Pin a concrete harness instead: agents run <harness>@<version>.`));
855
860
  process.exit(1);
@@ -896,7 +901,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
896
901
  process.exit(1);
897
902
  }
898
903
  resolvedResumeSource = outcome.session;
899
- const [requestedAgent, requestedVersion] = normalizedAgentSpec.split('@');
904
+ const [requestedAgent, requestedVersion] = normalizedAgentSpec.split('#')[0].split('@');
900
905
  if (!autoHarnessRequested && requestedAgent !== resolvedResumeSource.agent) {
901
906
  console.error(chalk.red(`Session ${resolvedResumeSource.shortId} belongs to ${resolvedResumeSource.agent}, not ${requestedAgent}. ` +
902
907
  `Use: agents sessions resume ${resolvedResumeSource.id}`));
@@ -982,9 +987,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
982
987
  accountPickerRequested,
983
988
  // `run auto` selects its harness after placement, so do not filter
984
989
  // candidates against an arbitrary proxy harness at this stage.
985
- agent: normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD
990
+ agent: normalizedAgentSpec.split('#')[0].split('@')[0] === RUN_AUTO_KEYWORD
986
991
  ? undefined
987
- : (resolveAgentName(normalizedAgentSpec.split('@')[0]) ?? undefined),
992
+ : (resolveAgentName(normalizedAgentSpec.split('#')[0].split('@')[0]) ?? undefined),
988
993
  });
989
994
  if (!options.quiet && result.deprecationSmart) {
990
995
  process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
@@ -1137,7 +1142,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1137
1142
  const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/accounting/rotate.js');
1138
1143
  const { profileExists, readProfile, resolveProfileEnv } = await import('../lib/profiles.js');
1139
1144
  const detected = await detectSignedInRuntimes();
1140
- const [agentName, rawLeaseVersion] = normalizedAgentSpec.split('@');
1145
+ const [agentName, rawLeaseVersion] = normalizedAgentSpec.split('#')[0].split('@');
1141
1146
  let runtime = null;
1142
1147
  let credentialRuntimes = [];
1143
1148
  let dispatchProfile;
@@ -1398,7 +1403,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1398
1403
  throw e;
1399
1404
  }
1400
1405
  try {
1401
- const [runAgent, rawRunVersion] = normalizedAgentSpec.split('@');
1406
+ const [runAgent, rawRunVersion] = normalizedAgentSpec.split('#')[0].split('@');
1402
1407
  // Forward the explicit @version pin verbatim. Resolving aliases like
1403
1408
  // @latest locally would check local installs, but the remote host may
1404
1409
  // have versions the laptop does not. The remote agents CLI resolves
@@ -1496,6 +1501,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1496
1501
  if (process.env.AGENTS_DISPATCH_DEBUG || options.verbose) {
1497
1502
  process.stderr.write(chalk.gray(`[hosts] dispatch interactive ${runAgent}${runVersion ? `@${runVersion}` : ''} -> ${host.name}\n`));
1498
1503
  }
1504
+ {
1505
+ const { connectionStartedNotice, startConnectionTarget } = await import('../lib/hosts/reconnect.js');
1506
+ const startTarget = startConnectionTarget({
1507
+ agent: runAgent,
1508
+ hostSessionId,
1509
+ resumeId,
1510
+ });
1511
+ if (startTarget) {
1512
+ const started = connectionStartedNotice(startTarget, host.name);
1513
+ if (started)
1514
+ process.stderr.write(chalk.gray(started));
1515
+ }
1516
+ }
1499
1517
  const exitCode = await runInteractiveOnHost(host, {
1500
1518
  agent: runAgent,
1501
1519
  version: resumeId ? undefined : runVersion,
@@ -1560,19 +1578,32 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1560
1578
  // `raw` runs aren't tmux wrapped, so there is nothing to reconnect to.
1561
1579
  // For `run auto` prefer the join-resolved id (the harness the remote
1562
1580
  // ACTUALLY picked) over the explicit --session-id only claude adopts.
1563
- const reconnectId = (runAgent === RUN_AUTO_KEYWORD
1564
- ? resolvedRemoteId ?? hostSessionId
1565
- : hostSessionId ?? resolvedRemoteId) ?? resumeId;
1566
- if (reconnectId && !isRaw) {
1567
- const { reconnectInteractiveSession, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
1568
- if (exitCode === SSH_CONN_FAILURE) {
1569
- process.exit(await reconnectInteractiveSession({
1570
- host,
1571
- sessionId: reconnectId,
1572
- initialExit: exitCode,
1573
- }));
1574
- }
1581
+ const { pickReconnectTarget, reconnectInteractiveSession, afterInteractiveRemoteExit, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
1582
+ const reconnectTarget = pickReconnectTarget({
1583
+ agent: runAgent,
1584
+ sessionId: hostSessionId,
1585
+ resolvedId: resolvedRemoteId,
1586
+ resumeId,
1587
+ launchId: correlationLaunchId,
1588
+ });
1589
+ // Raw is not tmux-wrapped, so it never auto-reconnects — but it
1590
+ // still prints the session id (EXEC-55). Bundling the notice
1591
+ // behind `!isRaw` left a dropped `--raw` tab as a bare shell.
1592
+ const next = afterInteractiveRemoteExit({
1593
+ target: reconnectTarget,
1594
+ host: host.name,
1595
+ exitCode,
1596
+ willReconnect: exitCode === SSH_CONN_FAILURE && !isRaw,
1597
+ });
1598
+ if (next.reconnect && reconnectTarget) {
1599
+ process.exit(await reconnectInteractiveSession({
1600
+ host,
1601
+ target: reconnectTarget,
1602
+ initialExit: exitCode,
1603
+ }));
1575
1604
  }
1605
+ if (next.notice)
1606
+ process.stderr.write(next.notice);
1576
1607
  process.exit(exitCode);
1577
1608
  }
1578
1609
  // Headless host run: launch detached, tail the remote log, and follow
@@ -1754,12 +1785,33 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1754
1785
  import('../lib/share/config.js'),
1755
1786
  ]);
1756
1787
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
1757
- // Parse agent@version
1758
- const [rawAgent, rawVersion] = normalizedAgentSpec.split('@');
1788
+ // Parse agent@version#label. The label selects native auth without pinning
1789
+ // the binary version; --account remains the equivalent flag form.
1790
+ const labelParts = normalizedAgentSpec.split('#');
1791
+ if (labelParts.length > 2 || labelParts[1] === '') {
1792
+ console.error(chalk.red(`Invalid account label in '${normalizedAgentSpec}'.`));
1793
+ process.exit(1);
1794
+ }
1795
+ const [rawAgent, rawVersion] = labelParts[0].split('@');
1796
+ const specAccountLabel = labelParts[1];
1797
+ if (resolveAgentName(rawAgent)) {
1798
+ const parsed = parseAgentVersionSpec(normalizedAgentSpec);
1799
+ if ('error' in parsed) {
1800
+ console.error(chalk.red(parsed.error));
1801
+ process.exit(1);
1802
+ }
1803
+ }
1804
+ if (specAccountLabel && options.account && specAccountLabel !== options.account) {
1805
+ console.error(chalk.red(`Account '${specAccountLabel}' from the agent spec conflicts with --account '${options.account}'.`));
1806
+ process.exit(1);
1807
+ }
1808
+ if (specAccountLabel)
1809
+ options.account = specAccountLabel;
1759
1810
  let agent;
1760
1811
  let version = rawVersion || undefined;
1761
1812
  let profileEnv;
1762
1813
  let accountEnv;
1814
+ let accountConfigVersion;
1763
1815
  let profileProvider;
1764
1816
  let fromProfile = false;
1765
1817
  let profileFallbackModel;
@@ -2126,29 +2178,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2126
2178
  console.error(chalk.red(`Account '${spawnAccount.name}' is a device-local ${spawnAccount.agent} login and cannot be forwarded to '${remoteTarget}'. Sign in on that device and name the login there.`));
2127
2179
  process.exit(1);
2128
2180
  }
2129
- if (version) {
2130
- // A pinned version must actually be signed in as the named identity.
2131
- const { getAccountInfo } = await import('../lib/agents.js');
2132
- const info = await getAccountInfo(agent, getVersionHomePath(agent, version));
2133
- const liveKey = info.accountKey ?? info.email?.toLowerCase() ?? null;
2134
- if (!info.signedIn || liveKey !== spawnAccount.identityKey) {
2135
- console.error(chalk.red(`Account '${spawnAccount.name}' names a specific ${spawnAccount.agent} identity, but ${agent}@${version} ${info.signedIn ? 'is signed in as a different identity' : 'is not signed in'}. Sign in as that identity, or re-name the account.`));
2136
- process.exit(1);
2137
- }
2181
+ const { CONFIG_ENV_ISOLATED_AGENTS } = await import('../lib/installations/shims.js');
2182
+ if (!CONFIG_ENV_ISOLATED_AGENTS.includes(agent)) {
2183
+ console.error(chalk.red(`${agent} cannot select native accounts independently of its installed version.`));
2184
+ process.exit(1);
2138
2185
  }
2139
- else {
2140
- // No version pinned: locate the installed version currently signed in
2141
- // as this identity and pin the run to it — do NOT assume the global
2142
- // default holds it (the account may live on another installed copy).
2143
- const { resolveAccountVersion } = await import('../lib/accounting/rotate.js');
2144
- const matched = await resolveAccountVersion(agent, spawnAccount.identityKey);
2145
- if (!matched) {
2146
- console.error(chalk.red(`No installed ${spawnAccount.agent} version is signed in as the identity named by account '${spawnAccount.name}'. Sign in as that identity, or attach a different account.`));
2147
- process.exit(1);
2148
- }
2149
- version = matched;
2186
+ const { resolveAccountVersion } = await import('../lib/accounting/rotate.js');
2187
+ accountConfigVersion = await resolveAccountVersion(agent, spawnAccount.identityKey) ?? undefined;
2188
+ if (!accountConfigVersion) {
2189
+ console.error(chalk.red(`No installed ${spawnAccount.agent} version is signed in as the identity labeled '${spawnAccount.name}'. Sign in as that identity, or label a different account.`));
2190
+ process.exit(1);
2150
2191
  }
2151
- // Native identity confirmed live; the harness reads it from its home.
2192
+ if (!options.quiet)
2193
+ process.stderr.write(chalk.gray(`[agents] account '${spawnAccount.name}' · ${agent} auth from ${accountConfigVersion}\n`));
2152
2194
  }
2153
2195
  else {
2154
2196
  accountEnv = spawnAccount.env;
@@ -2252,7 +2294,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2252
2294
  const peer = sessionRecoveryPeer(session);
2253
2295
  if (peer) {
2254
2296
  const { runOnPeer } = await import('../lib/session/remote-list.js');
2255
- const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true });
2297
+ const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true, sessionId: session.id });
2256
2298
  if (routed === 'no-target') {
2257
2299
  console.error(chalk.red(`Cannot recover session ${session.shortId}: origin device ${peer} is not a registered reachable peer.`));
2258
2300
  process.exitCode = 1;
@@ -2507,7 +2549,18 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2507
2549
  try {
2508
2550
  const { getAccountInfo } = await import('../lib/agents.js');
2509
2551
  const info = await getAccountInfo(agent);
2510
- if (!info.signedIn) {
2552
+ // Claude authenticates interactively from a per-version setup-token on a
2553
+ // keychain-less worker (the shim's .oauth_token fallback), which the
2554
+ // native-credential probe above can't see — so don't warn "logged out" when
2555
+ // a setup-token resolves for this version. No-op on macOS, where the
2556
+ // credential lives in the keychain and resolveClaudeSetupToken returns null.
2557
+ let authedViaSetupToken = false;
2558
+ if (!info.signedIn && agent === 'claude' && version) {
2559
+ const { resolveClaudeSetupToken } = await import('../lib/claude-account-token.js');
2560
+ const { getVersionHomePath } = await import('../lib/installations/versions.js');
2561
+ authedViaSetupToken = resolveClaudeSetupToken(getVersionHomePath('claude', version)) !== null;
2562
+ }
2563
+ if (!info.signedIn && !authedViaSetupToken) {
2511
2564
  process.stderr.write(chalk.yellow(`⚠ ${agent} looks logged out — log in with: ${loginHint(agent)}. Launching anyway...\n`));
2512
2565
  }
2513
2566
  }
@@ -2670,6 +2723,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2670
2723
  const execOptions = {
2671
2724
  agent,
2672
2725
  version,
2726
+ configVersion: accountConfigVersion,
2673
2727
  prompt,
2674
2728
  interactive: options.interactive || forceInteractive,
2675
2729
  mode: requestedMode,
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerFeedWatchCommand(parent: Command): void;
@@ -0,0 +1,28 @@
1
+ import { machineId } from '../lib/machine-id.js';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { watchFleetFeed, watchLocalFeed } from '../lib/feed/watch.js';
4
+ export function registerFeedWatchCommand(parent) {
5
+ const command = parent.command('watch').description('Stream the canonical agent, attention, and activity projection as NDJSON')
6
+ .option('--json', 'Emit versioned NDJSON envelopes').option('--local', 'Watch only this machine');
7
+ setHelpSections(command, { examples: `agents feed watch --json\nagents feed watch --json --local`, notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect.' });
8
+ command.action(async (_opts, invoked) => {
9
+ const opts = invoked.optsWithGlobals();
10
+ if (!opts.json)
11
+ invoked.error("error: required option '--json' not specified");
12
+ const controller = new AbortController();
13
+ const stop = () => controller.abort();
14
+ const emit = (event) => process.stdout.write(`${JSON.stringify(event)}\n`);
15
+ process.once('SIGINT', stop);
16
+ process.once('SIGTERM', stop);
17
+ try {
18
+ if (opts.local)
19
+ await watchLocalFeed({ scope: machineId(), signal: controller.signal, emit });
20
+ else
21
+ await watchFleetFeed({ signal: controller.signal, emit });
22
+ }
23
+ finally {
24
+ process.off('SIGINT', stop);
25
+ process.off('SIGTERM', stop);
26
+ }
27
+ });
28
+ }
@@ -13,6 +13,8 @@ import { relTime } from '../lib/format.js';
13
13
  import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
14
14
  import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
15
15
  import { notifyUrgentBlock } from '../lib/notify.js';
16
+ import { registerFeedWatchCommand } from './feed-watch.js';
17
+ import { claimAndRouteAttentionAnswer, forwardFeedAnswer } from '../lib/feed/answer.js';
16
18
  import { gcMailbox } from '../lib/mailbox-gc.js';
17
19
  import { isValidMailboxId } from '../lib/mailbox.js';
18
20
  import { getActiveSessions } from '../lib/session/active.js';
@@ -323,6 +325,31 @@ export function registerFeedCommand(program) {
323
325
  .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
324
326
  .option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
325
327
  .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task');
328
+ registerFeedWatchCommand(feed);
329
+ feed.command('answer <attention-key>')
330
+ .description('Atomically claim and deliver one answer to an open attention item')
331
+ .option('--choice <choice-id>', 'Stable choice id from the attention item')
332
+ .option('--text <answer>', 'Free-text answer')
333
+ .option('--as <operator>', 'Verified operator id for high-consequence answers')
334
+ .option('--json', 'Emit the delivery result as JSON')
335
+ .action(async (attentionKey, opts, invoked) => {
336
+ try {
337
+ const ownerHost = attentionKey.slice(0, attentionKey.indexOf('/'));
338
+ const result = ownerHost && ownerHost !== machineId()
339
+ ? await forwardFeedAnswer({ host: ownerHost, attentionKey, choiceId: opts.choice, text: opts.text, operatorId: opts.as })
340
+ : await claimAndRouteAttentionAnswer({
341
+ attentionKey, choiceId: opts.choice, text: opts.text,
342
+ operator: { id: opts.as, verified: Boolean(opts.as), label: opts.as },
343
+ });
344
+ if (opts.json || invoked.parent?.opts()?.json)
345
+ console.log(JSON.stringify(result));
346
+ else
347
+ console.log(result.status === 'delivered' ? `Delivered ${result.receipt.msgId}.` : `Already answered (${result.receipt.at}).`);
348
+ }
349
+ catch (error) {
350
+ invoked.error(`error: ${error instanceof Error ? error.message : String(error)}`);
351
+ }
352
+ });
326
353
  feed
327
354
  .command('post')
328
355
  .description('Post a status update to the fleet activity stream (for agents)')
@@ -3,10 +3,10 @@
3
3
  * pre-filled with version + OS + agent inventory; falls back to printing the
4
4
  * URL when no browser is available.
5
5
  */
6
- import { spawnSync } from 'node:child_process';
7
6
  import { arch, platform, release } from 'node:os';
8
7
  import { createRequire } from 'node:module';
9
8
  import chalk from 'chalk';
9
+ import { showUrl } from '../lib/open-url.js';
10
10
  const REPO = 'phnx-labs/agents-cli';
11
11
  const DISCUSSION_BASE = `https://github.com/${REPO}/discussions/new`;
12
12
  const ISSUE_BASE = `https://github.com/${REPO}/issues/new`;
@@ -20,22 +20,6 @@ function readCliVersion() {
20
20
  return 'unknown';
21
21
  }
22
22
  }
23
- function openInBrowser(url) {
24
- const openers = process.platform === 'darwin'
25
- ? [['open', [url]]]
26
- : process.platform === 'win32'
27
- ? [['cmd', ['/c', 'start', '""', url]]]
28
- : [
29
- ['xdg-open', [url]],
30
- ['gnome-open', [url]]
31
- ];
32
- for (const [cmd, args] of openers) {
33
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
34
- if (r.status === 0)
35
- return true;
36
- }
37
- return false;
38
- }
39
23
  function buildPrefill(kind, summary) {
40
24
  const version = readCliVersion();
41
25
  const os = `${platform()} ${release()} (${arch()})`;
@@ -69,7 +53,7 @@ export function registerFeedbackCommand(program) {
69
53
  .option('-i, --idea', 'File as a feature idea (Discussions → Ideas)')
70
54
  .option('-q, --question', 'Ask a question (Discussions → Q&A)')
71
55
  .option('--print', 'Print the URL instead of opening it')
72
- .action((summary, opts) => {
56
+ .action(async (summary, opts) => {
73
57
  const kind = opts.bug ? 'bug' : opts.idea ? 'idea' : 'question';
74
58
  const summaryText = (summary ?? []).join(' ').trim();
75
59
  const { url } = buildPrefill(kind, summaryText);
@@ -77,7 +61,7 @@ export function registerFeedbackCommand(program) {
77
61
  console.log(url);
78
62
  return;
79
63
  }
80
- const opened = openInBrowser(url);
64
+ const opened = (await showUrl(url)).via !== 'none';
81
65
  if (opened) {
82
66
  console.log(chalk.dim(`Opened ${kind} form in your browser:\n ${url}`));
83
67
  }
@@ -20,6 +20,19 @@ import { type LiveStatusFilter } from './sessions.js';
20
20
  import { openSurfaces, type Backend } from '../lib/terminal/index.js';
21
21
  /** Options for `sessions focus` — device scope + the `--active` live-state filters. */
22
22
  export interface FocusOptions {
23
+ /**
24
+ * Resolve the target from the launcher's AGENT_LAUNCH_ID instead of a session
25
+ * id (RUSH-3125).
26
+ *
27
+ * A `--device` launcher mints this id BEFORE it opens the connection and
28
+ * forwards it, so it is the one handle that is still known after the link
29
+ * dies. A session id is not: only Claude is handed one up front, and every
30
+ * other harness's real id had to be read back off the peer — over the very
31
+ * link that just dropped — which is why a blink left a Grok or Codex tab with
32
+ * nothing to reconnect to. The lookup runs HERE, on the box that owns the hook
33
+ * records, so it needs no network at the moment it is needed most.
34
+ */
35
+ launchId?: string;
23
36
  local?: boolean;
24
37
  attachOnly?: boolean;
25
38
  device?: string[];
@@ -54,6 +67,7 @@ export interface FocusOptions {
54
67
  abandoned?: boolean;
55
68
  queued?: boolean;
56
69
  unknown?: boolean;
70
+ reconnectReattach?: boolean;
57
71
  }
58
72
  /** Commander gives overlapping `sessions` flags to the parent command, even
59
73
  * when they appear after `focus`. Fold only values explicitly provided there
@@ -80,8 +80,10 @@ export function registerFocusCommand(program) {
80
80
  // console.warn here without first removing that delegation.
81
81
  .command('focus', { hidden: true })
82
82
  .argument('[selector]', 'Session id/prefix, agent@version, or topic/path search')
83
+ .option('--launch-id <id>', 'Target the run by its launcher AGENT_LAUNCH_ID instead of a session id (resolved from this machine\'s hook records)')
83
84
  .option('--local', 'Only this machine (skip the cross-host sweep)')
84
85
  .option('--attach-only', 'Attach only — never open a new tab / resume a copy (the old `go` behavior)')
86
+ .option('--reconnect-reattach', '(internal) Set by the reconnect loop — warns when the expected pane is dead and a fresh copy starts instead', false)
85
87
  .option('-D, --device <target...>', 'Scope the picker to live sessions on these devices (device alias from `agents devices`, user@host; repeatable)')
86
88
  .option('--active', 'Only sessions present in the live roster')
87
89
  .option('-a, --agent <agent>', 'Filter by harness and recorded version (for example claude@latest)')
@@ -162,6 +164,38 @@ export function selectFallback(attachOnly) {
162
164
  return attachOnly ? refuseFallback : resumeInNewTab;
163
165
  }
164
166
  export async function focusAction(id, opts) {
167
+ // RUSH-3125: `--launch-id` names the run by the handle its launcher minted,
168
+ // and this box holds the hook record that maps it to the real session id — a
169
+ // local read, so it still works when the network that started the run is gone.
170
+ // Resolved up front into the ordinary id path: everything below, including the
171
+ // attach-else-resume recovery, is identical once we know which session it is.
172
+ if (opts.launchId) {
173
+ if (id) {
174
+ console.error(chalk.red('Pass a session id or --launch-id, not both — they name the same thing two ways.'));
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+ // Read the launch-id index directly rather than through
179
+ // `resolveHookSessionRecord`: that helper kind-guards every hit, which exists
180
+ // to stop a REUSED PID crossing harnesses. A launch id is a per-launch uuid
181
+ // and cannot collide, and the caller here is a reattach that deliberately
182
+ // does not assume which harness the peer picked (`run auto` chooses it
183
+ // remotely), so demanding a kind would reject the exact case this serves.
184
+ const { loadHookSessionIndex } = await import('../lib/session/hook-sessions.js');
185
+ const resolved = loadHookSessionIndex().byLaunchId.get(opts.launchId)?.session_id;
186
+ if (!resolved) {
187
+ // Fail loud: a launch id with no record means the harness never got far
188
+ // enough to write one (it died at startup, or it has no SessionStart
189
+ // hook). Silently opening the picker here would strand an automated
190
+ // reattach in an interactive prompt nobody is watching.
191
+ console.error(chalk.red(`No session recorded for launch id ${opts.launchId} on ${machineId()}.`));
192
+ console.error(chalk.gray(' The run may have failed before its SessionStart hook fired.'));
193
+ console.error(chalk.gray(' Look for it: agents sessions --active'));
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+ id = resolved;
198
+ }
165
199
  const hosts = mergeFocusHosts(opts);
166
200
  const statuses = requestedLiveStatuses(opts);
167
201
  // A device scope needs the cross-host sweep; --local only wins when no host is named.
@@ -204,7 +238,7 @@ export async function focusAction(id, opts) {
204
238
  // still joined instead of resumed as a copy; a crashed session has no live
205
239
  // row and recovers in place.
206
240
  const { activeById } = await gatherLiveTargets(true, { statuses: [] });
207
- await focusResolvedSession(localMatch[0], activeById, self, fallback, opts.attachOnly === true);
241
+ await focusResolvedSession(localMatch[0], activeById, self, fallback, opts.attachOnly === true, opts.reconnectReattach === true);
208
242
  return;
209
243
  }
210
244
  }
@@ -290,7 +324,7 @@ export async function focusAction(id, opts) {
290
324
  exact = [focusTargetForResolved(filteredMatch, outcome.session)];
291
325
  }
292
326
  if (exact.length === 1) {
293
- await focusResolvedSession(exact[0], liveById, self, fallback, opts.attachOnly === true);
327
+ await focusResolvedSession(exact[0], liveById, self, fallback, opts.attachOnly === true, opts.reconnectReattach === true);
294
328
  return;
295
329
  }
296
330
  if (exact.length > 1) {
@@ -524,7 +558,7 @@ async function pickFocusCandidates(sessions, liveById, initialSearch) {
524
558
  export function focusTargetForResolved(poolMatch, resolved) {
525
559
  return poolMatch ?? resolved;
526
560
  }
527
- async function focusResolvedSession(meta, liveById, self, fallback, attachOnly) {
561
+ async function focusResolvedSession(meta, liveById, self, fallback, attachOnly, reconnectReattach = false) {
528
562
  const active = liveById.get(meta.id);
529
563
  if (active && isAttachableLiveSession(active)) {
530
564
  await jumpTo(active, self, fallback);
@@ -535,10 +569,19 @@ async function focusResolvedSession(meta, liveById, self, fallback, attachOnly)
535
569
  process.exitCode = 1;
536
570
  return;
537
571
  }
572
+ if (reconnectReattach) {
573
+ // The reconnect loop expected this pane to be alive. It isn't — the peer
574
+ // rebooted, tmux crashed, or the agent exited while the link was down.
575
+ // Warn before any recovery attempt (local resume OR remote hop) so the
576
+ // user always knows their original context was not directly recovered.
577
+ console.error(chalk.yellow(`\nWarning: ${meta.shortId}'s pane is gone (reboot, tmux crash, or agent exited).`));
578
+ console.error(chalk.gray(` Prior context is in the transcript — starting recovery.\n` +
579
+ ` To see what happened: agents sessions preview ${meta.shortId}`));
580
+ }
538
581
  const remote = sessionProcessHost(meta, self);
539
582
  if (remote) {
540
583
  console.log(chalk.gray(`Recovering ${meta.shortId} on ${remote}…`));
541
- const rc = await runOnPeer(sessionRecoveryRunArgs(meta), remote, { tty: true });
584
+ const rc = await runOnPeer(sessionRecoveryRunArgs(meta), remote, { tty: true, sessionId: meta.id });
542
585
  if (rc === 'no-target') {
543
586
  console.error(chalk.red(`Cannot recover ${meta.shortId}: ${remote} is unreachable.`));
544
587
  process.exitCode = 1;
@@ -765,7 +808,7 @@ const resumeInNewTab = async (s, remote) => {
765
808
  // it actually delivers you to the session (the peer picks the right version + HOME).
766
809
  if (remote) {
767
810
  console.log(chalk.gray(`${shortId(s)} has no live terminal on ${remote} — resuming it there over SSH…`));
768
- const rc = await runOnPeer(sessionRecoveryRunArgs({ id }), remote, { tty: true });
811
+ const rc = await runOnPeer(sessionRecoveryRunArgs({ id }), remote, { tty: true, sessionId: id });
769
812
  if (rc === 'no-target') {
770
813
  console.log(chalk.red(`${remote} isn't reachable as a device. Try: agents devices sync`));
771
814
  console.log(chalk.gray(` recovery must run on ${remote}, where the indexed session originated`));
@@ -66,6 +66,9 @@ export interface Where {
66
66
  label: string;
67
67
  action: string;
68
68
  }
69
+ /** What `jumpTo` writes after a remote tmux attach's SSH stream returns. Pure so
70
+ * the ControlMaster close path is unit-tested without SSH (RUSH-3227). */
71
+ export declare function remoteAttachEndedNotice(sessionId: string | undefined, host: string, code: number): string | undefined;
69
72
  /**
70
73
  * Pure, testable mirror of `jumpTo`'s path selection (jumpTo itself has side
71
74
  * effects — process.exit / ssh / osascript). Keep the branch ORDER in sync with