@phnx-labs/agents-cli 1.22.114 → 1.22.116

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 (243) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +210 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +214 -148
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/feed.js +65 -14
  19. package/dist/commands/focus.js +1 -1
  20. package/dist/commands/go.d.ts +0 -17
  21. package/dist/commands/go.js +2 -19
  22. package/dist/commands/logs.js +1 -1
  23. package/dist/commands/mcp.js +8 -83
  24. package/dist/commands/memory.js +4 -47
  25. package/dist/commands/message.js +4 -4
  26. package/dist/commands/plugins.js +1 -93
  27. package/dist/commands/repo.js +0 -44
  28. package/dist/commands/resume.js +4 -1
  29. package/dist/commands/secrets-passthrough.js +2 -2
  30. package/dist/commands/send.d.ts +4 -4
  31. package/dist/commands/send.js +6 -51
  32. package/dist/commands/sessions-backup-setup.js +1 -1
  33. package/dist/commands/sessions-picker.d.ts +11 -0
  34. package/dist/commands/sessions-picker.js +88 -7
  35. package/dist/commands/sessions-resume.js +0 -1
  36. package/dist/commands/sessions-share.d.ts +5 -7
  37. package/dist/commands/sessions-share.js +98 -49
  38. package/dist/commands/sessions.d.ts +21 -2
  39. package/dist/commands/sessions.js +158 -11
  40. package/dist/commands/setup-browser.js +18 -2
  41. package/dist/commands/setup-computer.js +20 -6
  42. package/dist/commands/setup-secrets.d.ts +2 -2
  43. package/dist/commands/setup-secrets.js +24 -4
  44. package/dist/commands/setup-term.d.ts +24 -0
  45. package/dist/commands/setup-term.js +70 -0
  46. package/dist/commands/setup-terminal.d.ts +3 -0
  47. package/dist/commands/setup-terminal.js +22 -0
  48. package/dist/commands/setup.d.ts +1 -1
  49. package/dist/commands/setup.js +29 -11
  50. package/dist/commands/skills.js +0 -8
  51. package/dist/commands/ssh.d.ts +6 -0
  52. package/dist/commands/ssh.js +93 -302
  53. package/dist/commands/sync.js +14 -5
  54. package/dist/commands/teams.js +1 -1
  55. package/dist/commands/traces.js +1 -1
  56. package/dist/lib/accounting/rotate.d.ts +63 -1
  57. package/dist/lib/accounting/rotate.js +56 -0
  58. package/dist/lib/accounts/add.d.ts +0 -5
  59. package/dist/lib/accounts/add.js +3 -9
  60. package/dist/lib/accounts/slots.js +32 -2
  61. package/dist/lib/answer-router.d.ts +11 -2
  62. package/dist/lib/answer-router.js +26 -2
  63. package/dist/lib/artifacts-client.d.ts +20 -0
  64. package/dist/lib/artifacts-client.js +46 -0
  65. package/dist/lib/auth-mint.d.ts +5 -4
  66. package/dist/lib/auth-mint.js +6 -5
  67. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  68. package/dist/lib/browser/runtime-state.d.ts +55 -0
  69. package/dist/lib/browser/runtime-state.js +99 -18
  70. package/dist/lib/browser/service.d.ts +10 -0
  71. package/dist/lib/browser/service.js +229 -37
  72. package/dist/lib/browser/types.d.ts +18 -0
  73. package/dist/lib/cli-resources.js +3 -1
  74. package/dist/lib/cloud/dispatch.js +1 -1
  75. package/dist/lib/cloudflare/creds.d.ts +10 -0
  76. package/dist/lib/cloudflare/creds.js +46 -0
  77. package/dist/lib/cloudflare/provision.d.ts +35 -0
  78. package/dist/lib/cloudflare/provision.js +144 -0
  79. package/dist/lib/computer/sessions-list.d.ts +55 -0
  80. package/dist/lib/computer/sessions-list.js +168 -1
  81. package/dist/lib/config-keys.d.ts +1 -1
  82. package/dist/lib/config-keys.js +5 -0
  83. package/dist/lib/daemon/daemon.js +8 -1
  84. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  85. package/dist/lib/daemon/feed-stream-service.js +40 -0
  86. package/dist/lib/daemon-services.d.ts +1 -1
  87. package/dist/lib/daemon-services.js +5 -0
  88. package/dist/lib/device-config.js +61 -0
  89. package/dist/lib/devices/connect.d.ts +49 -5
  90. package/dist/lib/devices/connect.js +169 -21
  91. package/dist/lib/devices/doctor-findings.js +2 -6
  92. package/dist/lib/feed/answer.d.ts +153 -4
  93. package/dist/lib/feed/answer.js +716 -105
  94. package/dist/lib/feed/envelope.d.ts +79 -0
  95. package/dist/lib/feed/envelope.js +23 -0
  96. package/dist/lib/feed/events.d.ts +6 -0
  97. package/dist/lib/feed/events.js +8 -0
  98. package/dist/lib/feed/feed.d.ts +61 -1
  99. package/dist/lib/feed/feed.js +226 -14
  100. package/dist/lib/feed/hub-server.d.ts +141 -0
  101. package/dist/lib/feed/hub-server.js +586 -0
  102. package/dist/lib/feed/hub.d.ts +95 -0
  103. package/dist/lib/feed/hub.js +255 -0
  104. package/dist/lib/feed/pr-status.d.ts +8 -0
  105. package/dist/lib/feed/pr-status.js +9 -1
  106. package/dist/lib/feed/tool-activity.d.ts +108 -0
  107. package/dist/lib/feed/tool-activity.js +313 -0
  108. package/dist/lib/feed/tools.d.ts +198 -0
  109. package/dist/lib/feed/tools.js +265 -0
  110. package/dist/lib/feed/watch.d.ts +50 -50
  111. package/dist/lib/feed/watch.js +147 -16
  112. package/dist/lib/feed-outcome.d.ts +1 -1
  113. package/dist/lib/feed-outcome.js +9 -2
  114. package/dist/lib/feed-policy.js +9 -3
  115. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  116. package/dist/lib/fleet/auth-sync.js +2 -89
  117. package/dist/lib/format.d.ts +1 -1
  118. package/dist/lib/format.js +1 -1
  119. package/dist/lib/git.d.ts +0 -16
  120. package/dist/lib/git.js +0 -58
  121. package/dist/lib/harness-auth-capabilities.js +7 -2
  122. package/dist/lib/helper-versions.js +1 -1
  123. package/dist/lib/hosts/dispatch.d.ts +20 -1
  124. package/dist/lib/hosts/dispatch.js +52 -30
  125. package/dist/lib/hosts/remote-cmd.d.ts +72 -1
  126. package/dist/lib/hosts/remote-cmd.js +151 -10
  127. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  128. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  129. package/dist/lib/mailbox.d.ts +12 -0
  130. package/dist/lib/mailbox.js +16 -2
  131. package/dist/lib/mcp.js +17 -11
  132. package/dist/lib/menubar/snapshot.d.ts +51 -0
  133. package/dist/lib/menubar/snapshot.js +42 -3
  134. package/dist/lib/open-url.js +2 -2
  135. package/dist/lib/probe.d.ts +4 -1
  136. package/dist/lib/probe.js +5 -2
  137. package/dist/lib/projects.d.ts +23 -0
  138. package/dist/lib/projects.js +78 -0
  139. package/dist/lib/pwsh.d.ts +33 -0
  140. package/dist/lib/pwsh.js +56 -0
  141. package/dist/lib/redact.d.ts +8 -0
  142. package/dist/lib/redact.js +11 -0
  143. package/dist/lib/refresh.d.ts +6 -2
  144. package/dist/lib/refresh.js +92 -72
  145. package/dist/lib/secrets-cli.d.ts +11 -0
  146. package/dist/lib/secrets-cli.js +30 -0
  147. package/dist/lib/secrets-client.js +3 -2
  148. package/dist/lib/session/active.d.ts +1 -0
  149. package/dist/lib/session/active.js +8 -0
  150. package/dist/lib/session/db.d.ts +67 -3
  151. package/dist/lib/session/db.js +381 -126
  152. package/dist/lib/session/detached.d.ts +7 -0
  153. package/dist/lib/session/detached.js +29 -0
  154. package/dist/lib/session/prompt.d.ts +23 -7
  155. package/dist/lib/session/prompt.js +46 -8
  156. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  157. package/dist/lib/session/remote/peer-stream.js +33 -6
  158. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  159. package/dist/lib/session/remote/remote-list.js +22 -6
  160. package/dist/lib/session/remote/watch.d.ts +12 -0
  161. package/dist/lib/session/remote/watch.js +9 -0
  162. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  163. package/dist/lib/session/remote-preview-cache.js +373 -0
  164. package/dist/lib/session/sync/backend.d.ts +3 -3
  165. package/dist/lib/session/sync/backend.js +3 -3
  166. package/dist/lib/session/sync/provision.d.ts +1 -1
  167. package/dist/lib/session/sync/provision.js +2 -2
  168. package/dist/lib/session/tail.d.ts +50 -0
  169. package/dist/lib/session/tail.js +219 -0
  170. package/dist/lib/sessions-client.js +0 -3
  171. package/dist/lib/setup-tool-install.d.ts +3 -0
  172. package/dist/lib/setup-tool-install.js +27 -0
  173. package/dist/lib/setup-tool-status.d.ts +22 -0
  174. package/dist/lib/setup-tool-status.js +220 -0
  175. package/dist/lib/share-runtime.d.ts +11 -0
  176. package/dist/lib/share-runtime.js +63 -0
  177. package/dist/lib/signin-badge.d.ts +19 -4
  178. package/dist/lib/signin-badge.js +29 -11
  179. package/dist/lib/smart-launch.d.ts +1 -5
  180. package/dist/lib/smart-launch.js +3 -11
  181. package/dist/lib/ssh-exec.d.ts +44 -0
  182. package/dist/lib/ssh-exec.js +119 -0
  183. package/dist/lib/startup/command-registry.d.ts +6 -4
  184. package/dist/lib/startup/command-registry.js +10 -7
  185. package/dist/lib/state.js +2 -2
  186. package/dist/lib/storage/selection.d.ts +2 -2
  187. package/dist/lib/storage/selection.js +2 -2
  188. package/dist/lib/sync-umbrella.d.ts +5 -0
  189. package/dist/lib/sync-umbrella.js +18 -10
  190. package/dist/lib/term-driver.d.ts +24 -0
  191. package/dist/lib/term-driver.js +36 -0
  192. package/dist/lib/terminal/index.d.ts +1 -1
  193. package/dist/lib/terminal/index.js +1 -1
  194. package/dist/lib/terminal/inject.d.ts +38 -0
  195. package/dist/lib/terminal/inject.js +55 -9
  196. package/dist/lib/terminal/transport.d.ts +15 -5
  197. package/dist/lib/terminal/transport.js +61 -11
  198. package/dist/lib/traces/backend.d.ts +1 -2
  199. package/dist/lib/traces/backend.js +1 -2
  200. package/dist/lib/traces/provision.d.ts +1 -1
  201. package/dist/lib/traces/provision.js +2 -2
  202. package/dist/lib/types.d.ts +8 -6
  203. package/package.json +2 -3
  204. package/dist/commands/artifacts-setup.d.ts +0 -53
  205. package/dist/commands/artifacts-setup.js +0 -161
  206. package/dist/commands/artifacts.d.ts +0 -18
  207. package/dist/commands/artifacts.js +0 -58
  208. package/dist/commands/attach.d.ts +0 -12
  209. package/dist/commands/attach.js +0 -86
  210. package/dist/commands/auth-mint.d.ts +0 -12
  211. package/dist/commands/auth-mint.js +0 -108
  212. package/dist/commands/reconnect.d.ts +0 -46
  213. package/dist/commands/reconnect.js +0 -115
  214. package/dist/commands/share.d.ts +0 -293
  215. package/dist/commands/share.js +0 -1424
  216. package/dist/lib/fleet/remote-login.d.ts +0 -170
  217. package/dist/lib/fleet/remote-login.js +0 -568
  218. package/dist/lib/share/analytics.d.ts +0 -13
  219. package/dist/lib/share/analytics.js +0 -45
  220. package/dist/lib/share/backend.d.ts +0 -120
  221. package/dist/lib/share/backend.js +0 -176
  222. package/dist/lib/share/capture.d.ts +0 -31
  223. package/dist/lib/share/capture.js +0 -174
  224. package/dist/lib/share/config.d.ts +0 -72
  225. package/dist/lib/share/config.js +0 -211
  226. package/dist/lib/share/delete.d.ts +0 -123
  227. package/dist/lib/share/delete.js +0 -173
  228. package/dist/lib/share/html.d.ts +0 -20
  229. package/dist/lib/share/html.js +0 -88
  230. package/dist/lib/share/http-error.d.ts +0 -53
  231. package/dist/lib/share/http-error.js +0 -65
  232. package/dist/lib/share/og.d.ts +0 -26
  233. package/dist/lib/share/og.js +0 -84
  234. package/dist/lib/share/provision.d.ts +0 -127
  235. package/dist/lib/share/provision.js +0 -285
  236. package/dist/lib/share/publish.d.ts +0 -379
  237. package/dist/lib/share/publish.js +0 -818
  238. package/dist/lib/share/worker-template.d.ts +0 -27
  239. package/dist/lib/share/worker-template.js +0 -2424
  240. package/dist/lib/storage/index.d.ts +0 -14
  241. package/dist/lib/storage/index.js +0 -14
  242. package/dist/lib/storage/visibility.d.ts +0 -82
  243. package/dist/lib/storage/visibility.js +0 -99
@@ -660,8 +660,6 @@ export function registerRunCommand(program) {
660
660
  // `--on` and `--computer` are hidden aliases of `--device` — same behavior.
661
661
  runCmd.addOption(new Option('--on <name>', 'Alias of --device.').hideHelp());
662
662
  runCmd.addOption(new Option('--computer <name>', 'Alias of --device.').hideHelp());
663
- // Deprecated one-release alias: `agents run … --smart` → treat as `--device auto`.
664
- runCmd.addOption(new Option('--smart', 'Deprecated: use --device auto (affinity host pick).').hideHelp());
665
663
  // Internal: the `--device` dispatch forwards this so the REMOTE run prints its
666
664
  // resolved session id as a one-line stdout sentinel (hosts/session-marker.ts),
667
665
  // letting the launcher relate the remote-created session back to itself for
@@ -1226,7 +1224,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1226
1224
  const defaultPlacement = bareInteractiveRunDefaultsToDeviceAuto(options, { prompt, devicePickerRequested }, { tty: isInteractiveTerminal(), json: options.json });
1227
1225
  if (defaultPlacement)
1228
1226
  options.device = 'auto';
1229
- // --device auto (and deprecated --smart): live fleet pick.
1227
+ // --device auto: live fleet pick.
1230
1228
  // Harness is always the agent the user typed — never auto-picked.
1231
1229
  // Placement failure propagates; an automatic request never becomes local.
1232
1230
  {
@@ -1252,9 +1250,6 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1252
1250
  console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --device ${machineId()}`));
1253
1251
  process.exit(1);
1254
1252
  }
1255
- if (!options.quiet && result.deprecationSmart) {
1256
- process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
1257
- }
1258
1253
  if (!options.quiet && result.banner) {
1259
1254
  const { hostLabel, deviceHint, acctNote } = result.banner;
1260
1255
  process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
@@ -2050,7 +2045,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2050
2045
  const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, implicitModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, readProfile, resolveProfileForRun },
2051
2046
  // Engine DATA ops resolve through the standalone `secrets` process client
2052
2047
  // (PHNX-3989); the agents-owned `bundle@host` policy helpers stay in agents-cli.
2053
- { readAndResolveBundleEnv, describeBundle, remoteResolveEnv }, { assertRemoteBundleFlagsUnsupported, splitBundleRef, resolveSecretsContextForRun }, { resolveHostSshTarget }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError, formatNoVerifiedUsageError, signInRecoverableCandidates }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
2048
+ { readAndResolveBundleEnv, describeBundle, remoteResolveEnv }, { assertRemoteBundleFlagsUnsupported, splitBundleRef, resolveSecretsContextForRun }, { resolveHostSshTarget }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, DEFAULT_ROTATION_FAILOVER_LIMIT, shouldArmRotationFailover, preflightFallbackHandoff, preflightHandoffEligible, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError, formatNoVerifiedUsageError, signInRecoverableCandidates }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
2054
2049
  import('../lib/exec.js'),
2055
2050
  import('../lib/agents.js'),
2056
2051
  import('../lib/profiles.js'),
@@ -2064,7 +2059,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2064
2059
  import('../lib/run-defaults.js'),
2065
2060
  import('../lib/mcp.js'),
2066
2061
  import('../lib/capabilities.js'),
2067
- import('../lib/share/config.js'),
2062
+ import('../lib/share-runtime.js'),
2068
2063
  ]);
2069
2064
  bootMark('run-deps:imported');
2070
2065
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
@@ -2595,6 +2590,21 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2595
2590
  // synthesize a same-agent fallback chain from the other healthy accounts
2596
2591
  // (issue #348). Stays null unless a non-pinned strategy actually rotated.
2597
2592
  let rotationResult = null;
2593
+ /**
2594
+ * The PRIMARY harness's configured run-default mode/effort, carried across a
2595
+ * preflight alternate-harness handoff (PHNX-3999 F19).
2596
+ *
2597
+ * Run defaults are resolved after the harness is known, so without this a
2598
+ * handoff would silently adopt the ALTERNATE's configured defaults — a
2599
+ * primary configured `mode: plan` becoming the alternate's `mode: skip`
2600
+ * escalates a read-only intent into an unattended writable run. The
2601
+ * canonical mid-run cascade keeps the primary's resolved mode for the same
2602
+ * reason (`runWithFallback` forwards `options.mode` and only re-derives the
2603
+ * mode that was never configured at all). An IMPLICIT mode is deliberately
2604
+ * not carried: it is the harness's own safe default, so the alternate's
2605
+ * applies (identical to `modeWasImplicit ? implicitModeFor(agent)`).
2606
+ */
2607
+ let handoffRunDefaults;
2598
2608
  // Precomputed launchable-signed-in verdict for the ACTUAL launched
2599
2609
  // candidate, fed to the pre-launch `run.launch` event so it need not
2600
2610
  // re-probe. Sourced per resolution branch from the candidate that WON, not
@@ -2627,155 +2637,202 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2627
2637
  // account and account rotation silently stops (the gh-monitor heal bug).
2628
2638
  // `pinned` still resolves so a logged-out default can yield to a signed-in
2629
2639
  // sibling on this device (PHNX-2685); an explicit @version pin is unchanged.
2630
- if (!accountPickerRequested && !configuredAccount && (!version || strategy !== 'pinned' || options.balanced || explicitStrategy)) {
2631
- if (version) {
2632
- process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
2633
- }
2634
- else if (fromProfile) {
2635
- process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: custom harness pins its own version/auth\n`));
2636
- }
2637
- else {
2638
- try {
2639
- // Account-centric candidate list: native version-home logins PLUS
2640
- // provider accounts (setup-token / API-key) that can auth this agent,
2641
- // so `--strategy balanced` spreads across ALL accounts, not just the
2642
- // ones sitting in a version home (RUSH-3182). Run-path only — the
2643
- // picker's other callers keep the native-only collector.
2644
- const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
2645
- bootMark('resolve-version:start');
2646
- const routingModel = options.model ?? resolvedResumeSource?.model ?? workflowModel
2647
- ?? (options.fallback ? undefined : resolveRunDefaults(agent, resolveVersion(agent, cwd), cwd).model);
2648
- const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun, routingModel);
2649
- bootMark('resolve-version:done');
2650
- if (resolved.exhausted) {
2651
- // Zero healthy accounts splits two ways, and conflating them is what
2652
- // stranded a logged-out harness with no way in at all (RUSH-2334):
2653
- //
2654
- // - THROTTLED (rate_limited / out_of_credits) -> fail loud (RUSH-2132).
2655
- // The old behavior warned "found no usable version; falling back to
2656
- // defaults" and launched the pinned default anyway — the exact move
2657
- // that loops a rotate into an exhausted account. Only a window reset
2658
- // clears it. The message text is a contract the Factory watchdog
2659
- // tail-detects; do not reword it.
2660
- // - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
2661
- // because the harness's own TUI is the login surface. So on a TTY we
2662
- // carry the user into that login instead of erroring. Exiting here
2663
- // made `agents run <agent>`, `agents run <agent>#`, and `agents use`
2664
- // all dead-end with no reachable way to authenticate.
2665
- const recoverable = signInRecoverableCandidates(resolved.exhausted);
2666
- const { signInLaunchDecision } = await import('./run-account-picker.js');
2667
- const decision = signInLaunchDecision({
2668
- recoverable: recoverable.length,
2669
- tty: isInteractiveTerminal(),
2670
- json: options.json === true,
2671
- });
2672
- if (decision === 'launch') {
2673
- const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
2674
- const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
2675
- // A cancelled prompt launches nothing — same contract as the
2676
- // trailing-# account picker above.
2677
- if (!signInVersion)
2678
- return;
2679
- version = signInVersion;
2680
- // We just told the user this account is logged out and why we're
2681
- // launching it, so suppress the downstream login preflight — it
2682
- // would print a second, near-identical "looks logged out" warning.
2683
- signInLaunch = true;
2640
+ // Bounded handoff loop: each iteration resolves an account for `agent`, and
2641
+ // the only `continue` is the preflight exhaustion handoff below, which
2642
+ // consumes one entry of the `--fallback` spec before retrying as that
2643
+ // alternate. The spec is finite, so this terminates (PHNX-3999 F19).
2644
+ preflight: for (;;) {
2645
+ if (!accountPickerRequested && !configuredAccount && (!version || strategy !== 'pinned' || options.balanced || explicitStrategy)) {
2646
+ if (version) {
2647
+ process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
2648
+ }
2649
+ else if (fromProfile) {
2650
+ process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: custom harness pins its own version/auth\n`));
2651
+ }
2652
+ else {
2653
+ try {
2654
+ // Account-centric candidate list: native version-home logins PLUS
2655
+ // provider accounts (setup-token / API-key) that can auth this agent,
2656
+ // so `--strategy balanced` spreads across ALL accounts, not just the
2657
+ // ones sitting in a version home (RUSH-3182). Run-path only — the
2658
+ // picker's other callers keep the native-only collector.
2659
+ const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
2660
+ bootMark('resolve-version:start');
2661
+ const routingModel = options.model ?? resolvedResumeSource?.model ?? workflowModel
2662
+ ?? (options.fallback ? undefined : resolveRunDefaults(agent, resolveVersion(agent, cwd), cwd).model);
2663
+ const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun, routingModel);
2664
+ bootMark('resolve-version:done');
2665
+ if (resolved.exhausted) {
2666
+ // Zero healthy accounts splits two ways, and conflating them is what
2667
+ // stranded a logged-out harness with no way in at all (RUSH-2334):
2668
+ //
2669
+ // - THROTTLED (rate_limited / out_of_credits) -> fail loud (RUSH-2132).
2670
+ // The old behavior warned "found no usable version; falling back to
2671
+ // defaults" and launched the pinned default anyway — the exact move
2672
+ // that loops a rotate into an exhausted account. Only a window reset
2673
+ // clears it. The message text is a contract the Factory watchdog
2674
+ // tail-detects; do not reword it.
2675
+ // - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
2676
+ // because the harness's own TUI is the login surface. So on a TTY we
2677
+ // carry the user into that login instead of erroring. Exiting here
2678
+ // made `agents run <agent>`, `agents run <agent>#`, and `agents use`
2679
+ // all dead-end with no reachable way to authenticate.
2680
+ const recoverable = signInRecoverableCandidates(resolved.exhausted);
2681
+ const { signInLaunchDecision } = await import('./run-account-picker.js');
2682
+ const decision = signInLaunchDecision({
2683
+ recoverable: recoverable.length,
2684
+ tty: isInteractiveTerminal(),
2685
+ json: options.json === true,
2686
+ });
2687
+ if (decision === 'launch') {
2688
+ const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
2689
+ const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
2690
+ // A cancelled prompt launches nothing — same contract as the
2691
+ // trailing-# account picker above.
2692
+ if (!signInVersion)
2693
+ return;
2694
+ version = signInVersion;
2695
+ // We just told the user this account is logged out and why we're
2696
+ // launching it, so suppress the downstream login preflight — it
2697
+ // would print a second, near-identical "looks logged out" warning.
2698
+ signInLaunch = true;
2699
+ }
2700
+ else {
2701
+ // Every account of this harness is throttled. A configured
2702
+ // alternate exists for exactly this case, so hand off to it here
2703
+ // rather than exiting — `runWithFallback` would only reach it
2704
+ // after a run that can no longer start (PHNX-3999 F19). The
2705
+ // alternate becomes the primary and is dropped from the spec, so
2706
+ // the chain that remains never lists the agent now running.
2707
+ //
2708
+ // ONLY for a run shape `--fallback` is valid on. The handoff
2709
+ // consumes the entry it uses, and the canonical --fallback
2710
+ // validation runs later (it rejects interactive/--acp/--loop/
2711
+ // --resume-checkpoint chains and requires a prompt) — so
2712
+ // switching first on an ineligible shape would empty the spec,
2713
+ // leave that guard nothing to reject, and start the alternate
2714
+ // harness on a run the CLI refuses today. An ineligible shape
2715
+ // never switches, so the spec survives and still fails there
2716
+ // with its own message. See preflightHandoffEligible.
2717
+ const handoff = preflightHandoffEligible({
2718
+ hasPrompt: prompt !== undefined,
2719
+ interactive: options.interactive === true,
2720
+ acp: options.acp === true,
2721
+ loop: options.loop === true,
2722
+ resumeCheckpoint: !!options.resumeCheckpoint,
2723
+ resume: !!options.resume,
2724
+ workflowScoped: !!workflowToolsRestrict || !!workflowMcpConfigPath,
2725
+ })
2726
+ ? preflightFallbackHandoff(options.fallback, agent, resolved.exhausted)
2727
+ : null;
2728
+ if (handoff) {
2729
+ process.stderr.write(chalk.yellow(`[agents] every ${agent} account is exhausted — handing off to ${handoff.agent}${handoff.version ? `@${handoff.version}` : ''}\n`));
2730
+ // Snapshot the PRIMARY's configured defaults BEFORE the switch —
2731
+ // this is the operator's intent for this run, not the alternate's.
2732
+ handoffRunDefaults ??= fromProfile ? undefined : resolveRunDefaults(agent, version ?? resolveVersion(agent, cwd), cwd);
2733
+ agent = handoff.agent;
2734
+ // Alias-resolve against the NEW harness's installs, exactly as
2735
+ // the canonical --fallback parse does for its own entries.
2736
+ version = resolveVersionAlias(agent, handoff.version);
2737
+ options.fallback = handoff.remainingSpec;
2738
+ rotationResult = null;
2739
+ continue preflight;
2740
+ }
2741
+ console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
2742
+ if (recoverable.length > 0) {
2743
+ // Off a TTY nobody can complete a login, so we still exit — but
2744
+ // name the actual fix rather than only offering --strategy pinned,
2745
+ // which would just pin the same unauthenticated account.
2746
+ const { loginHint } = await import('../lib/signin-badge.js');
2747
+ console.error(chalk.gray(`To sign in: ${loginHint(agent)} — or run \`agents run ${agent}\` from a terminal.`));
2748
+ }
2749
+ process.exit(1);
2750
+ }
2684
2751
  }
2685
- else {
2686
- console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
2687
- if (recoverable.length > 0) {
2688
- // Off a TTY nobody can complete a login, so we still exit — but
2689
- // name the actual fix rather than only offering --strategy pinned,
2690
- // which would just pin the same unauthenticated account.
2691
- const { loginHint } = await import('../lib/signin-badge.js');
2692
- console.error(chalk.gray(`To sign in: ${loginHint(agent)} — or run \`agents run ${agent}\` from a terminal.`));
2752
+ else if (resolved.noVerifiedUsage) {
2753
+ // Entirely stale usage (PHNX-2526): every eligible account carries
2754
+ // a stale-but-present usage number and none is verified, so the
2755
+ // route would be a guess. NEVER auto-pick it. Interactive: show the
2756
+ // account picker so a human chooses with the (stale) numbers in
2757
+ // view. Unattended: fail loud with NO_VERIFIED_USAGE. The stale
2758
+ // pool survives ONLY as `resolved.rotation.healthy` for bounded
2759
+ // post-rejection failover, never as the initial pick.
2760
+ const { noVerifiedUsageDecision, pickRunAccountCandidate } = await import('./run-account-picker.js');
2761
+ const decision = noVerifiedUsageDecision({
2762
+ tty: isInteractiveTerminal(),
2763
+ json: options.json === true,
2764
+ headless: options.headless === true,
2765
+ });
2766
+ if (decision === 'picker') {
2767
+ const selected = await pickRunAccountCandidate(agent);
2768
+ // A cancelled picker launches nothing — same contract as the
2769
+ // trailing-# account picker and the sign-in launch above.
2770
+ if (!selected)
2771
+ return;
2772
+ version = selected.version;
2773
+ // Source the run.launch verdict from the account the user ACTUALLY
2774
+ // picked — the picker may deliberately return a logged-out one
2775
+ // (RUSH-2334), so it can differ from rotationResult.picked.
2776
+ launchSignedIn = selected.signedIn;
2777
+ launchEmail = selected.email;
2778
+ // Keep the rotation so mid-run failover can still cascade across
2779
+ // the other (stale) healthy accounts after a real rejection.
2780
+ rotationResult = resolved.rotation;
2781
+ if (!options.quiet) {
2782
+ const identity = selected.accountLabel || 'signed-in account';
2783
+ process.stderr.write(chalk.gray(`[agents] no fresh usage for any ${agent} account — you picked ${identity} · ${agent}@${selected.version}\n`));
2784
+ }
2785
+ }
2786
+ else {
2787
+ console.error(chalk.red(formatNoVerifiedUsageError(agent, strategy, resolved.rotation?.healthy ?? [])));
2788
+ process.exit(1);
2693
2789
  }
2694
- process.exit(1);
2695
2790
  }
2696
- }
2697
- else if (resolved.noVerifiedUsage) {
2698
- // Entirely stale usage (PHNX-2526): every eligible account carries
2699
- // a stale-but-present usage number and none is verified, so the
2700
- // route would be a guess. NEVER auto-pick it. Interactive: show the
2701
- // account picker so a human chooses with the (stale) numbers in
2702
- // view. Unattended: fail loud with NO_VERIFIED_USAGE. The stale
2703
- // pool survives ONLY as `resolved.rotation.healthy` for bounded
2704
- // post-rejection failover, never as the initial pick.
2705
- const { noVerifiedUsageDecision, pickRunAccountCandidate } = await import('./run-account-picker.js');
2706
- const decision = noVerifiedUsageDecision({
2707
- tty: isInteractiveTerminal(),
2708
- json: options.json === true,
2709
- headless: options.headless === true,
2710
- });
2711
- if (decision === 'picker') {
2712
- const selected = await pickRunAccountCandidate(agent);
2713
- // A cancelled picker launches nothing — same contract as the
2714
- // trailing-# account picker and the sign-in launch above.
2715
- if (!selected)
2716
- return;
2717
- version = selected.version;
2718
- // Source the run.launch verdict from the account the user ACTUALLY
2719
- // picked — the picker may deliberately return a logged-out one
2720
- // (RUSH-2334), so it can differ from rotationResult.picked.
2721
- launchSignedIn = selected.signedIn;
2722
- launchEmail = selected.email;
2723
- // Keep the rotation so mid-run failover can still cascade across
2724
- // the other (stale) healthy accounts after a real rejection.
2791
+ else if (resolved.version) {
2792
+ version = resolved.version;
2725
2793
  rotationResult = resolved.rotation;
2726
- if (!options.quiet) {
2727
- const identity = selected.accountLabel || 'signed-in account';
2728
- process.stderr.write(chalk.gray(`[agents] no fresh usage for any ${agent} account — you picked ${identity} · ${agent}@${selected.version}\n`));
2794
+ // The auto-pick already computed the launchable-signed-in verdict for
2795
+ // this exact version via the same gate — reuse it for run.launch.
2796
+ if (resolved.rotation) {
2797
+ launchSignedIn = resolved.rotation.picked.signedIn;
2798
+ launchEmail = resolved.rotation.picked.email;
2729
2799
  }
2730
- }
2731
- else {
2732
- console.error(chalk.red(formatNoVerifiedUsageError(agent, strategy, resolved.rotation?.healthy ?? [])));
2733
- process.exit(1);
2734
- }
2735
- }
2736
- else if (resolved.version) {
2737
- version = resolved.version;
2738
- rotationResult = resolved.rotation;
2739
- // The auto-pick already computed the launchable-signed-in verdict for
2740
- // this exact version via the same gate — reuse it for run.launch.
2741
- if (resolved.rotation) {
2742
- launchSignedIn = resolved.rotation.picked.signedIn;
2743
- launchEmail = resolved.rotation.picked.email;
2744
- }
2745
- // A balanced/available pick of a PROVIDER account (setup-token /
2746
- // API-key) carries `providerAccount`. Resolve its env through the
2747
- // same `resolveSpawnAccount` path an explicit `--account` uses, so
2748
- // exec injects the credential; a native pick has no providerAccount
2749
- // and runs from its own version home unchanged (RUSH-3182).
2750
- const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
2751
- if (pickedProviderAccount) {
2752
- try {
2753
- const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
2754
- if (picked?.kind === 'provider')
2755
- accountEnv = picked.env;
2800
+ // A balanced/available pick of a PROVIDER account (setup-token /
2801
+ // API-key) carries `providerAccount`. Resolve its env through the
2802
+ // same `resolveSpawnAccount` path an explicit `--account` uses, so
2803
+ // exec injects the credential; a native pick has no providerAccount
2804
+ // and runs from its own version home unchanged (RUSH-3182).
2805
+ const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
2806
+ if (pickedProviderAccount) {
2807
+ try {
2808
+ const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
2809
+ if (picked?.kind === 'provider')
2810
+ accountEnv = picked.env;
2811
+ }
2812
+ catch (err) {
2813
+ console.error(chalk.red(err.message));
2814
+ process.exit(1);
2815
+ }
2756
2816
  }
2757
- catch (err) {
2758
- console.error(chalk.red(err.message));
2759
- process.exit(1);
2817
+ if (resolved.rotation && !options.quiet) {
2818
+ const banner = formatRotationBanner(resolved.rotation, strategy);
2819
+ process.stderr.write(chalk.gray(banner + '\n'));
2760
2820
  }
2761
2821
  }
2762
- if (resolved.rotation && !options.quiet) {
2763
- const banner = formatRotationBanner(resolved.rotation, strategy);
2764
- process.stderr.write(chalk.gray(banner + '\n'));
2822
+ else if (!options.quiet) {
2823
+ // No installed version at all (not "accounts exhausted" — that
2824
+ // fails loud above): keep the pre-existing default resolution.
2825
+ process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
2765
2826
  }
2766
2827
  }
2767
- else if (!options.quiet) {
2768
- // No installed version at all (not "accounts exhausted" — that
2769
- // fails loud above): keep the pre-existing default resolution.
2770
- process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
2771
- }
2772
- }
2773
- catch (err) {
2774
- if (!options.quiet) {
2775
- process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
2828
+ catch (err) {
2829
+ if (!options.quiet) {
2830
+ process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
2831
+ }
2776
2832
  }
2777
2833
  }
2778
2834
  }
2835
+ break;
2779
2836
  }
2780
2837
  // Self-heal the launch target. A gutted install (JS wrapper present,
2781
2838
  // native binary missing — a partial/raced npm extraction of the optional
@@ -2904,9 +2961,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2904
2961
  // alias for 'skip' (rewritten downstream by normalizeMode in exec.ts).
2905
2962
  let mode = options.mode;
2906
2963
  const modeSource = runCmd.getOptionValueSource('mode');
2907
- const modeFromRunDefault = modeSource === 'default' && !!runDefaults.mode;
2964
+ // A configured mode carried across a preflight handoff wins over the
2965
+ // alternate harness's own configured default (see handoffRunDefaults).
2966
+ const configuredMode = handoffRunDefaults?.mode ?? runDefaults.mode;
2967
+ const modeFromRunDefault = modeSource === 'default' && !!configuredMode;
2908
2968
  if (modeFromRunDefault) {
2909
- mode = runDefaults.mode;
2969
+ mode = configuredMode;
2910
2970
  }
2911
2971
  if (!['plan', 'edit', 'auto', 'skip', 'full'].includes(mode)) {
2912
2972
  console.error(chalk.red(`Invalid mode: ${mode}. Use plan, edit, auto, or skip ('full' accepted as alias for skip).`));
@@ -2957,7 +3017,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2957
3017
  process.exit(1);
2958
3018
  }
2959
3019
  const effortSource = runCmd.getOptionValueSource('effort');
2960
- const effort = (effortSource === 'default' && runDefaults.effort ? runDefaults.effort : options.effort);
3020
+ const configuredEffort = handoffRunDefaults?.effort ?? runDefaults.effort;
3021
+ const effort = (effortSource === 'default' && configuredEffort ? configuredEffort : options.effort);
2961
3022
  if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
2962
3023
  console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
2963
3024
  process.exit(1);
@@ -3195,7 +3256,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
3195
3256
  loop: !!options.loop,
3196
3257
  resumeCheckpoint: !!options.resumeCheckpoint,
3197
3258
  })) {
3198
- const failover = rotationFailoverChain(rotationResult, version);
3259
+ // With an explicit alternate harness configured, "use the alternate once
3260
+ // the primary's accounts are exhausted" is the policy — so EVERY healthy
3261
+ // same-agent account has to be tried first, not the default cap of three
3262
+ // (PHNX-3999 F19). Without one, the cap stands: it bounds how long a
3263
+ // rate-limited run keeps retrying itself.
3264
+ const failover = rotationFailoverChain(rotationResult, version, fallback.length > 0 ? (rotationResult.healthy.length || DEFAULT_ROTATION_FAILOVER_LIMIT) : undefined);
3199
3265
  if (failover.length > 0) {
3200
3266
  fallback.unshift(...failover);
3201
3267
  if (!options.quiet) {
@@ -1,10 +1,51 @@
1
- import { machineId } from '../lib/machine-id.js';
2
1
  import { setHelpSections } from '../lib/help.js';
3
- import { watchFleetFeed, watchLocalFeed } from '../lib/feed/watch.js';
2
+ import { streamFeedFromHub, waitForHub } from '../lib/feed/hub-server.js';
3
+ import { ensureDaemonStarted } from '../lib/daemon/daemon.js';
4
+ /**
5
+ * Attach to the daemon's shared collector, starting the daemon if it is not up.
6
+ *
7
+ * There is deliberately no in-process fallback: running `watchFleetFeed` here
8
+ * would give this reader its own ssh child per peer, which is exactly the
9
+ * per-caller fan-out the hub exists to collapse (see `lib/feed/hub.ts`). So a
10
+ * missing hub is answered by starting its owner and WAITING for it.
11
+ *
12
+ * The wait is what makes that honest. `ensureDaemonStarted()` returns once the
13
+ * process is spawned, long before it has loaded its services and bound the
14
+ * socket, so retrying immediately raced the bind and reported a daemon that was
15
+ * about to be perfectly healthy as unavailable. The final error is the one from
16
+ * the last real attempt, not the first.
17
+ */
18
+ async function attachToHub(signal, emit, scope) {
19
+ try {
20
+ await streamFeedFromHub({ signal, emit, scope });
21
+ return;
22
+ }
23
+ catch (first) {
24
+ if (signal.aborted)
25
+ return;
26
+ ensureDaemonStarted();
27
+ if (!await waitForHub()) {
28
+ throw new Error(`the shared feed stream did not come up: ${first.message}\n`
29
+ + 'Next: agents daemon status, then agents daemon services enable feed-stream');
30
+ }
31
+ try {
32
+ await streamFeedFromHub({ signal, emit, scope });
33
+ }
34
+ catch (second) {
35
+ throw new Error(`the shared feed stream is unavailable: ${second.message}\n`
36
+ + 'Next: agents daemon status, then agents daemon services enable feed-stream');
37
+ }
38
+ }
39
+ }
4
40
  export function registerFeedWatchCommand(parent) {
5
- const command = parent.command('watch').description('Stream the canonical agent, attention, and activity projection as NDJSON')
41
+ const command = parent.command('watch').description('Stream the canonical agent, attention, tool, and activity projection as NDJSON')
6
42
  .option('--json', 'Emit versioned NDJSON envelopes').option('--local', 'Watch only this machine');
7
- setHelpSections(command, { examples: `agents feed watch --json\nagents feed watch --json --local`, notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect.' });
43
+ setHelpSections(command, {
44
+ examples: `agents feed watch --json\nagents feed watch --json --local`,
45
+ notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect. '
46
+ + 'The fleet stream is served by the daemon\'s shared collector, so several readers cost one connection per peer; '
47
+ + '--local is the per-machine stream the collector itself subscribes to over ssh.',
48
+ });
8
49
  command.action(async (_opts, invoked) => {
9
50
  const opts = invoked.optsWithGlobals();
10
51
  if (!opts.json)
@@ -15,10 +56,13 @@ export function registerFeedWatchCommand(parent) {
15
56
  process.once('SIGINT', stop);
16
57
  process.once('SIGTERM', stop);
17
58
  try {
18
- if (opts.local)
19
- await watchLocalFeed({ scope: machineId(), signal: controller.signal, emit });
20
- else
21
- await watchFleetFeed({ signal: controller.signal, emit });
59
+ // BOTH scopes go through the daemon's shared collectors. `--local` used to
60
+ // run its own `watchLocalFeed`, so every observing box's ssh subscription
61
+ // built a separate activity cursor set, tool watcher and setup subscription
62
+ // on the SAME peer. The local collector is a distinct hub from the fleet one
63
+ // (`feed-stream-service.ts`) and its watcher dials no peer at all, so a
64
+ // local reader can never trigger a fan-out, recursively or otherwise.
65
+ await attachToHub(controller.signal, emit, opts.local ? 'local' : 'fleet');
22
66
  }
23
67
  finally {
24
68
  process.off('SIGINT', stop);