@phnx-labs/agents-cli 1.20.72 → 1.20.74

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 (234) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +21 -5
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
@@ -19,6 +19,7 @@ import { getCliVersion } from '../lib/version.js';
19
19
  import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
20
20
  import { machineId } from '../lib/session/sync/config.js';
21
21
  import { registerFleetCaptureCommand } from './fleet-capture.js';
22
+ import { registerFleetApplyAlias } from './apply.js';
22
23
  import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
23
24
  import { addControlToken } from '../lib/serve/token.js';
24
25
  import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
@@ -40,7 +41,10 @@ import { checkAllClis } from '../lib/teams/agents.js';
40
41
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
41
42
  import { sshExecAsync } from '../lib/ssh-exec.js';
42
43
  import { ALL_AGENT_IDS } from '../lib/agents.js';
43
- import { formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
44
+ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
45
+ import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
46
+ import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
47
+ import { runFleetLogin } from '../lib/fleet/remote-login.js';
44
48
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
45
49
  * command is running on so it stands out from the rest of the tailnet. */
46
50
  function deviceSummary(d, isSelf = false) {
@@ -130,6 +134,74 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
130
134
  }
131
135
  return lines;
132
136
  }
137
+ /**
138
+ * Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
139
+ * from `crabboxList()` — these are ephemeral crabbox leases, NEVER written into
140
+ * the device registry. Returns [] when crabbox is unavailable / has no creds /
141
+ * reports no boxes, so the section is simply omitted. `nowSecs` is injected so
142
+ * the row formatting is deterministic in tests.
143
+ */
144
+ export function renderLeasedBoxesSection(boxes, nowSecs) {
145
+ if (boxes.length === 0)
146
+ return [];
147
+ const lines = [];
148
+ lines.push('');
149
+ lines.push(chalk.bold('Leased boxes') + chalk.gray(' (ephemeral · via crabbox)'));
150
+ lines.push(' ' +
151
+ chalk.gray('box'.padEnd(16)) +
152
+ chalk.gray('class'.padEnd(10)) +
153
+ chalk.gray('address'.padEnd(24)) +
154
+ chalk.gray('status'.padEnd(9)) +
155
+ chalk.gray('idle'.padEnd(12)) +
156
+ chalk.gray('expires'));
157
+ for (const b of boxes) {
158
+ const addr = boxAddress(b) ?? '—';
159
+ lines.push(' ' +
160
+ chalk.cyan(b.slug.padEnd(16)) +
161
+ (b.class ?? '?').padEnd(10) +
162
+ addr.padEnd(24) +
163
+ boxStatus(b).padEnd(9) +
164
+ chalk.gray(fmtIdleShort(b, nowSecs).padEnd(12)) +
165
+ chalk.gray(fmtExpiresShort(b, nowSecs)));
166
+ }
167
+ lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents lease stop <slug>`'));
168
+ return lines;
169
+ }
170
+ /** The leased-box rows for the devices list, or [] when crabbox can't be read. */
171
+ function loadLeasedBoxesSection() {
172
+ try {
173
+ const boxes = crabboxList({ secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 5000 });
174
+ return renderLeasedBoxesSection(boxes, Math.floor(Date.now() / 1000));
175
+ }
176
+ catch {
177
+ return []; // crabbox not installed / no provider creds — omit the section
178
+ }
179
+ }
180
+ /**
181
+ * `agents ssh <slug>` targeting a leased crabbox box. crabbox provisions a
182
+ * per-lease identity key, so we ssh via crabbox's OWN emitted invocation
183
+ * (`crabboxSshArgv`) — a raw `ssh crabbox@ip` fails publickey. Returns false when
184
+ * `name` is not a known crabbox slug so the caller can fall through to the normal
185
+ * "Unknown device" error.
186
+ */
187
+ function trySshLeasedBox(name, cmd) {
188
+ let box;
189
+ try {
190
+ box = crabboxFind(name, { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 5000 });
191
+ }
192
+ catch {
193
+ return false; // crabbox unavailable — not a leased-box target
194
+ }
195
+ if (!box)
196
+ return false;
197
+ const sshArgv = crabboxSshArgv(name, { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 8000 });
198
+ if (!sshArgv) {
199
+ console.error(chalk.red(`Leased box '${name}' is not reachable yet (status: ${boxStatus(box)}).`));
200
+ process.exit(1);
201
+ }
202
+ const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...cmd], { stdio: 'inherit' });
203
+ process.exit(res.status ?? 1);
204
+ }
133
205
  /** Resolve a device or exit with a clear error. */
134
206
  async function mustGetDevice(name) {
135
207
  const d = await getDevice(name);
@@ -423,17 +495,28 @@ async function runFleetPing(opts) {
423
495
  if (opts.strict && anyBad)
424
496
  process.exitCode = 1;
425
497
  }
426
- /** Color a per-host×agent cell: green all-live, red any revoked/expired, yellow degraded. */
498
+ /** Resolve an {@link AuthCellColor} to a chalk painter. Single map for cells + labels. */
499
+ const CELL_PAINT = {
500
+ green: chalk.green,
501
+ yellow: chalk.yellow,
502
+ red: chalk.red,
503
+ gray: chalk.gray,
504
+ dim: chalk.dim,
505
+ };
506
+ /**
507
+ * Color a per-host×agent cell. The numerator counts accounts that are usable
508
+ * right now — live-verified PLUS signed-in-but-unverifiable (codex/grok) — over
509
+ * the total, so a logged-in codex fleet reads "1/1", not a scary "0/1". Color is
510
+ * the shared {@link authCellColor}: red only for revoked (re-login), yellow for
511
+ * soft/expired (self-refreshes), gray for present-but-unverifiable, green when
512
+ * all live.
513
+ */
427
514
  function authCell(summary, width) {
428
515
  if (summary.total === 0)
429
516
  return chalk.dim('·'.padEnd(width));
430
- const text = `${summary.live}/${summary.total}`;
431
- const padded = text.padEnd(width);
432
- if (summary.bad > 0)
433
- return chalk.red(padded);
434
- if (summary.warn > 0)
435
- return chalk.yellow(padded);
436
- return chalk.green(padded);
517
+ const ok = summary.live + summary.present;
518
+ const padded = `${ok}/${summary.total}`.padEnd(width);
519
+ return CELL_PAINT[authCellColor(summary)](padded);
437
520
  }
438
521
  /** Render the fleet auth matrix (device rows × agent columns) plus an optional per-account breakdown. */
439
522
  function renderAuthMatrix(results, opts) {
@@ -466,7 +549,7 @@ function renderAuthMatrix(results, opts) {
466
549
  lines.push(` ${r.host.padEnd(nameW)} ${cells.join(' ')}${note}`);
467
550
  }
468
551
  lines.push('');
469
- lines.push(chalk.gray(' cell = live/total accounts · green all live · red revoked (re-login) · yellow expired/unverified/limited'));
552
+ lines.push(chalk.gray(' cell = signed-in/total accounts · green live · gray signed-in (unverifiable: codex/grok) · yellow expired (self-refreshes) · red revoked (re-login)'));
470
553
  if (opts?.verbose) {
471
554
  lines.push('');
472
555
  lines.push(chalk.bold('Accounts'));
@@ -475,9 +558,7 @@ function renderAuthMatrix(results, opts) {
475
558
  continue;
476
559
  for (const row of r.rows.slice().sort((x, y) => (x.agent + x.version).localeCompare(y.agent + y.version))) {
477
560
  const v = row.health.verdict;
478
- const label = v === 'live' ? chalk.green(verdictLabel(v))
479
- : (v === 'revoked' || v === 'expired') ? chalk.red(verdictLabel(v))
480
- : chalk.yellow(verdictLabel(v));
561
+ const label = CELL_PAINT[verdictColor(v)](verdictLabel(v));
481
562
  const acctRaw = row.account ?? '—';
482
563
  const acct = row.account ? chalk.cyan(acctRaw.padEnd(28)) : chalk.dim(acctRaw.padEnd(28));
483
564
  const detail = row.health.detail ? chalk.dim(` ${row.health.detail}`) : '';
@@ -488,6 +569,46 @@ function renderAuthMatrix(results, opts) {
488
569
  }
489
570
  return lines;
490
571
  }
572
+ /** Render the final `fleet login` result summary in the house auth-matrix style. */
573
+ function renderLoginMatrix(results) {
574
+ const lines = [chalk.bold('Fleet login')];
575
+ if (results.length === 0) {
576
+ lines.push(chalk.gray(' Nothing pending — every requested account is already logged in.'));
577
+ return lines;
578
+ }
579
+ const nameW = Math.max(6, ...results.map((r) => `${r.agent}@${r.device}`.length));
580
+ for (const r of results) {
581
+ const who = `${r.agent}@${r.device}`.padEnd(nameW);
582
+ let badge;
583
+ switch (r.state) {
584
+ case 'authorized':
585
+ badge = chalk.green('authorized');
586
+ break;
587
+ case 'ready':
588
+ badge = chalk.cyan('code ready');
589
+ break;
590
+ case 'driving':
591
+ badge = chalk.yellow('driving');
592
+ break;
593
+ case 'skipped':
594
+ badge = chalk.gray('not remotable');
595
+ break;
596
+ case 'error':
597
+ badge = chalk.red('error');
598
+ break;
599
+ default:
600
+ badge = chalk.gray(r.state);
601
+ break;
602
+ }
603
+ const note = r.detail ? chalk.dim(` ${r.detail}`) : (r.reason && !r.remotable ? chalk.dim(` ${r.reason}`) : '');
604
+ lines.push(` ${who} ${badge}${note}`);
605
+ }
606
+ const auth = results.filter((r) => r.state === 'authorized').length;
607
+ const remotable = results.filter((r) => r.remotable).length;
608
+ lines.push('');
609
+ lines.push(chalk.gray(` ${auth}/${remotable} remotable logins authorized`));
610
+ return lines;
611
+ }
491
612
  /** Register the `agents devices` command tree (also aliased as `fleet`). */
492
613
  function registerDevicesCommands(program) {
493
614
  const devicesCmd = program
@@ -529,6 +650,9 @@ Typical workflow:
529
650
  });
530
651
  // `agents fleet capture` — snapshot live state into agents.yaml fleet:.
531
652
  registerFleetCaptureCommand(devicesCmd);
653
+ // `agents fleet apply` — same reconcile engine as the top-level `agents apply`,
654
+ // surfaced under the fleet tree for discoverability.
655
+ registerFleetApplyAlias(devicesCmd);
532
656
  devicesCmd
533
657
  .command('register <name>')
534
658
  .description('Register a discovered (pending) node by name — used by the menu-bar "NEW DEVICES → Register" action.')
@@ -576,16 +700,7 @@ Typical workflow:
576
700
  }
577
701
  console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
578
702
  });
579
- devicesCmd
580
- .command('list')
581
- .alias('ls')
582
- .description('List registered devices with platform, address, reachability, and live resource headroom.')
583
- .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
584
- .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
585
- .option('--refresh', 'force a live probe of every device, bypassing the cache')
586
- .option('--live', 'alias of --refresh (shorter to type)')
587
- .option('-f, --full', 'full mode: add per-device core count and free/total memory')
588
- .action(async (opts) => {
703
+ const runList = async (opts = {}) => {
589
704
  const reg = await loadDevices();
590
705
  const names = Object.keys(reg).sort();
591
706
  if (opts.json) {
@@ -629,7 +744,26 @@ Typical workflow:
629
744
  if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
630
745
  console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
631
746
  }
632
- });
747
+ // Ephemeral crabbox leases live alongside the registered fleet but are never
748
+ // written into the registry — surface them as their own live section. This is
749
+ // a live provider call, so honor --no-stats (the explicit "instant, no probes"
750
+ // opt-out) and bound it so a slow provider can't hang `agents devices`.
751
+ if (opts.stats !== false) {
752
+ for (const line of loadLeasedBoxesSection())
753
+ console.log(line);
754
+ }
755
+ };
756
+ devicesCmd.action(runList);
757
+ devicesCmd
758
+ .command('list')
759
+ .alias('ls')
760
+ .description('List registered devices with platform, address, reachability, and live resource headroom.')
761
+ .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
762
+ .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
763
+ .option('--refresh', 'force a live probe of every device, bypassing the cache')
764
+ .option('--live', 'alias of --refresh (shorter to type)')
765
+ .option('-f, --full', 'full mode: add per-device core count and free/total memory')
766
+ .action(runList);
633
767
  devicesCmd
634
768
  .command('status')
635
769
  .description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
@@ -648,8 +782,41 @@ Typical workflow:
648
782
  .option('--local', 'probe only this host (used internally for fan-out)')
649
783
  .option('--verbose', 'show a per-account breakdown, not just the per-host rollup')
650
784
  .option('--strict', 'exit non-zero when any account is revoked (expired is soft — it self-refreshes)')
785
+ .action(async (opts, cmd) => {
786
+ // The root program also defines a global `--verbose` (startup self-heal
787
+ // detail), and commander binds a shared long flag to the program, not the
788
+ // leaf — so `fleet ping --verbose` never set opts.verbose and the
789
+ // per-account breakdown was silently unreachable. Read the effective value
790
+ // from the merged globals so the flag works at either level.
791
+ const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
792
+ await runFleetPing({ ...opts, verbose });
793
+ });
794
+ devicesCmd
795
+ .command('login')
796
+ .description('Log agent CLIs into fleet boxes over SSH: drive each box\'s device-code OAuth, scrape the URL + code, and surface every pending login in one local browser page. Default drives all codes at once; --interactive walks one box at a time (codes requested just-in-time so they don\'t expire).')
797
+ .option('--agents <csv>', 'only these agents (comma-separated); default: every agent with a device-code flow')
798
+ .option('--devices <csv>', 'only these devices (comma-separated); default: every online box')
799
+ .option('--all', 'target every device-code pair regardless of cached login state (cold cache / forced re-login)')
800
+ .option('--interactive', 'guided one-box-at-a-time wizard (codes requested just-in-time)')
801
+ .option('--json', 'output the final result matrix as JSON')
651
802
  .action(async (opts) => {
652
- await runFleetPing(opts);
803
+ const csv = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
804
+ const results = await runFleetLogin({
805
+ agents: csv(opts.agents),
806
+ devices: csv(opts.devices),
807
+ all: opts.all,
808
+ interactive: opts.interactive,
809
+ json: opts.json,
810
+ });
811
+ if (opts.json) {
812
+ console.log(JSON.stringify(results, null, 2));
813
+ }
814
+ else {
815
+ for (const line of renderLoginMatrix(results))
816
+ console.log(line);
817
+ }
818
+ if (results.some((r) => r.remotable && r.state !== 'authorized'))
819
+ process.exitCode = 1;
653
820
  });
654
821
  devicesCmd
655
822
  .command('show <name>')
@@ -849,6 +1016,9 @@ secrets bundle via an askpass shim — the password never touches argv.
849
1016
  // A bare unregistered alias still errors as "Unknown device".
850
1017
  const device = resolveDeviceTarget(name, await loadDevices());
851
1018
  if (!device) {
1019
+ // Not a registered device — it may be a leased crabbox box slug. ssh into
1020
+ // it directly (crabbox@<tailnet|ip>:2222) before giving up.
1021
+ trySshLeasedBox(name, cmd); // exits the process on a match
852
1022
  console.error(chalk.red(`Unknown device '${name}'. See 'agents devices list'.`));
853
1023
  process.exit(1);
854
1024
  }
@@ -907,7 +1077,7 @@ async function runAskpass() {
907
1077
  process.exit(1);
908
1078
  }
909
1079
  try {
910
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', agentOnly: isHeadlessSecretsContext() });
1080
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
911
1081
  const value = env[key];
912
1082
  if (value === undefined) {
913
1083
  console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
@@ -544,7 +544,7 @@ function runLaunchMode(agent, version, cwd, quiet) {
544
544
  if (result.rulesCompiled)
545
545
  bits.push('rules');
546
546
  if (result.workspaceLinks > 0)
547
- bits.push(`${result.workspaceLinks} workspace link(s)`);
547
+ bits.push(`${result.workspaceLinks} project resource(s)`);
548
548
  const mpCount = Object.keys(result.marketplaces).length;
549
549
  if (mpCount > 0) {
550
550
  const pluginCount = Object.values(result.marketplaces).reduce((acc, names) => acc + names.length, 0);
@@ -7,6 +7,8 @@ import { AgentManager, AgentStatus, checkCliSignedIn, collectTeamsDoctorData, ge
7
7
  import { mailboxDir, enqueue } from '../lib/mailbox.js';
8
8
  import { resolveProvider } from '../lib/cloud/registry.js';
9
9
  import { emit } from '../lib/events.js';
10
+ import { maybeShowStarNudge } from '../lib/star-nudge.js';
11
+ import { shareRuntimeEnv } from '../lib/share/config.js';
10
12
  import { runSupervisor } from '../lib/teams/supervisor.js';
11
13
  import { debug } from '../lib/teams/debug.js';
12
14
  import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
@@ -49,8 +51,6 @@ const VALID_AGENTS = Object.keys(AGENT_NAMES);
49
51
  const VALID_MODES = ['plan', 'edit', 'auto', 'skip', 'full'];
50
52
  const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
51
53
  const VALID_CLOUD_PROVIDERS = ['rush', 'codex', 'factory'];
52
- // Auto-enable JSON mode when piped / not a TTY so AI agent consumers get
53
- // parseable output by default.
54
54
  function statusColor(status) {
55
55
  switch (status) {
56
56
  case 'pending': return chalk.blue;
@@ -1352,6 +1352,7 @@ export function registerTeamsCommands(program) {
1352
1352
  repo: opts.repo,
1353
1353
  branch: opts.branch,
1354
1354
  model: a.model ?? undefined,
1355
+ env: shareRuntimeEnv({ agentOnly: true }),
1355
1356
  };
1356
1357
  const cloudTask = await prov.dispatch(dispatchOpts);
1357
1358
  return { cloudSessionId: cloudTask.id };
@@ -1369,6 +1370,7 @@ export function registerTeamsCommands(program) {
1369
1370
  repo: opts.repo,
1370
1371
  branch: opts.branch,
1371
1372
  model: opts.model,
1373
+ env: shareRuntimeEnv({ agentOnly: true }),
1372
1374
  };
1373
1375
  try {
1374
1376
  const cloudTask = await prov.dispatch(dispatchOpts);
@@ -1594,6 +1596,13 @@ export function registerTeamsCommands(program) {
1594
1596
  emit('teams.complete', { module: 'teams', team, stoppedBy: result.stoppedBy, waves: result.waves, durationMs: result.elapsed_ms });
1595
1597
  if (result.stoppedBy === 'drained') {
1596
1598
  console.log(chalk.green(`Factory drained in ${elapsed}s (${result.waves} waves).`));
1599
+ // First-successful-team star nudge (one-time, non-nagging). "drained"
1600
+ // only means nothing is pending/running — teammates may have failed — so
1601
+ // gate on a clean drain (failed === 0). A team where every teammate
1602
+ // failed is not the success this nudge celebrates.
1603
+ if ((result.failed ?? 0) === 0) {
1604
+ maybeShowStarNudge({ quiet: opts.json });
1605
+ }
1597
1606
  }
1598
1607
  else if (result.stoppedBy === 'max-waves') {
1599
1608
  console.error(chalk.yellow(`Hit --max-waves=${maxWaves}; stopping. Re-run to continue.`));
@@ -8,9 +8,9 @@ import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo
8
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { viewAction } from './view.js';
10
10
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
11
- import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
11
+ 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
12
  import { carryForwardSettings } from '../lib/settings-manifest.js';
13
- import { createShim, createVersionedAlias, supportsIsolatedInstall, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
13
+ import { createShim, createVersionedAlias, supportsIsolatedInstall, isIsolationProtected, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
14
14
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
15
15
  import { tryAutoPull } from '../lib/git.js';
16
16
  import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
@@ -342,6 +342,20 @@ export function registerVersionsCommands(program) {
342
342
  // from the user's real ~/.<agent>. Agents without one isolate only by
343
343
  // adopting ~/.<agent> (which --isolated skips), so an isolated copy
344
344
  // would silently read/write the real config. Refuse rather than lie.
345
+ // A normal install of a currently isolated-only agent would adopt it: set the
346
+ // global default, create the bare shim, and repoint the real ~/.<agent>. The
347
+ // primitives refuse that outright, so catch it here — before spending a
348
+ // network install on something that cannot finish — and say what to do.
349
+ if (!isIsolated && isIsolationProtected(agent)) {
350
+ console.log(chalk.red(`${agentLabel(agentConfig.id)} is installed only as isolated copies.`));
351
+ console.log(chalk.gray(` A normal install would adopt ${agentConfig.configDir} and your ${agentConfig.cliCommand} launcher.`));
352
+ console.log(chalk.gray(` Keep the sandbox: agents add ${agent}@${version ?? 'latest'} --isolated`));
353
+ console.log(chalk.gray(' Or manage it normally by removing the isolated copies first:'));
354
+ for (const v of listInstalledVersions(agent)) {
355
+ console.log(chalk.gray(` agents remove ${agent}@${v} --isolated`));
356
+ }
357
+ continue;
358
+ }
345
359
  if (isIsolated && !supportsIsolatedInstall(agent)) {
346
360
  console.log(chalk.red(`${agentLabel(agentConfig.id)} does not support --isolated installs.`));
347
361
  console.log(chalk.gray(` It has no config-directory env var, so it can only isolate by adopting ${agentConfig.configDir} — which --isolated deliberately avoids.`));
@@ -705,10 +719,27 @@ export function registerVersionsCommands(program) {
705
719
  // Refuse for both the explicit `use <agent>@<isolated>` path (the picker
706
720
  // above already filters isolated versions out of the interactive path).
707
721
  // Isolated copies are launched explicitly via `agents run <agent>@<v>`.
722
+ // ...so `use` is scoped to the sandbox rather than refused. Setting the
723
+ // ISOLATED default records which copy a bare `agents run <agent>` should
724
+ // reach, and touches none of the five adopting side effects above. The
725
+ // pointer lives in `meta.isolatedAgents`, never in `meta.agents`, so
726
+ // `getGlobalDefault` still cannot return an isolated version.
708
727
  if (isVersionIsolated(agentId, finalVersion)) {
709
- console.log(chalk.red(`${agentLabel(agentConfig.id)}@${finalVersion} is an isolated install and can't be set as your active version.`));
710
- console.log(chalk.gray(`Isolated copies stay walled off from your real ${agentConfig.configDir}. Run it directly: agents run ${agentId}@${finalVersion}`));
711
- console.log(chalk.gray(`To install this version normally: agents add ${agentId}@${finalVersion}`));
728
+ if (options.project) {
729
+ console.log(chalk.yellow(`${agentLabel(agentConfig.id)}@${finalVersion} is an isolated install; --project pins are for shared versions.`));
730
+ console.log(chalk.gray(`Run it directly instead: agents run ${agentId}@${finalVersion}`));
731
+ return;
732
+ }
733
+ setIsolatedDefault(agentId, finalVersion);
734
+ console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as your default ISOLATED copy.`));
735
+ console.log(chalk.gray(` agents run ${agentId} now reaches it (no @version needed).`));
736
+ const globalDefault = getGlobalDefault(agentId);
737
+ if (globalDefault) {
738
+ console.log(chalk.gray(` Your default ${agentConfig.cliCommand} is still ${globalDefault}; ${agentConfig.configDir} is untouched.`));
739
+ }
740
+ else {
741
+ console.log(chalk.gray(` ${agentConfig.configDir} and your ${agentConfig.cliCommand} launcher are untouched.`));
742
+ }
712
743
  return;
713
744
  }
714
745
  if (options.project) {
@@ -4,13 +4,13 @@ import { visibleWidth, termLink } from '../lib/format.js';
4
4
  import ora from 'ora';
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanagedCliState, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
8
8
  import { 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
11
  import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
12
12
  import { readManifest } from '../lib/manifest.js';
13
- import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
13
+ import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
14
14
  import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
15
15
  import { getAgentResources } from '../lib/resources.js';
16
16
  import { resolveVersionFilter, AgentSpecError } from '../lib/agent-spec/index.js';
@@ -243,9 +243,27 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
243
243
  ? `Checking ${agentLabel(filterAgentId)} agents...`
244
244
  : 'Checking installed agents...';
245
245
  const spinner = ora({ text: spinnerText, isSilent: !process.stdout.isTTY }).start();
246
- const cliStates = await getAllCliStates();
247
- spinner.stop();
248
246
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
247
+ // A globally-installed CLI is superseded only by a NORMAL managed version — that
248
+ // is when agents-cli owns the launcher and a "global" row would just be our own
249
+ // shim reported back. `--isolated` promises the opposite: no default, no bare
250
+ // shim, no adopted launcher, the user's own `~/.<agent>` untouched. So an
251
+ // isolated-only install must not make that still-live global CLI disappear from
252
+ // `agents view` — the two are genuinely separate installs and both get listed.
253
+ const hasNonIsolatedVersion = (agentId) => listInstalledVersions(agentId).some((v) => !isVersionIsolated(agentId, v));
254
+ // Every `cliStates` read in this function feeds the "Not Managed by Agents CLI"
255
+ // block, so resolve it the way that block means it: the user's own CLI on PATH.
256
+ // `getCliState` would answer with a version-dir install — including an isolated
257
+ // copy that is deliberately absent from PATH — and print it as "(global)".
258
+ //
259
+ // Resolved only for agents that can actually reach that block. `getCliState`
260
+ // deliberately avoids subprocesses for a version-managed agent, and PATH
261
+ // resolution costs a `<cli> --version` spawn on a cold cache — so probing an
262
+ // agent whose global row is suppressed anyway would be pure added latency.
263
+ const cliStates = Object.fromEntries(await Promise.all(agentsToShow
264
+ .filter((agentId) => !hasNonIsolatedVersion(agentId))
265
+ .map(async (agentId) => [agentId, await getUnmanagedCliState(agentId)])));
266
+ spinner.stop();
249
267
  const showPaths = !!filterAgentId;
250
268
  const profilesByAgent = getProfilesByAgent(filterAgentId);
251
269
  const profileSummaries = [...profilesByAgent.values()].flat();
@@ -276,19 +294,18 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
276
294
  const infoFetches = [];
277
295
  const globalInfoFetches = [];
278
296
  for (const agentId of agentsToShow) {
279
- const versions = listInstalledVersions(agentId);
280
- if (versions.length > 0) {
281
- for (const ver of versions) {
282
- const home = getVersionHomePath(agentId, ver);
283
- infoFetches.push(getAccountInfo(agentId, home).then((info) => ({
284
- agentId,
285
- version: ver,
286
- home,
287
- info,
288
- })));
289
- }
297
+ for (const ver of listInstalledVersions(agentId)) {
298
+ const home = getVersionHomePath(agentId, ver);
299
+ infoFetches.push(getAccountInfo(agentId, home).then((info) => ({
300
+ agentId,
301
+ version: ver,
302
+ home,
303
+ info,
304
+ })));
290
305
  }
291
- else {
306
+ // Mirrors the classification below: fetch the global account whenever the
307
+ // global install will still be rendered (no versions at all, or isolated-only).
308
+ if (!hasNonIsolatedVersion(agentId)) {
292
309
  globalInfoFetches.push(getAccountInfo(agentId).then((info) => ({
293
310
  agentId,
294
311
  cliVersion: cliStates[agentId]?.version || null,
@@ -356,10 +373,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
356
373
  if (versions.length > 0) {
357
374
  versionManaged.push(agentId);
358
375
  }
359
- else if (cliState?.installed) {
360
- globallyInstalled.push(agentId);
376
+ if (cliState?.installed) {
377
+ // Isolated-only installs sit alongside the global CLI rather than replacing it.
378
+ if (!hasNonIsolatedVersion(agentId))
379
+ globallyInstalled.push(agentId);
361
380
  }
362
- else if (hasProfiles) {
381
+ else if (versions.length === 0 && hasProfiles) {
363
382
  profileOnly.push(agentId);
364
383
  }
365
384
  }
@@ -376,6 +395,22 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
376
395
  liveVersionByAgent.set(agentId, live);
377
396
  }));
378
397
  const displayVersion = (agentId, dirVersion) => liveVersionByAgent.get(agentId) ?? dirVersion;
398
+ // Uncolored row label, shared by the width pass and the render so padding lines
399
+ // up. An isolated copy is never the global default (installing one deliberately
400
+ // records no default), so the two tags can't collide.
401
+ const versionRowLabel = (agentId, version, globalDefault) => {
402
+ const shown = displayVersion(agentId, version);
403
+ if (version === globalDefault)
404
+ return `${shown} (default)`;
405
+ if (isVersionIsolated(agentId, version)) {
406
+ // The isolated default is what a bare `agents run <agent>` reaches, so it is
407
+ // worth distinguishing from the other isolated copies sitting beside it.
408
+ return getIsolatedDefault(agentId) === version
409
+ ? `${shown} (isolated default)`
410
+ : `${shown} (isolated)`;
411
+ }
412
+ return shown;
413
+ };
379
414
  // Show version-managed agents
380
415
  if (versionManaged.length > 0) {
381
416
  // Calculate column widths across all agents for alignment
@@ -388,9 +423,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
388
423
  const versions = listInstalledVersions(agentId);
389
424
  const globalDefault = getGlobalDefault(agentId);
390
425
  for (const v of versions) {
391
- const shown = displayVersion(agentId, v);
392
- const label = v === globalDefault ? `${shown} (default)` : shown;
393
- maxVerLabel = Math.max(maxVerLabel, label.length);
426
+ maxVerLabel = Math.max(maxVerLabel, versionRowLabel(agentId, v, globalDefault).length);
394
427
  const rawInfo = infoMap.get(`${agentId}:${v}`);
395
428
  const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
396
429
  const accountLabel = accountColumnLabel(info);
@@ -431,7 +464,14 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
431
464
  const globalDefault = getGlobalDefault(agentId);
432
465
  const runStrategy = getConfiguredRunStrategy(agentId);
433
466
  const strategyLabel = chalk.gray(` (${runStrategy})`);
434
- const noDefaultLabel = !globalDefault ? chalk.yellow(' (no default)') : '';
467
+ // `(no default)` is a nudge to go set one. It would read as a contradiction
468
+ // directly above a row tagged `(isolated default)`, and it would be bad
469
+ // advice besides: for an isolated-only agent the pointer below IS how a
470
+ // bare `agents run <agent>` resolves, and setting a global default is
471
+ // precisely what `--isolated` exists to avoid.
472
+ const noDefaultLabel = !globalDefault && !getIsolatedDefault(agentId)
473
+ ? chalk.yellow(' (no default)')
474
+ : '';
435
475
  console.log(` ${chalk.bold(agentLabel(agentId))}${strategyLabel}${noDefaultLabel}`);
436
476
  // Sort versions with default first, then by semver descending
437
477
  const sortedVersions = [...versions].sort((a, b) => {
@@ -443,10 +483,16 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
443
483
  });
444
484
  for (const version of sortedVersions) {
445
485
  const isDefault = version === globalDefault;
486
+ const isolated = !isDefault && isVersionIsolated(agentId, version);
487
+ const isolatedTag = getIsolatedDefault(agentId) === version ? ' (isolated default)' : ' (isolated)';
446
488
  const shown = displayVersion(agentId, version);
447
- const base = isDefault ? `${shown} (default)` : shown;
448
- const padded = base.padEnd(maxVerLabel);
449
- const label = isDefault ? `${shown}${chalk.green(' (default)')}${' '.repeat(maxVerLabel - base.length)}` : padded;
489
+ const base = versionRowLabel(agentId, version, globalDefault);
490
+ const tagPad = ' '.repeat(maxVerLabel - base.length);
491
+ const label = isDefault
492
+ ? `${shown}${chalk.green(' (default)')}${tagPad}`
493
+ : isolated
494
+ ? `${shown}${chalk.gray(isolatedTag)}${tagPad}`
495
+ : base.padEnd(maxVerLabel);
450
496
  const rawInfo = infoMap.get(`${agentId}:${version}`);
451
497
  const vInfo = rawInfo ? mergeCanonical(rawInfo) : undefined;
452
498
  const usageKey = getUsageLookupKey(vInfo);
@@ -580,7 +626,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
580
626
  // Profile rows under a globally-installed harness. Use a simpler
581
627
  // alignment here since this section doesn't share column state with
582
628
  // the version-managed block.
583
- const profilesHere = profilesByAgent.get(agentId) ?? [];
629
+ // An isolated-only agent now appears in BOTH blocks; its profiles already
630
+ // rendered under the version-managed one, so don't print them twice.
631
+ const profilesHere = versionManaged.includes(agentId) ? [] : (profilesByAgent.get(agentId) ?? []);
584
632
  if (profilesHere.length > 0) {
585
633
  const nameWidth = Math.max(globalMaxVerLabel, ...profilesHere.map((p) => p.name.length));
586
634
  const authWidth = Math.max(...profilesHere.map((p) => p.auth.length));