@phnx-labs/agents-cli 1.22.47 → 1.22.50

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 (272) hide show
  1. package/CHANGELOG.md +503 -17
  2. package/README.md +76 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +201 -39
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon-test-harness.d.ts +42 -0
  14. package/dist/commands/daemon-test-harness.js +90 -0
  15. package/dist/commands/daemon.d.ts +21 -0
  16. package/dist/commands/daemon.js +147 -12
  17. package/dist/commands/device-config-test-harness.d.ts +11 -0
  18. package/dist/commands/device-config-test-harness.js +59 -0
  19. package/dist/commands/exec.js +76 -16
  20. package/dist/commands/focus.d.ts +14 -0
  21. package/dist/commands/focus.js +48 -5
  22. package/dist/commands/go.d.ts +3 -0
  23. package/dist/commands/go.js +16 -3
  24. package/dist/commands/inspect.js +2 -2
  25. package/dist/commands/lease.d.ts +1 -1
  26. package/dist/commands/lease.js +4 -3
  27. package/dist/commands/mailboxes.js +4 -3
  28. package/dist/commands/menubar.js +11 -4
  29. package/dist/commands/projects.d.ts +16 -1
  30. package/dist/commands/projects.js +58 -64
  31. package/dist/commands/resume.js +1 -1
  32. package/dist/commands/route.js +4 -4
  33. package/dist/commands/routines.js +3 -2
  34. package/dist/commands/run-broadcast.d.ts +1 -1
  35. package/dist/commands/run-broadcast.js +1 -1
  36. package/dist/commands/sessions-picker.d.ts +22 -0
  37. package/dist/commands/sessions-picker.js +48 -9
  38. package/dist/commands/sessions-trace.js +1 -1
  39. package/dist/commands/sessions.js +1 -0
  40. package/dist/commands/setup-secrets.js +23 -0
  41. package/dist/commands/share.d.ts +20 -5
  42. package/dist/commands/share.js +158 -53
  43. package/dist/commands/ssh.js +54 -7
  44. package/dist/commands/teams.d.ts +12 -0
  45. package/dist/commands/teams.js +44 -67
  46. package/dist/commands/traces.d.ts +2 -0
  47. package/dist/commands/traces.js +188 -0
  48. package/dist/commands/view.d.ts +1 -1
  49. package/dist/commands/view.js +17 -34
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +5 -0
  52. package/dist/lib/account-provider-registry.d.ts +9 -0
  53. package/dist/lib/account-provider-registry.js +20 -0
  54. package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
  55. package/dist/lib/accounting/account-pool-collect.js +71 -0
  56. package/dist/lib/accounting/account-pool.d.ts +46 -0
  57. package/dist/lib/accounting/account-pool.js +33 -0
  58. package/dist/lib/accounting/capacity.d.ts +24 -0
  59. package/dist/lib/accounting/capacity.js +30 -0
  60. package/dist/lib/accounting/rotate.d.ts +11 -18
  61. package/dist/lib/accounting/rotate.js +6 -24
  62. package/dist/lib/accounting/usage.d.ts +14 -3
  63. package/dist/lib/accounting/usage.js +64 -18
  64. package/dist/lib/agent-spec/agents.js +11 -9
  65. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  66. package/dist/lib/browser/drivers/ssh.js +36 -9
  67. package/dist/lib/browser/ipc.js +8 -3
  68. package/dist/lib/browser/profiles.d.ts +35 -101
  69. package/dist/lib/browser/profiles.js +130 -301
  70. package/dist/lib/browser/registry.d.ts +39 -0
  71. package/dist/lib/browser/registry.js +149 -0
  72. package/dist/lib/browser/remote-control.d.ts +8 -6
  73. package/dist/lib/browser/remote-control.js +8 -6
  74. package/dist/lib/browser/resolve-target.d.ts +68 -0
  75. package/dist/lib/browser/resolve-target.js +250 -0
  76. package/dist/lib/browser/runtime-state.d.ts +40 -16
  77. package/dist/lib/browser/runtime-state.js +92 -25
  78. package/dist/lib/browser/service.d.ts +28 -9
  79. package/dist/lib/browser/service.js +139 -62
  80. package/dist/lib/browser/task-index.d.ts +70 -0
  81. package/dist/lib/browser/task-index.js +180 -0
  82. package/dist/lib/byok-usage.js +2 -2
  83. package/dist/lib/claude-statusline.d.ts +29 -0
  84. package/dist/lib/claude-statusline.js +139 -0
  85. package/dist/lib/cloud/host.d.ts +1 -1
  86. package/dist/lib/cloud/host.js +1 -1
  87. package/dist/lib/cloud/rush.js +1 -1
  88. package/dist/lib/computer/computer-rpc.js +2 -2
  89. package/dist/lib/computer/download.d.ts +15 -15
  90. package/dist/lib/computer/download.js +35 -98
  91. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  92. package/dist/lib/computer/ssh-tunnel.js +12 -22
  93. package/dist/lib/config-keys.d.ts +2 -8
  94. package/dist/lib/config-keys.js +2 -13
  95. package/dist/lib/crabbox/cli.d.ts +1 -1
  96. package/dist/lib/crabbox/cli.js +2 -2
  97. package/dist/lib/crabbox/lease.d.ts +9 -0
  98. package/dist/lib/crabbox/lease.js +70 -15
  99. package/dist/lib/crabbox/runtimes.js +1 -1
  100. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  101. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  102. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  103. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  104. package/dist/lib/daemon/daemon.d.ts +3 -0
  105. package/dist/lib/daemon/daemon.js +212 -444
  106. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  107. package/dist/lib/daemon/device-probe-service.js +47 -0
  108. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  109. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  110. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  111. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  112. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  113. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  114. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  115. package/dist/lib/daemon/self-heal-service.js +48 -0
  116. package/dist/lib/daemon/service.d.ts +102 -0
  117. package/dist/lib/daemon/service.js +98 -0
  118. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  119. package/dist/lib/daemon/session-index-service.js +40 -0
  120. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  121. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  122. package/dist/lib/daemon/supervisor.d.ts +87 -0
  123. package/dist/lib/daemon/supervisor.js +298 -0
  124. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  125. package/dist/lib/daemon/watchdog-service.js +40 -0
  126. package/dist/lib/daemon-health.d.ts +15 -0
  127. package/dist/lib/daemon-health.js +29 -5
  128. package/dist/lib/daemon-services.d.ts +8 -1
  129. package/dist/lib/daemon-services.js +55 -0
  130. package/dist/lib/daemon-ticks.d.ts +13 -2
  131. package/dist/lib/daemon-ticks.js +20 -18
  132. package/dist/lib/device-config.d.ts +0 -2
  133. package/dist/lib/device-config.js +1 -23
  134. package/dist/lib/devices/registry.d.ts +1 -1
  135. package/dist/lib/devices/registry.js +1 -1
  136. package/dist/lib/devices/stats-cache.d.ts +20 -0
  137. package/dist/lib/devices/stats-cache.js +49 -5
  138. package/dist/lib/devices/worker-pick.d.ts +48 -0
  139. package/dist/lib/devices/worker-pick.js +113 -0
  140. package/dist/lib/exec.d.ts +67 -13
  141. package/dist/lib/exec.js +85 -29
  142. package/dist/lib/harness/adapters/cursor.js +8 -11
  143. package/dist/lib/helper-download.d.ts +108 -0
  144. package/dist/lib/helper-download.js +194 -0
  145. package/dist/lib/helper-versions.d.ts +52 -0
  146. package/dist/lib/helper-versions.js +58 -0
  147. package/dist/lib/hooks/cache.js +11 -0
  148. package/dist/lib/hooks/install.js +2 -2
  149. package/dist/lib/hosts/credentials.d.ts +1 -1
  150. package/dist/lib/hosts/credentials.js +1 -1
  151. package/dist/lib/hosts/dispatch.d.ts +7 -1
  152. package/dist/lib/hosts/dispatch.js +24 -5
  153. package/dist/lib/hosts/passthrough.d.ts +8 -15
  154. package/dist/lib/hosts/passthrough.js +19 -16
  155. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  156. package/dist/lib/hosts/providers/devices.js +1 -1
  157. package/dist/lib/hosts/reconnect.d.ts +179 -21
  158. package/dist/lib/hosts/reconnect.js +266 -45
  159. package/dist/lib/installations/migrate.d.ts +4 -5
  160. package/dist/lib/installations/migrate.js +6 -7
  161. package/dist/lib/installations/shims.d.ts +1 -1
  162. package/dist/lib/installations/shims.js +23 -13
  163. package/dist/lib/installations/store.js +6 -3
  164. package/dist/lib/installations/versions.js +73 -48
  165. package/dist/lib/linear-autoclose.d.ts +1 -1
  166. package/dist/lib/linear-autoclose.js +1 -1
  167. package/dist/lib/loop.d.ts +2 -2
  168. package/dist/lib/loop.js +1 -1
  169. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  170. package/dist/lib/menubar/download-menubar.js +60 -0
  171. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  172. package/dist/lib/menubar/install-menubar.js +79 -21
  173. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  174. package/dist/lib/menubar/notify-desktop.js +6 -6
  175. package/dist/lib/model-tiers.d.ts +1 -1
  176. package/dist/lib/project-probe.d.ts +1 -1
  177. package/dist/lib/project-probe.js +1 -1
  178. package/dist/lib/project-pull.d.ts +1 -1
  179. package/dist/lib/project-pull.js +2 -2
  180. package/dist/lib/projects.d.ts +1 -1
  181. package/dist/lib/projects.js +1 -1
  182. package/dist/lib/sandbox.js +6 -7
  183. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  184. package/dist/lib/secrets/download-keychain.js +70 -0
  185. package/dist/lib/secrets/install-helper.d.ts +20 -1
  186. package/dist/lib/secrets/install-helper.js +44 -3
  187. package/dist/lib/session/active.js +17 -1
  188. package/dist/lib/session/db.d.ts +11 -1
  189. package/dist/lib/session/db.js +138 -6
  190. package/dist/lib/session/discover.d.ts +21 -1
  191. package/dist/lib/session/discover.js +33 -5
  192. package/dist/lib/session/highlights.d.ts +49 -1
  193. package/dist/lib/session/highlights.js +95 -0
  194. package/dist/lib/session/host-link.d.ts +16 -2
  195. package/dist/lib/session/host-link.js +7 -1
  196. package/dist/lib/session/parse.js +33 -1
  197. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  198. package/dist/lib/session/remote/remote-list.js +19 -2
  199. package/dist/lib/session/remote/watch.js +11 -2
  200. package/dist/lib/session/session-cache.d.ts +53 -0
  201. package/dist/lib/session/session-cache.js +88 -0
  202. package/dist/lib/session/state.js +1 -1
  203. package/dist/lib/session/tool-calls.d.ts +2 -0
  204. package/dist/lib/session/tool-calls.js +1 -1
  205. package/dist/lib/session/trajectory-html.d.ts +5 -1
  206. package/dist/lib/session/trajectory-html.js +42 -22
  207. package/dist/lib/session/trajectory-text.js +9 -2
  208. package/dist/lib/session/trajectory.js +53 -4
  209. package/dist/lib/session/types.d.ts +15 -0
  210. package/dist/lib/sha256-asset.d.ts +26 -0
  211. package/dist/lib/sha256-asset.js +41 -0
  212. package/dist/lib/share/backend.d.ts +95 -0
  213. package/dist/lib/share/backend.js +139 -0
  214. package/dist/lib/share/delete.d.ts +6 -2
  215. package/dist/lib/share/delete.js +8 -9
  216. package/dist/lib/share/html.d.ts +22 -0
  217. package/dist/lib/share/html.js +88 -0
  218. package/dist/lib/share/provision.d.ts +18 -4
  219. package/dist/lib/share/provision.js +38 -4
  220. package/dist/lib/share/publish.d.ts +44 -15
  221. package/dist/lib/share/publish.js +58 -26
  222. package/dist/lib/share/worker-template.js +402 -25
  223. package/dist/lib/sqlite.js +16 -3
  224. package/dist/lib/ssh-exec.d.ts +65 -0
  225. package/dist/lib/ssh-exec.js +120 -4
  226. package/dist/lib/startup/command-registry.js +1 -1
  227. package/dist/lib/startup/dev-build.js +3 -3
  228. package/dist/lib/state.d.ts +1 -1
  229. package/dist/lib/state.js +7 -3
  230. package/dist/lib/subagents-registry.js +1 -1
  231. package/dist/lib/teams/agents.d.ts +14 -12
  232. package/dist/lib/teams/agents.js +188 -73
  233. package/dist/lib/teams/api.d.ts +6 -1
  234. package/dist/lib/teams/api.js +2 -0
  235. package/dist/lib/teams/scheduler.d.ts +7 -0
  236. package/dist/lib/teams/scheduler.js +26 -8
  237. package/dist/lib/terminal/index.d.ts +1 -1
  238. package/dist/lib/terminal/types.d.ts +1 -1
  239. package/dist/lib/terminal/types.js +1 -1
  240. package/dist/lib/tmux/orphan-reap.js +1 -1
  241. package/dist/lib/tmux/session.js +31 -0
  242. package/dist/lib/traces/backend.d.ts +29 -0
  243. package/dist/lib/traces/backend.js +46 -0
  244. package/dist/lib/traces/classify.d.ts +52 -0
  245. package/dist/lib/traces/classify.js +72 -0
  246. package/dist/lib/traces/config.d.ts +10 -0
  247. package/dist/lib/traces/config.js +2 -0
  248. package/dist/lib/traces/provision.d.ts +19 -0
  249. package/dist/lib/traces/provision.js +30 -0
  250. package/dist/lib/traces/sync.d.ts +150 -0
  251. package/dist/lib/traces/sync.js +459 -0
  252. package/dist/lib/traces/worker-template.d.ts +4 -0
  253. package/dist/lib/traces/worker-template.js +159 -0
  254. package/dist/lib/types.d.ts +18 -19
  255. package/dist/lib/types.js +14 -0
  256. package/dist/lib/usage-refresh.d.ts +1 -1
  257. package/dist/lib/usage-refresh.js +11 -8
  258. package/dist/lib/workflows.d.ts +1 -1
  259. package/package.json +4 -6
  260. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  261. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  262. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  263. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  264. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  265. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  266. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  267. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  268. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  269. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  270. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  271. package/dist/lib/usage-fleet.d.ts +0 -32
  272. package/dist/lib/usage-fleet.js +0 -125
@@ -508,7 +508,7 @@ export function registerRunCommand(program) {
508
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.')
509
509
  .option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
510
510
  .option('--broadcast', 'Run the same prompt or --task across multiple agents (comma-separated [agent]) × --model cells. Replaces the former `agents bench` group.')
511
- .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/')
512
512
  .option('--list-tasks', 'With --broadcast: list available broadcast task ids')
513
513
  .option('--results [run-id]', 'With --broadcast: show one saved matrix run, or list saved runs newest first')
514
514
  .option('--concurrency <n>', 'With --broadcast: maximum cells running at once', '3')
@@ -1501,6 +1501,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1501
1501
  if (process.env.AGENTS_DISPATCH_DEBUG || options.verbose) {
1502
1502
  process.stderr.write(chalk.gray(`[hosts] dispatch interactive ${runAgent}${runVersion ? `@${runVersion}` : ''} -> ${host.name}\n`));
1503
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
+ }
1504
1517
  const exitCode = await runInteractiveOnHost(host, {
1505
1518
  agent: runAgent,
1506
1519
  version: resumeId ? undefined : runVersion,
@@ -1565,19 +1578,32 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1565
1578
  // `raw` runs aren't tmux wrapped, so there is nothing to reconnect to.
1566
1579
  // For `run auto` prefer the join-resolved id (the harness the remote
1567
1580
  // ACTUALLY picked) over the explicit --session-id only claude adopts.
1568
- const reconnectId = (runAgent === RUN_AUTO_KEYWORD
1569
- ? resolvedRemoteId ?? hostSessionId
1570
- : hostSessionId ?? resolvedRemoteId) ?? resumeId;
1571
- if (reconnectId && !isRaw) {
1572
- const { reconnectInteractiveSession, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
1573
- if (exitCode === SSH_CONN_FAILURE) {
1574
- process.exit(await reconnectInteractiveSession({
1575
- host,
1576
- sessionId: reconnectId,
1577
- initialExit: exitCode,
1578
- }));
1579
- }
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
+ }));
1580
1604
  }
1605
+ if (next.notice)
1606
+ process.stderr.write(next.notice);
1581
1607
  process.exit(exitCode);
1582
1608
  }
1583
1609
  // Headless host run: launch detached, tail the remote log, and follow
@@ -2268,7 +2294,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2268
2294
  const peer = sessionRecoveryPeer(session);
2269
2295
  if (peer) {
2270
2296
  const { runOnPeer } = await import('../lib/session/remote-list.js');
2271
- const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true });
2297
+ const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true, sessionId: session.id });
2272
2298
  if (routed === 'no-target') {
2273
2299
  console.error(chalk.red(`Cannot recover session ${session.shortId}: origin device ${peer} is not a registered reachable peer.`));
2274
2300
  process.exitCode = 1;
@@ -2351,7 +2377,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2351
2377
  }
2352
2378
  else {
2353
2379
  try {
2354
- const resolved = await resolveRunVersion(agent, strategy, cwd);
2380
+ // Account-centric candidate list: native version-home logins PLUS
2381
+ // provider accounts (setup-token / API-key) that can auth this agent,
2382
+ // so `--strategy balanced` spreads across ALL accounts, not just the
2383
+ // ones sitting in a version home (RUSH-3182). Run-path only — the
2384
+ // picker's other callers keep the native-only collector.
2385
+ const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
2386
+ const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun);
2355
2387
  if (resolved.exhausted) {
2356
2388
  // Zero healthy accounts splits two ways, and conflating them is what
2357
2389
  // stranded a logged-out harness with no way in at all (RUSH-2334):
@@ -2402,6 +2434,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2402
2434
  else if (resolved.version) {
2403
2435
  version = resolved.version;
2404
2436
  rotationResult = resolved.rotation;
2437
+ // A balanced/available pick of a PROVIDER account (setup-token /
2438
+ // API-key) carries `providerAccount`. Resolve its env through the
2439
+ // same `resolveSpawnAccount` path an explicit `--account` uses, so
2440
+ // exec injects the credential; a native pick has no providerAccount
2441
+ // and runs from its own version home unchanged (RUSH-3182).
2442
+ const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
2443
+ if (pickedProviderAccount) {
2444
+ try {
2445
+ const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
2446
+ if (picked?.kind === 'provider')
2447
+ accountEnv = picked.env;
2448
+ }
2449
+ catch (err) {
2450
+ console.error(chalk.red(err.message));
2451
+ process.exit(1);
2452
+ }
2453
+ }
2405
2454
  if (resolved.rotation && !options.quiet) {
2406
2455
  const banner = formatRotationBanner(resolved.rotation, strategy);
2407
2456
  process.stderr.write(chalk.gray(banner + '\n'));
@@ -2523,7 +2572,18 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2523
2572
  try {
2524
2573
  const { getAccountInfo } = await import('../lib/agents.js');
2525
2574
  const info = await getAccountInfo(agent);
2526
- if (!info.signedIn) {
2575
+ // Claude authenticates interactively from a per-version setup-token on a
2576
+ // keychain-less worker (the shim's .oauth_token fallback), which the
2577
+ // native-credential probe above can't see — so don't warn "logged out" when
2578
+ // a setup-token resolves for this version. No-op on macOS, where the
2579
+ // credential lives in the keychain and resolveClaudeSetupToken returns null.
2580
+ let authedViaSetupToken = false;
2581
+ if (!info.signedIn && agent === 'claude' && version) {
2582
+ const { resolveClaudeSetupToken } = await import('../lib/claude-account-token.js');
2583
+ const { getVersionHomePath } = await import('../lib/installations/versions.js');
2584
+ authedViaSetupToken = resolveClaudeSetupToken(getVersionHomePath('claude', version)) !== null;
2585
+ }
2586
+ if (!info.signedIn && !authedViaSetupToken) {
2527
2587
  process.stderr.write(chalk.yellow(`⚠ ${agent} looks logged out — log in with: ${loginHint(agent)}. Launching anyway...\n`));
2528
2588
  }
2529
2589
  }
@@ -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
@@ -30,7 +30,8 @@ import { machineId } from '../lib/session/sync/config.js';
30
30
  import { attachTmux, runTmux } from '../lib/tmux/binary.js';
31
31
  import { ensureSessionHookRepaired, paneExitStatus } from '../lib/tmux/session.js';
32
32
  import { getDefaultSocketPath } from '../lib/tmux/paths.js';
33
- import { sshExec, sshStream, assertValidSshTarget, shellQuote } from '../lib/ssh-exec.js';
33
+ import { sshExec, sshStream, assertValidSshTarget, shellQuote, SSH_CONN_FAILURE_CODE } from '../lib/ssh-exec.js';
34
+ import { connectionEndedNotice } from '../lib/hosts/reconnect.js';
34
35
  import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
35
36
  const execFileAsync = promisify(execFile);
36
37
  export function registerGoCommand(program) {
@@ -191,6 +192,13 @@ function synthMeta(s, self) {
191
192
  function shortId(s) {
192
193
  return (s.sessionId ?? '').slice(0, 8) || '-';
193
194
  }
195
+ /** What `jumpTo` writes after a remote tmux attach's SSH stream returns. Pure so
196
+ * the ControlMaster close path is unit-tested without SSH (RUSH-3227). */
197
+ export function remoteAttachEndedNotice(sessionId, host, code) {
198
+ if (!sessionId)
199
+ return undefined;
200
+ return connectionEndedNotice({ kind: 'session', id: sessionId }, host, { dropped: code === SSH_CONN_FAILURE_CODE });
201
+ }
194
202
  /**
195
203
  * Pure, testable mirror of `jumpTo`'s path selection (jumpTo itself has side
196
204
  * effects — process.exit / ssh / osascript). Keep the branch ORDER in sync with
@@ -276,8 +284,13 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
276
284
  `sess=$(tmux ${sock}display-message -pt ${p} '#{session_name}' 2>/dev/null); ` +
277
285
  `[ -n "$w" ] && tmux ${sock}select-window -t "$w" 2>/dev/null; ` +
278
286
  `exec tmux ${sock}attach-session -t "\${sess:-${p}}"`;
279
- console.log(chalk.gray(`Attaching ${shortId(s)} on ${remote} over SSH — Ctrl-b d to detach.`));
280
- process.exit(sshStream(remote, remoteCmd, { tty: true }));
287
+ const attachId = s.sessionId || shortId(s);
288
+ console.log(chalk.gray(`Attaching ${attachId} on ${remote} over SSH — Ctrl-b d to detach.`));
289
+ const code = sshStream(remote, remoteCmd, { tty: true });
290
+ const notice = remoteAttachEndedNotice(s.sessionId, remote, code);
291
+ if (notice)
292
+ process.stderr.write(notice);
293
+ process.exit(code);
281
294
  }
282
295
  // Remote, not in tmux → hand off to the fallback (go: shell; focus: resume in a tab).
283
296
  await fallback(s, remote);
@@ -274,8 +274,8 @@ function isDotAgentsRoot(dir) {
274
274
  for (const kind of DRILLABLE_KINDS) {
275
275
  // `routines/` alone is NOT a marker. Shipping example routine YAML for
276
276
  // `agents routines add` is a documented pattern (this repo's own
277
- // apps/cli/routines/), and that directory is the ONLY kind dir there — so
278
- // counting it would make `agents inspect apps/cli` resolve a source tree as
277
+ // cli/routines/), and that directory is the ONLY kind dir there — so
278
+ // counting it would make `agents inspect cli` resolve a source tree as
279
279
  // a DotAgents repo. Same reasoning as the nested-`.agents/` case above.
280
280
  if (kind === 'routines')
281
281
  continue;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents devices lease` — manage the disposable cloud boxes used by `agents run --lease`.
3
3
  *
4
- * Today: `agents devices lease gc`, which stops expired + idle "orphan" boxes that are
4
+ * Today: `agents devices lease prune`, which stops expired + idle "orphan" boxes that are
5
5
  * holding a provider's server quota (the cause of the `server_limit` 403 a new
6
6
  * lease hits). Reaping is conservative: only boxes whose lease has expired AND
7
7
  * that have been untouched for a safety window are eligible (see `isReapSafe`),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents devices lease` — manage the disposable cloud boxes used by `agents run --lease`.
3
3
  *
4
- * Today: `agents devices lease gc`, which stops expired + idle "orphan" boxes that are
4
+ * Today: `agents devices lease prune`, which stops expired + idle "orphan" boxes that are
5
5
  * holding a provider's server quota (the cause of the `server_limit` 403 a new
6
6
  * lease hits). Reaping is conservative: only boxes whose lease has expired AND
7
7
  * that have been untouched for a safety window are eligible (see `isReapSafe`),
@@ -256,7 +256,8 @@ export function registerLeaseCommand(devicesCommand) {
256
256
  process.exit(ok ? 0 : 1);
257
257
  });
258
258
  lease
259
- .command('gc')
259
+ .command('prune')
260
+ .alias('gc')
260
261
  .description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
261
262
  .option('--dry-run', 'List reap-safe orphan boxes without stopping any', false)
262
263
  .option('--yes', 'Stop them without the interactive confirm', false)
@@ -269,7 +270,7 @@ export function registerLeaseCommand(devicesCommand) {
269
270
  candidates = reapSafeOrphans(crabboxList(boxOpts), nowSecs);
270
271
  }
271
272
  catch (e) {
272
- console.error(chalk.red(`lease gc: ${e.message}`));
273
+ console.error(chalk.red(`lease prune: ${e.message}`));
273
274
  process.exit(1);
274
275
  return;
275
276
  }
@@ -405,17 +405,18 @@ export function registerMailboxesCommand(program) {
405
405
  renderOverview(boxes, limit, filters);
406
406
  });
407
407
  const gcCmd = cmd
408
- .command('gc')
408
+ .command('prune')
409
+ .alias('gc')
409
410
  .description('Run a liveness sweep: archive pending messages in dead boxes and prune stale consumed mail.')
410
411
  .option('--json', 'Emit the GC result as JSON')
411
412
  .action(async (_opts, command) => runGcCommand({ json: command.optsWithGlobals().json === true }));
412
413
  setHelpSections(gcCmd, {
413
414
  examples: `
414
415
  # One-shot sweep using the live session set as the liveness source
415
- agents mailboxes gc
416
+ agents mailboxes prune
416
417
 
417
418
  # Machine-readable summary (for scripts / monitors)
418
- agents mailboxes gc --json
419
+ agents mailboxes prune --json
419
420
  `,
420
421
  notes: `
421
422
  A box is considered dead when no live session (the same source
@@ -115,7 +115,7 @@ export function registerMenubarCommands(program) {
115
115
  .description('Configure AGI Menu end-to-end: one instance, started at login')
116
116
  .option('--check', 'Report the current state, change nothing')
117
117
  .option('--json', 'Emit machine-readable JSON')
118
- .action((options) => {
118
+ .action(async (options) => {
119
119
  if (options.check) {
120
120
  const s = getMenubarStatus();
121
121
  if (options.json) {
@@ -129,7 +129,7 @@ export function registerMenubarCommands(program) {
129
129
  }
130
130
  if (!options.json && notMac())
131
131
  return;
132
- const r = runMenubarSetup();
132
+ const r = await runMenubarSetup();
133
133
  if (options.json) {
134
134
  process.stdout.write(JSON.stringify(r) + '\n');
135
135
  }
@@ -174,10 +174,17 @@ export function registerMenubarCommands(program) {
174
174
  menubar
175
175
  .command('enable')
176
176
  .description('Install and start AGI Menu (launches at login)')
177
- .action(() => {
177
+ .action(async () => {
178
178
  if (notMac())
179
179
  return;
180
- const ok = enableMenubarService({ clearOptOut: true });
180
+ let ok = false;
181
+ try {
182
+ ok = await enableMenubarService({ clearOptOut: true });
183
+ }
184
+ catch (e) {
185
+ console.log(chalk.red(`Could not enable AGI Menu: ${e.message}`));
186
+ return;
187
+ }
181
188
  if (!ok) {
182
189
  console.log(chalk.red('Could not enable: no AGI Menu bundle ships with this install.'));
183
190
  console.log(chalk.gray(' This build may predate the helper, or be a non-macOS package.'));
@@ -8,7 +8,7 @@
8
8
  * lifecycle state, plan completion, open PRs, and tickets in flight.
9
9
  */
10
10
  import type { Command } from 'commander';
11
- import { type ProjectRepo } from '../lib/projects.js';
11
+ import { type ProjectDef, type ProjectRepo } from '../lib/projects.js';
12
12
  import { type LinearMilestone } from '../lib/linear-project-counts.js';
13
13
  /** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
14
14
  export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
@@ -101,4 +101,19 @@ export declare function formatMilestoneDue(targetDate: string, nowMs: number): s
101
101
  export declare function formatMilestoneLines(milestones: LinearMilestone[], next: LinearMilestone | undefined, nowMs: number, limit: number): string[];
102
102
  /** The `next` card line: what this project is due to hit, and how far along it is. */
103
103
  export declare function formatNextMilestone(ms: LinearMilestone, nowMs: number): string;
104
+ /** Options shared by `status` and `view`. */
105
+ export type ProjectCardOpts = {
106
+ json?: boolean;
107
+ window?: string;
108
+ remote?: boolean;
109
+ /** Scope the fleet fan-out to specific devices; undefined = the whole fleet. */
110
+ deviceFilter?: string[];
111
+ };
112
+ /**
113
+ * Merge `--device a b` (variadic) and `--devices a,b,c` (comma list) into one
114
+ * deduped device filter; undefined when neither was given (= whole fleet).
115
+ */
116
+ export declare function resolveDeviceFilter(device?: string[], devices?: string): string[] | undefined;
117
+ /** Print the YAML-side fields that sit under the shared card in `view` mode. */
118
+ export declare function printProjectDefinition(def: ProjectDef, name: string): void;
104
119
  export declare function registerProjectsCommands(program: Command): void;