@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -18,10 +18,12 @@ import ora from 'ora';
18
18
  import { getCliVersion } from '../lib/version.js';
19
19
  import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
20
20
  import { machineId } from '../lib/session/sync/config.js';
21
+ import { assertRegistrableDeviceName } from '../lib/devices/registry.js';
21
22
  import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
23
+ import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../lib/devices/interactive-host.js';
22
24
  import { registerFleetCaptureCommand } from './fleet-capture.js';
23
25
  import { registerFleetApplyAlias } from './apply.js';
24
- import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
26
+ import { addIgnored, getDevice, loadDevices, loadIgnored, loadIgnoredEntries, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
25
27
  import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
26
28
  import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
27
29
  import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
@@ -47,7 +49,7 @@ import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
47
49
  import { listTasks, resolveTaskRef } from '../lib/hosts/tasks.js';
48
50
  import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
49
51
  import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
50
- import { terminalWidth, truncateToWidth } from '../lib/session/width.js';
52
+ import { stringWidth, stripAnsi, terminalWidth, truncateToWidth } from '../lib/session/width.js';
51
53
  import { sshExecAsync } from '../lib/ssh-exec.js';
52
54
  import { ALL_AGENT_IDS } from '../lib/agents.js';
53
55
  import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
@@ -113,30 +115,99 @@ function pctCell(v, width) {
113
115
  return chalk.yellow(s);
114
116
  return chalk.red(s);
115
117
  }
118
+ /** Floor for the `spec` column; the real width is measured from the rows.
119
+ *
120
+ * A fixed width cannot work here: `fmtBytes` emits an optional decimal, so a
121
+ * spec string runs from `8c 16G 256G` (11) to `10c 23.5G 460G` (14) depending
122
+ * purely on the hardware behind it. Anything narrower than the widest row
123
+ * pushes load/mem/disk/headroom out of alignment row-to-row AND against the
124
+ * header — which defeats the scannability this column exists for. */
125
+ const SPEC_WIDTH_MIN = 12;
126
+ /** The static hardware as one compact cell — `12c 64G 1T`: cores, total RAM,
127
+ * total root disk via fmtBytes. `—` when the probe never saw the box (specs
128
+ * ride the long-TTL static tier, so a warm cache almost always has them).
129
+ * Unpadded; {@link renderDeviceTable} pads to the measured column width. */
130
+ function specText(stats) {
131
+ if (!stats?.reachable || !stats.ncpu)
132
+ return '—';
133
+ return `${stats.ncpu}c ${fmtBytes(stats.memTotalBytes)} ${fmtBytes(stats.diskTotalBytes)}`;
134
+ }
135
+ /** The widest spec string across the rows actually being rendered, floored at
136
+ * SPEC_WIDTH_MIN so a short fleet still lines up with the header. */
137
+ function specColumnWidth(names, statsMap) {
138
+ return Math.max(SPEC_WIDTH_MIN, ...names.map((n) => stringWidth(specText(statsMap.get(n)))));
139
+ }
140
+ /** Colour + pad one spec cell to the measured column width. */
141
+ function specCell(stats, width) {
142
+ const text = specText(stats);
143
+ return (text === '—' ? chalk.gray : chalk.greenBright)(text.padEnd(width));
144
+ }
145
+ /** The one-line `description` config value per device (absent when unset). */
146
+ function listDeviceDescriptions(names) {
147
+ const out = {};
148
+ for (const n of names) {
149
+ const v = getConfigValue('description', { device: n }).value;
150
+ if (typeof v === 'string' && v.length > 0)
151
+ out[n] = v;
152
+ }
153
+ return out;
154
+ }
155
+ /**
156
+ * Fit a device row to the terminal width. The description is the FIRST thing
157
+ * to truncate, then the role tag; the fixed columns (device, platform, spec,
158
+ * load/mem/disk numbers, headroom, and the ▸/★/←/relay markers) never
159
+ * truncate. `role` and `desc` arrive fully rendered (ansi + leading gap).
160
+ */
161
+ function fitDeviceRow(fixed, role, desc, width) {
162
+ const w = (s) => stringWidth(stripAnsi(s));
163
+ let over = w(fixed) + w(role) + w(desc) - width;
164
+ if (over > 0 && desc) {
165
+ // `desc` is ' ' + gray(text) — truncate the text, keep the gap.
166
+ const text = stripAnsi(desc).slice(2);
167
+ const budget = w(text) - over;
168
+ desc = budget > 0 ? ' ' + chalk.gray(truncateToWidth(text, budget)) : '';
169
+ over = w(fixed) + w(role) + w(desc) - width;
170
+ }
171
+ // A partial role word ("work…") reads as a bug — when it can't fit whole,
172
+ // it drops entirely. The numbers are never touched.
173
+ if (over > 0 && role)
174
+ role = '';
175
+ return fixed + role + desc;
176
+ }
116
177
  /**
117
178
  * Render the device list. When `statsMap` is provided, resource columns are
118
- * appended — normalized load, memory, a headroom badge, and (in `full` mode)
119
- * core count and free/total memory so it's obvious which boxes have room.
179
+ * appended — a `spec` cell (cores / total RAM / total disk), normalized load,
180
+ * memory and disk-used percentages, a headroom badge, and (in `full` mode)
181
+ * free/total memory — with the per-device role and `description` riding the
182
+ * tail, so it's obvious which boxes have room and what each box is for.
120
183
  * Without it (probe skipped) the classic reachability line is used. A fleet
121
184
  * capacity summary is appended whenever stats were gathered.
122
185
  */
123
- function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
186
+ export function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost, opts = {}) {
124
187
  if (!statsMap) {
125
188
  const roles = listConfiguredDeviceRoles(names);
126
189
  return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost, roles));
127
190
  }
128
191
  const deviceRoles = listConfiguredDeviceRoles(names);
192
+ const specWidth = specColumnWidth(names, statsMap);
193
+ const descriptions = listDeviceDescriptions(names);
194
+ const width = opts.width ?? terminalWidth();
129
195
  const lines = [];
130
196
  const head = ' ' +
131
197
  chalk.gray('device'.padEnd(16)) +
132
198
  chalk.gray('platform'.padEnd(8)) +
133
199
  ' ' +
134
- (full ? chalk.gray('cores'.padStart(6)) : '') +
200
+ chalk.gray('spec'.padEnd(specWidth)) +
135
201
  chalk.gray('load'.padStart(5)) +
136
202
  chalk.gray('mem'.padStart(6)) +
203
+ chalk.gray('disk'.padStart(5)) +
137
204
  (full ? ' ' + chalk.gray('free/total'.padEnd(12)) : '') +
138
205
  ' ' +
139
- chalk.gray('headroom');
206
+ chalk.gray('headroom') +
207
+ ' ' +
208
+ chalk.gray('role') +
209
+ ' ' +
210
+ chalk.gray('description');
140
211
  lines.push(head);
141
212
  for (const name of names) {
142
213
  // Effective profile: the operator's central config (ssh.*/platform/user)
@@ -147,18 +218,20 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
147
218
  const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
148
219
  const plat = String(d.platform).padEnd(8);
149
220
  const stats = statsMap.get(name);
221
+ const role = roleTag(name, deviceRoles);
222
+ const desc = descriptions[name] ? ' ' + chalk.gray(descriptions[name]) : '';
150
223
  // Prefer this run's live probe, then the written-back verdict, over the
151
224
  // stale tailscale.online snapshot — so a reachable box never renders
152
225
  // "offline" while its live load/mem sit one column over (RUSH-1965).
153
226
  const offline = deviceOnlineState(d, stats) === 'offline';
154
227
  if (offline) {
155
- lines.push(`${marker}${label}${plat} ${chalk.gray('offline')}`);
228
+ lines.push(fitDeviceRow(`${marker}${label}${plat} ${chalk.gray('offline')}`, role, desc, width));
156
229
  continue;
157
230
  }
158
231
  const relay = !isSelf && d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' relay') : '';
159
- const cores = full ? chalk.gray(String(stats?.ncpu ?? '—').padStart(6)) : '';
160
232
  const load = pctCell(stats?.loadPercent, 5);
161
233
  const mem = pctCell(stats?.memPercent, 6);
234
+ const disk = pctCell(stats?.diskUsedPercent, 5);
162
235
  const freeTotal = full
163
236
  ? ' ' +
164
237
  (stats?.reachable && stats.memTotalBytes
@@ -168,13 +241,21 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
168
241
  const badge = HEADROOM_BADGE[headroom(stats)];
169
242
  const here = isSelf ? chalk.cyan(' ← this machine') : '';
170
243
  const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
171
- lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}${roleTag(name, deviceRoles)}`);
244
+ lines.push(fitDeviceRow(`${marker}${label}${plat} ${specCell(stats, specWidth)}${load}${mem}${disk}${freeTotal} ${badge}${relay}${here}${interactive}`, role, desc, width));
172
245
  }
173
- // Fleet capacity summary — total cores + how much RAM is free right now.
246
+ // Fleet capacity summary — total cores, how much RAM is free right now, and
247
+ // total free root disk across the reachable fleet.
174
248
  const cap = fleetCapacity(statsMap.values());
175
249
  if (cap.reachable > 0) {
176
250
  const freePct = cap.memTotalBytes > 0 ? Math.round((cap.memFreeBytes / cap.memTotalBytes) * 100) : 0;
177
- lines.push(chalk.gray(` Fleet capacity: ${cap.cores} cores · ${fmtBytes(cap.memFreeBytes)} free / ${fmtBytes(cap.memTotalBytes)} RAM (${freePct}% free) across ${cap.reachable} reachable device${cap.reachable === 1 ? '' : 's'}`));
251
+ let diskFreeBytes = 0;
252
+ for (const s of statsMap.values())
253
+ if (s.reachable)
254
+ diskFreeBytes += s.diskFreeBytes ?? 0;
255
+ lines.push(chalk.gray(` Fleet capacity: ${cap.cores} cores · ${fmtBytes(cap.memFreeBytes)} free / ${fmtBytes(cap.memTotalBytes)} RAM (${freePct}% free) · ${fmtBytes(diskFreeBytes)} disk free across ${cap.reachable} reachable device${cap.reachable === 1 ? '' : 's'}`));
256
+ }
257
+ if (opts.ignoredCount) {
258
+ lines.push(chalk.gray(` ${opts.ignoredCount} ignored node${opts.ignoredCount === 1 ? '' : 's'} not listed — 'agents devices ignored'`));
178
259
  }
179
260
  return lines;
180
261
  }
@@ -975,6 +1056,7 @@ function registerDevicesCommands(program) {
975
1056
  agents devices sync # pick which tailscale nodes to keep (TTY)
976
1057
  agents devices sync --yes # register all non-ignored nodes
977
1058
  agents devices ignore ipad165 # dismiss a node so it's never re-suggested
1059
+ agents devices ignored # list dismissed nodes (when / which machine)
978
1060
 
979
1061
  Inspect:
980
1062
  agents devices list # what's registered (★ = interactive host)
@@ -990,6 +1072,7 @@ function registerDevicesCommands(program) {
990
1072
  agents devices config win-mini ssh.auth password
991
1073
  agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
992
1074
  agents devices config mac-mini auto-launch.enabled off
1075
+ agents devices describe mark-1 "gpu box — cuda 12.4" # one-line purpose, shown in the list
993
1076
  agents devices config mac-mini interactive.host zion # where agents show YOU artifacts
994
1077
  agents devices render --write # write ~/.ssh/config.d/agents include
995
1078
 
@@ -1002,10 +1085,10 @@ function registerDevicesCommands(program) {
1002
1085
  registerLeaseCommand(devicesCmd);
1003
1086
  registerSnapshotCommand(devicesCmd);
1004
1087
  registerCommandGroups(devicesCmd, [
1005
- { title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'rm'] },
1088
+ { title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'ignored', 'rm'] },
1006
1089
  { title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
1007
1090
  { title: 'Disposable devices', names: ['lease'] },
1008
- { title: 'Configure a device', names: ['config', 'render'] },
1091
+ { title: 'Configure a device', names: ['config', 'describe', 'render'] },
1009
1092
  { title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply'] },
1010
1093
  ]);
1011
1094
  devicesCmd
@@ -1085,6 +1168,47 @@ function registerDevicesCommands(program) {
1085
1168
  setDeviceDiscoveryStatus(name, undefined);
1086
1169
  console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
1087
1170
  });
1171
+ const ignoredCmd = devicesCmd
1172
+ .command('ignored')
1173
+ .description('List dismissed tailscale nodes — what was dismissed, when, and on which machine.')
1174
+ .option('--json', 'output machine-readable JSON')
1175
+ .action((opts) => {
1176
+ try {
1177
+ const entries = loadIgnoredEntries();
1178
+ if (opts.json) {
1179
+ process.stdout.write(JSON.stringify(entries, null, 2) + '\n');
1180
+ return;
1181
+ }
1182
+ if (entries.length === 0) {
1183
+ console.log(chalk.gray("No ignored nodes. 'agents devices ignore <name>' dismisses one — it won't be re-suggested."));
1184
+ return;
1185
+ }
1186
+ console.log(chalk.bold(`Ignored nodes (${entries.length})`));
1187
+ for (const e of entries) {
1188
+ const age = formatCheckedAge(Date.parse(e.ignoredAt));
1189
+ console.log(` ${chalk.bold(e.name.padEnd(24))} ${chalk.gray(`${age} · dismissed on ${e.ignoredOn}`)}`);
1190
+ }
1191
+ console.log(chalk.gray("Undo one with 'agents devices unignore <name>'."));
1192
+ }
1193
+ catch (err) {
1194
+ console.error(chalk.red(err.message));
1195
+ process.exit(1);
1196
+ }
1197
+ });
1198
+ setHelpSections(ignoredCmd, {
1199
+ examples: `
1200
+ agents devices ignored # what was dismissed, when, and on which machine
1201
+ agents devices ignored --json # machine-readable [{ name, ignoredAt, ignoredOn }]
1202
+ agents devices unignore old-laptop # undo a dismissal
1203
+ `,
1204
+ notes: `
1205
+ Dismissals live in the tracked central agents.yaml (fleet.ignored) and
1206
+ sync with 'agents repo push/pull', so a node dismissed on one box stays
1207
+ dismissed everywhere. An ignored node is not a device — it never enters
1208
+ the registry, so 'agents devices list' never shows it; this command is
1209
+ where dismissals are visible.
1210
+ `,
1211
+ });
1088
1212
  // ─── devices config (unified settings surface) ────────────────────────────
1089
1213
  //
1090
1214
  // ONE command for every per-device setting: `agents devices config <name>
@@ -1461,6 +1585,8 @@ function registerDevicesCommands(program) {
1461
1585
  `,
1462
1586
  notes: `
1463
1587
  Keys: role (worker|personal), see 'agents devices role',
1588
+ description (one line saying what the box is for — see
1589
+ 'agents devices describe'; renders in the devices list tail),
1464
1590
  agents.max-concurrent, scheduler.enabled, daemon.enabled,
1465
1591
  watchdog.enabled, tmux.enabled, browser.remote-control,
1466
1592
  browser.task-idle-minutes, browser.profile,
@@ -1529,6 +1655,40 @@ function registerDevicesCommands(program) {
1529
1655
  box stays excluded, since that is what the mark is for.
1530
1656
  `,
1531
1657
  });
1658
+ const describeCmd = devicesCmd
1659
+ .command('describe <name> [text...]')
1660
+ .description('Show or set the one-line description of what a device is FOR ("gpu box — cuda 12.4"). ' +
1661
+ 'Rendered as the tail column of `agents devices list` and synced fleet-wide. ' +
1662
+ 'Same key as `agents devices config <name> description` — one store, two names.')
1663
+ .option('--unset', 'remove the description (the device falls back to the fleet default / unset)')
1664
+ .option('--json', 'output machine-readable JSON')
1665
+ .action(async (name, textParts, opts) => {
1666
+ try {
1667
+ // Thin sugar over the 'description' config key — the same engine that
1668
+ // backs `agents devices config <name> description`, not a second path.
1669
+ await runDevicesConfig(name, 'description', textParts ?? [], opts);
1670
+ }
1671
+ catch (err) {
1672
+ console.error(chalk.red(err.message));
1673
+ process.exit(1);
1674
+ }
1675
+ });
1676
+ setHelpSections(describeCmd, {
1677
+ examples: `
1678
+ agents devices describe mark-1 "gpu box — cuda 12.4" # set it (80 chars, one line)
1679
+ agents devices describe mark-1 # read it back
1680
+ agents devices describe mark-1 --unset # remove it
1681
+ agents devices describe mark-1 --json # machine-readable
1682
+ agents devices config mark-1 description "gpu box" # equivalent — same store
1683
+ `,
1684
+ notes: `
1685
+ The description is the device-scope 'description' config key: stored in
1686
+ the tracked per-device doc (devices/<name>/agents.yaml config: block),
1687
+ synced fleet-wide with 'agents repo push/pull', and rendered as the tail
1688
+ column of 'agents devices list'. It replaces on each set — for appended
1689
+ long-form scratch use 'agents devices config <name> notes "…"'.
1690
+ `,
1691
+ });
1532
1692
  /** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
1533
1693
  const configTombstoneNotice = (retired, replacement) => {
1534
1694
  console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
@@ -1753,6 +1913,7 @@ function registerDevicesCommands(program) {
1753
1913
  process.stdout.write(JSON.stringify(names.map((name) => {
1754
1914
  const config = deviceConfigJson(name);
1755
1915
  const health = statsMap?.get(name);
1916
+ const description = getConfigValue('description', { device: name }).value;
1756
1917
  return {
1757
1918
  ...resolveDeviceProfile(reg[name]),
1758
1919
  interactive: name === interactiveHost,
@@ -1760,6 +1921,7 @@ function registerDevicesCommands(program) {
1760
1921
  // marked (absent when unmarked), `autoPool` is the answer that
1761
1922
  // matters to a caller — may `--device auto` pick this box.
1762
1923
  ...(jsonRoles[name] ? { role: jsonRoles[name] } : {}),
1924
+ ...(typeof description === 'string' && description ? { description } : {}),
1763
1925
  autoPool: autoPool.has(name),
1764
1926
  ...(config ? { config } : {}),
1765
1927
  ...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
@@ -1768,7 +1930,10 @@ function registerDevicesCommands(program) {
1768
1930
  return;
1769
1931
  }
1770
1932
  console.log(chalk.bold(`Devices (${names.length})`));
1771
- for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost))
1933
+ // Dismissed nodes are not devices (never in the registry) surface their
1934
+ // count under the table so a "missing" node is explainable from the list.
1935
+ const ignoredCount = loadIgnoredEntries().length;
1936
+ for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost, { ignoredCount }))
1772
1937
  console.log(line);
1773
1938
  if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
1774
1939
  console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
@@ -1789,7 +1954,7 @@ function registerDevicesCommands(program) {
1789
1954
  devicesCmd
1790
1955
  .command('list')
1791
1956
  .alias('ls')
1792
- .description('List registered devices with platform, address, reachability, and live resource headroom.')
1957
+ .description('List registered devices with platform, spec (cores/RAM/disk), live load/mem/disk headroom, role, and description.')
1793
1958
  .option('--json', 'output effective device profiles, config, and health as a JSON array')
1794
1959
  .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
1795
1960
  .option('--refresh', 'force a live probe of every device, bypassing the cache')
@@ -1924,6 +2089,10 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
1924
2089
  .option('--platform <platform>', 'windows | linux | macos')
1925
2090
  .action(async (name, target, opts) => {
1926
2091
  try {
2092
+ // The one place a device name is CHOSEN rather than observed, so the one
2093
+ // place the reserved-sentinel policy belongs. upsertDevice itself stays
2094
+ // shape-only — `devices sync` feeds it tailnet node names in a loop.
2095
+ assertRegistrableDeviceName(name);
1927
2096
  const { host, user } = splitUserHost(target);
1928
2097
  const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
1929
2098
  const d = await upsertDevice(name, {
@@ -2190,6 +2359,15 @@ the target when it exists, else the remote home. Same portable-cwd rule as
2190
2359
  // nothing to resolve "self" to, and mis-report the pick as "Unknown
2191
2360
  // device").
2192
2361
  let target = name;
2362
+ if (isDeviceInteractive(name)) {
2363
+ const pinned = resolveInteractiveDevice();
2364
+ if (!pinned) {
2365
+ console.error(chalk.red(interactiveUnsetError()));
2366
+ process.exit(1);
2367
+ }
2368
+ process.stderr.write(chalk.gray(`[agents] device=interactive → ${pinned}\n`));
2369
+ target = pinned;
2370
+ }
2193
2371
  if (isDeviceAuto(name)) {
2194
2372
  const plan = resolveDeviceAffinity({});
2195
2373
  if (!plan.host) {
@@ -18,6 +18,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
18
18
  import { setHelpSections } from '../lib/help.js';
19
19
  import { createWorktree, commitsBehindDefault, isGitRepo, hasUncommittedChanges, removeWorktree, worktreeCheckoutExists, worktreeExists, } from '../lib/teams/worktree.js';
20
20
  import { resolveHost } from '../lib/hosts/registry.js';
21
+ import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../lib/devices/interactive-host.js';
21
22
  import { isDeviceAuto, resolveDeviceAuto } from '../lib/smart-launch.js';
22
23
  import { sshTargetFor } from '../lib/hosts/types.js';
23
24
  import { ensureHostReady } from '../lib/hosts/ready.js';
@@ -1369,7 +1370,22 @@ export function registerTeamsCommands(program) {
1369
1370
  // trim, drop blanks, dedupe (preserving first-seen order).
1370
1371
  const rawPool = [opts.devices, opts.hosts].filter(Boolean).join(',');
1371
1372
  const devices = [];
1372
- for (const d of rawPool.split(',').map((s) => s.trim()).filter(Boolean)) {
1373
+ for (const raw of rawPool.split(',').map((s) => s.trim()).filter(Boolean)) {
1374
+ // Resolve `interactive` to the concrete device HERE, the same way the
1375
+ // single `--device` pin does. A pool is a set of specific machines the
1376
+ // scheduler picks between, so persisting the sentinel would let the
1377
+ // pool's membership change silently the next time `interactive.host`
1378
+ // is re-pinned — and every later friction message would quote
1379
+ // "interactive" instead of a box the user can act on.
1380
+ let d = raw;
1381
+ if (isDeviceInteractive(d)) {
1382
+ const pinned = resolveInteractiveDevice();
1383
+ if (!pinned) {
1384
+ dieFriction('teams', 'pool-device-interactive-unset', interactiveUnsetError());
1385
+ }
1386
+ process.stderr.write(chalk.gray(`[teams] pool device=interactive → ${pinned}\n`));
1387
+ d = pinned;
1388
+ }
1373
1389
  if (!devices.includes(d))
1374
1390
  devices.push(d);
1375
1391
  }
@@ -1444,7 +1460,7 @@ export function registerTeamsCommands(program) {
1444
1460
  .alias('a')
1445
1461
  .description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
1446
1462
  .option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
1447
- .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/cursor/droid/opencode; kimi/grok/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
1463
+ .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (more autonomous than edit, mechanism per-harness see 'agents modes <agent>') | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/cursor/droid/opencode; kimi/grok/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
1448
1464
  .option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
1449
1465
  .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
1450
1466
  .option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
@@ -1507,6 +1523,22 @@ export function registerTeamsCommands(program) {
1507
1523
  process.stderr.write(chalk.gray(`[teams] device=auto → ${picked === machineId() ? 'local' : picked}\n`));
1508
1524
  explicitDevice = picked;
1509
1525
  }
1526
+ // `interactive` needs the same up-front resolution, and for a sharper
1527
+ // reason: `teams add`/`create` bail out of the fleet passthrough
1528
+ // (passthrough.ts, the teams subcommand guard), so the sentinel block
1529
+ // there never runs and this is the only resolver. Left literal, the
1530
+ // local-machine check below compares 'interactive' against the machine id,
1531
+ // takes the remote branch on the pinned box itself, and pins the teammate
1532
+ // to an SSH hop into this same machine — skipping three local preflights
1533
+ // on the way.
1534
+ if (explicitDevice && isDeviceInteractive(explicitDevice)) {
1535
+ const pinned = resolveInteractiveDevice();
1536
+ if (!pinned) {
1537
+ dieFriction('teams', 'device-interactive-unset', interactiveUnsetError());
1538
+ }
1539
+ process.stderr.write(chalk.gray(`[teams] device=interactive → ${pinned === machineId() ? 'local' : pinned}\n`));
1540
+ explicitDevice = pinned;
1541
+ }
1510
1542
  // Distributed teams: --device <name> PINS this teammate to a machine over
1511
1543
  // SSH. Resolve + validate the placement here so a bad target fails at `add`
1512
1544
  // time, not silently at launch. Persisted (hostName/hostTarget/repoPath) so
@@ -10,10 +10,12 @@ 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 { FormatUsageSummaryOpts, UsageInfo } from '../lib/accounting/usage.js';
13
14
  import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
14
15
  export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
15
16
  import { type ProfileSummary } from '../lib/profiles.js';
16
17
  import { type ByokUsageResult } from '../lib/byok-usage.js';
18
+ export declare function viewUsageSummaryOptions(agentId: AgentId, signedIn: boolean, usageInfo: UsageInfo | undefined, maxWindows: number | undefined): FormatUsageSummaryOpts;
17
19
  /** Shared account identity formatter, re-exported for the view-specific tests. */
18
20
  export declare const accountColumnLabel: typeof accountDisplayLabel;
19
21
  /**
@@ -8,7 +8,7 @@ import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanag
8
8
  import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
9
9
  import { machineId } from '../lib/machine-id.js';
10
10
  import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
11
- import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
11
+ import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
12
12
  import { readManifest } from '../lib/manifest.js';
13
13
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
14
14
  import { ensureVersionedAliasCurrent, removeShim, } from '../lib/installations/shims.js';
@@ -32,6 +32,19 @@ import { renderHarnessDetail } from './harness.js';
32
32
  import { confirm } from '@inquirer/prompts';
33
33
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
34
34
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
35
+ export function viewUsageSummaryOptions(agentId, signedIn, usageInfo, maxWindows) {
36
+ const headless = isUsageHeadlessScopeError(usageInfo?.error);
37
+ const benignState = usageInfo ? getUsageBenignState(usageInfo) : null;
38
+ return {
39
+ unavailable: agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot && !headless && !benignState,
40
+ unverified: !headless && !!usageInfo?.snapshot && !!usageInfo.error,
41
+ headless,
42
+ maxWindows,
43
+ errorKind: classifyUsageErrorKind(usageInfo?.error),
44
+ errorDetail: usageInfo?.error ?? null,
45
+ benignState,
46
+ };
47
+ }
35
48
  /** Shared account identity formatter, re-exported for the view-specific tests. */
36
49
  export const accountColumnLabel = accountDisplayLabel;
37
50
  /**
@@ -535,15 +548,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
535
548
  const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
536
549
  const usageKey = getUsageLookupKey(info);
537
550
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
538
- const headless = isUsageHeadlessScopeError(usageInfo?.error);
539
- const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot && !headless;
540
- const usageUnverified = !headless && !!usageInfo?.snapshot && !!usageInfo.error;
541
- const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
542
- unavailable: usageUnavailable,
543
- unverified: usageUnverified,
544
- headless,
545
- maxWindows: usageWindowCap,
546
- });
551
+ const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, !!info?.signedIn, usageInfo, usageWindowCap));
547
552
  maxUsageWidth = Math.max(maxUsageWidth, stringWidth(usageStr));
548
553
  const statusStr = formatUsageStatusBadge(info?.usageStatus);
549
554
  maxStatusWidth = Math.max(maxStatusWidth, stringWidth(statusStr));
@@ -603,15 +608,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
603
608
  }
604
609
  const hasEmail = !!vInfo?.email;
605
610
  const signedIn = !!vInfo?.signedIn;
606
- const headless = isUsageHeadlessScopeError(usageInfo?.error);
607
- const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot && !headless;
608
- const usageUnverified = !headless && !!usageInfo?.snapshot && !!usageInfo.error;
609
- const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
610
- unavailable: usageUnavailable,
611
- unverified: usageUnverified,
612
- headless,
613
- maxWindows: usageWindowCap,
614
- });
611
+ const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, signedIn, usageInfo, usageWindowCap));
615
612
  const hasUsage = usageStr.length > 0;
616
613
  // Only show lastActive for versions with an actual logged-in account.
617
614
  // Otherwise it reflects install time (misleading "just now" for fresh installs).
@@ -731,12 +728,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
731
728
  gMaxStatusWidth = Math.max(gMaxStatusWidth, stringWidth(formatUsageStatusBadge(gInfo?.usageStatus)));
732
729
  const gUsageKey = getUsageLookupKey(gInfo);
733
730
  const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
734
- const gHeadless = isUsageHeadlessScopeError(gUsage?.error);
735
- const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
736
- unverified: !gHeadless && !!gUsage?.snapshot && !!gUsage.error,
737
- headless: gHeadless,
738
- maxWindows: usageWindowCap,
739
- });
731
+ const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, viewUsageSummaryOptions(agentId, !!gInfo?.signedIn, gUsage, usageWindowCap));
740
732
  gMaxUsageWidth = Math.max(gMaxUsageWidth, stringWidth(gUsageStr));
741
733
  const gDisplay = accountColumnLabel(gInfo);
742
734
  if (gDisplay)
@@ -754,12 +746,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
754
746
  const parts = [` ${verLabel}${padding}`];
755
747
  const gUsageKey = getUsageLookupKey(gInfo);
756
748
  const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
757
- const gHeadless = isUsageHeadlessScopeError(gUsage?.error);
758
- const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
759
- unverified: !gHeadless && !!gUsage?.snapshot && !!gUsage.error,
760
- headless: gHeadless,
761
- maxWindows: usageWindowCap,
762
- });
749
+ const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, viewUsageSummaryOptions(agentId, !!gInfo?.signedIn, gUsage, usageWindowCap));
763
750
  const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
764
751
  if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
765
752
  const gDisplay = accountColumnLabel(gInfo);
@@ -144,8 +144,7 @@ export function registerWatchdogCommand(program) {
144
144
  .option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
145
145
  .option('--dormant <dur>', 'Idle time after which a session is left alone (dormant)', humanMs(DEFAULT_THRESHOLDS.dormantMs))
146
146
  .option('--text <text>', 'Nudge text delivered into the terminal', 'Continue.')
147
- .option('--smart', 'Use the LLM decider (agents run) instead of the deterministic path (non-reproducible)')
148
- .option('--smart-agent <agent>', 'Agent the --smart decider runs as', 'claude')
147
+ .option('--smart-agent <agent>', 'Agent the watchdog decider runs as', 'claude')
149
148
  .option('--allow-ghostty-focus', 'Permit the coarse, focus-stealing Ghostty path (off by default)')
150
149
  .option('--verbose', 'Show healthy and non-actionable session inspections too')
151
150
  .option('--json', 'Emit the tick result as JSON (for the menu-bar / scripts)')
@@ -163,7 +162,6 @@ export function registerWatchdogCommand(program) {
163
162
  const tickOnce = async (willInject, sessions) => runWatchdogPass({
164
163
  nudge: willInject,
165
164
  nudgeText: opts.text,
166
- smart: opts.smart === true,
167
165
  smartAgent: opts.smartAgent,
168
166
  thresholds,
169
167
  allowGhosttyFocus: opts.allowGhosttyFocus === true,
@@ -233,17 +231,14 @@ export function registerWatchdogCommand(program) {
233
231
  agents watchdog rotate off
234
232
  `,
235
233
  notes: `
236
- Decision path: a cheap deterministic pre-filter resolves the obvious cases
237
- (clearly complete -> skip; a clear promise-without-toolcall -> nudge) and
238
- ESCALATES the judgment-heavy cases -- a session parked on a question, or an
239
- ambiguous stall -- to a smart brain. The brain drives the agent to finish
240
- end-to-end when it asked a needless / already-authorized question or paused
241
- with work left, and leaves it for the human on genuine cases (credentials,
242
- an irreversible or outward-facing action, a real ambiguous decision, or a
243
- completed task). The brain is a customizable 'watchdog' workflow (drop a
244
- WORKFLOW.md in project/user workflows/ to override the prompt + model);
245
- absent one, the built-in prompt runs via 'agents run --mode plan'. Pass
246
- --smart to force every stalled candidate through the brain.
234
+ Decision path: the watchdog is an AGENT, not a heuristic script. Every idle
235
+ session on this machine (its task + transcript tail) is handed to ONE
236
+ 'agents run --mode plan' call per tick, which judges each: idle-but-unfinished
237
+ (was given a task, went quiet, not finished/handed off) -> NUDGE that drives
238
+ it to finish; idle-and-done or genuinely-needs-human -> SKIP. The agent is a
239
+ customizable 'watchdog' workflow (drop a WORKFLOW.md in project/user
240
+ workflows/ to override the prompt + model); absent one, the built-in prompt
241
+ runs. Nothing is nudged when nothing is idle (no agent is spawned).
247
242
 
248
243
  Delivery (answer-router): a running agent is steered via its mailbox; a
249
244
  parked-on-question agent is answered into its EXACT split -- tmux / iTerm /
@@ -19,6 +19,10 @@ export interface NativeAccountCapability {
19
19
  scope: 'version' | 'device' | 'unsupported';
20
20
  status: 'supported' | 'conditional' | 'discovery-only' | 'unsupported';
21
21
  }
22
+ /** Native-login selector coverage required by RUSH-3053. */
23
+ export declare const NATIVE_ACCOUNT_SELECTOR_AGENTS: readonly ["claude", "codex", "cursor", "grok", "kimi"];
24
+ /** Explicit dispositions for config-isolated harnesses outside that selector. */
25
+ export declare const NATIVE_ACCOUNT_SELECTOR_EXCLUSIONS: Partial<Record<AgentId, string>>;
22
26
  /** Canonical truth for native-account naming and attachment semantics. */
23
27
  export declare const NATIVE_ACCOUNT_CAPABILITIES: Record<AgentId, NativeAccountCapability>;
24
28
  export declare function nativeAccountCapability(agent: AgentId): NativeAccountCapability;