@phnx-labs/agents-cli 1.22.117 → 1.22.118

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 (248) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.js +53 -1
  9. package/dist/commands/repo.js +13 -49
  10. package/dist/commands/route.js +5 -5
  11. package/dist/commands/routines.js +39 -12
  12. package/dist/commands/sessions-render.d.ts +1 -1
  13. package/dist/commands/sessions-render.js +0 -1
  14. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  15. package/dist/commands/sessions.test-fixture.js +0 -37
  16. package/dist/commands/setup-browser.d.ts +17 -12
  17. package/dist/commands/setup-browser.js +52 -87
  18. package/dist/commands/setup-preferences.d.ts +0 -17
  19. package/dist/commands/setup-preferences.js +18 -49
  20. package/dist/commands/setup.js +9 -9
  21. package/dist/commands/ssh.js +10 -1
  22. package/dist/commands/sync.js +6 -44
  23. package/dist/commands/teams.js +0 -1
  24. package/dist/commands/update.d.ts +19 -0
  25. package/dist/commands/update.js +34 -6
  26. package/dist/commands/view.d.ts +12 -1
  27. package/dist/commands/view.js +47 -2
  28. package/dist/index.js +107 -26
  29. package/dist/lib/account-capabilities.js +0 -2
  30. package/dist/lib/account-catalog.d.ts +38 -0
  31. package/dist/lib/account-catalog.js +110 -26
  32. package/dist/lib/account-provider-registry.js +1 -1
  33. package/dist/lib/account-registry.d.ts +1 -1
  34. package/dist/lib/account-registry.js +36 -2
  35. package/dist/lib/accounting/rotate.js +22 -8
  36. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  37. package/dist/lib/accounting/usage-ingest.js +115 -45
  38. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  39. package/dist/lib/accounting/usage-sync.js +227 -66
  40. package/dist/lib/accounting/usage.d.ts +16 -3
  41. package/dist/lib/accounting/usage.js +26 -8
  42. package/dist/lib/accounts/slots.d.ts +8 -0
  43. package/dist/lib/accounts/slots.js +17 -0
  44. package/dist/lib/add-dir.js +0 -1
  45. package/dist/lib/agent-cli-commands.js +0 -1
  46. package/dist/lib/agent-spec/agents.d.ts +6 -7
  47. package/dist/lib/agent-spec/agents.js +6 -60
  48. package/dist/lib/auth-health.d.ts +69 -1
  49. package/dist/lib/auth-health.js +133 -4
  50. package/dist/lib/browser/context.d.ts +82 -0
  51. package/dist/lib/browser/context.js +88 -0
  52. package/dist/lib/browser/paths.d.ts +25 -0
  53. package/dist/lib/browser/paths.js +51 -0
  54. package/dist/lib/browser/record.d.ts +38 -0
  55. package/dist/lib/browser/record.js +57 -0
  56. package/dist/lib/browser/sessions-list.js +1 -2
  57. package/dist/lib/browser-client.d.ts +158 -0
  58. package/dist/lib/browser-client.js +234 -0
  59. package/dist/lib/channels/owner-sink.d.ts +1 -18
  60. package/dist/lib/channels/owner-sink.js +29 -82
  61. package/dist/lib/channels/providers/rush.d.ts +7 -7
  62. package/dist/lib/channels/providers/rush.js +127 -57
  63. package/dist/lib/claude-statusline.d.ts +1 -1
  64. package/dist/lib/claude-statusline.js +15 -3
  65. package/dist/lib/commands.js +1 -1
  66. package/dist/lib/crabbox/lease.js +1 -1
  67. package/dist/lib/crabbox/runtimes.js +1 -3
  68. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  69. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  70. package/dist/lib/daemon/auth-sync-service.js +37 -43
  71. package/dist/lib/daemon/daemon.d.ts +25 -14
  72. package/dist/lib/daemon/daemon.js +92 -154
  73. package/dist/lib/daemon/runner.d.ts +2 -2
  74. package/dist/lib/daemon/runner.js +32 -9
  75. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  76. package/dist/lib/daemon/self-update-service.js +40 -36
  77. package/dist/lib/daemon/service.d.ts +12 -8
  78. package/dist/lib/daemon/supervisor.d.ts +65 -54
  79. package/dist/lib/daemon/supervisor.js +136 -171
  80. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  81. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  82. package/dist/lib/daemon/usage-sync-service.js +46 -78
  83. package/dist/lib/daemon-health.d.ts +22 -5
  84. package/dist/lib/daemon-health.js +47 -3
  85. package/dist/lib/daemon-services.d.ts +1 -1
  86. package/dist/lib/daemon-services.js +0 -10
  87. package/dist/lib/daemon-ticks.js +4 -2
  88. package/dist/lib/devices/doctor-findings.js +8 -11
  89. package/dist/lib/devices/harness-inventory.js +14 -5
  90. package/dist/lib/exec-account-home.js +5 -0
  91. package/dist/lib/exec.js +0 -11
  92. package/dist/lib/feed/events.js +0 -1
  93. package/dist/lib/feed/tool-activity.js +5 -2
  94. package/dist/lib/fleet-shared-state.d.ts +45 -0
  95. package/dist/lib/fleet-shared-state.js +78 -9
  96. package/dist/lib/fs-atomic.js +25 -0
  97. package/dist/lib/harness-auth-capabilities.js +0 -1
  98. package/dist/lib/helper-versions.js +1 -1
  99. package/dist/lib/hooks/cache.d.ts +7 -0
  100. package/dist/lib/hooks/cache.js +27 -2
  101. package/dist/lib/hooks/install.js +7 -1
  102. package/dist/lib/hosts/ready.d.ts +38 -8
  103. package/dist/lib/hosts/ready.js +62 -14
  104. package/dist/lib/installations/active-check.d.ts +27 -0
  105. package/dist/lib/installations/active-check.js +61 -0
  106. package/dist/lib/installations/migrate.js +6 -3
  107. package/dist/lib/installations/shims.js +9 -7
  108. package/dist/lib/installations/update.js +21 -15
  109. package/dist/lib/installations/versions.js +12 -1
  110. package/dist/lib/manifest.js +1 -1
  111. package/dist/lib/models.js +0 -2
  112. package/dist/lib/open-url.js +28 -54
  113. package/dist/lib/profiles.js +0 -2
  114. package/dist/lib/refresh.js +1 -1
  115. package/dist/lib/routine-readiness.d.ts +35 -0
  116. package/dist/lib/routine-readiness.js +74 -16
  117. package/dist/lib/rules/compile.js +6 -0
  118. package/dist/lib/sandbox.js +0 -2
  119. package/dist/lib/secrets-policy.d.ts +89 -11
  120. package/dist/lib/secrets-policy.js +175 -28
  121. package/dist/lib/session/db.d.ts +1 -1
  122. package/dist/lib/session/db.js +65 -18
  123. package/dist/lib/session/discover.js +1 -283
  124. package/dist/lib/session/mirror.d.ts +4 -3
  125. package/dist/lib/session/mirror.js +4 -3
  126. package/dist/lib/session/parse.d.ts +0 -2
  127. package/dist/lib/session/parse.js +1 -138
  128. package/dist/lib/session/throughput.d.ts +1 -3
  129. package/dist/lib/session/throughput.js +0 -20
  130. package/dist/lib/session/types.d.ts +2 -2
  131. package/dist/lib/session/types.js +2 -2
  132. package/dist/lib/sessions-client.d.ts +60 -1
  133. package/dist/lib/sessions-client.js +227 -4
  134. package/dist/lib/setup-tool-status.js +5 -10
  135. package/dist/lib/signin-badge.d.ts +3 -2
  136. package/dist/lib/signin-badge.js +6 -3
  137. package/dist/lib/smart-launch.js +4 -1
  138. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  139. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  140. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  141. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  142. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  143. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  144. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  147. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  148. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  149. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  150. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  151. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  152. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  153. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  154. package/dist/lib/staleness/writers/sources.js +5 -0
  155. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  156. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  157. package/dist/lib/state.d.ts +6 -4
  158. package/dist/lib/state.js +7 -17
  159. package/dist/lib/teams/parsers.d.ts +1 -1
  160. package/dist/lib/teams/parsers.js +1 -136
  161. package/dist/lib/teams/placement-probe.js +50 -15
  162. package/dist/lib/teams/scheduler.d.ts +7 -0
  163. package/dist/lib/types.d.ts +10 -1
  164. package/dist/lib/types.js +1 -1
  165. package/dist/lib/view-types.d.ts +32 -0
  166. package/dist/lib/watchdog/read.js +0 -1
  167. package/dist/lib/watchdog/rotate.js +1 -2
  168. package/dist/lib/watchdog/runner.js +2 -2
  169. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  170. package/dist/lib/watchdog/watchdogTail.js +0 -31
  171. package/package.json +1 -1
  172. package/scripts/postinstall.js +8 -8
  173. package/dist/browser.d.ts +0 -2
  174. package/dist/browser.js +0 -17
  175. package/dist/commands/browser-picker.d.ts +0 -18
  176. package/dist/commands/browser-picker.js +0 -97
  177. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  178. package/dist/lib/browser/arc-discovery.js +0 -197
  179. package/dist/lib/browser/arc-dom.d.ts +0 -14
  180. package/dist/lib/browser/arc-dom.js +0 -121
  181. package/dist/lib/browser/caller-identity.d.ts +0 -40
  182. package/dist/lib/browser/caller-identity.js +0 -175
  183. package/dist/lib/browser/cdp.d.ts +0 -51
  184. package/dist/lib/browser/cdp.js +0 -254
  185. package/dist/lib/browser/chrome.d.ts +0 -141
  186. package/dist/lib/browser/chrome.js +0 -704
  187. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  188. package/dist/lib/browser/chromium-discovery.js +0 -96
  189. package/dist/lib/browser/devices.d.ts +0 -34
  190. package/dist/lib/browser/devices.js +0 -61
  191. package/dist/lib/browser/domain-skills.d.ts +0 -71
  192. package/dist/lib/browser/domain-skills.js +0 -195
  193. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  194. package/dist/lib/browser/drivers/arc.js +0 -276
  195. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  196. package/dist/lib/browser/drivers/firefox.js +0 -377
  197. package/dist/lib/browser/drivers/local.d.ts +0 -55
  198. package/dist/lib/browser/drivers/local.js +0 -266
  199. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  200. package/dist/lib/browser/drivers/ssh.js +0 -467
  201. package/dist/lib/browser/editor.d.ts +0 -3
  202. package/dist/lib/browser/editor.js +0 -50
  203. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  204. package/dist/lib/browser/ffmpeg.js +0 -184
  205. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  206. package/dist/lib/browser/firefox-discovery.js +0 -162
  207. package/dist/lib/browser/har.d.ts +0 -85
  208. package/dist/lib/browser/har.js +0 -77
  209. package/dist/lib/browser/hygiene.d.ts +0 -97
  210. package/dist/lib/browser/hygiene.js +0 -153
  211. package/dist/lib/browser/index.d.ts +0 -5
  212. package/dist/lib/browser/index.js +0 -5
  213. package/dist/lib/browser/input.d.ts +0 -7
  214. package/dist/lib/browser/input.js +0 -92
  215. package/dist/lib/browser/ipc.d.ts +0 -204
  216. package/dist/lib/browser/ipc.js +0 -1453
  217. package/dist/lib/browser/login-detection.d.ts +0 -87
  218. package/dist/lib/browser/login-detection.js +0 -274
  219. package/dist/lib/browser/profiles.d.ts +0 -422
  220. package/dist/lib/browser/profiles.js +0 -1157
  221. package/dist/lib/browser/refs.d.ts +0 -89
  222. package/dist/lib/browser/refs.js +0 -191
  223. package/dist/lib/browser/registry.d.ts +0 -70
  224. package/dist/lib/browser/registry.js +0 -252
  225. package/dist/lib/browser/remote-control.d.ts +0 -59
  226. package/dist/lib/browser/remote-control.js +0 -85
  227. package/dist/lib/browser/resolve-target.d.ts +0 -66
  228. package/dist/lib/browser/resolve-target.js +0 -264
  229. package/dist/lib/browser/runtime-state.d.ts +0 -291
  230. package/dist/lib/browser/runtime-state.js +0 -584
  231. package/dist/lib/browser/secret-ref.d.ts +0 -10
  232. package/dist/lib/browser/secret-ref.js +0 -14
  233. package/dist/lib/browser/service.d.ts +0 -812
  234. package/dist/lib/browser/service.js +0 -4693
  235. package/dist/lib/browser/stream.d.ts +0 -17
  236. package/dist/lib/browser/stream.js +0 -72
  237. package/dist/lib/browser/task-index.d.ts +0 -78
  238. package/dist/lib/browser/task-index.js +0 -199
  239. package/dist/lib/browser/types.d.ts +0 -647
  240. package/dist/lib/browser/types.js +0 -61
  241. package/dist/lib/browser/upload.d.ts +0 -29
  242. package/dist/lib/browser/upload.js +0 -298
  243. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  244. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  245. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  246. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  247. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  248. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -246,7 +246,7 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = tru
246
246
  : lastStatus === 'missed' ? chalk.magenta
247
247
  : chalk.gray;
248
248
  const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
249
- // Append the concrete reason a routine did not complete (auth_failed, wedged,
249
+ // Append the concrete reason a routine did not complete (auth_failed,
250
250
  // blocked, missed) right in the Last Status cell, so the list answers "why"
251
251
  // without a drill-in. Only for non-completed local runs; peer rows stay blank.
252
252
  const reason = latestRun ? runFailureReason(latestRun) : null;
@@ -650,12 +650,29 @@ function routineMatchesQuery(job, q) {
650
650
  .toLowerCase()
651
651
  .includes(q);
652
652
  }
653
- /** Friendly one-liners for the claim a `skipped` run lost. */
653
+ /**
654
+ * Friendly one-liners for the claim a `skipped` run lost — the reasons whose
655
+ * message is static. `active_run` is dynamic (it names the live run that owns
656
+ * the slot and when it started), built in {@link runFailureReason}: since
657
+ * PHNX-4116 there is no "wedged" daemon state, so an overlapping routine reads
658
+ * as `blocked` on the live run, not "wedged".
659
+ */
654
660
  const SKIP_REASON_LABEL = {
655
- active_run: 'wedged: a prior run is still active',
656
661
  duplicate_slot: 'duplicate slot (already fired)',
657
662
  wrong_owner: 'pinned to another device',
658
663
  };
664
+ /**
665
+ * A run id is an ISO timestamp with `:`/`.` replaced by `-` (`slotRunId` /
666
+ * `generateRunId`), so it reverses losslessly to the run's start time. Returns
667
+ * the ISO string, or null when the id is not that shape (a hand-set id).
668
+ */
669
+ function runIdToStartIso(runId) {
670
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/.exec(runId);
671
+ if (!m)
672
+ return null;
673
+ const iso = `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`;
674
+ return Number.isFinite(Date.parse(iso)) ? iso : null;
675
+ }
659
676
  /**
660
677
  * The short, human reason a run did not simply complete — for inline display in
661
678
  * the list/detail so "why did it fail" needs no dig into the run dir. Prefers the
@@ -670,6 +687,21 @@ export function runFailureReason(run) {
670
687
  const one = s.replace(/\s+/g, ' ').trim();
671
688
  return one.length > 80 ? one.slice(0, 79) + '…' : one;
672
689
  };
690
+ // An overlap skip names the live run it lost to (PHNX-4116): `blocked: run
691
+ // <id> active since <t>`, not the removed "wedged" wording. The structured
692
+ // `activeRunId` is preferred; a legacy record without it falls back to the
693
+ // errorMessage (which embeds the same id) so nothing regresses to vaguer text.
694
+ if (run.skipReason === 'active_run') {
695
+ if (run.activeRunId) {
696
+ const since = runIdToStartIso(run.activeRunId);
697
+ return since
698
+ ? `blocked: run ${run.activeRunId} active since ${since}`
699
+ : `blocked: run ${run.activeRunId} still active`;
700
+ }
701
+ if (run.errorMessage)
702
+ return compact(run.errorMessage);
703
+ return 'blocked: a prior run is still active';
704
+ }
673
705
  if (run.errorMessage)
674
706
  return compact(run.errorMessage);
675
707
  if (run.status === 'blocked' && run.readiness) {
@@ -751,8 +783,8 @@ function buildRoutineDetail(job, scheduler, now) {
751
783
  : chalk.yellow(run.status);
752
784
  const dur = run.completedAt ? ` ${formatRunDuration(run.startedAt, run.completedAt)}` : '';
753
785
  // Surface WHY a run did not complete, inline, so "looking at status" does not
754
- // require digging into the run dir. auth_failed / OAuth-revoked, a wedged
755
- // active-run skip, or a readiness block all live on the RunMeta already.
786
+ // require digging into the run dir. auth_failed / OAuth-revoked, an
787
+ // active-run overlap skip, or a readiness block all live on the RunMeta already.
756
788
  const reason = runFailureReason(run);
757
789
  const why = reason ? chalk.gray(` — ${reason}`) : '';
758
790
  lines.push(` ${run.startedAt} ${status}${dur}${why}`);
@@ -1941,7 +1973,7 @@ export function registerRoutinesCommands(program) {
1941
1973
  chalk.gray(` ${run.startedAt}`) +
1942
1974
  chalk.gray(formatRunDuration(run.startedAt, run.completedAt)) +
1943
1975
  (run.exitCode !== null && run.exitCode !== undefined ? chalk.gray(` exit ${run.exitCode}`) : ''));
1944
- // The structured reason (auth_failed, blocked readiness, wedged skip) — the
1976
+ // The structured reason (auth_failed, blocked readiness, overlap skip) — the
1945
1977
  // report/stdout tail below often buries or omits it, so name it up front.
1946
1978
  const logsReason = runFailureReason(run);
1947
1979
  if (logsReason)
@@ -2300,9 +2332,7 @@ export function registerRoutinesCommands(program) {
2300
2332
  console.log(chalk.bold('Scheduler\n'));
2301
2333
  const stateLabel = schedulerState === 'running'
2302
2334
  ? chalk.green('running')
2303
- : schedulerState === 'wedged'
2304
- ? chalk.red('wedged')
2305
- : chalk.gray('stopped');
2335
+ : chalk.gray('stopped');
2306
2336
  console.log(` Status: ${stateLabel}`);
2307
2337
  console.log(` Daemon: ${status.running ? chalk.green('running') : chalk.gray('stopped')}`);
2308
2338
  if (status.pid)
@@ -2316,9 +2346,6 @@ export function registerRoutinesCommands(program) {
2316
2346
  const jobs = listAllJobs();
2317
2347
  const enabled = jobs.filter((j) => j.enabled);
2318
2348
  console.log(` Routines: ${enabled.length} enabled / ${jobs.length} total`);
2319
- if (status.state === 'wedged') {
2320
- console.log(chalk.red('\n The shared daemon is wedged (heartbeat stale). Restart deliberately with: agents daemon restart'));
2321
- }
2322
2349
  if (schedulerState === 'running' && enabled.length > 0) {
2323
2350
  const scheduler = new JobScheduler(async () => { });
2324
2351
  scheduler.loadAll();
@@ -1,6 +1,6 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionMeta } from '../lib/session/types.js';
3
- export declare const MARKDOWN_RENDER_AGENTS: readonly ["claude", "codex", "gemini", "antigravity", "opencode", "grok", "rush", "hermes", "kimi", "droid", "cursor"];
3
+ export declare const MARKDOWN_RENDER_AGENTS: readonly ["claude", "codex", "antigravity", "opencode", "grok", "rush", "hermes", "kimi", "droid", "cursor"];
4
4
  export type ReasoningMode = 'omit' | 'fold' | 'include';
5
5
  /** Build one shareable Markdown document from the canonical preview and event model. */
6
6
  export declare function renderSessionMarkdownDocument(session: SessionMeta, options?: {
@@ -14,7 +14,6 @@ import { selectSessions } from './sessions-export.js';
14
14
  export const MARKDOWN_RENDER_AGENTS = [
15
15
  'claude',
16
16
  'codex',
17
- 'gemini',
18
17
  'antigravity',
19
18
  'opencode',
20
19
  'grok',
@@ -15,7 +15,6 @@ export declare const tsxLoaderUrl: string;
15
15
  export declare function writeUpdateCache(tempHome: string): void;
16
16
  export declare function writeClaudeSession(tempHome: string, projectKey: string, sessionId: string, cwd: string, content: string, timestamp: string): void;
17
17
  export declare function writeCodexSession(tempHome: string, sessionId: string, cwd: string, prompt: string, timestamp: string): void;
18
- export declare function writeGeminiSession(tempHome: string, sessionId: string, cwd: string, prompt: string, timestamp: string, version?: string): void;
19
18
  export declare function writeOpenClawSetup(tempHome: string, version?: string): string;
20
19
  export declare function runAgents(args: string[], cwd: string, home: string, envOverrides?: Record<string, string>): import("node:child_process").SpawnSyncReturns<string>;
21
20
  export declare function outputOf(result: {
@@ -1,7 +1,6 @@
1
1
  import { describe } from 'vitest';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
- import * as crypto from 'crypto';
5
4
  import { spawnSync } from 'child_process';
6
5
  import { createRequire } from 'node:module';
7
6
  import { pathToFileURL } from 'node:url';
@@ -121,42 +120,6 @@ export function writeCodexSession(tempHome, sessionId, cwd, prompt, timestamp) {
121
120
  ];
122
121
  fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
123
122
  }
124
- export function writeGeminiSession(tempHome, sessionId, cwd, prompt, timestamp, version = '0.29.5') {
125
- const versionHome = path.join(tempHome, '.agents', '.history', 'versions', 'gemini', version, 'home');
126
- const geminiHome = path.join(versionHome, '.gemini');
127
- const projectHash = crypto.createHash('sha256').update(cwd).digest('hex');
128
- const chatsDir = path.join(geminiHome, 'tmp', projectHash, 'chats');
129
- fs.mkdirSync(cwd, { recursive: true });
130
- fs.mkdirSync(chatsDir, { recursive: true });
131
- fs.mkdirSync(path.dirname(path.join(tempHome, '.gemini')), { recursive: true });
132
- const activeGeminiHome = path.join(tempHome, '.gemini');
133
- if (!fs.existsSync(activeGeminiHome)) {
134
- fs.symlinkSync(geminiHome, activeGeminiHome);
135
- }
136
- fs.writeFileSync(path.join(geminiHome, 'projects.json'), JSON.stringify({ projects: [cwd] }), 'utf-8');
137
- fs.writeFileSync(path.join(chatsDir, `session-${timestamp.replace(/[:.]/g, '-')}-${sessionId.slice(0, 8)}.json`), JSON.stringify({
138
- sessionId,
139
- projectHash,
140
- startTime: timestamp,
141
- lastUpdated: timestamp,
142
- messages: [
143
- {
144
- id: `${sessionId}-user`,
145
- timestamp,
146
- type: 'user',
147
- content: [{ text: prompt }],
148
- },
149
- {
150
- id: `${sessionId}-assistant`,
151
- timestamp,
152
- type: 'gemini',
153
- content: 'Investigating now.',
154
- model: 'gemini-3-flash-preview',
155
- tokens: { total: 1234 },
156
- },
157
- ],
158
- }, null, 2), 'utf-8');
159
- }
160
123
  export function writeOpenClawSetup(tempHome, version = '2026.3.8') {
161
124
  const managedHome = path.join(tempHome, '.agents', '.history', 'versions', 'openclaw', version, 'home', '.openclaw');
162
125
  fs.mkdirSync(managedHome, { recursive: true });
@@ -1,20 +1,25 @@
1
1
  /**
2
- * `agents setup browser` — interactive wizard to get `agents browser` working on
3
- * a fresh machine: detect an installed Chromium-family browser, create the
4
- * `default` profile pinned to it, optionally make it this machine's default, and
5
- * point the user at the one manual step we can't automate (first-run + sign-in).
2
+ * `agents setup browser` — get `agents browser` working on a fresh machine:
3
+ * install the standalone `browser` CLI (@phnx-labs/browser-cli), let it detect
4
+ * installed browsers and create machine-local profiles (`browser profiles seed`),
5
+ * then pick this machine's default (`browser use`).
6
6
  *
7
- * Idempotent: re-running shows the current default profile and offers to change
8
- * the pinned browser or re-point the device default.
7
+ * The engine owns profile declarations and browser detection now (PHNX-4101), so
8
+ * this wizard DELEGATES to it rather than crafting profiles itself. What stays
9
+ * agents-cli's is the onboarding flow and the readiness check over the shared
10
+ * `browser.profile` config key both CLIs read/write.
11
+ *
12
+ * Idempotent: `profiles seed` leaves existing profiles untouched, and `use`
13
+ * re-points the device default.
9
14
  */
10
15
  import type { Command } from 'commander';
11
16
  /**
12
- * Interactive browser setup. Returns true if a usable default profile exists
13
- * afterwards, false if none exists / the user backed out. Non-interactively it
14
- * only RECOGNIZES an existing profile — it never creates one (PHNX-3296), since
15
- * silently minting a logged-out `auto-chrome` on a headless box is the exact bug
16
- * that removed; a headless box gets its browser from the fleet hub. Never throws
17
- * on cancel — the `agents setup` hub relies on that.
17
+ * Interactive browser setup. Returns true if a default profile is configured
18
+ * afterwards, false if none is / the user backed out. Non-interactively it only
19
+ * RECOGNIZES an existing default — it never creates one (PHNX-3296), since
20
+ * silently minting a logged-out profile on a headless box is the exact bug that
21
+ * removed; a headless box gets its browser from the fleet hub. Never throws on
22
+ * cancel — the `agents setup` hub relies on that.
18
23
  */
19
24
  export declare function runBrowserWizard(): Promise<boolean>;
20
25
  /** Register `agents setup browser` under the parent `setup` command. */
@@ -1,124 +1,89 @@
1
1
  /**
2
- * `agents setup browser` — interactive wizard to get `agents browser` working on
3
- * a fresh machine: detect an installed Chromium-family browser, create the
4
- * `default` profile pinned to it, optionally make it this machine's default, and
5
- * point the user at the one manual step we can't automate (first-run + sign-in).
2
+ * `agents setup browser` — get `agents browser` working on a fresh machine:
3
+ * install the standalone `browser` CLI (@phnx-labs/browser-cli), let it detect
4
+ * installed browsers and create machine-local profiles (`browser profiles seed`),
5
+ * then pick this machine's default (`browser use`).
6
6
  *
7
- * Idempotent: re-running shows the current default profile and offers to change
8
- * the pinned browser or re-point the device default.
7
+ * The engine owns profile declarations and browser detection now (PHNX-4101), so
8
+ * this wizard DELEGATES to it rather than crafting profiles itself. What stays
9
+ * agents-cli's is the onboarding flow and the readiness check over the shared
10
+ * `browser.profile` config key both CLIs read/write.
11
+ *
12
+ * Idempotent: `profiles seed` leaves existing profiles untouched, and `use`
13
+ * re-points the device default.
9
14
  */
10
15
  import { openSetupTerminal } from './setup-terminal.js';
11
16
  import chalk from 'chalk';
12
- import { setConfigValue } from '../lib/device-config.js';
13
- import { listInstalledBrowsers } from '../lib/browser/chrome.js';
14
- import { DEFAULT_BROWSER_PROFILE_NAME, getAutoDetectedProfile, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, } from '../lib/browser/profiles.js';
15
- import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
17
+ import { getConfigValue } from '../lib/device-config.js';
16
18
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
17
- import { defaultBrowserChoice } from './setup-preferences.js';
18
19
  import { installSetupTool } from '../lib/setup-tool-install.js';
19
20
  import { refreshToolSetup } from '../lib/setup-tool-status.js';
20
- const INSTALL_HINT = 'Install one of: Google Chrome, Brave, Microsoft Edge, Chromium, Comet, or Arc, then re-run `agents setup browser`.\n' +
21
- '(This wizard picks a Chromium-family browser. An installed Firefox is not auto-picked here; its profiles are ' +
22
- 'discovered from profiles.ini and selected with `agents browser use firefox-<name>`. Safari is not supported.)';
21
+ import { browserInstalled, runBrowser } from '../lib/browser-client.js';
22
+ import { buildBrowserContext } from '../lib/browser/context.js';
23
+ /** The device-local default profile, as both agents-cli and browser-cli see it. */
24
+ function configuredDefaultProfile() {
25
+ return getConfigValue('browser.profile').value || undefined;
26
+ }
27
+ /** Run one standalone `browser` verb, inheriting the terminal, and return its exit code. */
28
+ async function runBrowserVerb(argv) {
29
+ const { exitCode } = await runBrowser({ argv, context: await buildBrowserContext() });
30
+ return exitCode;
31
+ }
23
32
  /**
24
- * Interactive browser setup. Returns true if a usable default profile exists
25
- * afterwards, false if none exists / the user backed out. Non-interactively it
26
- * only RECOGNIZES an existing profile — it never creates one (PHNX-3296), since
27
- * silently minting a logged-out `auto-chrome` on a headless box is the exact bug
28
- * that removed; a headless box gets its browser from the fleet hub. Never throws
29
- * on cancel — the `agents setup` hub relies on that.
33
+ * Interactive browser setup. Returns true if a default profile is configured
34
+ * afterwards, false if none is / the user backed out. Non-interactively it only
35
+ * RECOGNIZES an existing default — it never creates one (PHNX-3296), since
36
+ * silently minting a logged-out profile on a headless box is the exact bug that
37
+ * removed; a headless box gets its browser from the fleet hub. Never throws on
38
+ * cancel — the `agents setup` hub relies on that.
30
39
  */
31
40
  export async function runBrowserWizard() {
32
41
  if (!isInteractiveTerminal()) {
33
- // Non-interactive / headless: recognize an existing default, but NEVER mint
34
- // one. Silently auto-detecting an installed browser and creating an
35
- // `auto-chrome` here is exactly the bug PHNX-3296 removed — a headless box
36
- // gets its browser from the fleet hub (`browser.device`), or from an
37
- // interactive pick later.
38
- const existing = await getAutoDetectedProfile();
42
+ const existing = configuredDefaultProfile();
39
43
  if (existing) {
40
- console.log(chalk.dim(`Browser profile "${existing.name}" already exists.`));
44
+ console.log(chalk.dim(`Default browser profile "${existing}" already configured on this machine.`));
41
45
  return true;
42
46
  }
43
47
  console.log(chalk.dim('No default browser profile on this machine. Re-run `agents setup browser` in an ' +
44
48
  'interactive terminal to pick one, or use the fleet hub: agents config set browser.device <host>.'));
45
49
  return false;
46
50
  }
47
- const installed = listInstalledBrowsers();
48
- if (installed.length === 0) {
49
- console.error(chalk.red('No supported browser found on this machine.'));
50
- console.log(chalk.dim(INSTALL_HINT));
51
- return false;
52
- }
53
- const { confirm, select } = await import('@inquirer/prompts');
54
- // If a default profile already exists, this is a reconfigure.
55
- const existing = await getAutoDetectedProfile();
56
- if (existing) {
57
- console.log(chalk.dim(`Browser profile "${existing.name}" already exists (${existing.browser}${existing.binary ? ` · ${existing.binary}` : ''}).`));
58
- const change = await confirm({ message: 'Re-create it (e.g. to pin a different browser)?', default: false });
59
- if (!change) {
60
- await maybeSetDeviceDefault(existing.name, confirm);
61
- printOnboardingNextStep(existing.name);
62
- return true;
51
+ if (!browserInstalled()) {
52
+ console.log(chalk.dim('Installing the standalone Browser CLI (@phnx-labs/browser-cli)…'));
53
+ if (!(await installSetupTool('browser'))) {
54
+ console.error(chalk.red('Could not install @phnx-labs/browser-cli. Install it manually with `npm i -g @phnx-labs/browser-cli`, then re-run.'));
55
+ return false;
63
56
  }
64
- // Re-create: drop the old one so createProfile doesn't collide.
65
- const { deleteProfile } = await import('../lib/browser/profiles.js');
66
- await deleteProfile(existing.name);
67
57
  }
68
- // Pick which installed browser to pin (auto-select if only one). The picker
69
- // highlights the same browser auto-detect would win — choosing is an
70
- // override of the default, never a guess.
71
- let chosen = installed[0];
72
- if (installed.length > 1) {
73
- const value = await select({
74
- message: 'Which browser should the default profile use?',
75
- default: defaultBrowserChoice(installed) ?? undefined,
76
- choices: installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
77
- });
78
- chosen = installed.find((b) => b.browserType === value) ?? installed[0];
58
+ // Let the engine detect installed browsers and create a machine-local profile
59
+ // for each (idempotent), then open its own picker to set this machine's default.
60
+ const seedCode = await runBrowserVerb(['profiles', 'seed']);
61
+ if (seedCode !== 0) {
62
+ console.error(chalk.red('`browser profiles seed` failed — see the output above.'));
63
+ return false;
79
64
  }
80
- const freePort = await findFreeProfilePort();
81
- const profile = {
82
- name: DEFAULT_BROWSER_PROFILE_NAME,
83
- description: `${chosen.browserType} profile (agents setup browser)`,
84
- browser: chosen.browserType,
85
- binary: chosen.binary,
86
- endpoints: [`cdp://127.0.0.1:${freePort}`],
87
- viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
88
- };
89
- await createProfile(profile);
90
- console.log(chalk.green(`\nCreated browser profile "${profile.name}" → ${chosen.browserType} (CDP 127.0.0.1:${freePort}).`));
91
- await maybeSetDeviceDefault(profile.name, confirm);
92
- printOnboardingNextStep(profile.name);
93
- return true;
94
- }
95
- /** Offer to make `name` this machine's default browser profile (device-local). */
96
- async function maybeSetDeviceDefault(name, confirm) {
97
- const current = getConfiguredDefaultProfileName();
98
- if (current === name)
99
- return; // already the device default
100
- const set = await confirm({
101
- message: `Make "${name}" this machine's default browser profile?`,
102
- default: true,
103
- });
104
- if (set) {
105
- setConfigValue('browser.profile', name);
106
- console.log(chalk.dim(`Bare \`agents browser start\` will now use "${name}" on this machine.`));
65
+ await runBrowserVerb(['use']);
66
+ const chosen = configuredDefaultProfile();
67
+ if (!chosen) {
68
+ console.log(chalk.dim('No default profile chosen. Run `agents browser use <name>` later to set one.'));
69
+ return false;
107
70
  }
71
+ printOnboardingNextStep(chosen);
72
+ return true;
108
73
  }
109
- /** The one step we can't automate: Chrome's first-run + your own sign-in. */
74
+ /** The one step we can't automate: the browser's first-run + your own sign-in. */
110
75
  function printOnboardingNextStep(name) {
111
76
  console.log(chalk.bold('\nOne manual step left:'));
112
77
  console.log(' ' +
113
78
  chalk.cyan(`agents browser start --profile ${name}`) +
114
- chalk.dim(' # finish Chrome first-run + sign in to any sites you want automated'));
79
+ chalk.dim(' # finish the browser first-run + sign in to any sites you want automated'));
115
80
  console.log(chalk.dim(` Then check it's ready: agents browser profiles doctor ${name}`));
116
81
  }
117
82
  /** Register `agents setup browser` under the parent `setup` command. */
118
83
  export function registerSetupBrowserCommand(setupCmd) {
119
84
  setupCmd
120
85
  .command('browser')
121
- .description('Set up `agents browser` — detect an installed browser and create the default profile.')
86
+ .description('Set up `agents browser` — install the Browser CLI, seed profiles, and pick this machine\'s default.')
122
87
  .option('--install-only', 'Install the standalone Browser CLI without changing profiles or starting a browser')
123
88
  .option('--terminal [backend]', 'Open interactive setup in a detected or selected terminal')
124
89
  .action(async (options) => {
@@ -21,14 +21,6 @@ export declare function macDeviceNames(reg: DeviceRegistry): string[];
21
21
  * first candidate. null when there are no candidates.
22
22
  */
23
23
  export declare function defaultInteractiveHostChoice(candidates: string[], self?: string): string | null;
24
- /**
25
- * The browser picker's highlighted default — the same browser auto-detect
26
- * would win, since `listInstalledBrowsers` returns platform priority order
27
- * (macOS: chrome first). null when nothing is installed.
28
- */
29
- export declare function defaultBrowserChoice<T extends {
30
- browserType: string;
31
- }>(installed: T[]): T['browserType'] | null;
32
24
  /**
33
25
  * Offer to set the interactive host when none is configured and the registry
34
26
  * has more than one macOS device. Returns true when a host was set. Silent
@@ -53,15 +45,6 @@ export declare function maybePickInteractiveHost(): Promise<boolean>;
53
45
  export declare function maybePickBrowserProfile(deps?: {
54
46
  /** Force interactivity in a test; defaults to a real TTY probe. */
55
47
  interactive?: boolean;
56
- /** Inject the prompt so the pick is testable without a TTY. */
57
- select?: (config: {
58
- message: string;
59
- default?: string;
60
- choices: Array<{
61
- name: string;
62
- value: string;
63
- }>;
64
- }) => Promise<string>;
65
48
  }): Promise<boolean>;
66
49
  /**
67
50
  * The bare-`agents setup` preferences step: interactive host, then browser.
@@ -13,9 +13,8 @@
13
13
  * partial state.
14
14
  */
15
15
  import chalk from 'chalk';
16
- import { listInstalledBrowsers } from '../lib/browser/chrome.js';
17
- import { DEFAULT_BROWSER_PROFILE_NAME, getAutoDetectedProfile, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, } from '../lib/browser/profiles.js';
18
- import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
16
+ import { browserInstalled, runBrowser } from '../lib/browser-client.js';
17
+ import { buildBrowserContext } from '../lib/browser/context.js';
19
18
  import { getConfigValue, setConfigValue } from '../lib/device-config.js';
20
19
  import { loadDevices } from '../lib/devices/registry.js';
21
20
  import { machineId } from '../lib/machine-id.js';
@@ -38,14 +37,6 @@ export function defaultInteractiveHostChoice(candidates, self = machineId()) {
38
37
  return null;
39
38
  return candidates.includes(self) ? self : candidates[0];
40
39
  }
41
- /**
42
- * The browser picker's highlighted default — the same browser auto-detect
43
- * would win, since `listInstalledBrowsers` returns platform priority order
44
- * (macOS: chrome first). null when nothing is installed.
45
- */
46
- export function defaultBrowserChoice(installed) {
47
- return installed.length > 0 ? installed[0].browserType : null;
48
- }
49
40
  /**
50
41
  * Offer to set the interactive host when none is configured and the registry
51
42
  * has more than one macOS device. Returns true when a host was set. Silent
@@ -94,48 +85,26 @@ export async function maybePickBrowserProfile(deps = {}) {
94
85
  const interactive = deps.interactive ?? isInteractiveTerminal();
95
86
  if (!interactive)
96
87
  return false;
97
- if (getConfiguredDefaultProfileName())
88
+ // Already chosen (by an earlier setup, `agents browser use`, or browser-cli
89
+ // directly) — never re-pin behind the user's back.
90
+ if (getConfigValue('browser.profile').value)
98
91
  return false;
99
- if (await getAutoDetectedProfile())
92
+ // The engine owns detection and profile creation (PHNX-4101); it is optional,
93
+ // so skip the pick quietly when it is not installed rather than nagging.
94
+ if (!browserInstalled())
100
95
  return false;
101
- const installed = listInstalledBrowsers();
102
- if (installed.length === 0)
96
+ // Let the engine detect installed browsers and create a machine-local profile
97
+ // for each (idempotent), then open its own picker to set this machine's default.
98
+ const context = await buildBrowserContext();
99
+ const seed = await runBrowser({ argv: ['profiles', 'seed'], context });
100
+ if (seed.exitCode !== 0)
103
101
  return false;
104
- const config = {
105
- message: 'Which browser should agents drive on THIS machine?',
106
- default: defaultBrowserChoice(installed) ?? SKIP,
107
- choices: [
108
- ...installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
109
- { name: `None ${chalk.dim('— this box uses the fleet hub (agents config set browser.device <host>)')}`, value: SKIP },
110
- ],
111
- };
112
- // The prompt is the only non-testable boundary here, so it is injectable —
113
- // everything after (createProfile, setConfigValue) runs for real.
114
- let picked;
115
- if (deps.select) {
116
- picked = await deps.select(config);
117
- }
118
- else {
119
- const { select } = await import('@inquirer/prompts');
120
- picked = await select(config);
121
- }
122
- if (picked === SKIP)
102
+ await runBrowser({ argv: ['use'], context });
103
+ const chosen = getConfigValue('browser.profile').value;
104
+ if (!chosen)
123
105
  return false;
124
- const chosen = installed.find((b) => b.browserType === picked) ?? installed[0];
125
- const freePort = await findFreeProfilePort();
126
- const profile = {
127
- name: DEFAULT_BROWSER_PROFILE_NAME,
128
- description: `${chosen.browserType} profile (chosen during setup)`,
129
- browser: chosen.browserType,
130
- binary: chosen.binary,
131
- endpoints: [`cdp://127.0.0.1:${freePort}`],
132
- viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
133
- };
134
- await createProfile(profile);
135
- // The same key `agents browser use` writes (device-local).
136
- setConfigValue('browser.profile', profile.name);
137
- console.log(chalk.green(`Browser: '${chosen.browserType}'`) +
138
- chalk.dim(` — profile "${profile.name}" is this machine's default (agents browser use to change).`));
106
+ console.log(chalk.green(`Browser: '${chosen}'`) +
107
+ chalk.dim(' — this machine\'s default (agents browser use to change).'));
139
108
  return true;
140
109
  }
141
110
  /**
@@ -31,8 +31,7 @@ import { registerAliasCommand } from './alias.js';
31
31
  import { registerBetaCommands } from './beta.js';
32
32
  import { addUrlSchemeSubcommands } from './open.js';
33
33
  import { runPreferencesStep } from './setup-preferences.js';
34
- import { getConfiguredDefaultProfileName, getProfile, getAutoDetectedProfile, isProfileLaunchableHere } from '../lib/browser/profiles.js';
35
- import { listInstalledBrowsers } from '../lib/browser/chrome.js';
34
+ import { browserInstalled } from '../lib/browser-client.js';
36
35
  import { probeComputerTrust } from './computer.js';
37
36
  import { loadDevices } from '../lib/devices/registry.js';
38
37
  import { getConfigValue } from '../lib/device-config.js';
@@ -269,12 +268,13 @@ export async function ensureInitialized(program) {
269
268
  await runSetup(program, { suppressFooter: true });
270
269
  }
271
270
  export async function getSetupStatus() {
272
- const configuredBrowserProfile = getConfiguredDefaultProfileName();
273
- const browserProfile = configuredBrowserProfile
274
- ? await getProfile(configuredBrowserProfile)
275
- : await getAutoDetectedProfile();
276
- const browserReady = browserProfile !== null && isProfileLaunchableHere(browserProfile);
277
- const installedBrowsers = listInstalledBrowsers();
271
+ // Browser readiness is now config + standalone presence (PHNX-4101): the engine
272
+ // (@phnx-labs/browser-cli) owns profile declarations and launchability, so
273
+ // agents-cli reads the shared default-profile key it and browser-cli both write
274
+ // rather than resolving a profile through a deleted in-repo engine.
275
+ const browserCliInstalled = browserInstalled();
276
+ const configuredBrowserProfile = getConfigValue('browser.profile').value;
277
+ const browserReady = browserCliInstalled && !!configuredBrowserProfile;
278
278
  const computerState = process.platform === 'darwin' ? (await probeComputerTrust() ? 'ready' : 'missing') : 'n/a';
279
279
  const devices = await loadDevices();
280
280
  const coreReady = isGitRepo(getAgentsDir());
@@ -286,7 +286,7 @@ export async function getSetupStatus() {
286
286
  const defaultBrowser = getConfigValue('browser.profile').value;
287
287
  return [
288
288
  { phase: 'core', state: coreReady ? 'ready' : 'missing', detail: coreReady ? 'system repo ready' : 'system repo missing' },
289
- { phase: 'browser', state: browserReady ? 'ready' : 'missing', detail: browserReady ? `profile ${browserProfile.name}` : browserProfile ? `profile ${browserProfile.name} cannot launch here` : installedBrowsers.length ? 'no default profile' : 'no supported browser found' },
289
+ { phase: 'browser', state: browserReady ? 'ready' : 'missing', detail: browserReady ? `profile ${configuredBrowserProfile}` : !browserCliInstalled ? 'browser CLI not installed (npm i -g @phnx-labs/browser-cli)' : 'no default profile (agents setup browser)' },
290
290
  { phase: 'computer', state: computerState, detail: computerState === 'ready' ? 'helper trusted' : computerState === 'n/a' ? 'macOS local setup only' : 'helper not running or not trusted' },
291
291
  { phase: 'secrets', state: secretsReady ? 'ready' : 'missing', detail: secretsReady ? 'defaults chosen' : 'defaults not chosen' },
292
292
  { phase: 'term', state: termReady ? 'ready' : 'missing', detail: termReady ? 'installed' : 'not installed (accounts add/login spawn it)' },
@@ -789,7 +789,16 @@ async function runFleetPing(opts) {
789
789
  if (opts.local) {
790
790
  const { authRows: rows } = await refreshLocalFleetAuthState({ force: true });
791
791
  if (opts.json) {
792
- console.log(JSON.stringify({ host: self, rows }));
792
+ // Per-agent launchability from the SAME run-router path the local readiness
793
+ // check uses (`collectRunCandidates`/`isLaunchableSignedIn`), so a consumer
794
+ // can tell a box that simply could not probe — a worker's setup-token box,
795
+ // or a headed box that spent its usage-endpoint budget, both DROP their
796
+ // `no_evidence` row — from a box with no credential at all. From `rows`
797
+ // alone the two are indistinguishable, which broke `--host` routine
798
+ // readiness on every worker (PHNX-4116).
799
+ const { collectRunCandidates } = await import('../lib/accounting/rotate.js');
800
+ const launchable = (await Promise.all(ALL_AGENT_IDS.map(async (agent) => (await collectRunCandidates(agent)).some((candidate) => candidate.signedIn) ? agent : null))).filter((agent) => agent !== null);
801
+ console.log(JSON.stringify({ host: self, rows, launchable }));
793
802
  }
794
803
  else {
795
804
  for (const line of renderAuthMatrix([{ host: self, rows }], { verbose: opts.verbose }))