@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
@@ -35,8 +35,6 @@ import { Option } from 'commander';
35
35
  import chalk from 'chalk';
36
36
  import { resolveSyncPassphraseFromEnv } from '../lib/sync-passphrase.js';
37
37
  import { agentLabel, resolveAgentName, MANAGED_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
38
- import { autoEvictCentralBrowserProfiles } from '../lib/browser/registry.js';
39
- import { shouldAutoClaimCentralProfile } from '../lib/browser/profiles.js';
40
38
  import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, healDanglingVersionPointers, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildSelection, mergeRepoScopedSelections, listRepoNames, getVersionHomePath, } from '../lib/installations/versions.js';
41
39
  import { capableAgents } from '../lib/capabilities.js';
42
40
  import { parseHookManifest, registerHooksToSettings } from '../lib/hooks/install.js';
@@ -397,12 +395,9 @@ async function runInteractiveReconcile(opts, outLog, errLog) {
397
395
  else
398
396
  outLog(chalk.yellow(` ! ${repo}: ${(res.error ?? 'pull failed').split('\n')[0]}`));
399
397
  }
400
- // Drain the legacy central `browser:` tombstone now that the repos are pulled.
401
- // Running AFTER the pull is load-bearing: it acts on a view of central that
402
- // already reflects peers' drains, so it never re-claims a profile another box
403
- // already migrated (which would flip that profile's kind identity->fungible).
404
- // The interactive path is always non-quiet, non-json.
405
- evictCentralBrowserProfilesForSync(false, false, outLog, errLog);
398
+ // Central browser-profile claiming is the standalone `browser` CLI's now
399
+ // (PHNX-4101): `browser profiles claim` / `browser profiles prune` own it.
400
+ // agents-cli no longer evicts central `browser:` tombstones on sync.
406
401
  // 2. One selection spanning the chosen repos.
407
402
  const selection = mergeRepoScopedSelections(repos, cwd);
408
403
  const hasResources = selection.memory === 'all' || Object.entries(selection).some(([kind, v]) => kind !== 'memory' && Array.isArray(v) && v.length > 0);
@@ -443,36 +438,6 @@ async function runInteractiveReconcile(opts, outLog, errLog) {
443
438
  if (repairHadFailures(umbrellaRepair))
444
439
  process.exitCode = 1;
445
440
  }
446
- /**
447
- * Drain the legacy central `browser:` tombstone during `agents sync` (PHNX-3315).
448
- * New profiles write the per-device doc, but profiles created before the
449
- * device-scoped store lingered in the shared top-level `agents.yaml` and churned
450
- * every fleet pull until someone ran `agents browser profiles claim` by hand.
451
- * Fold the ones THIS box can host into its device doc — host-gated, and the
452
- * selection is computed under the meta lock (see autoEvictCentralBrowserProfiles)
453
- * so it never races itself. Callers MUST invoke this AFTER the repo pull: acting
454
- * on a pre-pull view of central risks re-claiming a profile a peer already
455
- * drained, which would flip its kind identity->fungible. Non-fatal so a hiccup
456
- * can never wedge the sync.
457
- */
458
- function evictCentralBrowserProfilesForSync(quiet, json, outLog, errLog) {
459
- try {
460
- // Auto-claim ONLY remote (ssh://) tombstones — they are fungible by design,
461
- // so a concurrent cross-machine double-claim is harmless. Local/cdp profiles
462
- // have no per-machine ownership signal and are left central for an explicit
463
- // `agents browser profiles claim` (PHNX-3315 review). See
464
- // shouldAutoClaimCentralProfile.
465
- const result = autoEvictCentralBrowserProfiles(shouldAutoClaimCentralProfile);
466
- if (!quiet && !json && result.claimed.length > 0) {
467
- outLog(chalk.gray(` Claimed ${result.claimed.length} central browser profile(s) into this device: ${result.claimed.join(', ')}`));
468
- }
469
- }
470
- catch (err) {
471
- if (!quiet && !json) {
472
- errLog(chalk.yellow(` ! browser profile eviction skipped: ${err.message}`));
473
- }
474
- }
475
- }
476
441
  /**
477
442
  * The umbrella verb: bare `agents sync` (no agent) makes this machine current.
478
443
  * Resolves the flags + a secrets passphrase (env-only for now; tokenized auth
@@ -550,12 +515,9 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
550
515
  log: (msg) => { if (!quiet && !json)
551
516
  outLog(chalk.gray(` ${msg}`)); },
552
517
  });
553
- // Drain the legacy central `browser:` tombstone AFTER the umbrella pull, so
554
- // we act on central as converged by this sync rather than a stale pre-pull
555
- // copy that could re-claim a profile a peer already migrated. Skipped under
556
- // --cloud (fetch-only, no local reconcile).
557
- if (!opts.cloud && !kindSelection)
558
- evictCentralBrowserProfilesForSync(quiet, json, outLog, errLog);
518
+ // Central browser-profile claiming moved to the standalone `browser` CLI
519
+ // (PHNX-4101: `browser profiles claim` / `prune`), so `agents sync` no longer
520
+ // drains the central `browser:` tombstone.
559
521
  // Post-reconcile verification (PHNX-3186): re-read every version the reconcile
560
522
  // wrote into and confirm it now matches source. Without this the umbrella
561
523
  // printed `✓ sync: reconciled` unconditionally, the exact false-success the
@@ -42,7 +42,6 @@ import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requir
42
42
  const AGENT_NAMES = {
43
43
  claude: 'Claude',
44
44
  codex: 'Codex',
45
- gemini: 'Gemini',
46
45
  cursor: 'Cursor',
47
46
  opencode: 'OpenCode',
48
47
  grok: 'Grok',
@@ -1,5 +1,24 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { AgentId } from '../lib/types.js';
3
+ /**
4
+ * Split `<agent>[@<selector>]`. The selector names an INSTALLATION — its frozen
5
+ * label, or the release it currently carries — never a release to install; that
6
+ * is `--to`. Keeping them separate is what lets `agents update claude@2.0.65
7
+ * --to 2.0.71` read unambiguously.
8
+ */
9
+ /**
10
+ * `<agent>[@<installed-version>][#<account>]`. The `#<account>` form is the
11
+ * same selector `agents run claude#work` takes: accounts run on the harness's
12
+ * one managed installation (PHNX-3940), so it names WHICH login the user means
13
+ * and resolves to that installation. Refusing it as "Unknown agent
14
+ * 'claude#work'" sent operators hunting for a per-account install that does
15
+ * not exist.
16
+ */
17
+ export declare function parseTarget(raw: string): {
18
+ agent: AgentId;
19
+ selector?: string;
20
+ account?: string;
21
+ };
3
22
  /**
4
23
  * One line naming the installations a bare `agents update <agent>` did NOT
5
24
  * touch — isolated (`--isolated`) copies and legacy pre-v2 per-version homes —
@@ -3,23 +3,48 @@ import { AGENTS, formatAgentError, resolveAgentName } from '../lib/agents.js';
3
3
  import { setHelpSections } from '../lib/help.js';
4
4
  import { describeInstallation, listInstallations, planAutoUpdates, resolveInstallation, runAutoUpdatePass, selectUpdateStrategy, supportsPinnedUpdate, updateInstallation, } from '../lib/installations/index.js';
5
5
  import { resolveManagedInstallation } from '../lib/installations/index.js';
6
+ import { findUnifiedAccount } from '../lib/account-registry.js';
7
+ import { readMeta } from '../lib/state.js';
6
8
  /**
7
9
  * Split `<agent>[@<selector>]`. The selector names an INSTALLATION — its frozen
8
10
  * label, or the release it currently carries — never a release to install; that
9
11
  * is `--to`. Keeping them separate is what lets `agents update claude@2.0.65
10
12
  * --to 2.0.71` read unambiguously.
11
13
  */
12
- function parseTarget(raw) {
13
- const at = raw.indexOf('@');
14
- const name = at === -1 ? raw : raw.slice(0, at);
15
- const selector = at === -1 ? undefined : raw.slice(at + 1).trim();
14
+ /**
15
+ * `<agent>[@<installed-version>][#<account>]`. The `#<account>` form is the
16
+ * same selector `agents run claude#work` takes: accounts run on the harness's
17
+ * one managed installation (PHNX-3940), so it names WHICH login the user means
18
+ * and resolves to that installation. Refusing it as "Unknown agent
19
+ * 'claude#work'" sent operators hunting for a per-account install that does
20
+ * not exist.
21
+ */
22
+ export function parseTarget(raw) {
23
+ const hash = raw.indexOf('#');
24
+ const spec = hash === -1 ? raw : raw.slice(0, hash);
25
+ const account = hash === -1 ? undefined : raw.slice(hash + 1).trim();
26
+ if (hash !== -1 && !account) {
27
+ throw new Error(`Select an account after # in '${raw}', e.g. claude#work — or just <agent>.`);
28
+ }
29
+ if (account?.includes('@')) {
30
+ throw new Error(`Put the installation before the account: <agent>@<installed-version>#<account>, not '${raw}'.`);
31
+ }
32
+ const at = spec.indexOf('@');
33
+ const name = at === -1 ? spec : spec.slice(0, at);
34
+ const selector = at === -1 ? undefined : spec.slice(at + 1).trim();
16
35
  if (at !== -1 && !selector) {
17
36
  throw new Error(`Missing installation in '${raw}'. Use <agent>@<installed-version>, or just <agent>.`);
18
37
  }
19
38
  const agent = resolveAgentName(name);
20
39
  if (!agent)
21
40
  throw new Error(formatAgentError(name));
22
- return { agent, selector };
41
+ if (account) {
42
+ const known = findUnifiedAccount(account, readMeta(), undefined, agent);
43
+ if (!known) {
44
+ throw new Error(`Unknown ${AGENTS[agent].name} account '${account}'. See: agents accounts list ${agent}`);
45
+ }
46
+ }
47
+ return { agent, selector, account };
23
48
  }
24
49
  /**
25
50
  * One line naming the installations a bare `agents update <agent>` did NOT
@@ -219,7 +244,10 @@ export function registerUpdateCommand(program) {
219
244
  if (!target) {
220
245
  throw new Error('Which agent? Use: agents update <agent>[@<installed-version>], or agents update --auto.');
221
246
  }
222
- const { agent, selector } = parseTarget(target);
247
+ const { agent, selector, account } = parseTarget(target);
248
+ if (account && !options.json) {
249
+ console.log(chalk.gray(` account '${account}' runs on the managed ${AGENTS[agent].name} installation; updating that.`));
250
+ }
223
251
  if (options.check) {
224
252
  const plan = (await planAutoUpdates({ agents: [agent] })).filter((entry) => !selector || entry.installation.label === selector || entry.installation.releaseVersion === selector);
225
253
  if (selector && plan.length === 0) {
@@ -4,7 +4,7 @@ import type { AccountInfo } from '../lib/agents.js';
4
4
  import type { AgentId } from '../lib/types.js';
5
5
  import { type ConfiguredDeviceRole } from '../lib/device-config.js';
6
6
  import { type NativeAccountCatalogRow } from '../lib/account-catalog.js';
7
- import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
7
+ import type { ResourceSection, ViewJsonAgent, ViewJsonRunReady } from '../lib/view-types.js';
8
8
  export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
9
9
  import { type ProfileSummary } from '../lib/profiles.js';
10
10
  import { type ByokUsageResult } from '../lib/byok-usage.js';
@@ -76,6 +76,17 @@ export declare function parseResourceSections(options: {
76
76
  resources?: string | boolean;
77
77
  detailed?: boolean;
78
78
  } & ViewSectionFilter, jsonMode: boolean): Set<ResourceSection>;
79
+ /**
80
+ * The one run-readiness gate for one agent on THIS box (PHNX-4116). Runs the
81
+ * SAME enumeration the local router picks from — `collectRunCandidates` (native
82
+ * slots + version homes) through `readinessFromCandidate` — so the answer
83
+ * `agents view --json` publishes is exactly what a `run` here would find, and a
84
+ * remote `--device auto` dispatcher can read it instead of re-deriving freshness
85
+ * on its own box (which drifted: view.ts lists version homes, the router lists
86
+ * slots). Returns undefined when the agent has no candidates at all (nothing
87
+ * installed), so the field is simply absent rather than a misleading verdict.
88
+ */
89
+ export declare function computeAgentRunReady(agentId: AgentId): Promise<ViewJsonRunReady | undefined>;
79
90
  /**
80
91
  * Collect structured info for one or more agents without rendering to the
81
92
  * terminal. Used by `--json` output and any programmatic consumer (e.g. the
@@ -39,7 +39,7 @@ import { selectUpdateStrategy } from '../lib/installations/strategies.js';
39
39
  import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
40
40
  import { getCentralRulesFileName } from '../lib/rules/rules.js';
41
41
  import { composeRulesFromState } from '../lib/rules/compose.js';
42
- import { getConfiguredRunStrategy, isLaunchableSignedIn } from '../lib/accounting/rotate.js';
42
+ import { getConfiguredRunStrategy, isLaunchableSignedIn, collectRunCandidates, readinessFromCandidate } from '../lib/accounting/rotate.js';
43
43
  import { resolveRunDefaults } from '../lib/run-defaults.js';
44
44
  import { resolveConfiguredModel } from '../lib/models.js';
45
45
  import { listProfiles, profileExists, profileSummary, readProfile } from '../lib/profiles.js';
@@ -1272,6 +1272,36 @@ export function parseResourceSections(options, jsonMode) {
1272
1272
  }
1273
1273
  return set;
1274
1274
  }
1275
+ /**
1276
+ * The one run-readiness gate for one agent on THIS box (PHNX-4116). Runs the
1277
+ * SAME enumeration the local router picks from — `collectRunCandidates` (native
1278
+ * slots + version homes) through `readinessFromCandidate` — so the answer
1279
+ * `agents view --json` publishes is exactly what a `run` here would find, and a
1280
+ * remote `--device auto` dispatcher can read it instead of re-deriving freshness
1281
+ * on its own box (which drifted: view.ts lists version homes, the router lists
1282
+ * slots). Returns undefined when the agent has no candidates at all (nothing
1283
+ * installed), so the field is simply absent rather than a misleading verdict.
1284
+ */
1285
+ export async function computeAgentRunReady(agentId) {
1286
+ const candidates = await collectRunCandidates(agentId);
1287
+ if (candidates.length === 0)
1288
+ return undefined;
1289
+ const accounts = candidates.map((c) => {
1290
+ const readiness = readinessFromCandidate(c);
1291
+ const name = c.nativeAccount || c.accountLabel || c.email || c.version;
1292
+ return { name, ready: readiness.ready, reason: readiness.ready ? 'ready' : readiness.reason };
1293
+ });
1294
+ const readyAccount = accounts.find((a) => a.ready);
1295
+ let reason;
1296
+ if (readyAccount) {
1297
+ reason = `ready (${readyAccount.name})`;
1298
+ }
1299
+ else {
1300
+ const reasons = [...new Set(accounts.map((a) => a.reason))];
1301
+ reason = reasons.length === 1 ? `all ${reasons[0]}` : reasons.join(', ');
1302
+ }
1303
+ return { ready: !!readyAccount, reason, accounts };
1304
+ }
1275
1305
  /**
1276
1306
  * Collect structured info for one or more agents without rendering to the
1277
1307
  * terminal. Used by `--json` output and any programmatic consumer (e.g. the
@@ -1395,6 +1425,15 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1395
1425
  const note = secretsUnavailableNote(catalog);
1396
1426
  if (note)
1397
1427
  console.error(chalk.yellow(note)); // stderr — never corrupts --json stdout
1428
+ // The one run-readiness gate per agent, computed off the router's own
1429
+ // enumeration (PHNX-4116). Runs in parallel across agents; a harness with
1430
+ // nothing installed yields undefined and the field is omitted.
1431
+ const runReadyByAgent = new Map();
1432
+ await Promise.all(agentsToShow.map(async (agentId) => {
1433
+ const runReady = await computeAgentRunReady(agentId);
1434
+ if (runReady)
1435
+ runReadyByAgent.set(agentId, runReady);
1436
+ }));
1398
1437
  const out = [];
1399
1438
  for (const agentId of agentsToShow) {
1400
1439
  const versions = byAgent.get(agentId) ?? [];
@@ -1406,7 +1445,13 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1406
1445
  // Project through the public JSON v2 serializer — the internal catalog row
1407
1446
  // (identityKey, home, installations) is not the machine contract.
1408
1447
  const accounts = accountListJson(catalog.native.filter((row) => row.agent === agentId), catalog.provider.filter((row) => row.harnesses.includes(agentId)), agentId).accounts;
1409
- out.push({ agent: agentId, versions, accounts, harnesses: harnesses.filter((h) => h.agent === agentId) });
1448
+ out.push({
1449
+ agent: agentId,
1450
+ versions,
1451
+ accounts,
1452
+ runReady: runReadyByAgent.get(agentId),
1453
+ harnesses: harnesses.filter((h) => h.agent === agentId),
1454
+ });
1410
1455
  }
1411
1456
  return out;
1412
1457
  }
package/dist/index.js CHANGED
@@ -78,10 +78,12 @@ if (process.argv[2] === '__claude-statusline') {
78
78
  const { runClaudeStatusLine } = await import('./lib/claude-statusline.js');
79
79
  process.exit(await runClaudeStatusLine());
80
80
  }
81
- // Fleet usage-sync receiver: a headed peer pipes its identity-keyed usage rows to
82
- // stdin, we merge them newest-wins into this box's cache (PHNX-3392). Above the
83
- // bootstrap line for the same reason as __claude-statusline — no update check, no
84
- // detached sync fork, and nothing writes to stdout to corrupt the caller's view.
81
+ // Fleet usage-sync receiver: a headed peer pipes its daemon-state envelope to
82
+ // stdin; we store it as that peer's file, merge its usage rows newest-wins into
83
+ // this box's cache, and with `--reply` print our own envelope back (PHNX-3392,
84
+ // PHNX-4116). Above the bootstrap line for the same reason as
85
+ // __claude-statusline — no update check, no detached sync fork, and without
86
+ // `--reply` nothing writes to stdout to corrupt the ready probe's view.
85
87
  if (process.argv[2] === '__usage-ingest') {
86
88
  const { runUsageIngest } = await import('./lib/accounting/usage-ingest.js');
87
89
  process.exit(await runUsageIngest());
@@ -110,9 +112,10 @@ if (process.argv[2] === '__daemon-run') {
110
112
  // stack to whatever the service manager had wired to stdout and never reaching
111
113
  // the daemon's own structured log. Route both into log() so the failure is in
112
114
  // logs.jsonl where `agents daemon logs` reads it, then exit non-zero and
113
- // DELIBERATELY let the supervisor restart us — now paced by the plist's
114
- // ThrottleInterval / the unit's StartLimitBurst. Swallowing here would be the
115
- // worse failure: a daemon left alive with a dead subsystem.
115
+ // DELIBERATELY let the supervisor restart us — paced by the plist's
116
+ // ThrottleInterval / the unit's RestartSec, and never abandoned
117
+ // (StartLimitIntervalSec=0, PHNX-4116). Swallowing here would be the worse
118
+ // failure: a daemon left alive with a dead subsystem.
116
119
  const crash = (kind) => (err) => {
117
120
  const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
118
121
  try {
@@ -137,28 +140,102 @@ if (process.argv[2] === '__daemon-run') {
137
140
  // Lifecycle verbs (resume/stop/inject/watch/--active/--markdown) fall through.
138
141
  if (process.argv[2] === 'sessions') {
139
142
  const forwarded = process.argv.slice(3);
140
- const { isReadQuery, resolveSessionsBin, invocation, SessionsClientError, SESSIONS_INSTALL_HINT, } = await import('./lib/sessions-client.js');
141
- if (isReadQuery(forwarded)) {
142
- const { spawnSync } = await import('node:child_process');
143
- let bin = null;
144
- try {
145
- bin = resolveSessionsBin();
146
- }
147
- catch (err) {
148
- // No standalone on this box: fall through to the in-repo engine below
149
- // rather than refusing the query. The fast path is an optimization for a
150
- // box that has `sessions` installed (skips bootstrap and the sessions
151
- // module); a box without it must still answer `agents sessions --json`
152
- // exactly as it did before PHNX-4012 — every worker in the fleet and
153
- // every CI runner is such a box until @phnx-labs/sessions-cli is
154
- // published, and refusing here turned main's full suite red.
155
- if (!(err instanceof SessionsClientError && err.code === 'SESSIONS_BIN_MISSING'))
156
- throw err;
157
- if (process.env.AGENTS_SESSIONS_FASTPATH_HINT !== '0') {
158
- process.stderr.write(`agents: standalone \`sessions\` not installed; using the in-process engine (${SESSIONS_INSTALL_HINT})\n`);
143
+ const { isReadQuery, usesFilterFlags, usesHostFlag, sessionsBinSupportsFilters, sessionsBinSupportsHost, planDeviceHostRead, resolveSessionsBin, invocation, SessionsClientError, SESSIONS_INSTALL_HINT, } = await import('./lib/sessions-client.js');
144
+ // Resolve the bin up front (a cheap PATH lookup): its presence decides whether
145
+ // we can route at all, and its VERSION decides whether the 0.2.0 filter flags
146
+ // (`--project`/`--since`/`--until`/`--sort`, `@version`, the shorthands) and the
147
+ // 0.2.1 `--host <target>` point-to-one remote read flag are recognized — an
148
+ // older `sessions` would mis-read them as FTS tokens, so those queries stay on
149
+ // the in-repo engine (which implements the same filters and the `--device`
150
+ // fan-out). The two floors are checked independently: a 0.2.0 binary takes the
151
+ // filters but not `--host`.
152
+ let bin = null;
153
+ try {
154
+ bin = resolveSessionsBin();
155
+ }
156
+ catch (err) {
157
+ // No standalone on this box: fall through to the in-repo engine below rather
158
+ // than refusing the query. The fast path is an optimization for a box that
159
+ // has `sessions` installed (skips bootstrap and the sessions module); a box
160
+ // without it must still answer `agents sessions --json` exactly as before.
161
+ if (!(err instanceof SessionsClientError && err.code === 'SESSIONS_BIN_MISSING'))
162
+ throw err;
163
+ }
164
+ const filters = bin !== null && usesFilterFlags(forwarded) ? sessionsBinSupportsFilters(bin) : false;
165
+ const host = bin !== null && usesHostFlag(forwarded) ? sessionsBinSupportsHost(bin) : false;
166
+ // PHNX-4012 local-orchestration collapse: a read query carrying `--device
167
+ // <name>` (and no explicit `--host`) routes through the standalone's
168
+ // point-to-one remote read — `sessions <read args> --host ssh://<resolved>` —
169
+ // instead of the in-repo `--device` peer fan-out, WHEN this box's standalone
170
+ // supports `--host` (>=0.2.1). `planDeviceHostRead` is pure and probes nothing,
171
+ // so a non-device read never pays a `sessions --version` spawn (the probe fires
172
+ // only after a device read is detected). If the PEER lacks the standalone the
173
+ // ssh `bash -lc` command-not-found surfaces as exit 127 — a CAPABILITY gate
174
+ // (same spirit as #3687's version gate, keyed on a specific documented signal),
175
+ // on which we FALL THROUGH to the in-repo `--device` fan-out below so the read
176
+ // still succeeds. Everything else (e.g. 255 unreachable) propagates verbatim.
177
+ // The in-repo fan-out is deliberately KEPT here as the path for un-upgraded
178
+ // peers; its eventual removal is a follow-up gated on fleet-wide 0.2.1 (a
179
+ // separate PHNX-4012 follow-up, see the PR body), not this PR.
180
+ if (bin !== null) {
181
+ const deviceRead = planDeviceHostRead(forwarded, { filters });
182
+ if (deviceRead && sessionsBinSupportsHost(bin)) {
183
+ let target = null;
184
+ try {
185
+ const { resolveRemoteDevice } = await import('./lib/ssh-tunnel.js');
186
+ target = (await resolveRemoteDevice(deviceRead.device, {})).target;
187
+ }
188
+ catch {
189
+ // Unknown/unresolvable device — fall through to the in-repo path, which
190
+ // resolves `--device` against the same fleet registry and emits the
191
+ // canonical error; do not duplicate that error here.
192
+ target = null;
193
+ }
194
+ if (target !== null) {
195
+ const { spawnSync } = await import('node:child_process');
196
+ const { command, prefix } = invocation(bin);
197
+ const hostArgs = [...deviceRead.readArgs, '--host', `ssh://${target}`];
198
+ // Capture (not `stdio: 'inherit'`) so a 127 capability miss can fall
199
+ // through silently instead of leaking the peer's command-not-found before
200
+ // the in-repo read re-answers. Reads are bounded, so buffering is fine;
201
+ // force color + geometry through the capture when the caller is at a real
202
+ // terminal and not asking for `--json`, matching the passthrough renderer.
203
+ const childEnv = { ...process.env };
204
+ if (process.stdout.isTTY && !hostArgs.includes('--json')) {
205
+ if (childEnv.FORCE_COLOR === undefined)
206
+ childEnv.FORCE_COLOR = '1';
207
+ if (process.stdout.columns)
208
+ childEnv.COLUMNS = String(process.stdout.columns);
209
+ if (process.stdout.rows)
210
+ childEnv.LINES = String(process.stdout.rows);
211
+ }
212
+ const res = spawnSync(command, [...prefix, ...hostArgs], {
213
+ encoding: 'utf8',
214
+ env: childEnv,
215
+ maxBuffer: 256 * 1024 * 1024,
216
+ });
217
+ if (res.error) {
218
+ process.stderr.write(`Failed to run \`sessions\`: ${res.error.message}\n`);
219
+ process.exit(1);
220
+ }
221
+ // Exit 127 = the peer has no standalone `sessions` on PATH (ssh/`bash
222
+ // -lc` command-not-found). Fall through to the in-repo fan-out rather
223
+ // than surfacing a broken read.
224
+ if (res.status !== 127) {
225
+ if (res.stdout)
226
+ process.stdout.write(res.stdout);
227
+ if (res.stderr)
228
+ process.stderr.write(res.stderr);
229
+ process.exit(res.status ?? 1);
230
+ }
231
+ // 127: fall through. `forwarded` still carries `--device`, so the
232
+ // `isReadQuery` check below is false and bootstrap's in-repo path answers.
159
233
  }
160
234
  }
235
+ }
236
+ if (isReadQuery(forwarded, { filters, host })) {
161
237
  if (bin) {
238
+ const { spawnSync } = await import('node:child_process');
162
239
  const { command, prefix } = invocation(bin);
163
240
  const res = spawnSync(command, [...prefix, ...forwarded], { stdio: 'inherit' });
164
241
  if (res.error) {
@@ -167,6 +244,10 @@ if (process.argv[2] === 'sessions') {
167
244
  }
168
245
  process.exit(res.status ?? 1);
169
246
  }
247
+ // A read query on a box with no standalone: hint once, then fall through.
248
+ if (process.env.AGENTS_SESSIONS_FASTPATH_HINT !== '0') {
249
+ process.stderr.write(`agents: standalone \`sessions\` not installed; using the in-process engine (${SESSIONS_INSTALL_HINT})\n`);
250
+ }
170
251
  }
171
252
  }
172
253
  // Full CLI: commander tree, update checks, migrations, parse. Static imports
@@ -30,8 +30,6 @@ export const NATIVE_ACCOUNT_CAPABILITIES = {
30
30
  antigravity: { inspection: 'opaque', scope: 'device', status: 'unsupported' },
31
31
  droid: { inspection: 'opaque', scope: 'device', status: 'unsupported' },
32
32
  opencode: { inspection: 'opaque', scope: 'device', status: 'unsupported' },
33
- // Discoverable in the list, but not nameable.
34
- gemini: { inspection: 'email', scope: 'unsupported', status: 'discovery-only' },
35
33
  copilot: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
36
34
  openclaw: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
37
35
  amp: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
@@ -90,6 +90,19 @@ export interface NativeAccountCatalogRow {
90
90
  usageSnapshot?: UsageSnapshot | null;
91
91
  /** Raw usage fetch error, for headless/unverified labeling alongside the bars. */
92
92
  usageError?: string | null;
93
+ /**
94
+ * The token FACT for this account ON THIS BOX (PHNX-4116): the credential kind
95
+ * plus its file date — `sk-ant-oat01 (Sep 16)` for a Claude slot's
96
+ * `.claude/.oauth_token`, `api key (present)` for a provider-key harness — or
97
+ * `no token` when the credential file is absent. A fact, never a verdict word.
98
+ */
99
+ token: string;
100
+ /**
101
+ * The auth FACT for this account ON THIS BOX (PHNX-4116): `last used ok 12m
102
+ * ago` / `last auth failure 401 Sep 20 14:02` / `rate-limited until 15:00` /
103
+ * `not used on this box yet`, from the recorded run outcome (or a real probe).
104
+ */
105
+ lastAuth: string;
93
106
  fix: string | null;
94
107
  }
95
108
  export interface AccountDeviceVerdict {
@@ -155,6 +168,17 @@ export interface AccountListEntryJson {
155
168
  /** The live usage snapshot backing `usage`, when available — additive, not breaking. */
156
169
  usageSnapshot?: UsageSnapshot | null;
157
170
  usageError?: string | null;
171
+ /**
172
+ * The token FACT for this account on the box that emitted this JSON (PHNX-4116):
173
+ * `sk-ant-oat01 (Sep 16)` / `api key (present)` / `no token`. A fact, not a verdict.
174
+ */
175
+ token: string;
176
+ /**
177
+ * The auth FACT for this account on the box that emitted this JSON (PHNX-4116):
178
+ * `last used ok 12m ago` / `last auth failure 401 Sep 20 14:02` / `not used on
179
+ * this box yet`. Reflects the emitting box only; `devices[]` carries the fleet.
180
+ */
181
+ lastAuth: string;
158
182
  fix: string | null;
159
183
  }
160
184
  interface AccountListJson {
@@ -184,6 +208,14 @@ type CatalogMeta = Pick<Meta, 'accounts' | 'deviceAccounts'>;
184
208
  * silently dropped from the list nor shown as connected.
185
209
  */
186
210
  export declare function buildNativeCatalog(rows: NativeHomeRow[], meta: CatalogMeta, globalDefault?: (agent: AgentId) => string | null): NativeAccountCatalogRow[];
211
+ /**
212
+ * The token FACT for a Claude account ON THIS BOX (PHNX-4116): the slot/home
213
+ * `.claude/.oauth_token` scheme prefix + its file date, else a present native
214
+ * credential, else `no token`. For non-Claude native rows it reports credential
215
+ * presence generically. `dir` is the slot dir when the account has one, else the
216
+ * local version home. A fact, never a verdict.
217
+ */
218
+ export declare function readTokenFact(agent: AgentId, dir: string | null): string;
187
219
  export declare function toProviderRow(account: CredentialAccount, meta: Pick<Meta, 'accounts'>): ProviderAccountCatalogRow;
188
220
  /**
189
221
  * Wire the real collectors to the pure builder. This is the canonical
@@ -215,6 +247,12 @@ export declare const OVERVIEW_MAX_USAGE_WINDOWS = 2;
215
247
  * cannot drift.
216
248
  */
217
249
  export declare const ACCOUNT_LISTING_LEGEND = "* stale usage \u00B7 a healthy account carries no state \u00B7 identity + per-box state: agents accounts list --fleet";
250
+ /**
251
+ * Colour the auth FACT by what it states (PHNX-4116): a recent success is green,
252
+ * a server rejection red, a throttle yellow, and "not used on this box yet" the
253
+ * neutral gray of a fact that carries no alarm.
254
+ */
255
+ export declare function authFactNote(lastAuth: string): string;
218
256
  /** Shared text renderer used by both `accounts list` and account-first `view`. */
219
257
  export declare function renderAccountRows(rows: NativeAccountCatalogRow[], opts: {
220
258
  heading?: boolean;