@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -37,7 +37,7 @@ export async function runComputerWizard() {
37
37
  return false;
38
38
  }
39
39
  // 1. Download + verify + install the signed, notarized helper.
40
- console.log(chalk.bold('Installing the Computer Helper...'));
40
+ console.log(chalk.bold('Installing the Agents Computer helper...'));
41
41
  try {
42
42
  await installComputerHelperMacLocal();
43
43
  }
@@ -57,7 +57,7 @@ export async function runComputerWizard() {
57
57
  }
58
58
  // 3. Guide the two permission grants if not already trusted.
59
59
  if (!trusted) {
60
- console.log(chalk.bold('\nGrant two permissions to "Computer Helper" (one-time):'));
60
+ console.log(chalk.bold('\nGrant two permissions to "Agents Computer" (one-time):'));
61
61
  console.log(' 1. ' + chalk.cyan('Accessibility') + chalk.dim(' — lets it click/type'));
62
62
  console.log(' 2. ' + chalk.cyan('Screen Recording') + chalk.dim(' — lets it screenshot windows'));
63
63
  console.log(chalk.dim('\nOpening System Settings > Privacy & Security ...'));
@@ -10,6 +10,7 @@
10
10
  * rsync / `agents sessions --host` resolve the same logical names.
11
11
  */
12
12
  import type { Command } from 'commander';
13
+ import { type FanOutDeviceTarget } from '../lib/devices/fleet.js';
13
14
  import { type CrabboxBox } from '../lib/crabbox/cli.js';
14
15
  /**
15
16
  * Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
@@ -19,5 +20,17 @@ import { type CrabboxBox } from '../lib/crabbox/cli.js';
19
20
  * the row formatting is deterministic in tests.
20
21
  */
21
22
  export declare function renderLeasedBoxesSection(boxes: CrabboxBox[], nowSecs: number): string[];
23
+ /**
24
+ * Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
25
+ *
26
+ * If `fanOut` settles first the result passes through unchanged. If the
27
+ * deadline fires first every pending remote target is mapped to `failed` (or
28
+ * `skipped` for pre-skipped targets), so callers always get a complete result
29
+ * array and the command exits promptly rather than hanging.
30
+ *
31
+ * Exported so the unit test can exercise the real path with a hanging probe
32
+ * instead of reimplementing the logic.
33
+ */
34
+ export declare function raceFleetPingDeadline<T, Target extends FanOutDeviceTarget>(fanOut: Promise<import('../lib/devices/fleet.js').FanOutDeviceResult<T>[]>, remoteTargets: Target[], overallTimeoutMs: number): Promise<import('../lib/devices/fleet.js').FanOutDeviceResult<T>[]>;
22
35
  /** Register both `agents ssh` and `agents devices`. */
23
36
  export declare function registerSshCommands(program: Command): void;
@@ -20,21 +20,22 @@ import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secret
20
20
  import { machineId } from '../lib/session/sync/config.js';
21
21
  import { registerFleetCaptureCommand } from './fleet-capture.js';
22
22
  import { registerFleetApplyAlias } from './apply.js';
23
- import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
23
+ import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
24
+ import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
24
25
  import { addControlToken } from '../lib/serve/token.js';
25
26
  import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
26
27
  import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
27
- import { localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
28
+ import { defaultPickerChecked, localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
28
29
  import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
29
30
  import { clearPendingSentinel } from '../lib/devices/pending.js';
30
31
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
31
32
  import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
32
- import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
33
+ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, ASKPASS_AGENT_ONLY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
33
34
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
34
35
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
35
- import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
36
+ import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
36
37
  import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
37
- import { buildFleetHealthReport, renderFleetMatrix, renderFleetWarnings, } from '../lib/devices/health-report.js';
38
+ import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
38
39
  import { loadFleetStats, readStatsCache } from '../lib/devices/stats-cache.js';
39
40
  import { checkSyncStatus, countOrphans } from '../lib/drift.js';
40
41
  import { checkAllClis } from '../lib/teams/agents.js';
@@ -47,14 +48,17 @@ import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, re
47
48
  import { runFleetLogin } from '../lib/fleet/remote-login.js';
48
49
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
49
50
  * command is running on so it stands out from the rest of the tailnet. */
50
- function deviceSummary(d, isSelf = false) {
51
+ function deviceSummary(d, isSelf = false, stats) {
51
52
  const addr = hostNameFor(d) ?? chalk.gray('no address');
52
- const online = d.tailscale
53
- ? d.tailscale.online
54
- ? chalk.green('online')
55
- : chalk.gray('offline')
56
- : chalk.gray('unknown');
57
- const reach = d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' (relayed)') : '';
53
+ // Prefer a fresh live verdict (this run's probe, else the written-back
54
+ // reachability) over the stale tailscale.online snapshot (RUSH-1965).
55
+ const state = deviceOnlineState(d, stats);
56
+ const online = state === 'online'
57
+ ? chalk.green('online')
58
+ : state === 'offline'
59
+ ? chalk.gray('offline')
60
+ : chalk.gray('unknown');
61
+ const reach = state === 'online' && d.tailscale && !d.tailscale.direct ? chalk.yellow(' (relayed)') : '';
58
62
  const marker = isSelf ? chalk.cyan('▸ ') : ' ';
59
63
  const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
60
64
  const here = isSelf ? chalk.cyan(' ← this machine') : '';
@@ -106,8 +110,11 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
106
110
  const marker = isSelf ? chalk.cyan('▸ ') : ' ';
107
111
  const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
108
112
  const plat = String(d.platform).padEnd(8);
109
- const offline = d.tailscale && !d.tailscale.online;
110
113
  const stats = statsMap.get(name);
114
+ // Prefer this run's live probe, then the written-back verdict, over the
115
+ // stale tailscale.online snapshot — so a reachable box never renders
116
+ // "offline" while its live load/mem sit one column over (RUSH-1965).
117
+ const offline = deviceOnlineState(d, stats) === 'offline';
111
118
  if (offline) {
112
119
  lines.push(`${marker}${label}${plat} ${chalk.gray('offline')}`);
113
120
  continue;
@@ -243,13 +250,16 @@ async function runInteractiveDeviceSync() {
243
250
  // Everything not already dismissed starts checked, so pressing Enter keeps
244
251
  // the fleet as-is (matching what auto-sync would register). Unchecking a
245
252
  // device removes it AND dismisses it so auto-sync never re-adds it.
253
+ // Sharee nodes (shared in by another user) start unchecked unless already
254
+ // registered — registering one must be a deliberate check, never the
255
+ // default Enter.
246
256
  message: 'Your fleet — uncheck a device to remove and stop suggesting it:',
247
257
  pageSize: Math.min(nodes.length, 20),
248
258
  choices: nodes.map((n) => {
249
- const flags = [n.platform, n.online ? undefined : 'offline', ignored.has(n.name) ? 'ignored' : undefined]
259
+ const flags = [n.platform, n.online ? undefined : 'offline', n.sharee ? 'shared' : undefined, ignored.has(n.name) ? 'ignored' : undefined]
250
260
  .filter(Boolean)
251
261
  .join(', ');
252
- return { value: n.name, name: `${n.name} ${chalk.gray(`(${flags})`)}`, checked: !ignored.has(n.name) };
262
+ return { value: n.name, name: `${n.name} ${chalk.gray(`(${flags})`)}`, checked: defaultPickerChecked(n, registered, ignored) };
253
263
  }),
254
264
  });
255
265
  }
@@ -347,26 +357,23 @@ async function runFleetStatus(opts) {
347
357
  const statsMap = opts.stats === false
348
358
  ? new Map()
349
359
  : (await loadFleetStats(probeable, { forceRefresh, selfName: self })).stats;
360
+ // Persist the live probe's reachability verdict so the online/offline word is
361
+ // read from a fresh probe, not a stale tailscale snapshot (RUSH-1965).
362
+ // Best-effort: a registry write must never break the status render.
363
+ await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
350
364
  const rows = [localHealthRow(self, statsMap.get(self))];
351
365
  const remoteTargets = remoteFleetTargets(planned, self)
352
366
  .map((t) => ({
353
367
  name: t.device.name,
354
368
  platform: t.device.platform,
355
- skip: t.skip,
369
+ // Fail fast: gate the expensive version+doctor dials on the reachability
370
+ // verdict the cheap stats probe already computed one step earlier. A box
371
+ // it found unreachable skips straight to an `unreachable` row instead of
372
+ // burning 15s+30s per box — so one genuinely-offline device can't stall
373
+ // the matrix for ~60s (RUSH-1964). See {@link fleetHealthSkip} for why
374
+ // this is trusted on the default path, not just under `--refresh`.
375
+ skip: fleetHealthSkip(t.skip, statsMap.get(t.device.name)),
356
376
  dialTarget: fleetDialTarget(t.device),
357
- // Fail fast: the stats probe already tried this box with the same
358
- // (registry) address. If it came back unreachable, don't spend another
359
- // 15s+30s on version+doctor — skip straight to an unreachable row so one
360
- // genuinely-offline device can't stall the whole matrix.
361
- //
362
- // Only trust this on a FRESH probe (`--refresh`/`--live`). loadFleetStats
363
- // is cache-first (daemon-warmed ~3min), so a box that came back online
364
- // since the last warm would otherwise be skipped to "unreachable" without
365
- // a live attempt — a false negative. On a default run we still do the live
366
- // probe (fast now that it dials the registry address).
367
- ...(forceRefresh && statsMap.get(t.device.name)?.reachable === false && !t.skip
368
- ? { skip: 'unreachable' }
369
- : {}),
370
377
  }));
371
378
  const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
372
379
  for (const result of remote) {
@@ -400,18 +407,32 @@ async function runFleetStatus(opts) {
400
407
  for (const row of rows) {
401
408
  if (!row.auth)
402
409
  row.auth = summarizeHostAuth(authCache, row.name);
410
+ // Resolve the online/offline verdict and last-seen once (RUSH-1966) so the
411
+ // summary view reads one truth — the same `deviceOnlineState` ordering the
412
+ // registry write-back uses — instead of re-deriving it from error/skipped.
413
+ const profile = reg[row.name];
414
+ if (profile) {
415
+ row.online = deviceOnlineState(profile, statsMap.get(row.name));
416
+ row.lastSeen = profile.tailscale?.lastSeen ?? profile.reachability?.checkedAt;
417
+ }
403
418
  }
404
419
  const report = buildFleetHealthReport(rows);
405
420
  if (opts.json) {
406
421
  console.log(JSON.stringify(report, null, 2));
407
422
  }
408
- else {
423
+ else if (opts.verbose) {
424
+ // Full grid: the auth/CLI/sync/version columns and the warnings rollup.
409
425
  for (const line of renderFleetWarnings(report))
410
426
  console.log(line);
411
427
  console.log();
412
428
  for (const line of renderFleetMatrix(report))
413
429
  console.log(line);
414
430
  }
431
+ else {
432
+ // Default: rollup + NEEDS ATTENTION + OS-grouped quiet rows + footer.
433
+ for (const line of renderFleetSummary(report, { self }))
434
+ console.log(line);
435
+ }
415
436
  if (opts.strict && report.hasWarnings)
416
437
  process.exitCode = 1;
417
438
  }
@@ -420,13 +441,43 @@ async function probeRemoteAuth(target) {
420
441
  const isWin = /^win/i.test((target.platform ?? '').trim());
421
442
  const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
422
443
  const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
423
- const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 60000, multiplex: true });
444
+ const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
424
445
  if (res.code !== 0) {
425
446
  throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
426
447
  }
427
448
  const parsed = JSON.parse(res.stdout);
428
449
  return parsed.rows ?? [];
429
450
  }
451
+ /**
452
+ * Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
453
+ *
454
+ * If `fanOut` settles first the result passes through unchanged. If the
455
+ * deadline fires first every pending remote target is mapped to `failed` (or
456
+ * `skipped` for pre-skipped targets), so callers always get a complete result
457
+ * array and the command exits promptly rather than hanging.
458
+ *
459
+ * Exported so the unit test can exercise the real path with a hanging probe
460
+ * instead of reimplementing the logic.
461
+ */
462
+ export async function raceFleetPingDeadline(fanOut, remoteTargets, overallTimeoutMs) {
463
+ const overallDeadline = new Promise((_, reject) => setTimeout(() => reject(new Error('fleet ping overall deadline exceeded')), overallTimeoutMs));
464
+ try {
465
+ return await Promise.race([fanOut, overallDeadline]);
466
+ }
467
+ catch (err) {
468
+ // Overall deadline hit before all devices settled — mark every pending
469
+ // device as failed so the command exits promptly. Individual probes that
470
+ // already settled are not retrievable (Promise.all internals), so we
471
+ // record all remote targets as timed out / skipped.
472
+ const errMsg = err instanceof Error ? err.message : String(err);
473
+ return remoteTargets.map((t) => ({
474
+ name: t.name,
475
+ status: t.skip ? 'skipped' : 'failed',
476
+ reason: t.skip,
477
+ error: t.skip ? undefined : errMsg,
478
+ }));
479
+ }
480
+ }
430
481
  async function runFleetPing(opts) {
431
482
  const self = machineId();
432
483
  const cliVersion = getCliVersion();
@@ -463,9 +514,16 @@ async function runFleetPing(opts) {
463
514
  const spinner = isInteractiveTerminal() && !opts.json
464
515
  ? ora(`Pinging ${probeable} device${probeable === 1 ? '' : 's'}…`).start()
465
516
  : undefined;
517
+ // Per-device: 15 s (matches the version probe budget; enough for the ~8 s
518
+ // provider-fetch inside the remote local auth probe, with headroom).
519
+ // Overall: 30 s hard cap so the command can never outlast a reasonable
520
+ // budget when several devices are simultaneously unreachable (RUSH-2041).
521
+ const FLEET_PING_DEVICE_TIMEOUT_MS = 15_000;
522
+ const FLEET_PING_OVERALL_TIMEOUT_MS = 30_000;
466
523
  let remote;
467
524
  try {
468
- remote = await fanOutDevices(remoteTargets, probeRemoteAuth);
525
+ const fanOut = fanOutDevices(remoteTargets, probeRemoteAuth, { perDeviceTimeoutMs: FLEET_PING_DEVICE_TIMEOUT_MS });
526
+ remote = await raceFleetPingDeadline(fanOut, remoteTargets, FLEET_PING_OVERALL_TIMEOUT_MS);
469
527
  }
470
528
  finally {
471
529
  spinner?.stop();
@@ -737,6 +795,10 @@ Typical workflow:
737
795
  finally {
738
796
  spinner?.stop();
739
797
  }
798
+ // Write the live verdict back so this and every other consumer read one
799
+ // reachability truth instead of the stale tailscale snapshot (RUSH-1965).
800
+ if (statsMap)
801
+ await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
740
802
  }
741
803
  console.log(chalk.bold(`Devices (${names.length})`));
742
804
  for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
@@ -766,14 +828,20 @@ Typical workflow:
766
828
  .action(runList);
767
829
  devicesCmd
768
830
  .command('status')
769
- .description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
831
+ .description('Fleet health at a glance: online/offline rollup, a NEEDS ATTENTION list (each with its fix command), and quiet per-device rows grouped by OS. Use --verbose for the full auth/CLI/sync grid.')
770
832
  .option('--json', 'output machine-readable JSON')
771
833
  .option('--strict', 'exit non-zero when any device has drift or is unreachable')
772
834
  .option('--no-stats', 'skip the live resource probe')
773
835
  .option('--refresh', 'force a live probe of every device, bypassing the cache')
774
836
  .option('--live', 'alias of --refresh (shorter to type)')
775
- .action(async (opts) => {
776
- await runFleetStatus(opts);
837
+ .option('--verbose', 'show the full per-device auth/CLI/sync/version grid instead of the summary')
838
+ .action(async (opts, cmd) => {
839
+ // The root program also defines a global `--verbose`; commander binds a
840
+ // shared long flag to the program, not the leaf. Read the effective value
841
+ // from the merged globals so `fleet status --verbose` works at either level
842
+ // (same pattern as `fleet ping --verbose`).
843
+ const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
844
+ await runFleetStatus({ ...opts, verbose });
777
845
  });
778
846
  devicesCmd
779
847
  .command('ping')
@@ -1014,7 +1082,7 @@ secrets bundle via an askpass shim — the password never touches argv.
1014
1082
  // `user@device` (same device, login user overridden — dialed via its
1015
1083
  // Tailscale route, not LAN DNS), or an ad-hoc `user@host`/`host` literal.
1016
1084
  // A bare unregistered alias still errors as "Unknown device".
1017
- const device = resolveDeviceTarget(name, await loadDevices());
1085
+ const device = await resolveDeviceTarget(name);
1018
1086
  if (!device) {
1019
1087
  // Not a registered device — it may be a leased crabbox box slug. ssh into
1020
1088
  // it directly (crabbox@<tailnet|ip>:2222) before giving up.
@@ -1076,8 +1144,13 @@ async function runAskpass() {
1076
1144
  console.error(`askpass: ${ASKPASS_BUNDLE_ENV} not set`);
1077
1145
  process.exit(1);
1078
1146
  }
1147
+ // A read-only stats probe sets ASKPASS_AGENT_ONLY_ENV to force a broker-only
1148
+ // resolve even under a TTY — so `agents devices` never pops Touch ID just to
1149
+ // render load/mem for an uncached password-auth device (RUSH-1970). Otherwise
1150
+ // fall back to the headless-context heuristic.
1151
+ const agentOnly = process.env[ASKPASS_AGENT_ONLY_ENV] === '1' || isHeadlessSecretsContext();
1079
1152
  try {
1080
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
1153
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly });
1081
1154
  const value = env[key];
1082
1155
  if (value === undefined) {
1083
1156
  console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
@@ -13,6 +13,7 @@ import { AGENTS } from '../lib/agents.js';
13
13
  import { setHelpSections } from '../lib/help.js';
14
14
  import { computeSyncStatus } from '../lib/sync-status.js';
15
15
  import { promptDriftSync } from '../lib/drift-sync.js';
16
+ import { resolveConfiguredModel } from '../lib/models.js';
16
17
  import { resolveSurface } from './utils.js';
17
18
  const agentName = (id) => AGENTS[id]?.name ?? id;
18
19
  function versionSummary(v) {
@@ -75,8 +76,15 @@ export function registerStatusCommand(program) {
75
76
  console.log(chalk.gray(' (no installed agent versions)'));
76
77
  }
77
78
  for (const v of status.agents) {
78
- const label = `${agentName(v.agent)}@${v.version}${v.isDefault ? chalk.gray(' (default)') : ''}`;
79
- console.log(` ${label.padEnd(28)} ${versionSummary(v)}`);
79
+ // Model sits right beside the version, same priority (no label).
80
+ const model = resolveConfiguredModel(v.agent, v.version)?.model;
81
+ const defaultTag = v.isDefault ? ' (default)' : '';
82
+ const plain = `${agentName(v.agent)}@${v.version}${model ? ` · ${model}` : ''}${defaultTag}`;
83
+ const shown = `${agentName(v.agent)}@${v.version}`
84
+ + (model ? ` ${chalk.gray('·')} ${chalk.yellow(model)}` : '')
85
+ + (v.isDefault ? chalk.gray(' (default)') : '');
86
+ const pad = ' '.repeat(Math.max(1, 34 - plain.length));
87
+ console.log(` ${shown}${pad}${versionSummary(v)}`);
80
88
  }
81
89
  if (status.totals.orphan > 0) {
82
90
  console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
@@ -7,6 +7,7 @@ import { select, confirm, checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
8
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { viewAction } from './view.js';
10
+ import { resolveConfiguredModel, formatAgentIdentity } from '../lib/models.js';
10
11
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
11
12
  import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, setIsolatedDefault, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
12
13
  import { carryForwardSettings } from '../lib/settings-manifest.js';
@@ -410,7 +411,10 @@ export function registerVersionsCommands(program) {
410
411
  spinner.text = msg;
411
412
  });
412
413
  if (result.success) {
413
- spinner.succeed(`Installed ${agentLabel(agentConfig.id)}@${result.installedVersion}`);
414
+ const installedVer = result.installedVersion || version;
415
+ const installedModel = resolveConfiguredModel(agentConfig.id, installedVer)?.model;
416
+ const installedIdentity = formatAgentIdentity(`${agentLabel(agentConfig.id)}@${result.installedVersion}`, installedModel ? chalk.yellow(installedModel) : null);
417
+ spinner.succeed(`Installed ${installedIdentity}`);
414
418
  const installedVersion = result.installedVersion || version;
415
419
  // Track the concrete version so a `--project` pin records it instead
416
420
  // of the `latest`/`oldest` alias.
@@ -756,8 +760,9 @@ export function registerVersionsCommands(program) {
756
760
  manifest.agents[agentId] = finalVersion;
757
761
  writeManifest(process.cwd(), manifest);
758
762
  const projEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
759
- const projEmailStr = projEmail ? chalk.cyan(` (${projEmail})`) : '';
760
- console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} for this project`) + projEmailStr);
763
+ const projModel = resolveConfiguredModel(agentId, finalVersion)?.model;
764
+ const projIdentity = formatAgentIdentity(chalk.green(`${agentLabel(agentConfig.id)}@${finalVersion}`), projModel ? chalk.yellow(projModel) : null, projEmail ? chalk.cyan(projEmail) : null);
765
+ console.log(`Set ${projIdentity} for this project`);
761
766
  }
762
767
  else {
763
768
  // Smart resource detection: compare available vs ACTUALLY synced (source of truth: files, not tracking)
@@ -895,15 +900,19 @@ export function registerVersionsCommands(program) {
895
900
  switchHomeFileSymlinks(agentId, finalVersion);
896
901
  warnIfShimShadowed(agentId);
897
902
  const useEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
898
- const useEmailStr = useEmail ? chalk.cyan(` (${useEmail})`) : '';
903
+ const useModel = resolveConfiguredModel(agentId, finalVersion)?.model;
904
+ const useModelStr = useModel ? chalk.yellow(useModel) : null;
905
+ const useAcctStr = useEmail ? chalk.cyan(useEmail) : null;
899
906
  // Self-updating agents are one binary; `use` only swaps the config
900
907
  // symlink (the profile), it does not change which binary runs — so say
901
908
  // "profile", not "version", to match what actually happened.
902
909
  if (isSelfUpdatingAgent(agentId)) {
903
- console.log(chalk.green(`Switched ${agentLabel(agentConfig.id)} to config profile ${finalVersion}`) + useEmailStr);
910
+ const identity = formatAgentIdentity(chalk.green(agentLabel(agentConfig.id)), useModelStr, useAcctStr);
911
+ console.log(`Switched ${identity} to config profile ${chalk.green(finalVersion)}`);
904
912
  }
905
913
  else {
906
- console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as global default`) + useEmailStr);
914
+ const identity = formatAgentIdentity(chalk.green(`${agentLabel(agentConfig.id)}@${finalVersion}`), useModelStr, useAcctStr);
915
+ console.log(`Set ${identity} as global default`);
907
916
  }
908
917
  }
909
918
  }
@@ -10,6 +10,7 @@ import type { Command } from 'commander';
10
10
  import { accountDisplayLabel } from '../lib/agents.js';
11
11
  import type { AccountInfo } from '../lib/agents.js';
12
12
  import type { AgentId } from '../lib/types.js';
13
+ import { type ConfiguredModelSource } from '../lib/models.js';
13
14
  import { type ProfileSummary } from '../lib/profiles.js';
14
15
  /** Shared account identity formatter, re-exported for the view-specific tests. */
15
16
  export declare const accountColumnLabel: typeof accountDisplayLabel;
@@ -51,6 +52,10 @@ export interface ViewJsonVersion {
51
52
  }>;
52
53
  lastActive: string | null;
53
54
  path: string;
55
+ configuredModel?: {
56
+ model: string;
57
+ source: ConfiguredModelSource;
58
+ } | null;
54
59
  /** Present only when --resources / --detailed (or --json with a section
55
60
  * flag) is passed: this version's resource inventory with git sync-state. */
56
61
  resources?: VersionResourcesJson;
@@ -23,6 +23,7 @@ import { getCentralRulesFileName } from '../lib/rules/rules.js';
23
23
  import { composeRulesFromState } from '../lib/rules/compose.js';
24
24
  import { getConfiguredRunStrategy } from '../lib/rotate.js';
25
25
  import { resolveRunDefaults } from '../lib/run-defaults.js';
26
+ import { resolveConfiguredModel } from '../lib/models.js';
26
27
  import { listProfiles, profileSummary } from '../lib/profiles.js';
27
28
  import { loadManifest, isStale } from '../lib/staleness/index.js';
28
29
  import { confirm } from '@inquirer/prompts';
@@ -419,6 +420,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
419
420
  let maxPlanWidth = 3;
420
421
  let maxUsageWidth = 0;
421
422
  let maxStatusWidth = 0;
423
+ // The configured model sits right after the version, at the same priority.
424
+ // Resolve once here (fs + catalog reads) and reuse in the render loop.
425
+ let maxModelWidth = 0;
426
+ const modelByKey = new Map();
422
427
  for (const agentId of versionManaged) {
423
428
  const versions = listInstalledVersions(agentId);
424
429
  const globalDefault = getGlobalDefault(agentId);
@@ -431,6 +436,11 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
431
436
  maxEmail = Math.max(maxEmail, accountLabel.length);
432
437
  if (info?.plan)
433
438
  maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
439
+ const model = resolveConfiguredModel(agentId, v)?.model;
440
+ if (model) {
441
+ modelByKey.set(`${agentId}:${v}`, model);
442
+ maxModelWidth = Math.max(maxModelWidth, model.length);
443
+ }
434
444
  }
435
445
  // Profile rows share these columns with version rows so they line up.
436
446
  for (const profile of profilesByAgent.get(agentId) ?? []) {
@@ -499,6 +509,11 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
499
509
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
500
510
  // Build columns, trimming trailing whitespace when columns are empty
501
511
  const parts = [` ${label}`];
512
+ // Configured model — same priority as the version, right beside it.
513
+ if (maxModelWidth > 0) {
514
+ const model = modelByKey.get(`${agentId}:${version}`) ?? '';
515
+ parts.push(chalk.yellow(model.padEnd(maxModelWidth)));
516
+ }
502
517
  const hasEmail = !!vInfo?.email;
503
518
  const signedIn = !!vInfo?.signedIn;
504
519
  const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
@@ -508,12 +523,11 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
508
523
  // Otherwise it reflects install time (misleading "just now" for fresh installs).
509
524
  const activeStr = vInfo && hasEmail ? formatLastActive(vInfo.lastActive) : '';
510
525
  const hasActive = activeStr.length > 0;
526
+ // The model now has its own column above; keep only the run mode here.
511
527
  const runDefaults = resolveRunDefaults(agentId, version);
512
528
  const runDefaultBits = [];
513
529
  if (runDefaults.mode)
514
530
  runDefaultBits.push(`mode:${runDefaults.mode}`);
515
- if (runDefaults.model)
516
- runDefaultBits.push(`model:${runDefaults.model}`);
517
531
  if (!hasEmail && !hasUsage && !signedIn) {
518
532
  // Installed but never signed in
519
533
  parts.push(chalk.gray('(logged out — log in with: ' + loginHint(agentId) + ')'));
@@ -555,10 +569,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
555
569
  // No status badge, no last-active — profiles don't accumulate usage state.
556
570
  for (const profile of profilesByAgent.get(agentId) ?? []) {
557
571
  const nameCol = chalk.cyan(profile.name.padEnd(maxVerLabel));
572
+ // Pad the model column so profile rows line up with version rows.
573
+ const modelPad = maxModelWidth > 0 ? `${' '.repeat(maxModelWidth)} ` : '';
558
574
  const authCol = chalk.gray(profile.auth.padEnd(maxEmail));
559
575
  const usageEquivalent = profileKindAndModel(profile.model, maxPlanWidth);
560
576
  const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageEquivalent)));
561
- console.log(` ${nameCol} ${authCol} ${chalk.gray(usageEquivalent + usagePad)}`);
577
+ console.log(` ${nameCol} ${modelPad}${authCol} ${chalk.gray(usageEquivalent + usagePad)}`);
562
578
  if (showPaths) {
563
579
  console.log(chalk.gray(` ${profile.path}`));
564
580
  }
@@ -881,9 +897,12 @@ async function showAgentResources(agentId, requestedVersion, filter) {
881
897
  const accountLabel = accountColumnLabel(accountInfo);
882
898
  const emailStr = accountLabel ? chalk.cyan(` ${accountLabel}`) : '';
883
899
  const status = chalk.green(version);
900
+ // Configured model sits right beside the version, same priority (no label).
901
+ const configuredModel = resolveConfiguredModel(agentId, version);
902
+ const modelStr = configuredModel ? chalk.yellow(` ${configuredModel.model}`) : '';
884
903
  const usageStr = formatUsageSummary(usageInfo.snapshot?.plan ?? accountInfo.plan, null);
885
904
  const usagePart = usageStr ? ` ${usageStr}` : '';
886
- console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${emailStr}${usagePart}`);
905
+ console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${modelStr}${emailStr}${usagePart}`);
887
906
  const usageLines = formatUsageSection(usageInfo);
888
907
  if (usageLines.length > 0) {
889
908
  console.log();
@@ -1221,6 +1240,7 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
1221
1240
  : [],
1222
1241
  lastActive: info.lastActive ? info.lastActive.toISOString() : null,
1223
1242
  path: getVersionDir(agentId, version),
1243
+ configuredModel: resolveConfiguredModel(agentId, version),
1224
1244
  };
1225
1245
  if (wantResources && resourceSync) {
1226
1246
  entry.resources = collectVersionResources(agentId, home, process.cwd(), cliStates?.[agentId]?.installed ?? false, resourceSections, resourceSync);
@@ -8,10 +8,15 @@
8
8
  *
9
9
  * agents watchdog one tick, dry — prints what it WOULD nudge/skip and why
10
10
  * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
- * agents watchdog --watch daemon loop (poll every --interval)
11
+ * agents watchdog --watch manual poll loop (dry unless --nudge)
12
12
  * agents watchdog --json machine-readable tick output (for the menu-bar)
13
- * agents watchdog enable|disable flip the global auto-nudge sentinel (default OFF)
13
+ * agents watchdog enable|disable turn the always-on watchdog routine on/off
14
14
  * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
+ *
16
+ * The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
17
+ * hand-rolled loop: `enable` creates/enables a `watchdog` command routine
18
+ * (`agents watchdog --nudge` every couple of minutes) and reloads the daemon;
19
+ * `disable` pauses it. See ../lib/watchdog/routine.ts.
15
20
  */
16
21
  import type { Command } from 'commander';
17
22
  /** Register the `agents watchdog` command tree. */