@phnx-labs/agents-cli 1.20.63 → 1.20.65

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 (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. package/package.json +2 -1
@@ -4,7 +4,7 @@ import { listProfiles, getProfile, createProfile, deleteProfile, ensureDefaultBr
4
4
  import { updateMeta } from '../lib/state.js';
5
5
  import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
6
6
  import { parseSecretRef } from '../lib/browser/secret-ref.js';
7
- import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
7
+ import { readAndResolveBundleEnv, isHeadlessSecretsContext, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
8
8
  import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
9
9
  import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
10
10
  import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
@@ -1420,7 +1420,7 @@ function registerTaskCommands(browser) {
1420
1420
  process.exit(1);
1421
1421
  }
1422
1422
  try {
1423
- const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key] });
1423
+ const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key], agentOnly: isHeadlessSecretsContext() });
1424
1424
  if (!(parsed.key in env)) {
1425
1425
  console.error(`Key "${parsed.key}" not in bundle "${parsed.bundle}".`);
1426
1426
  process.exit(1);
@@ -8,6 +8,7 @@ import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks
8
8
  import { renderStream } from '../lib/cloud/stream.js';
9
9
  import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
10
10
  import { normalizeTriggerEvent, validateTrigger, writeJob, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
11
+ import { machineId } from '../lib/machine-id.js';
11
12
  import { emit } from '../lib/events.js';
12
13
  /** Map a supported image file extension to its wire mimeType. Rejects anything else. */
13
14
  function imageMimeFromPath(file) {
@@ -145,7 +146,7 @@ Examples:
145
146
  cloud
146
147
  .command('run [prompt]')
147
148
  .description('Dispatch a task to a cloud agent.')
148
- .option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity (overrides agent auto-routing)')
149
+ .option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity, host (overrides agent auto-routing)')
149
150
  .option('--agent <name>', 'Agent to run: claude, codex, droid, antigravity (auto-routes to its native cloud)')
150
151
  .option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (value, previous) => {
151
152
  const acc = Array.isArray(previous) ? previous : [];
@@ -160,6 +161,9 @@ Examples:
160
161
  .option('--model <model>', 'Model override')
161
162
  .option('--env <id>', 'Codex Cloud environment ID')
162
163
  .option('--computer <name>', 'Factory/Droid computer target')
164
+ .option('--host <name>', 'One of your machines as the target (a registered host, device, capability tag, or user@host). Implies --provider host.')
165
+ .option('--remote-cwd <dir>', 'Working directory on the host (--provider host only)')
166
+ .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match')
163
167
  .option('--autonomy <level>', 'Factory/Droid autonomy: low, medium, high (default high)')
164
168
  .option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
165
169
  .option('--image <path>', `Attach an image (.png/.jpg/.webp) for vision dispatch. Repeatable, up to ${MAX_IMAGES_PER_DISPATCH} (Rush Cloud only).`, (value, previous) => {
@@ -189,6 +193,10 @@ Examples:
189
193
  # Codex Cloud
190
194
  agents cloud run "add auth tests" --provider codex --env env_abc123
191
195
 
196
+ # One of your own machines (agents hosts / agents devices), over SSH
197
+ agents cloud run "run the nightly benchmark" --host gpu-box --agent claude
198
+ agents cloud run "rebuild the index" --host gpu --any --remote-cwd ~/proj
199
+
192
200
  # Default provider (set in ~/.agents/agents.yaml)
193
201
  agents cloud run "refactor auth module" --repo user/repo
194
202
  `)
@@ -208,9 +216,16 @@ Examples:
208
216
  process.stderr.write(chalk.dim(`Reading prompt from ${filePath} (${sizeKB} KB)\n`));
209
217
  }
210
218
  }
219
+ // --host names one of YOUR machines as the target — that only means
220
+ // something to the host provider, so it implies --provider host rather
221
+ // than silently riding along to a cloud backend that would ignore it.
222
+ if (options.host && options.provider && options.provider !== 'host') {
223
+ die(`--host targets your own machines (--provider host), not ${options.provider}. Drop --host, or use --provider host.`);
224
+ }
225
+ const explicitProvider = options.provider ?? (options.host ? 'host' : undefined);
211
226
  // Agent-aware: with no --provider, the agent routes to its native cloud
212
227
  // (claude→rush, codex→codex, droid→factory, antigravity→antigravity).
213
- const provider = resolveProvider(options.provider, options.agent);
228
+ const provider = resolveProvider(explicitProvider, options.agent);
214
229
  // --repo is repeatable: commander gives us an array via our collector.
215
230
  // A single --repo value arrives as a one-element array; keep the legacy
216
231
  // singular `repo` field in sync so providers that only know that field
@@ -234,6 +249,12 @@ Examples:
234
249
  dispatchOptions.providerOptions.env = options.env;
235
250
  if (options.computer)
236
251
  dispatchOptions.providerOptions.computer = options.computer;
252
+ if (options.host)
253
+ dispatchOptions.providerOptions.host = options.host;
254
+ if (options.remoteCwd)
255
+ dispatchOptions.providerOptions.remoteCwd = options.remoteCwd;
256
+ if (options.any)
257
+ dispatchOptions.providerOptions.any = true;
237
258
  if (options.autonomy)
238
259
  dispatchOptions.providerOptions.autonomy = options.autonomy;
239
260
  if (options.mode)
@@ -279,6 +300,15 @@ Examples:
279
300
  };
280
301
  if (repoValues[0])
281
302
  routine.repo = repoValues[0];
303
+ // --host with --on: the webhook-fired run places on that machine (the
304
+ // routine carries the placement, and firing pins to THIS device so a
305
+ // fleet of receivers can't each dispatch a duplicate).
306
+ if (options.host) {
307
+ routine.host = options.host;
308
+ if (options.remoteCwd)
309
+ routine.remoteCwd = options.remoteCwd;
310
+ routine.devices = [machineId()];
311
+ }
282
312
  writeJob(routine);
283
313
  if (json) {
284
314
  console.log(JSON.stringify({ ok: true, registered: routineName, trigger }, null, 2));
@@ -1,6 +1,6 @@
1
1
  import { addHostOption } from '../lib/hosts/option.js';
2
2
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
3
- import { loadDevices } from '../lib/devices/registry.js';
3
+ import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
4
4
  import { resolveHost } from '../lib/hosts/registry.js';
5
5
  import { sshExec } from '../lib/ssh-exec.js';
6
6
  import { sshTargetFor } from '../lib/hosts/types.js';
@@ -249,6 +249,9 @@ async function resolveFleetTargets(opts) {
249
249
  const localName = machineId();
250
250
  return Object.values(registry)
251
251
  .filter((d) => d.name.toLowerCase() !== localName)
252
+ // Control devices (a cockpit) never run agents — skip them in the fleet
253
+ // fan-out (an explicit --device <name> still resolves above).
254
+ .filter((d) => !isControlDevice(d))
252
255
  .map((d) => ({
253
256
  name: d.name,
254
257
  sshTarget: d.name,
@@ -329,41 +329,69 @@ export function registerRunCommand(program) {
329
329
  process.exit(1);
330
330
  }
331
331
  const backend = typeof options.lease === 'string' ? options.lease : undefined;
332
- const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
332
+ // First-run: no provider credential resolves (no env var, no config, no
333
+ // detectable bundle) → guide the user through one-time setup, then continue.
334
+ const { resolveLeaseBundle } = await import('../lib/crabbox/cli.js');
335
+ if (!resolveLeaseBundle()) {
336
+ const { runLeaseSetup } = await import('./lease.js');
337
+ const ok = await runLeaseSetup({ provider: backend ?? 'hetzner' });
338
+ if (!ok) {
339
+ console.error(chalk.yellow('Leasing needs a cloud provider set up. Run `agents lease setup` and retry.'));
340
+ process.exit(1);
341
+ }
342
+ }
343
+ const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime } = await import('../lib/crabbox/runtimes.js');
333
344
  const { leaseAndRun } = await import('../lib/crabbox/lease.js');
334
- const { confirm } = await import('@inquirer/prompts');
345
+ const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
335
346
  const detected = await detectSignedInRuntimes();
336
- const runtimes = await pickRuntimes(detected);
337
- if (runtimes.length === 0) {
338
- console.error(chalk.yellow('No runtimes selected. Sign into one locally (e.g. run `claude` once) then retry.'));
347
+ const agentName = agentSpec.split('@')[0];
348
+ const leaseCwd = options.cwd ?? process.cwd();
349
+ // `--lease` requires a prompt (guarded above), so it is headless by
350
+ // contract — never block on an interactive picker. Provision exactly the
351
+ // one runtime this run needs, inferred from the agent, not every
352
+ // signed-in CLI (which would ship unrelated tokens to a throwaway box).
353
+ const runtime = inferLeaseRuntime(agentName, detected);
354
+ if (!runtime) {
355
+ console.error(chalk.yellow('No signed-in runtime to provision on the box. Sign into one locally (e.g. run `claude` once) then retry.'));
339
356
  process.exit(1);
340
357
  }
341
- // Security gate: copying auth tokens to an ephemeral cloud box is opt-in
342
- // per run. Name the runtimes + accounts so the user sees what ships.
343
- const names = runtimes
344
- .map((id) => {
345
- const d = detected.find((x) => x.id === id);
346
- return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
347
- })
348
- .join(', ');
349
- // Claude ships its OAuth token (not just the .claude.json config) name it
350
- // explicitly so the consent covers the actual credential transferred.
351
- const whatShips = runtimes.includes('claude') ? 'credentials + Claude OAuth token' : 'credentials';
352
- const ok = await confirm({
353
- message: `Copy ${whatShips} for ${names} to a disposable cloud box, run there, then destroy it?`,
354
- default: false,
355
- });
356
- if (!ok) {
357
- console.error(chalk.yellow('Aborted no credentials pushed, no box leased.'));
358
- process.exit(1);
358
+ const runtimes = [runtime];
359
+ // Copy the account the run's OWN strategy would pick (default `balanced`:
360
+ // a healthy, non-rate-limited account weighted by remaining headroom) —
361
+ // never the raw default signed-in one, which could be throttled or out of
362
+ // credits and would boot the box straight into a wall.
363
+ //
364
+ // Only claude's token is account-switched: resolveClaudeCredentialsBlob
365
+ // honors preferEmail across version-homes. The other runtimes'
366
+ // buildCredentialScript copies the single currently-active credential file
367
+ // with no switching, so run the balanced picker only for claude — otherwise
368
+ // the notice below would name a balanced-picked account that is NOT the one
369
+ // actually shipped. Best-effort: fall back to the default account on error.
370
+ let leaseEmail = detected.find((d) => d.id === runtime)?.email ?? null;
371
+ if (runtime === 'claude') {
372
+ try {
373
+ const strategy = getConfiguredRunStrategy(runtime, leaseCwd);
374
+ const { rotation } = await resolveRunVersion(runtime, strategy, leaseCwd);
375
+ if (rotation?.picked.email)
376
+ leaseEmail = rotation.picked.email;
377
+ }
378
+ catch {
379
+ /* strategy resolution is best-effort; keep the default signed-in account */
380
+ }
359
381
  }
382
+ // Headless-by-contract: don't prompt, but print exactly what ships and
383
+ // where — copying an auth token to a cloud box is a credential transfer.
384
+ // The box is destroyed after the run, so the credential's lifetime is
385
+ // bounded by the run.
386
+ const whatShips = runtime === 'claude' ? 'credentials + Claude OAuth token' : 'credentials';
387
+ console.error(chalk.gray(`Leasing a ${backend ?? 'hetzner'} box · shipping ${runtime}${leaseEmail ? ` (${leaseEmail})` : ''} ${whatShips}; the box is destroyed after the run.`));
360
388
  // Read the Claude OAuth token from the local Keychain (silent) so it can be
361
389
  // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
362
- // "Not logged in". Consent above already covered this transfer.
390
+ // "Not logged in". `preferEmail` targets the strategy-picked account so the
391
+ // token matches the account this run resolved to.
363
392
  let claudeCredentialsJson = null;
364
- if (runtimes.includes('claude')) {
365
- const claudeEmail = detected.find((d) => d.id === 'claude')?.email ?? null;
366
- claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
393
+ if (runtime === 'claude') {
394
+ claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: leaseEmail });
367
395
  if (!claudeCredentialsJson) {
368
396
  console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
369
397
  }
@@ -374,7 +402,6 @@ export function registerRunCommand(program) {
374
402
  // box-side marker. Rule: only ONE spinner phase is active at a time, and no
375
403
  // other output is written to stderr while it spins — so it can never storm.
376
404
  const { createLeaseOutputRouter, createSpinner } = await import('../lib/crabbox/progress.js');
377
- const agentName = agentSpec.split('@')[0];
378
405
  const spinner = createSpinner({ stream: process.stderr });
379
406
  let warmupTimer;
380
407
  const stopTimer = () => { if (warmupTimer) {
@@ -480,33 +507,45 @@ export function registerRunCommand(program) {
480
507
  process.exit(1);
481
508
  }
482
509
  const hostName = hostGiven[0];
483
- const { resolveHost, resolveHostByCap } = await import('../lib/hosts/registry.js');
484
- const { dispatchToHost, runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
485
- const { registerHostSession, registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
510
+ const { resolveHostRunTarget, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
511
+ const { runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
512
+ const { registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
513
+ const { RUN_OPTION_REJECT_MESSAGES } = await import('../lib/hosts/remote-cmd.js');
486
514
  const { normalizeRunStrategy, RUN_STRATEGIES } = await import('../lib/rotate.js');
487
- // A password-auth device throws DeviceOffloadUnsupportedError here; it's
488
- // printed cleanly by the top-level catch in index.ts (covers every
489
- // resolveHost caller), so it never falls through to capability routing.
490
- let host = await resolveHost(hostName);
491
- if (!host) {
492
- // Not a host name — try capability routing (e.g. --host gpu). A
493
- // "Multiple hosts tagged…" error is actionable and must surface;
494
- // only "no host tagged" falls through to the generic unknown-host msg.
495
- try {
496
- host = await resolveHostByCap(hostName, options.any);
497
- }
498
- catch (e) {
499
- const msg = e.message ?? '';
500
- if (msg.startsWith('Multiple hosts')) {
501
- console.error(chalk.red(msg));
502
- process.exit(1);
503
- }
504
- }
505
- }
506
- if (!host) {
507
- console.error(chalk.red(`Unknown host "${hostName}". List hosts: agents hosts list`));
515
+ // The forwarding contract (RUN_OPTION_FORWARDING): options that cannot
516
+ // cross the SSH boundary fail loud BEFORE dispatch never a silent
517
+ // drop. Value-aware: only reject what was actually passed.
518
+ const hostRejects = [];
519
+ if (options.secrets.length > 0)
520
+ hostRejects.push(RUN_OPTION_REJECT_MESSAGES.secrets);
521
+ if (options.secretsKeys)
522
+ hostRejects.push(RUN_OPTION_REJECT_MESSAGES.secretsKeys);
523
+ if (options.allowExpired)
524
+ hostRejects.push(RUN_OPTION_REJECT_MESSAGES.allowExpired);
525
+ if (options.resumeCheckpoint)
526
+ hostRejects.push(RUN_OPTION_REJECT_MESSAGES.resumeCheckpoint);
527
+ if (options.resume === true)
528
+ hostRejects.push(RUN_OPTION_REJECT_MESSAGES.resumeBare);
529
+ if (hostRejects.length > 0) {
530
+ for (const msg of hostRejects)
531
+ console.error(chalk.red(msg));
508
532
  process.exit(1);
509
533
  }
534
+ // Shared resolution (name → capability tag → error). A password-auth
535
+ // device throws DeviceOffloadUnsupportedError inside the helper and
536
+ // propagates untouched — it's printed cleanly by the top-level catch in
537
+ // index.ts (covers every resolveHost caller).
538
+ let host;
539
+ try {
540
+ host = await resolveHostRunTarget(hostName, { any: options.any });
541
+ }
542
+ catch (e) {
543
+ if (e instanceof HostResolutionError) {
544
+ console.error(chalk.red(e.message));
545
+ process.exit(1);
546
+ }
547
+ throw e;
548
+ }
510
549
  try {
511
550
  const [runAgent, rawRunVersion] = agentSpec.split('@');
512
551
  // Forward the explicit @version pin verbatim. Resolving aliases like
@@ -609,10 +648,12 @@ export function registerRunCommand(program) {
609
648
  agent: runAgent,
610
649
  version: resumeId ? undefined : runVersion,
611
650
  strategy: resumeId ? undefined : runStrategy,
651
+ fallback: options.fallback,
612
652
  prompt,
613
653
  mode: options.mode,
614
654
  model: options.model,
615
655
  effort: options.effort,
656
+ env: options.env,
616
657
  addDir: hostAddDirs,
617
658
  json: options.json,
618
659
  verbose: options.verbose,
@@ -636,32 +677,37 @@ export function registerRunCommand(program) {
636
677
  console.error(chalk.red('A prompt is required for headless host runs: agents run <agent> "<task>" --host <name>'));
637
678
  process.exit(1);
638
679
  }
639
- const hostSessionId = runAgent === 'claude' && !resumeId ? randomUUID() : undefined;
640
- const { task, exitCode } = await dispatchToHost(host, {
680
+ // Session-id mint, detached dispatch, and local session-index
681
+ // registration all live in the shared helper (lib/hosts/run-target.ts).
682
+ const { task, exitCode } = await dispatchPromptToHost(host, {
641
683
  agent: runAgent,
642
684
  version: resumeId ? undefined : runVersion,
643
685
  strategy: resumeId ? undefined : runStrategy,
686
+ fallback: options.fallback,
644
687
  prompt,
645
688
  mode: options.mode,
646
689
  model: options.model,
647
690
  effort: options.effort,
691
+ env: options.env,
648
692
  addDir: hostAddDirs,
693
+ timeout: options.timeout,
694
+ loop: options.loop,
695
+ maxIterations: options.maxIterations,
696
+ budget: options.budget,
697
+ until: options.until,
698
+ interval: options.interval,
649
699
  json: options.json,
650
700
  verbose: options.verbose,
651
- timeout: options.timeout,
652
701
  yes: options.yes,
653
702
  acp: options.acp,
703
+ autoSecrets: options.autoSecrets,
654
704
  remoteCwd: hostCwd,
655
- sessionId: hostSessionId,
656
705
  name: options.name,
657
706
  resume: resumeId,
658
707
  follow: options.follow !== false,
708
+ passthroughArgs,
659
709
  copyCreds: hostCopyCreds,
660
710
  });
661
- // Register the dispatched run in the LOCAL session index so it shows
662
- // up in `agents sessions` and resolves by id/name, even though its
663
- // transcript lives on the host. No-op when no session id was captured.
664
- registerHostSession(task, { cwd: process.cwd(), prompt });
665
711
  if (options.follow === false) {
666
712
  // The handle the caller uses to check on the run: the name if given,
667
713
  // else the real host-task id (never the old literal `<id>`). Steer
@@ -776,7 +822,7 @@ export function registerRunCommand(program) {
776
822
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
777
823
  process.exit(resumeExit);
778
824
  }
779
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
825
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { 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([
780
826
  import('../lib/exec.js'),
781
827
  import('../lib/agents.js'),
782
828
  import('../lib/profiles.js'),
@@ -814,6 +860,11 @@ export function registerRunCommand(program) {
814
860
  // produced item, and drives the teams supervisor to drain — no `teams`
815
861
  // subcommands needed.
816
862
  let workflowForEach;
863
+ // True once this run copies ≥1 dispatchable subagent into the shared agents
864
+ // dir. Used below to keep the `Task` tool in a `tools:`-restricted workflow —
865
+ // an orchestrator handed subagents but denied `Task` cannot reach them and
866
+ // degenerates to a no-op ("I'll wait for the completion notification").
867
+ let workflowHasSubagents = false;
817
868
  const cwd = options.cwd ?? process.cwd();
818
869
  if (isValidAgent(rawAgent)) {
819
870
  agent = rawAgent;
@@ -895,6 +946,8 @@ export function registerRunCommand(program) {
895
946
  workflowSubagentTargets.push(dest);
896
947
  copied++;
897
948
  }
949
+ if (copied > 0)
950
+ workflowHasSubagents = true;
898
951
  if (allowedAgents !== undefined) {
899
952
  // Surface any allowedAgents entry with no matching subagent file, and
900
953
  // report how many were filtered out, so the scope is auditable.
@@ -938,7 +991,9 @@ export function registerRunCommand(program) {
938
991
  }
939
992
  // Auto-inject secrets bundles declared in the workflow's frontmatter `secrets:` field.
940
993
  // Union with any --secrets flags the user passed; dedupe. Skip when --no-auto-secrets is set.
941
- if (!options.noAutoSecrets) {
994
+ // (Commander stores the negated flag as `autoSecrets: false` — the old
995
+ // `noAutoSecrets` read was never populated, making the flag a no-op.)
996
+ if (options.autoSecrets !== false) {
942
997
  const declared = workflowFrontmatter?.secrets ?? [];
943
998
  if (declared.length > 0) {
944
999
  const existing = new Set(options.secrets);
@@ -975,8 +1030,14 @@ export function registerRunCommand(program) {
975
1030
  }
976
1031
  else if (hasScoping) {
977
1032
  if (tools && tools.length > 0) {
978
- workflowToolsRestrict = tools;
979
- process.stderr.write(chalk.gray(`[workflow] restricting available tools to: ${tools.join(', ')} (Write/Bash/Edit unavailable unless listed)\n`));
1033
+ // An orchestrator with dispatchable subagents MUST retain the `Task`
1034
+ // tool, or it can't reach the subagents this run just installed for it
1035
+ // — the run silently no-ops ("I'll wait for the completion notification").
1036
+ workflowToolsRestrict = ensureSubagentDispatchTool(tools, workflowHasSubagents);
1037
+ process.stderr.write(chalk.gray(`[workflow] restricting available tools to: ${workflowToolsRestrict.join(', ')} (Write/Bash/Edit unavailable unless listed)\n`));
1038
+ if (workflowToolsRestrict.length !== tools.length) {
1039
+ process.stderr.write(chalk.gray(`[workflow] kept Task tool: workflow ships subagents to dispatch\n`));
1040
+ }
980
1041
  }
981
1042
  if (mcpServerNames && mcpServerNames.length > 0) {
982
1043
  const servers = getMcpServersByName(mcpServerNames, { cwd });
@@ -1159,8 +1220,12 @@ export function registerRunCommand(program) {
1159
1220
  process.exit(1);
1160
1221
  }
1161
1222
  const strategy = options.balanced ? 'balanced' : explicitStrategy ?? configuredStrategy;
1162
- // Strategy only applies to bare agent invocations. Explicit @version,
1163
- // profiles, and fallback chains already define their execution target.
1223
+ // Strategy only applies to bare agent invocations. Explicit @version and
1224
+ // profiles already define their execution target. A --fallback chain does
1225
+ // NOT pin the primary: it only names where to cascade on a rate limit, so
1226
+ // the bare primary still resolves through the strategy — otherwise every
1227
+ // `agents run claude --fallback codex` run lands on the pinned default
1228
+ // account and account rotation silently stops (the gh-monitor heal bug).
1164
1229
  if (strategy !== 'pinned' || options.balanced || explicitStrategy) {
1165
1230
  if (version) {
1166
1231
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
@@ -1168,9 +1233,6 @@ export function registerRunCommand(program) {
1168
1233
  else if (fromProfile) {
1169
1234
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: profile pins its own version/auth\n`));
1170
1235
  }
1171
- else if (options.fallback) {
1172
- process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: --fallback pins versions directly\n`));
1173
- }
1174
1236
  else {
1175
1237
  try {
1176
1238
  const resolved = await resolveRunVersion(agent, strategy, cwd);
@@ -1323,6 +1385,10 @@ export function registerRunCommand(program) {
1323
1385
  caller: `agent ${agent}`,
1324
1386
  keys: secretsKeysSubset,
1325
1387
  allowExpired: options.allowExpired,
1388
+ // A headless/background run (routine, teammate, detached) must not
1389
+ // pop a Touch ID sheet nobody can answer — resolve broker-only and
1390
+ // fail fast with an actionable error. Interactive runs still prompt.
1391
+ agentOnly: isHeadlessSecretsContext(),
1326
1392
  });
1327
1393
  const entries = describeBundle(bundle);
1328
1394
  const counts = {};
@@ -1443,17 +1509,23 @@ export function registerRunCommand(program) {
1443
1509
  // path (continuing the session via /continue). Because this injects into
1444
1510
  // the same `fallback` array `--fallback` uses, it must only arm for run
1445
1511
  // shapes that accept a fallback chain — shouldArmRotationFailover excludes
1446
- // acp/loop/resume-checkpoint (which reject a non-empty fallback below),
1447
- // interactive/no-prompt runs, and runs that already have an explicit or
1448
- // profile fallback. Pinned/single-account runs stay unchanged because
1449
- // rotationResult is null or rotationFailoverChain returns []. version is
1450
- // set here because rotationResult is only populated when resolveRunVersion
1451
- // picked one.
1512
+ // acp/loop/resume-checkpoint (which reject a non-empty fallback below)
1513
+ // and interactive/no-prompt runs. Pinned/single-account runs stay
1514
+ // unchanged because rotationResult is null or rotationFailoverChain
1515
+ // returns []. version is set here because rotationResult is only
1516
+ // populated when resolveRunVersion picked one.
1517
+ //
1518
+ // Composes with an explicit --fallback chain: the same-agent accounts are
1519
+ // UNSHIFTED ahead of the user's cross-agent entries, so a rate limit
1520
+ // first tries the other accounts of the same agent (cheapest recovery —
1521
+ // same CLI, session continues) and only then cascades to codex/gemini/etc.
1522
+ // Profiles never compose: strategy is skipped for them, rotationResult
1523
+ // stays null. (fromProfile's model-swap unshift above is therefore never
1524
+ // displaced by this one.)
1452
1525
  if (shouldArmRotationFailover({
1453
1526
  hasRotation: !!rotationResult,
1454
1527
  hasVersion: !!version,
1455
1528
  hasPrompt: prompt !== undefined,
1456
- explicitFallback: fallback.length > 0,
1457
1529
  interactive: !!options.interactive,
1458
1530
  acp: !!options.acp,
1459
1531
  loop: !!options.loop,
@@ -1461,7 +1533,7 @@ export function registerRunCommand(program) {
1461
1533
  })) {
1462
1534
  const failover = rotationFailoverChain(rotationResult, version);
1463
1535
  if (failover.length > 0) {
1464
- fallback.push(...failover);
1536
+ fallback.unshift(...failover);
1465
1537
  if (!options.quiet) {
1466
1538
  const accounts = failover.map(f => `${f.agent}@${f.version}`).join(', ');
1467
1539
  process.stderr.write(chalk.gray(`[agents] rate-limit failover armed: ${accounts}\n`));
@@ -13,6 +13,10 @@ import type { Command } from 'commander';
13
13
  import { type OpenBlock } from '../lib/feed.js';
14
14
  import { type OutcomeGroup, type SessionOutcomeHint } from '../lib/feed-outcome.js';
15
15
  export declare const FEED_NO_FANOUT_ENV = "AGENTS_FEED_LOCAL";
16
+ /** Right-hand masthead summary: `N blocks · M agents`. */
17
+ export declare function formatFeedMastheadRight(blocks: OpenBlock[]): string;
18
+ /** Reply hint matching the shared fleet-comms reply line. */
19
+ export declare function formatFeedReplyHint(mailboxId: string): string;
16
20
  export declare function parseRemoteFeed(stdout: string, machine: string): OpenBlock[];
17
21
  /** Merge local and remote rows, keeping the first copy of a host/session block. */
18
22
  export declare function mergeFeedBlocks(...groups: OpenBlock[][]): OpenBlock[];