@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
@@ -8,10 +8,12 @@
8
8
  import { Option } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import { setHelpSections } from '../lib/help.js';
11
- import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
11
+ import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
12
+ import { getUserAgentsDir } from '../lib/state.js';
12
13
  import { parseLoopInterval } from '../lib/loop.js';
13
14
  import { AGENTS } from '../lib/agents.js';
14
15
  import { recordDispatchedRun } from '../lib/audit/log.js';
16
+ import { maybeShowStarNudge } from '../lib/star-nudge.js';
15
17
  import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
16
18
  import { isHostPinned, pinHostKey, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
17
19
  import { sshResolve } from '../lib/hosts/ssh-config.js';
@@ -19,6 +21,7 @@ import * as fs from 'fs';
19
21
  import * as path from 'path';
20
22
  import * as os from 'os';
21
23
  import { randomUUID } from 'crypto';
24
+ import { spawnSync } from 'child_process';
22
25
  /** Distinguish a terminal account-picker marker from an explicit @version pin. */
23
26
  export function parseRunAccountPickerRequest(agentSpec) {
24
27
  const requested = agentSpec.endsWith('@');
@@ -40,6 +43,8 @@ export function runAccountPickerConflicts(options) {
40
43
  conflicts.push('--balanced');
41
44
  if (options.lease)
42
45
  conflicts.push('--lease');
46
+ if (options.box)
47
+ conflicts.push('--box');
43
48
  if (options.host || options.device || options.on || options.computer)
44
49
  conflicts.push('--host/--device');
45
50
  return conflicts;
@@ -55,6 +60,77 @@ function formatRotationBanner(result, verb = 'balanced') {
55
60
  const ratio = `${healthy.length} of ${healthy.length + excluded.length} healthy`;
56
61
  return `[agents] ${verb} picked ${label} (${ratio})`;
57
62
  }
63
+ /**
64
+ * Whether `cwd` is inside a git work tree.
65
+ *
66
+ * `--lease` / `--box` sync the working directory to the box through crabbox,
67
+ * which enumerates the files to copy with `git ls-files`. Outside a git repo
68
+ * that exits 128 (`fatal: not a git repository`) and the whole run dies at
69
+ * "build sync file list: exit status 128" — AFTER the box is provisioned and
70
+ * billed. Checking this up front lets the caller fail fast, before provisioning.
71
+ */
72
+ export function isInsideGitWorkTree(cwd) {
73
+ const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--is-inside-work-tree'], {
74
+ encoding: 'utf-8',
75
+ });
76
+ return r.status === 0 && r.stdout.trim() === 'true';
77
+ }
78
+ /** Absolute git toplevel for `cwd`, or null when it is not a git repo. */
79
+ export function gitToplevel(cwd) {
80
+ const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf-8' });
81
+ return r.status === 0 ? r.stdout.trim() : null;
82
+ }
83
+ /**
84
+ * Network mode for a `--lease` run (F5, RUSH-1924). `--tailscale` forces the
85
+ * tailnet, `--no-tailscale` forces public, and neither (undefined) defaults to
86
+ * the tailnet ONLY in a reuse context (`--reuse`, `--box`, or a picked warm
87
+ * box) — a one-shot solo `--lease` stays public. Pure so it is unit-testable;
88
+ * the caller downgrades to `'public'` when no auth key is configured.
89
+ */
90
+ export function computeNetMode(opts) {
91
+ if (opts.tailscale === false)
92
+ return 'public'; // --no-tailscale wins
93
+ if (opts.tailscale === true)
94
+ return 'tailscale'; // explicit --tailscale
95
+ return opts.reuseContext ? 'tailscale' : 'public';
96
+ }
97
+ // ── "Always provision fresh" per-repo memory (F3, RUSH-1922) ─────────────────
98
+ // The picker's "Always provision fresh (remember for this repo)" choice is
99
+ // persisted as a list of git-toplevel paths in a small state file, so a repo
100
+ // that opted out of the reuse picker is never prompted again.
101
+ /** True when `repoRoot` is in the remembered always-fresh set. Pure. */
102
+ export function isAlwaysFreshRepo(repos, repoRoot) {
103
+ return repos.includes(repoRoot);
104
+ }
105
+ /** Add `repoRoot` to the always-fresh set (idempotent). Pure. */
106
+ export function addAlwaysFreshRepo(repos, repoRoot) {
107
+ return repos.includes(repoRoot) ? repos : [...repos, repoRoot];
108
+ }
109
+ /** Path to the always-fresh state file under the USER agents dir (CLI-written per-user preference, never the maintainer-owned `.system` repo). */
110
+ export function leaseFreshReposPath() {
111
+ return path.join(getUserAgentsDir(), 'lease-fresh-repos.json');
112
+ }
113
+ /** Read the remembered always-fresh repo roots (empty on any read/parse error). */
114
+ export function readAlwaysFreshRepos() {
115
+ try {
116
+ const parsed = JSON.parse(fs.readFileSync(leaseFreshReposPath(), 'utf-8'));
117
+ return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : [];
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ }
123
+ /** Persist the always-fresh repo roots. Best-effort — never throws. */
124
+ export function writeAlwaysFreshRepos(repos) {
125
+ try {
126
+ const p = leaseFreshReposPath();
127
+ fs.mkdirSync(path.dirname(p), { recursive: true });
128
+ fs.writeFileSync(p, JSON.stringify(repos, null, 2));
129
+ }
130
+ catch {
131
+ /* best-effort — losing the preference just means the picker shows next time */
132
+ }
133
+ }
58
134
  /**
59
135
  * Decide whether `--copy-creds` may ship credentials (and the Claude OAuth
60
136
  * token) to `host` (RUSH-1767). Credentials ship only to a host whose SSH host
@@ -313,7 +389,12 @@ export function registerRunCommand(program) {
313
389
  .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
314
390
  .option('--copy-creds', 'With --host, copy the picked runtime credentials (and Claude OAuth token) to the host, then shred them after the run. Opt-in per run.')
315
391
  .option('--lease [backend]', 'Invent a disposable cloud box for this run and tear it down after (via crabbox). Optional backend selects the cloud (hetzner/aws/do). Unlike --host, no machine is registered.')
316
- .option('--keep-box', 'With --lease, keep the box after the run instead of stopping it.');
392
+ .option('--box <slug>', 'Reuse an existing warm crabbox box for this run instead of provisioning a disposable --lease box.')
393
+ .option('--keep-box', 'With --lease, keep the box after the run instead of stopping it.')
394
+ .option('--reuse', 'With --lease, reuse the most-recently-used warm box if one exists (else provision fresh). The scriptable form of the interactive reuse picker.')
395
+ .option('--bare', 'With --lease, skip copying your local ~/.agents setup (skills/hooks/commands/MCP) onto the box.')
396
+ .option('--tailscale', 'Lease the box onto your tailnet (reachable only over Tailscale) rather than a public IP.')
397
+ .option('--no-tailscale', 'Force a public-IP lease even when a reuse context would default to Tailscale.');
317
398
  // `--on` and `--computer` are hidden aliases of `--host` — same behavior.
318
399
  runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
319
400
  runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
@@ -422,17 +503,31 @@ export function registerRunCommand(program) {
422
503
  }
423
504
  }
424
505
  // --lease: invent a disposable cloud box for this run (via crabbox), run
425
- // the agent there, then tear it down. Unlike --host, nothing is registered.
426
- if (options.lease) {
506
+ // the agent there, then tear it down. --box: reuse a named warm crabbox
507
+ // box, run the same bootstrap there, and leave the box running.
508
+ if (options.lease || options.box) {
427
509
  if (prompt === undefined) {
428
- console.error(chalk.red('A prompt is required for leased runs: agents run <agent> "<task>" --lease'));
510
+ console.error(chalk.red(`A prompt is required for crabbox runs: agents run <agent> "<task>" ${options.box ? '--box <slug>' : '--lease'}`));
511
+ process.exit(1);
512
+ }
513
+ if (options.lease && options.box) {
514
+ console.error(chalk.red('Pass either --lease to provision a disposable box, or --box <slug> to reuse a warm box — not both.'));
429
515
  process.exit(1);
430
516
  }
431
517
  const backend = typeof options.lease === 'string' ? options.lease : undefined;
518
+ // crabbox syncs this directory to the box via `git ls-files`; outside a
519
+ // git repo that fails with "build sync file list: exit status 128" — but
520
+ // only AFTER the box is provisioned and billed. Fail fast here instead.
521
+ const leaseCwd = options.cwd ?? process.cwd();
522
+ if (!isInsideGitWorkTree(leaseCwd)) {
523
+ console.error(chalk.red(`${options.box ? '--box' : '--lease'} syncs the working directory to the box, but ${leaseCwd} is not a git repository.`));
524
+ console.error(chalk.yellow(`Run from inside a git repo, or initialize one: (cd ${leaseCwd} && git init)`));
525
+ process.exit(1);
526
+ }
432
527
  // First-run: no provider credential resolves (no env var, no config, no
433
528
  // detectable bundle) → guide the user through one-time setup, then continue.
434
529
  const { resolveLeaseBundle } = await import('../lib/crabbox/cli.js');
435
- if (!resolveLeaseBundle()) {
530
+ if (options.lease && !resolveLeaseBundle()) {
436
531
  const { runLeaseSetup } = await import('./lease.js');
437
532
  const ok = await runLeaseSetup({ provider: backend ?? 'hetzner' });
438
533
  if (!ok) {
@@ -440,22 +535,140 @@ export function registerRunCommand(program) {
440
535
  process.exit(1);
441
536
  }
442
537
  }
443
- const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime } = await import('../lib/crabbox/runtimes.js');
538
+ // ── F3 reuse (RUSH-1922) + F5 net-mode (RUSH-1924) ───────────────────
539
+ // Resolve which box this run targets and how it is networked BEFORE any
540
+ // provisioning. `--box` is an explicit reuse; otherwise, on an
541
+ // interactive tty, offer the warm boxes as a reuse picker (headless /
542
+ // --json never blocks — it provisions fresh unless --reuse/--box).
543
+ const leaseSecretsBundle = process.env.AGENTS_LEASE_SECRETS_BUNDLE;
544
+ const nowSecs = Math.floor(Date.now() / 1000);
545
+ let reuseSlug = options.box;
546
+ if (options.lease && !reuseSlug) {
547
+ const { crabboxList } = await import('../lib/crabbox/cli.js');
548
+ const { reusableBoxes, formatBoxRow } = await import('./lease.js');
549
+ let warm = [];
550
+ try {
551
+ warm = reusableBoxes(crabboxList({ secretsBundle: leaseSecretsBundle }), nowSecs);
552
+ }
553
+ catch {
554
+ warm = []; // crabbox unavailable / no creds → just provision fresh
555
+ }
556
+ const repoRoot = gitToplevel(leaseCwd);
557
+ const alwaysFresh = repoRoot ? isAlwaysFreshRepo(readAlwaysFreshRepos(), repoRoot) : false;
558
+ if (warm.length > 0 && !alwaysFresh) {
559
+ if (options.reuse) {
560
+ reuseSlug = warm[0].slug; // scriptable: most-recently-touched warm box
561
+ }
562
+ else if (isInteractiveTerminal() && options.json !== true) {
563
+ const { select } = await import('@inquirer/prompts');
564
+ try {
565
+ const choice = await select({
566
+ message: 'Reuse a warm box, or provision a fresh one?',
567
+ choices: [
568
+ ...warm.map((b) => ({ name: formatBoxRow(b, nowSecs), value: b.slug })),
569
+ { name: 'Provision a fresh box', value: '__fresh__' },
570
+ { name: 'Always provision fresh (remember for this repo)', value: '__always_fresh__' },
571
+ ],
572
+ });
573
+ if (choice === '__always_fresh__') {
574
+ if (repoRoot) {
575
+ writeAlwaysFreshRepos(addAlwaysFreshRepo(readAlwaysFreshRepos(), repoRoot));
576
+ console.error(chalk.dim(`Will always provision fresh for ${repoRoot} (edit ${leaseFreshReposPath()} to undo).`));
577
+ }
578
+ }
579
+ else if (choice !== '__fresh__') {
580
+ reuseSlug = choice;
581
+ }
582
+ }
583
+ catch (e) {
584
+ if (!isPromptCancelled(e))
585
+ throw e;
586
+ console.error(chalk.yellow('Selection cancelled — provisioning a fresh box.'));
587
+ }
588
+ }
589
+ // Headless with no --reuse falls through here → provision fresh.
590
+ }
591
+ else if (options.reuse && warm.length > 0) {
592
+ // --reuse still honors a warm box even when the picker is suppressed.
593
+ reuseSlug = warm[0].slug;
594
+ }
595
+ }
596
+ // reuseContext: an existing box (picked, --box, or --reuse) defaults the
597
+ // network to the tailnet; a solo one-shot --lease stays public.
598
+ const reuseContext = !!reuseSlug || !!options.reuse;
599
+ let netMode = computeNetMode({ tailscale: options.tailscale, reuseContext });
600
+ const copySetup = !options.bare;
601
+ // Tailscale requested but no auth key configured → downgrade to public
602
+ // with an actionable hint instead of hard-failing the run (F5).
603
+ if (netMode === 'tailscale') {
604
+ const { pickTailscaleBundleFromList } = await import('../lib/crabbox/cli.js');
605
+ const { listBundles } = await import('../lib/secrets/bundles.js');
606
+ let hasKey = !!process.env.CRABBOX_TAILSCALE_AUTH_KEY;
607
+ if (!hasKey) {
608
+ try {
609
+ hasKey = !!pickTailscaleBundleFromList(listBundles());
610
+ }
611
+ catch {
612
+ hasKey = false;
613
+ }
614
+ }
615
+ if (!hasKey) {
616
+ console.error(chalk.yellow('Tailscale requested but no auth key is configured — falling back to a public-IP lease.'));
617
+ console.error(chalk.gray('Set one up with `agents lease setup` (mint an EPHEMERAL, pre-authorized, tag:crabbox key), or store CRABBOX_TAILSCALE_AUTH_KEY in a secrets bundle.'));
618
+ netMode = 'public';
619
+ }
620
+ }
621
+ const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime, profileNeedsBaseRuntimeCredentials } = await import('../lib/crabbox/runtimes.js');
444
622
  const { leaseAndRun } = await import('../lib/crabbox/lease.js');
623
+ const { boxAddress } = await import('./lease.js');
445
624
  const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
625
+ const { profileExists, readProfile, resolveProfileEnv } = await import('../lib/profiles.js');
446
626
  const detected = await detectSignedInRuntimes();
447
- const agentName = normalizedAgentSpec.split('@')[0];
448
- const leaseCwd = options.cwd ?? process.cwd();
627
+ const [agentName, rawLeaseVersion] = normalizedAgentSpec.split('@');
628
+ let runtime = null;
629
+ let credentialRuntimes = [];
630
+ let dispatchProfile;
449
631
  // `--lease` requires a prompt (guarded above), so it is headless by
450
632
  // contract — never block on an interactive picker. Provision exactly the
451
633
  // one runtime this run needs, inferred from the agent, not every
452
634
  // signed-in CLI (which would ship unrelated tokens to a throwaway box).
453
- const runtime = inferLeaseRuntime(agentName, detected);
635
+ if (profileExists(agentName)) {
636
+ try {
637
+ const profile = readProfile(agentName);
638
+ const profileEnv = resolveProfileEnv(profile);
639
+ runtime = profile.host.agent;
640
+ const profileNeedsCredentials = profileNeedsBaseRuntimeCredentials(runtime, profileEnv, profile.auth?.envVar);
641
+ credentialRuntimes = profileNeedsCredentials ? [runtime] : [];
642
+ dispatchProfile = {
643
+ name: profile.name,
644
+ agent: profile.host.agent,
645
+ version: rawLeaseVersion || profile.host.version,
646
+ env: profileEnv,
647
+ description: profile.description,
648
+ preset: profile.preset,
649
+ provider: profile.provider,
650
+ fallbackModel: profile.fallback_model,
651
+ };
652
+ }
653
+ catch (err) {
654
+ console.error(chalk.red(err.message));
655
+ process.exit(1);
656
+ }
657
+ }
658
+ else {
659
+ runtime = inferLeaseRuntime(agentName, detected);
660
+ if (runtime)
661
+ credentialRuntimes = [runtime];
662
+ }
454
663
  if (!runtime) {
455
664
  console.error(chalk.yellow('No signed-in runtime to provision on the box. Sign into one locally (e.g. run `claude` once) then retry.'));
456
665
  process.exit(1);
457
666
  }
458
667
  const runtimes = [runtime];
668
+ if (credentialRuntimes.length > 0 && !detected.some((d) => d.id === runtime && d.signedIn && d.credPath)) {
669
+ console.error(chalk.yellow(`Profile '${agentName}' needs ${runtime} credentials, but ${runtime} is not signed in locally. Sign in locally, then retry.`));
670
+ process.exit(1);
671
+ }
459
672
  // Copy the account the run's OWN strategy would pick (default `balanced`:
460
673
  // a healthy, non-rate-limited account weighted by remaining headroom) —
461
674
  // never the raw default signed-in one, which could be throttled or out of
@@ -468,7 +681,7 @@ export function registerRunCommand(program) {
468
681
  // the notice below would name a balanced-picked account that is NOT the one
469
682
  // actually shipped. Best-effort: fall back to the default account on error.
470
683
  let leaseEmail = detected.find((d) => d.id === runtime)?.email ?? null;
471
- if (runtime === 'claude') {
684
+ if (credentialRuntimes.includes('claude')) {
472
685
  try {
473
686
  const strategy = getConfiguredRunStrategy(runtime, leaseCwd);
474
687
  const { rotation } = await resolveRunVersion(runtime, strategy, leaseCwd);
@@ -483,43 +696,82 @@ export function registerRunCommand(program) {
483
696
  // where — copying an auth token to a cloud box is a credential transfer.
484
697
  // The box is destroyed after the run, so the credential's lifetime is
485
698
  // bounded by the run.
486
- const whatShips = runtime === 'claude' ? 'credentials + Claude OAuth token' : 'credentials';
487
- console.error(chalk.gray(`Leasing a ${backend ?? 'hetzner'} box · shipping ${runtime}${leaseEmail ? ` (${leaseEmail})` : ''} ${whatShips}; the box is destroyed after the run.`));
699
+ const whatShips = credentialRuntimes.includes('claude')
700
+ ? `${dispatchProfile ? `profile '${dispatchProfile.name}', ` : ''}${runtime} credentials + Claude OAuth token`
701
+ : credentialRuntimes.length > 0
702
+ ? `${dispatchProfile ? `profile '${dispatchProfile.name}', ` : ''}${runtime} credentials`
703
+ : dispatchProfile
704
+ ? `profile '${dispatchProfile.name}'`
705
+ : `${runtime} credentials`;
706
+ const boxLifecycle = reuseSlug
707
+ ? `Reusing crabbox box ${reuseSlug}`
708
+ : `Leasing a ${backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' on your tailnet' : ''}`;
709
+ const boxAfterRun = reuseSlug
710
+ ? 'the box is kept after the run'
711
+ : options.keepBox
712
+ ? 'the box is kept after the run'
713
+ : 'the box is destroyed after the run';
714
+ console.error(chalk.gray(`${boxLifecycle} · shipping ${whatShips}${credentialRuntimes.length > 0 && leaseEmail ? ` (${leaseEmail})` : ''}; ${boxAfterRun}.`));
488
715
  // Read the Claude OAuth token from the local Keychain (silent) so it can be
489
716
  // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
490
717
  // "Not logged in". `preferEmail` targets the strategy-picked account so the
491
718
  // token matches the account this run resolved to.
492
719
  let claudeCredentialsJson = null;
493
- if (runtime === 'claude') {
720
+ if (credentialRuntimes.includes('claude')) {
494
721
  claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: leaseEmail });
495
722
  if (!claudeCredentialsJson) {
496
723
  console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
497
724
  }
498
725
  }
499
- // Progress UI. A self-throttled spinner (NOT ora — see progress.ts) covers
500
- // the otherwise-silent provisioning + box-setup phases; the agent's own
501
- // output then prints verbatim. The router splits the crabbox stream at the
502
- // box-side marker. Rule: only ONE spinner phase is active at a time, and no
503
- // other output is written to stderr while it spins so it can never storm.
504
- const { createLeaseOutputRouter, createSpinner } = await import('../lib/crabbox/progress.js');
726
+ // Progress UI (F2, RUSH-1921). A self-throttled spinner (NOT ora — see
727
+ // progress.ts) covers provisioning; the box-side bootstrap then streams a
728
+ // structured step stream (sync install runtime creds …) via the
729
+ // router's `onStep`, and each step renders as a checklist line through the
730
+ // lib's `renderStepLine` (✔ <Step><detail> (<elapsed>)). The agent's own
731
+ // output prints verbatim after the box-side marker. Rule: only ONE spinner
732
+ // phase is active at a time, so it can never storm.
733
+ const { createLeaseOutputRouter, createSpinner, renderStepLine } = await import('../lib/crabbox/progress.js');
505
734
  const spinner = createSpinner({ stream: process.stderr });
506
735
  let warmupTimer;
507
736
  const stopTimer = () => { if (warmupTimer) {
508
737
  clearInterval(warmupTimer);
509
738
  warmupTimer = undefined;
510
739
  } };
511
- const fit = (s) => {
512
- const w = Math.max(20, (process.stderr.columns || 80) - 24);
513
- return s.length > w ? s.slice(0, w - 1) + '…' : s;
740
+ const jsonMode = options.json === true;
741
+ const stepsTty = Boolean(process.stderr.isTTY) && !jsonMode;
742
+ // The step currently in flight (its label spins on a TTY). It is persisted
743
+ // as a ✔ line when the NEXT step arrives (whose elapsedMs measures how long
744
+ // THIS step's block took) — or, for the last step, when agent output or
745
+ // teardown begins.
746
+ let activeStep = null;
747
+ const flushStep = (elapsedMs) => {
748
+ if (!activeStep)
749
+ return;
750
+ const done = activeStep;
751
+ activeStep = null;
752
+ if (jsonMode) {
753
+ process.stdout.write(JSON.stringify({ phase: 'setup', name: done.name, elapsedMs: elapsedMs ?? null }) + '\n');
754
+ }
755
+ else {
756
+ spinner.stopAndPersist('✔', renderStepLine({ ...done, elapsedMs }));
757
+ }
514
758
  };
515
759
  const router = createLeaseOutputRouter({
516
- // Setup lines only ever update spinner text (no direct stderr writes),
517
- // so the spinner stays the single writer during the setup phase.
518
- onSetupLine: (line) => spinner.update(`Setting up box ${fit(line)}`),
760
+ now: () => Date.now(),
761
+ // Raw setup lines stay captured for a failure dump (router.setupLines());
762
+ // the structured step stream drives the visible checklist, so plain lines
763
+ // are not shown (they would fight the step spinner).
764
+ onSetupLine: () => { },
765
+ onStep: (step) => {
766
+ flushStep(step.elapsedMs); // persist the previous step, timed by this one
767
+ activeStep = step;
768
+ if (stepsTty)
769
+ spinner.start(renderStepLine(step));
770
+ },
519
771
  onAgentChunk: (chunk) => {
520
- // First agent byte: stop the setup spinner, THEN stream to stdout.
772
+ flushStep(); // last setup step done (no following sentinel to time it)
521
773
  if (spinner.active)
522
- spinner.stopAndPersist('✔', chalk.gray(`Box provisioned — ${agentName} output:`));
774
+ spinner.stop();
523
775
  process.stdout.write(chunk);
524
776
  },
525
777
  });
@@ -531,30 +783,41 @@ export function registerRunCommand(program) {
531
783
  model: options.model,
532
784
  backend,
533
785
  runtimes,
786
+ credentialRuntimes,
534
787
  detected,
788
+ dispatchProfile,
535
789
  claudeCredentialsJson,
536
- secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
790
+ secretsBundle: leaseSecretsBundle,
537
791
  keep: options.keepBox,
792
+ reuseBox: reuseSlug,
793
+ copySetup,
794
+ netMode,
538
795
  onData: (chunk) => router.push(chunk),
539
796
  onPhase: (phase) => {
540
797
  if (phase.kind === 'warmup') {
541
- const label = `Leasing a ${phase.backend ?? 'hetzner'} box`;
798
+ const label = `Leasing a ${phase.backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' (tailnet)' : ''}`;
542
799
  spinner.start(`${label}…`);
543
800
  const t0 = Date.now();
544
801
  warmupTimer = setInterval(() => spinner.update(`${label}… (${Math.round((Date.now() - t0) / 1000)}s)`), 1000);
545
802
  }
803
+ else if (phase.kind === 'reuse') {
804
+ spinner.start(`Reusing crabbox box ${phase.slug}…`);
805
+ }
546
806
  else if (phase.kind === 'ready') {
547
807
  stopTimer();
548
- spinner.stopAndPersist('✔', `Box ${phase.box.slug} ready${phase.box.ip ? ` (${phase.box.ip})` : ''} · ${Math.round(phase.elapsedMs / 1000)}s`);
549
- spinner.start('Setting up box…');
808
+ const addr = boxAddress(phase.box);
809
+ // Steps drive the spinner from here — no generic "Setting up box…".
810
+ spinner.stopAndPersist('✔', `Box ${phase.box.slug} ready${addr ? ` (${addr})` : ''} · ${Math.round(phase.elapsedMs / 1000)}s`);
550
811
  }
551
812
  else if (phase.kind === 'teardown') {
813
+ flushStep();
552
814
  if (spinner.active)
553
815
  spinner.stop();
554
816
  }
555
817
  },
556
818
  });
557
819
  router.end();
820
+ flushStep();
558
821
  stopTimer();
559
822
  if (spinner.active)
560
823
  spinner.stop();
@@ -566,11 +829,13 @@ export function registerRunCommand(program) {
566
829
  if (log.length)
567
830
  process.stderr.write(chalk.dim(log.join('\n')) + '\n');
568
831
  }
569
- console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop ${box.slug}`));
832
+ const keptAddr = boxAddress(box);
833
+ console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${keptAddr ? ` (${keptAddr})` : ''}. Stop it: agents lease stop ${box.slug}`));
570
834
  process.exit(exitCode === null ? 1 : exitCode);
571
835
  }
572
836
  catch (err) {
573
837
  stopTimer();
838
+ flushStep();
574
839
  if (spinner.active)
575
840
  spinner.stopAndPersist('✖', chalk.red('Lease failed'));
576
841
  const log = router.setupLines();
@@ -956,7 +1221,7 @@ export function registerRunCommand(program) {
956
1221
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
957
1222
  process.exit(resumeExit);
958
1223
  }
959
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
1224
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports }, { shareRuntimeEnv },] = await Promise.all([
960
1225
  import('../lib/exec.js'),
961
1226
  import('../lib/agents.js'),
962
1227
  import('../lib/profiles.js'),
@@ -969,6 +1234,7 @@ export function registerRunCommand(program) {
969
1234
  import('../lib/run-defaults.js'),
970
1235
  import('../lib/mcp.js'),
971
1236
  import('../lib/capabilities.js'),
1237
+ import('../lib/share/config.js'),
972
1238
  ]);
973
1239
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
974
1240
  // Parse agent@version
@@ -1433,7 +1699,14 @@ export function registerRunCommand(program) {
1433
1699
  if (launchTarget) {
1434
1700
  const healed = await ensureAgentRunnable(agent, launchTarget, options.quiet ? undefined : (m) => process.stderr.write(chalk.yellow(`[agents] ${m}\n`)));
1435
1701
  if (healed === null) {
1436
- console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: agents add ${agent}@latest`));
1702
+ // An isolated copy is never repaired by adopting another version, so
1703
+ // `add <agent>@latest` would build an unrelated NORMAL install rather
1704
+ // than fix what the user asked to run. Point at the isolated re-add.
1705
+ const { isVersionIsolated } = await import('../lib/versions.js');
1706
+ const hint = isVersionIsolated(agent, launchTarget)
1707
+ ? `agents add ${agent}@${launchTarget} --isolated`
1708
+ : `agents add ${agent}@latest`;
1709
+ console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: ${hint}`));
1437
1710
  process.exit(1);
1438
1711
  }
1439
1712
  // Always adopt the healed version explicitly. In the version-undefined
@@ -1608,12 +1881,17 @@ export function registerRunCommand(program) {
1608
1881
  process.exit(1);
1609
1882
  }
1610
1883
  }
1611
- // Merge order (later wins): profile env < secrets bundles < --env K=V.
1884
+ const autoShareEnv = options.autoSecrets !== false
1885
+ ? shareRuntimeEnv({ agentOnly: isHeadlessSecretsContext() })
1886
+ : undefined;
1887
+ // Merge order (later wins): profile env < auto share token < secrets bundles < --env K=V.
1612
1888
  // Profile carries provider auth; secrets bundles carry user-defined
1613
- // values; --env is the per-invocation override.
1614
- const hasOverrides = profileEnv || options.secrets.length > 0 || userEnv;
1889
+ // values; --env is the per-invocation override. The share token is
1890
+ // best-effort: if it is not already in env or an unlocked bundle, unrelated
1891
+ // runs keep working, and `agents share` itself still fails loudly on use.
1892
+ const hasOverrides = profileEnv || autoShareEnv || options.secrets.length > 0 || userEnv;
1615
1893
  const env = hasOverrides
1616
- ? { ...(profileEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
1894
+ ? { ...(profileEnv ?? {}), ...(autoShareEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
1617
1895
  : undefined;
1618
1896
  const modelSource = runCmd.getOptionValueSource('model');
1619
1897
  const model = options.model
@@ -1940,6 +2218,22 @@ export function registerRunCommand(program) {
1940
2218
  process.exit(1);
1941
2219
  }
1942
2220
  }
2221
+ // Agent footgun (RUSH-1829): a run with no prompt and no explicit
2222
+ // --interactive resolves to interactive intent, but in a non-TTY shell
2223
+ // (a headless agent, a pipe, CI) there is no terminal to host the REPL —
2224
+ // the TUI attaches to dead stdin and hangs forever. Fail fast with the
2225
+ // headless alternatives instead of launching a doomed interactive session.
2226
+ if (inferredInteractiveWithoutTty(execOptions, isInteractiveTerminal())) {
2227
+ // Tear down the workflow MCP config + subagents staged above before we
2228
+ // exit — same as every sibling exit path; requireInteractiveSelection
2229
+ // process.exits, so cleanup must happen first or it leaks.
2230
+ cleanupWorkflowMcpConfig();
2231
+ cleanupWorkflowSubagents();
2232
+ requireInteractiveSelection(`Launching ${agent} interactively`, [
2233
+ `agents run ${agent} "<your task>" # headless: prints the agent's result`,
2234
+ `agents run ${agent} --headless # headless: reads the prompt from stdin`,
2235
+ ]);
2236
+ }
1943
2237
  try {
1944
2238
  let exitCode;
1945
2239
  let ranAgent = agent;
@@ -1968,6 +2262,10 @@ export function registerRunCommand(program) {
1968
2262
  // Governance chokepoint (#347): every dispatched run finalizes here.
1969
2263
  // ONE tamper-evident audit record per run — non-fatal by contract.
1970
2264
  recordDispatchedRun({ agent: ranAgent, version: ranVersion ?? 'unknown', mode, cwd, exitCode });
2265
+ // First-successful-run star nudge (one-time, non-nagging). Only on a
2266
+ // clean run, and never when output is machine-readable/quiet.
2267
+ if (exitCode === 0)
2268
+ maybeShowStarNudge({ quiet: options.json || options.quiet });
1971
2269
  process.exit(exitCode);
1972
2270
  }
1973
2271
  catch (err) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `agents export <agent>[@<version>]` — copy an isolated install's config out to the
3
+ * user's real `~/.<agent>`.
4
+ *
5
+ * The exit door for `--isolated`. Default is additive: your files are never modified,
6
+ * and anything that collides is written beside yours so you can diff and take what you
7
+ * want. A receipt records what came from the export, so "which of these are mine?" has
8
+ * an answer and the whole thing can be undone.
9
+ */
10
+ import type { Command } from 'commander';
11
+ export declare function registerExportCommand(program: Command): void;