@phnx-labs/agents-cli 1.22.117 → 1.22.119

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 (271) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/accounts.js +7 -4
  4. package/dist/commands/browser.d.ts +52 -70
  5. package/dist/commands/browser.js +192 -3562
  6. package/dist/commands/daemon.d.ts +39 -2
  7. package/dist/commands/daemon.js +72 -73
  8. package/dist/commands/events.js +1 -1
  9. package/dist/commands/exec.js +53 -1
  10. package/dist/commands/projects.js +92 -7
  11. package/dist/commands/repo.js +13 -49
  12. package/dist/commands/route.js +5 -5
  13. package/dist/commands/routines.js +39 -12
  14. package/dist/commands/sessions-render.d.ts +1 -1
  15. package/dist/commands/sessions-render.js +0 -1
  16. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  17. package/dist/commands/sessions.test-fixture.js +0 -37
  18. package/dist/commands/setup-browser.d.ts +17 -12
  19. package/dist/commands/setup-browser.js +52 -87
  20. package/dist/commands/setup-preferences.d.ts +0 -17
  21. package/dist/commands/setup-preferences.js +18 -49
  22. package/dist/commands/setup.js +9 -9
  23. package/dist/commands/ssh.js +10 -1
  24. package/dist/commands/sync.js +6 -44
  25. package/dist/commands/teams.js +0 -1
  26. package/dist/commands/update.d.ts +19 -0
  27. package/dist/commands/update.js +34 -6
  28. package/dist/commands/view.d.ts +12 -1
  29. package/dist/commands/view.js +47 -2
  30. package/dist/index.js +107 -26
  31. package/dist/lib/account-capabilities.js +0 -2
  32. package/dist/lib/account-catalog.d.ts +38 -0
  33. package/dist/lib/account-catalog.js +110 -26
  34. package/dist/lib/account-provider-registry.js +1 -1
  35. package/dist/lib/account-registry.d.ts +1 -1
  36. package/dist/lib/account-registry.js +36 -2
  37. package/dist/lib/accounting/rotate.js +22 -8
  38. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  39. package/dist/lib/accounting/usage-ingest.js +115 -45
  40. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  41. package/dist/lib/accounting/usage-sync.js +227 -66
  42. package/dist/lib/accounting/usage.d.ts +16 -3
  43. package/dist/lib/accounting/usage.js +26 -8
  44. package/dist/lib/accounts/add.d.ts +3 -1
  45. package/dist/lib/accounts/add.js +24 -13
  46. package/dist/lib/accounts/slots.d.ts +8 -0
  47. package/dist/lib/accounts/slots.js +17 -0
  48. package/dist/lib/add-dir.js +0 -1
  49. package/dist/lib/agent-cli-commands.js +0 -1
  50. package/dist/lib/agent-spec/agents.d.ts +6 -7
  51. package/dist/lib/agent-spec/agents.js +7 -60
  52. package/dist/lib/auth-health.d.ts +69 -1
  53. package/dist/lib/auth-health.js +133 -4
  54. package/dist/lib/browser/context.d.ts +82 -0
  55. package/dist/lib/browser/context.js +88 -0
  56. package/dist/lib/browser/paths.d.ts +25 -0
  57. package/dist/lib/browser/paths.js +51 -0
  58. package/dist/lib/browser/record.d.ts +38 -0
  59. package/dist/lib/browser/record.js +57 -0
  60. package/dist/lib/browser/sessions-list.js +1 -2
  61. package/dist/lib/browser-client.d.ts +158 -0
  62. package/dist/lib/browser-client.js +234 -0
  63. package/dist/lib/channels/owner-sink.d.ts +1 -18
  64. package/dist/lib/channels/owner-sink.js +29 -82
  65. package/dist/lib/channels/providers/rush.d.ts +7 -7
  66. package/dist/lib/channels/providers/rush.js +127 -57
  67. package/dist/lib/claude-account-token.d.ts +7 -1
  68. package/dist/lib/claude-account-token.js +26 -4
  69. package/dist/lib/claude-statusline.d.ts +1 -1
  70. package/dist/lib/claude-statusline.js +15 -3
  71. package/dist/lib/commands.js +1 -1
  72. package/dist/lib/crabbox/lease.js +1 -1
  73. package/dist/lib/crabbox/runtimes.js +1 -3
  74. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  75. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  76. package/dist/lib/daemon/auth-sync-service.js +37 -43
  77. package/dist/lib/daemon/daemon.d.ts +25 -14
  78. package/dist/lib/daemon/daemon.js +92 -154
  79. package/dist/lib/daemon/runner.d.ts +2 -2
  80. package/dist/lib/daemon/runner.js +32 -9
  81. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  82. package/dist/lib/daemon/self-update-service.js +40 -36
  83. package/dist/lib/daemon/service.d.ts +12 -8
  84. package/dist/lib/daemon/supervisor.d.ts +65 -54
  85. package/dist/lib/daemon/supervisor.js +136 -171
  86. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  87. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  88. package/dist/lib/daemon/usage-sync-service.js +46 -78
  89. package/dist/lib/daemon-health.d.ts +22 -5
  90. package/dist/lib/daemon-health.js +47 -3
  91. package/dist/lib/daemon-services.d.ts +1 -1
  92. package/dist/lib/daemon-services.js +0 -10
  93. package/dist/lib/daemon-ticks.js +4 -2
  94. package/dist/lib/devices/doctor-findings.js +8 -11
  95. package/dist/lib/devices/harness-inventory.js +14 -5
  96. package/dist/lib/exec-account-home.js +5 -0
  97. package/dist/lib/exec.js +10 -13
  98. package/dist/lib/feed/events.js +0 -1
  99. package/dist/lib/feed/tool-activity.js +5 -2
  100. package/dist/lib/fleet-shared-state.d.ts +45 -0
  101. package/dist/lib/fleet-shared-state.js +78 -9
  102. package/dist/lib/fs-atomic.js +25 -0
  103. package/dist/lib/github/pr-mergeable.d.ts +4 -2
  104. package/dist/lib/github/pr-mergeable.js +5 -3
  105. package/dist/lib/github/project-prs.d.ts +58 -3
  106. package/dist/lib/github/project-prs.js +205 -7
  107. package/dist/lib/harness/adapters/cursor.js +14 -4
  108. package/dist/lib/harness-auth-capabilities.d.ts +4 -3
  109. package/dist/lib/harness-auth-capabilities.js +1 -2
  110. package/dist/lib/helper-versions.js +1 -1
  111. package/dist/lib/hooks/cache.d.ts +7 -0
  112. package/dist/lib/hooks/cache.js +27 -2
  113. package/dist/lib/hooks/install.js +7 -1
  114. package/dist/lib/hosts/ready.d.ts +38 -8
  115. package/dist/lib/hosts/ready.js +62 -14
  116. package/dist/lib/installations/active-check.d.ts +27 -0
  117. package/dist/lib/installations/active-check.js +61 -0
  118. package/dist/lib/installations/migrate.js +6 -3
  119. package/dist/lib/installations/shims.d.ts +1 -1
  120. package/dist/lib/installations/shims.js +32 -12
  121. package/dist/lib/installations/store.d.ts +9 -0
  122. package/dist/lib/installations/store.js +24 -0
  123. package/dist/lib/installations/update.js +21 -15
  124. package/dist/lib/installations/versions.js +15 -3
  125. package/dist/lib/manifest.js +1 -1
  126. package/dist/lib/models.js +0 -2
  127. package/dist/lib/open-url.js +28 -54
  128. package/dist/lib/profiles.js +0 -2
  129. package/dist/lib/projects.d.ts +16 -0
  130. package/dist/lib/projects.js +31 -0
  131. package/dist/lib/refresh.js +1 -1
  132. package/dist/lib/routine-readiness.d.ts +35 -0
  133. package/dist/lib/routine-readiness.js +74 -16
  134. package/dist/lib/rules/compile.js +6 -0
  135. package/dist/lib/sandbox.js +0 -2
  136. package/dist/lib/secrets-policy.d.ts +89 -11
  137. package/dist/lib/secrets-policy.js +175 -28
  138. package/dist/lib/session/active.d.ts +17 -6
  139. package/dist/lib/session/active.js +105 -49
  140. package/dist/lib/session/db.d.ts +1 -1
  141. package/dist/lib/session/db.js +65 -18
  142. package/dist/lib/session/discover.js +1 -283
  143. package/dist/lib/session/mirror.d.ts +4 -3
  144. package/dist/lib/session/mirror.js +4 -3
  145. package/dist/lib/session/parse.d.ts +0 -2
  146. package/dist/lib/session/parse.js +1 -138
  147. package/dist/lib/session/pid-registry.d.ts +6 -3
  148. package/dist/lib/session/pid-registry.js +18 -8
  149. package/dist/lib/session/remote/watch.d.ts +1 -1
  150. package/dist/lib/session/remote/watch.js +7 -3
  151. package/dist/lib/session/throughput.d.ts +1 -3
  152. package/dist/lib/session/throughput.js +0 -20
  153. package/dist/lib/session/types.d.ts +2 -2
  154. package/dist/lib/session/types.js +2 -2
  155. package/dist/lib/sessions-client.d.ts +60 -1
  156. package/dist/lib/sessions-client.js +227 -4
  157. package/dist/lib/setup-tool-status.js +5 -10
  158. package/dist/lib/signin-badge.d.ts +3 -2
  159. package/dist/lib/signin-badge.js +6 -3
  160. package/dist/lib/smart-launch.js +4 -1
  161. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  162. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  163. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  164. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  165. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  166. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  167. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  168. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  169. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  170. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  171. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  172. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  173. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  174. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  175. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  176. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  177. package/dist/lib/staleness/writers/sources.js +5 -0
  178. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  179. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  180. package/dist/lib/state.d.ts +6 -4
  181. package/dist/lib/state.js +7 -17
  182. package/dist/lib/teams/parsers.d.ts +1 -1
  183. package/dist/lib/teams/parsers.js +1 -136
  184. package/dist/lib/teams/placement-probe.js +50 -15
  185. package/dist/lib/teams/scheduler.d.ts +7 -0
  186. package/dist/lib/types.d.ts +16 -1
  187. package/dist/lib/types.js +1 -1
  188. package/dist/lib/view-types.d.ts +32 -0
  189. package/dist/lib/watchdog/read.js +0 -1
  190. package/dist/lib/watchdog/rotate.js +1 -2
  191. package/dist/lib/watchdog/runner.js +2 -2
  192. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  193. package/dist/lib/watchdog/watchdogTail.js +0 -31
  194. package/package.json +1 -1
  195. package/scripts/postinstall.js +8 -8
  196. package/dist/browser.d.ts +0 -2
  197. package/dist/browser.js +0 -17
  198. package/dist/commands/browser-picker.d.ts +0 -18
  199. package/dist/commands/browser-picker.js +0 -97
  200. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  201. package/dist/lib/browser/arc-discovery.js +0 -197
  202. package/dist/lib/browser/arc-dom.d.ts +0 -14
  203. package/dist/lib/browser/arc-dom.js +0 -121
  204. package/dist/lib/browser/caller-identity.d.ts +0 -40
  205. package/dist/lib/browser/caller-identity.js +0 -175
  206. package/dist/lib/browser/cdp.d.ts +0 -51
  207. package/dist/lib/browser/cdp.js +0 -254
  208. package/dist/lib/browser/chrome.d.ts +0 -141
  209. package/dist/lib/browser/chrome.js +0 -704
  210. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  211. package/dist/lib/browser/chromium-discovery.js +0 -96
  212. package/dist/lib/browser/devices.d.ts +0 -34
  213. package/dist/lib/browser/devices.js +0 -61
  214. package/dist/lib/browser/domain-skills.d.ts +0 -71
  215. package/dist/lib/browser/domain-skills.js +0 -195
  216. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  217. package/dist/lib/browser/drivers/arc.js +0 -276
  218. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  219. package/dist/lib/browser/drivers/firefox.js +0 -377
  220. package/dist/lib/browser/drivers/local.d.ts +0 -55
  221. package/dist/lib/browser/drivers/local.js +0 -266
  222. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  223. package/dist/lib/browser/drivers/ssh.js +0 -467
  224. package/dist/lib/browser/editor.d.ts +0 -3
  225. package/dist/lib/browser/editor.js +0 -50
  226. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  227. package/dist/lib/browser/ffmpeg.js +0 -184
  228. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  229. package/dist/lib/browser/firefox-discovery.js +0 -162
  230. package/dist/lib/browser/har.d.ts +0 -85
  231. package/dist/lib/browser/har.js +0 -77
  232. package/dist/lib/browser/hygiene.d.ts +0 -97
  233. package/dist/lib/browser/hygiene.js +0 -153
  234. package/dist/lib/browser/index.d.ts +0 -5
  235. package/dist/lib/browser/index.js +0 -5
  236. package/dist/lib/browser/input.d.ts +0 -7
  237. package/dist/lib/browser/input.js +0 -92
  238. package/dist/lib/browser/ipc.d.ts +0 -204
  239. package/dist/lib/browser/ipc.js +0 -1453
  240. package/dist/lib/browser/login-detection.d.ts +0 -87
  241. package/dist/lib/browser/login-detection.js +0 -274
  242. package/dist/lib/browser/profiles.d.ts +0 -422
  243. package/dist/lib/browser/profiles.js +0 -1157
  244. package/dist/lib/browser/refs.d.ts +0 -89
  245. package/dist/lib/browser/refs.js +0 -191
  246. package/dist/lib/browser/registry.d.ts +0 -70
  247. package/dist/lib/browser/registry.js +0 -252
  248. package/dist/lib/browser/remote-control.d.ts +0 -59
  249. package/dist/lib/browser/remote-control.js +0 -85
  250. package/dist/lib/browser/resolve-target.d.ts +0 -66
  251. package/dist/lib/browser/resolve-target.js +0 -264
  252. package/dist/lib/browser/runtime-state.d.ts +0 -291
  253. package/dist/lib/browser/runtime-state.js +0 -584
  254. package/dist/lib/browser/secret-ref.d.ts +0 -10
  255. package/dist/lib/browser/secret-ref.js +0 -14
  256. package/dist/lib/browser/service.d.ts +0 -812
  257. package/dist/lib/browser/service.js +0 -4693
  258. package/dist/lib/browser/stream.d.ts +0 -17
  259. package/dist/lib/browser/stream.js +0 -72
  260. package/dist/lib/browser/task-index.d.ts +0 -78
  261. package/dist/lib/browser/task-index.js +0 -199
  262. package/dist/lib/browser/types.d.ts +0 -647
  263. package/dist/lib/browser/types.js +0 -61
  264. package/dist/lib/browser/upload.d.ts +0 -29
  265. package/dist/lib/browser/upload.js +0 -298
  266. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  267. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  268. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  269. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  270. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  271. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -41,7 +41,7 @@
41
41
  * it would claim a distribution responsibility agents-cli no longer has.
42
42
  */
43
43
  export const HELPER_RELEASES = {
44
- menubar: { tagPrefix: 'menubar', floor: '1.5.0' },
44
+ menubar: { tagPrefix: 'menubar', floor: '1.6.2' },
45
45
  };
46
46
  /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
47
47
  export function helperTag(helper, version) {
@@ -45,6 +45,13 @@ export declare function generateHookShim(args: {
45
45
  cache?: HookCacheConfig | null;
46
46
  matches?: HookMatches;
47
47
  paths?: HookShimPaths;
48
+ /**
49
+ * Deny the tool call (exit 2) when the source script is missing instead of
50
+ * failing with 127, which every harness reads as "allow". Set for
51
+ * PreToolUse hooks: a guard whose script vanished (version pruned, sync
52
+ * rewriting the hooks dir) must not silently wave tool calls through.
53
+ */
54
+ failClosed?: boolean;
48
55
  }): string;
49
56
  /**
50
57
  * Remove a hook's shim. Called by the registrar's garbage collection when a
@@ -119,7 +119,7 @@ export function generateHookShim(args) {
119
119
  const logsDir = args.paths?.logsDir ?? getLogsDir();
120
120
  const perfDir = args.paths?.perfDir ?? getPerfDir();
121
121
  const shimPath = resolveContainedHookShimPath(shimsDir, args.name);
122
- const content = renderShim(args.name, args.scriptPath, args.cache ?? null, args.matches, { cacheDir, logsDir, perfDir });
122
+ const content = renderShim(args.name, args.scriptPath, args.cache ?? null, args.matches, { cacheDir, logsDir, perfDir }, args.failClosed === true);
123
123
  fs.mkdirSync(shimsDir, { recursive: true });
124
124
  let existing = null;
125
125
  if (fs.existsSync(shimPath)) {
@@ -363,7 +363,7 @@ exit "$EXIT"`;
363
363
  * a gate-only pass-through: it forwards stdin to the script and its stdout back,
364
364
  * with no cache read/write.
365
365
  */
366
- function renderShim(name, scriptPath, cache, matches, paths) {
366
+ function renderShim(name, scriptPath, cache, matches, paths, failClosed = false) {
367
367
  const ttl = cache ? (typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0)) : 0;
368
368
  const key = cache?.key ?? 'global';
369
369
  const prefetch = cache?.prefetch ?? 'none';
@@ -394,6 +394,7 @@ TTL=${ttl}
394
394
  PREFETCH=${q(prefetch)}
395
395
  KEY_MODE=${q(key)}
396
396
  MATCHES_JSON=${q(matchesJson)}
397
+ FAIL_CLOSED=${failClosed ? 1 : 0}
397
398
 
398
399
  mkdir -p "$CACHE_DIR" "$LOGS_DIR" "$PERF_DIR"
399
400
 
@@ -458,6 +459,30 @@ if [ -n "$MATCHES_JSON" ]; then
458
459
  exit 0
459
460
  fi
460
461
  fi
462
+ # A source that is not on disk cannot be executed: bash exits 127, and the
463
+ # harnesses that share the exit-2 deny contract (Claude Code, Codex, Droid)
464
+ # read anything else as "allow" -- a guard silently disabled. Deny instead and
465
+ # name the repair, but only for a PreToolUse firing that the matches: gate
466
+ # above let through: one shim serves every event a hook declares, so a
467
+ # SessionStart or Stop leg of the same hook must not wedge a session, and a
468
+ # fire the predicates would have skipped must not turn into a denial. Other
469
+ # shims keep their existing path (a cached hook still serves its cache).
470
+ if [ "$FAIL_CLOSED" = 1 ] && [ ! -f "$SOURCE" ]; then
471
+ _EVT="$(printf '%s' "$STDIN_PAYLOAD" | "$PY" -c 'import json, sys
472
+ try:
473
+ d = json.load(sys.stdin)
474
+ except Exception:
475
+ d = {}
476
+ print(d.get("hook_event_name") or d.get("hookEventName") or "")' 2>/dev/null || true)"
477
+ if [ -z "$_EVT" ] || [ "$_EVT" = PreToolUse ]; then
478
+ _TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
479
+ printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":0,"cache":"missing-source","exit":2}\\n' \\
480
+ "$_TS" "$HOOK_NAME" >>"$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl" 2>/dev/null || true
481
+ printf '%s: hook source is missing (%s); refusing the tool call unchecked (fail-closed). Run: agents hooks sync\\n' \\
482
+ "$HOOK_NAME" "$SOURCE" >&2
483
+ exit 2
484
+ fi
485
+ fi
461
486
  ${cache ? CACHE_TAIL : PASSTHROUGH_TAIL}
462
487
  `;
463
488
  }
@@ -363,7 +363,13 @@ function resolveHookCommand(name, hookDef, resolveScript) {
363
363
  // script when predicates don't hold) and, when `cache:` is set, layers the
364
364
  // cache/timing machinery on top; with neither, it is a pure pass-through
365
365
  // timing wrapper.
366
- return toPortableCommand(generateHookShim({ name, scriptPath, cache, matches }));
366
+ return toPortableCommand(generateHookShim({
367
+ name,
368
+ scriptPath,
369
+ cache,
370
+ matches,
371
+ failClosed: hookDef.events.includes('PreToolUse'),
372
+ }));
367
373
  }
368
374
  /**
369
375
  * Extensions that are NEVER hooks — docs, configuration, plain data. A file
@@ -57,8 +57,19 @@ export interface ReadyProbe {
57
57
  * `$LASTEXITCODE` (no `printf`/`||`); `parseReadyProbe` keys off the sentinel
58
58
  * substring, so the missing leading newline vs the POSIX `printf '\n…'` form is
59
59
  * absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
60
+ *
61
+ * With `ingestUsage`, the probe first runs `agents __usage-ingest` on the
62
+ * dispatcher's daemon-state envelope, which the caller pipes to stdin
63
+ * (PHNX-4116): the chosen worker then holds the dispatcher's current usage
64
+ * numbers at dispatch instead of waiting for the next 15-minute usage-sync
65
+ * tick. The ingest writes nothing to stdout (only `--reply` does), so the
66
+ * version/sentinel/listing shape `parseReadyProbe` reads is unchanged. On
67
+ * Windows the `agents.ps1` shim drops ssh-piped stdin, so PowerShell reads it
68
+ * into a temp file and hands the verb `--from <path>`.
60
69
  */
61
- export declare function buildReadyProbeCommand(os?: string): string;
70
+ export declare function buildReadyProbeCommand(os?: string, opts?: {
71
+ ingestUsage?: boolean;
72
+ }): string;
62
73
  export declare function readyProbe(target: string, os?: string, extraSshArgs?: string[]): ReadyProbe;
63
74
  /** Pure parser for `readyProbe` output (unit-tested without ssh). */
64
75
  export declare function parseReadyProbe(stdout: string): ReadyProbe;
@@ -92,6 +103,12 @@ interface ViewAgentAccountEligibility {
92
103
  signedIn: boolean | undefined;
93
104
  /** At least one account can launch immediately or enter the harness login flow. */
94
105
  pickerEligible: boolean | undefined;
106
+ /**
107
+ * The remote box's own aggregate exclusion reason when it is not signed in
108
+ * (`runReady.reason` — e.g. `all signed_out`), for the operator-facing
109
+ * placement error. Undefined when ready, absent, or on an older remote CLI.
110
+ */
111
+ reason?: string;
95
112
  }
96
113
  /**
97
114
  * Read the two account gates automatic placement needs from `agents view
@@ -99,13 +116,26 @@ interface ViewAgentAccountEligibility {
99
116
  * the login flow, but it must not route to a device whose every signed-in
100
117
  * account is throttled.
101
118
  *
102
- * The sign-in gate reads the per-version `launchable` field — the strict
103
- * per-version launch truth (`isLaunchableSignedIn`) — so a remote box is judged
104
- * by the SAME launchability the local candidate uses (`collectRunCandidates` →
105
- * `isLaunchableSignedIn`), not the display `signedIn` that inherits the
106
- * active/global HOME login and passes a box that dies at spawn (PHNX-3466). An
107
- * older remote CLI omits `launchable`, so it falls back to `signedIn` — the
108
- * pre-fix behavior, so a rolling fleet does not regress.
119
+ * ONE readiness gate, computed on the box that runs (PHNX-4116). The remote box
120
+ * publishes `runReady` — the router's own `collectRunCandidates` →
121
+ * `readinessFromCandidate` verdict over its native slots AND version homes — so
122
+ * this reads that answer rather than re-deriving freshness here. The
123
+ * dispatching box re-deriving from the per-version `versions[]` list was the bug:
124
+ * that list enumerates version homes, misses the account slots a run picks from,
125
+ * and applied a 40-minute usage-freshness refusal a synced-only worker could
126
+ * never satisfy, turning a usage-sync lag into "no ready device" while
127
+ * `--device <name>` on the same box launched fine.
128
+ *
129
+ * An older remote CLI omits `runReady`; the one-release fallback derives the
130
+ * verdict from the per-version list the same way the pre-PHNX-4116 dispatcher
131
+ * did: the strict `launchable` signal (`isLaunchableSignedIn`, PHNX-3466) for
132
+ * sign-in, MINUS a FRESH throttle (`rate_limited`/`out_of_credits` captured
133
+ * within {@link USAGE_STALE_REFUSAL_MAX_AGE_MS}) or a FRESH dead auth verdict
134
+ * (checked within {@link AUTH_PROBE_MAX_AGE_MS}, {@link isDeadVerdict}). A stale
135
+ * reading is UNVERIFIED, not disqualifying (PHNX-4116/#3700), so a usage-sync
136
+ * lag never bars an old-CLI worker `--device <name>` would launch fine — but a
137
+ * throttled-but-launchable worker no longer reads `signedIn: true` and slips
138
+ * into `--device auto`'s pick during a rolling upgrade.
109
139
  */
110
140
  export declare function viewAgentAccountEligibility(view: string, agent: string, now?: number): ViewAgentAccountEligibility;
111
141
  export declare function viewAgentSignedIn(view: string, agent: string): boolean | undefined;
@@ -102,15 +102,31 @@ const READY_MARKER = '@@AGENTS_READY@@';
102
102
  * `$LASTEXITCODE` (no `printf`/`||`); `parseReadyProbe` keys off the sentinel
103
103
  * substring, so the missing leading newline vs the POSIX `printf '\n…'` form is
104
104
  * absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
105
+ *
106
+ * With `ingestUsage`, the probe first runs `agents __usage-ingest` on the
107
+ * dispatcher's daemon-state envelope, which the caller pipes to stdin
108
+ * (PHNX-4116): the chosen worker then holds the dispatcher's current usage
109
+ * numbers at dispatch instead of waiting for the next 15-minute usage-sync
110
+ * tick. The ingest writes nothing to stdout (only `--reply` does), so the
111
+ * version/sentinel/listing shape `parseReadyProbe` reads is unchanged. On
112
+ * Windows the `agents.ps1` shim drops ssh-piped stdin, so PowerShell reads it
113
+ * into a temp file and hands the verb `--from <path>`.
105
114
  */
106
- export function buildReadyProbeCommand(os) {
115
+ export function buildReadyProbeCommand(os, opts = {}) {
107
116
  if (remoteShellFor(os) === 'powershell') {
108
- const script = `${POWERSHELL_PROGRESS_SILENCE}; ` +
117
+ const ingest = opts.ingestUsage
118
+ ? '$in = [Console]::In.ReadToEnd(); $tmp = $null; ' +
119
+ 'try { $tmp = [System.IO.Path]::GetTempFileName(); [System.IO.File]::WriteAllText($tmp, $in); ' +
120
+ 'agents __usage-ingest --from $tmp 2>$null } ' +
121
+ 'finally { if ($tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } }; '
122
+ : '';
123
+ const script = `${POWERSHELL_PROGRESS_SILENCE}; ${ingest}` +
109
124
  `agents --version 2>$null; Write-Output "${READY_MARKER}"; ` +
110
125
  `agents view --json 2>$null; if ($LASTEXITCODE -ne 0) { agents list 2>$null }`;
111
126
  return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
112
127
  }
113
- const script = `agents --version 2>/dev/null; printf '\\n${READY_MARKER}\\n'; ` +
128
+ const script = `${opts.ingestUsage ? 'agents __usage-ingest 2>/dev/null; ' : ''}` +
129
+ `agents --version 2>/dev/null; printf '\\n${READY_MARKER}\\n'; ` +
114
130
  `agents view --json 2>/dev/null || agents list 2>/dev/null`;
115
131
  return `bash -lc ${shellQuote(script)}`;
116
132
  }
@@ -207,13 +223,26 @@ export function missingPinnedVersionMessage(hostName, agent, version, installed)
207
223
  * the login flow, but it must not route to a device whose every signed-in
208
224
  * account is throttled.
209
225
  *
210
- * The sign-in gate reads the per-version `launchable` field — the strict
211
- * per-version launch truth (`isLaunchableSignedIn`) — so a remote box is judged
212
- * by the SAME launchability the local candidate uses (`collectRunCandidates` →
213
- * `isLaunchableSignedIn`), not the display `signedIn` that inherits the
214
- * active/global HOME login and passes a box that dies at spawn (PHNX-3466). An
215
- * older remote CLI omits `launchable`, so it falls back to `signedIn` — the
216
- * pre-fix behavior, so a rolling fleet does not regress.
226
+ * ONE readiness gate, computed on the box that runs (PHNX-4116). The remote box
227
+ * publishes `runReady` — the router's own `collectRunCandidates` →
228
+ * `readinessFromCandidate` verdict over its native slots AND version homes — so
229
+ * this reads that answer rather than re-deriving freshness here. The
230
+ * dispatching box re-deriving from the per-version `versions[]` list was the bug:
231
+ * that list enumerates version homes, misses the account slots a run picks from,
232
+ * and applied a 40-minute usage-freshness refusal a synced-only worker could
233
+ * never satisfy, turning a usage-sync lag into "no ready device" while
234
+ * `--device <name>` on the same box launched fine.
235
+ *
236
+ * An older remote CLI omits `runReady`; the one-release fallback derives the
237
+ * verdict from the per-version list the same way the pre-PHNX-4116 dispatcher
238
+ * did: the strict `launchable` signal (`isLaunchableSignedIn`, PHNX-3466) for
239
+ * sign-in, MINUS a FRESH throttle (`rate_limited`/`out_of_credits` captured
240
+ * within {@link USAGE_STALE_REFUSAL_MAX_AGE_MS}) or a FRESH dead auth verdict
241
+ * (checked within {@link AUTH_PROBE_MAX_AGE_MS}, {@link isDeadVerdict}). A stale
242
+ * reading is UNVERIFIED, not disqualifying (PHNX-4116/#3700), so a usage-sync
243
+ * lag never bars an old-CLI worker `--device <name>` would launch fine — but a
244
+ * throttled-but-launchable worker no longer reads `signedIn: true` and slips
245
+ * into `--device auto`'s pick during a rolling upgrade.
217
246
  */
218
247
  export function viewAgentAccountEligibility(view, agent, now = Date.now()) {
219
248
  try {
@@ -221,11 +250,25 @@ export function viewAgentAccountEligibility(view, agent, now = Date.now()) {
221
250
  const row = rows.find((candidate) => candidate.agent?.toLowerCase() === agent.toLowerCase());
222
251
  if (!row)
223
252
  return { signedIn: undefined, pickerEligible: undefined };
253
+ const runReady = row.runReady;
254
+ if (runReady && typeof runReady.ready === 'boolean') {
255
+ // A signed-out / revoked account is picker-eligible even when not ready —
256
+ // launching it IS the login flow. A throttled account is not.
257
+ const accounts = runReady.accounts ?? [];
258
+ const pickerEligible = runReady.ready
259
+ || accounts.some((a) => a.reason === 'signed_out' || a.reason === 'revoked');
260
+ return {
261
+ signedIn: runReady.ready,
262
+ pickerEligible,
263
+ reason: runReady.ready ? undefined : runReady.reason,
264
+ };
265
+ }
266
+ // One-release fallback for an older remote CLI without `runReady`.
224
267
  const verdicts = (row.versions ?? []).flatMap((version) => {
225
268
  if (typeof version.signedIn !== 'boolean')
226
269
  return [];
227
270
  // Prefer the strict per-version launch signal; fall back to the display
228
- // `signedIn` for an older remote CLI that does not emit `launchable`.
271
+ // `signedIn` for a CLI old enough to omit `launchable` too.
229
272
  const launchable = typeof version.launchable === 'boolean' ? version.launchable : version.signedIn;
230
273
  const usageCapturedAt = version.usageCapturedAt ? Date.parse(version.usageCapturedAt) : Number.NaN;
231
274
  const usageFresh = Number.isFinite(usageCapturedAt)
@@ -240,9 +283,14 @@ export function viewAgentAccountEligibility(view, agent, now = Date.now()) {
240
283
  && version.authVerdict !== undefined
241
284
  && authFresh
242
285
  && isDeadVerdict(version.authVerdict);
243
- const freshnessKnown = version.usageCapturedAt !== undefined || version.authCheckedAt !== undefined;
244
- const fresh = !freshnessKnown || (usageFresh && authFresh);
245
- const ready = launchable && fresh && !authBlocked && !throttled;
286
+ // A stale usage/auth reading makes the account UNVERIFIED, not unusable:
287
+ // the number is ignored (`throttled`/`authBlocked` only trust a fresh one)
288
+ // and the remote `agents run` weights an unverified account at the floor.
289
+ // Refusing the whole device on a stale reading turned a fleet-wide
290
+ // usage-sync lag into "no healthy device can run claude" while
291
+ // `--device <name>` on the same box launched fine (PHNX-4116/#3700). Only a
292
+ // FRESH throttle or a FRESH dead auth verdict disqualifies.
293
+ const ready = launchable && !authBlocked && !throttled;
246
294
  return [{ ready, pickerEligible: ready || !launchable || authBlocked }];
247
295
  });
248
296
  if (verdicts.length === 0)
@@ -25,6 +25,14 @@ import type { Installation } from './types.js';
25
25
  */
26
26
  export interface ProcessSnapshot {
27
27
  listCommandLines(): Promise<string[]>;
28
+ /** Optional richer listing (pid, elapsed, tty) for naming a blocking process. */
29
+ listProcessRows?(): Promise<ProcessRow[]>;
30
+ }
31
+ export interface ProcessRow {
32
+ pid?: number;
33
+ elapsed?: string;
34
+ tty?: string;
35
+ args: string;
28
36
  }
29
37
  export declare const realProcessSnapshot: ProcessSnapshot;
30
38
  /**
@@ -46,3 +54,22 @@ export declare function installationLooksActive(installation: Pick<Installation,
46
54
  * update to a harness this check simply failed to observe.
47
55
  */
48
56
  export declare function isInstallationLikelyActive(installation: Pick<Installation, 'agent' | 'label'>, snapshot?: ProcessSnapshot): Promise<boolean>;
57
+ export interface InstallationActivity {
58
+ active: boolean;
59
+ /** A launch lease is held (a launch is starting and may not be in the process table yet). */
60
+ lease: boolean;
61
+ /** Live processes naming this installation's directory, one line each. */
62
+ processes: string[];
63
+ /** The process scan failed; `active` is the fail-closed default. */
64
+ scanError?: string;
65
+ }
66
+ /**
67
+ * Same verdict as {@link isInstallationLikelyActive}, plus WHAT is holding the
68
+ * installation, so a refused update can name the session to finish instead of
69
+ * "in use; retry later" (PHNX-4116 follow-up: an operator on yosemite-s0 had a
70
+ * one-hour-old resumed session on another tty and no way to see it from the
71
+ * refusal).
72
+ */
73
+ export declare function describeInstallationActivity(installation: Pick<Installation, 'agent' | 'label'>, snapshot?: ProcessSnapshot): Promise<InstallationActivity>;
74
+ /** The one-line refusal an update prints for a busy installation. */
75
+ export declare function formatInUseDeferral(name: string, activity: InstallationActivity): string;
@@ -22,6 +22,16 @@ import { promisify } from 'util';
22
22
  import { getVersionDir } from './store.js';
23
23
  import { hasLiveLaunchLease } from './shims.js';
24
24
  const execFileAsync = promisify(execFile);
25
+ async function listProcessRowsPosix() {
26
+ const { stdout } = await execFileAsync('ps', ['-Ao', 'pid=,etime=,tty=,args='], {
27
+ timeout: 5_000,
28
+ maxBuffer: 16 * 1024 * 1024,
29
+ });
30
+ return stdout.split('\n').flatMap((line) => {
31
+ const m = /^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line);
32
+ return m ? [{ pid: Number(m[1]), elapsed: m[2], tty: m[3], args: m[4] }] : [];
33
+ });
34
+ }
25
35
  async function listCommandLinesPosix() {
26
36
  try {
27
37
  const { stdout } = await execFileAsync('ps', ['-Ao', 'args'], {
@@ -49,6 +59,9 @@ async function listCommandLinesWindows() {
49
59
  }
50
60
  export const realProcessSnapshot = {
51
61
  listCommandLines: () => (process.platform === 'win32' ? listCommandLinesWindows() : listCommandLinesPosix()),
62
+ listProcessRows: async () => (process.platform === 'win32'
63
+ ? (await listCommandLinesWindows()).map((args) => ({ args }))
64
+ : listProcessRowsPosix()),
52
65
  };
53
66
  /**
54
67
  * Does any live process reference this installation's own directory? Matches
@@ -82,3 +95,51 @@ export async function isInstallationLikelyActive(installation, snapshot = realPr
82
95
  return true;
83
96
  }
84
97
  }
98
+ /** `ps` prints `?` (Linux), `??` (macOS) or `-` for a process with no controlling terminal. */
99
+ const NO_TTY_MARKERS = new Set(['?', '??', '-']);
100
+ function shortenArgs(args, versionDir) {
101
+ // Strip the long install path so the line reads as the command the user ran.
102
+ const trimmed = args.replace(versionDir, '…').replace(/\/node_modules\/\.bin\//, '/');
103
+ return trimmed.length > 110 ? `${trimmed.slice(0, 107)}...` : trimmed;
104
+ }
105
+ /**
106
+ * Same verdict as {@link isInstallationLikelyActive}, plus WHAT is holding the
107
+ * installation, so a refused update can name the session to finish instead of
108
+ * "in use; retry later" (PHNX-4116 follow-up: an operator on yosemite-s0 had a
109
+ * one-hour-old resumed session on another tty and no way to see it from the
110
+ * refusal).
111
+ */
112
+ export async function describeInstallationActivity(installation, snapshot = realProcessSnapshot) {
113
+ const lease = hasLiveLaunchLease(installation.agent, installation.label);
114
+ const versionDir = getVersionDir(installation.agent, installation.label);
115
+ try {
116
+ const rows = snapshot.listProcessRows
117
+ ? await snapshot.listProcessRows()
118
+ : (await snapshot.listCommandLines()).map((args) => ({ args }));
119
+ const processes = rows
120
+ .filter((row) => row.args.includes(versionDir))
121
+ .map((row) => {
122
+ const meta = [row.pid !== undefined ? `pid ${row.pid}` : null, row.elapsed ? `up ${row.elapsed}` : null, row.tty && !NO_TTY_MARKERS.has(row.tty) ? row.tty : null]
123
+ .filter((part) => part !== null)
124
+ .join(', ');
125
+ return meta ? `${meta}: ${shortenArgs(row.args, versionDir)}` : shortenArgs(row.args, versionDir);
126
+ });
127
+ return { active: lease || processes.length > 0, lease, processes };
128
+ }
129
+ catch (err) {
130
+ return { active: true, lease, processes: [], scanError: err instanceof Error ? err.message : String(err) };
131
+ }
132
+ }
133
+ /** The one-line refusal an update prints for a busy installation. */
134
+ export function formatInUseDeferral(name, activity) {
135
+ if (activity.scanError)
136
+ return `${name}: could not confirm nothing is running (${activity.scanError}); not updating.`;
137
+ if (activity.processes.length > 0) {
138
+ const list = activity.processes.length === 1
139
+ ? activity.processes[0]
140
+ : activity.processes.map((p) => `\n ${p}`).join('');
141
+ return `${name} is in use by ${activity.processes.length === 1 ? 'a running process' : `${activity.processes.length} running processes`} — ${list}. `
142
+ + 'Finish that session (agents sessions stop <id> for an agents session), then retry.';
143
+ }
144
+ return `${name} has a launch in flight; retry once it has started or exited.`;
145
+ }
@@ -17,10 +17,13 @@ import { enabledRoutineNames, replaceEnabledRoutines } from '../routine-activati
17
17
  import { evaluateActivationReadiness } from '../routine-readiness.js';
18
18
  import { migrateDeviceConfigStores } from '../devices/config-migration.js';
19
19
  import { detrackViaGitExclude } from '../project-resources.js';
20
- // Two constants only, never the read/write API — migrations still operate on raw
21
- // YAML so they never take the meta lock or prime the meta cache mid-migration.
22
- import { LEGACY_DEFAULT_BROWSER_PROFILE_NAME } from '../browser/profiles.js';
20
+ // Migrations operate on raw YAML so they never take the meta lock or prime the
21
+ // meta cache mid-migration.
23
22
  import { META_HEADER as DEVICE_META_HEADER } from '../state.js';
23
+ /** The pre-device-scope central browser profile name this one-shot migration
24
+ * folds into the device doc. Inlined (the in-repo browser engine that defined it
25
+ * was removed with PHNX-4101); the legacy on-disk name is unchanged. */
26
+ const LEGACY_DEFAULT_BROWSER_PROFILE_NAME = 'default';
24
27
  import { COMPILED_HEADER_PROJECT } from '../rules/compile.js';
25
28
  const HOME = process.env.HOME ?? os.homedir();
26
29
  const USER_DIR = path.join(HOME, '.agents');
@@ -64,7 +64,7 @@ export interface ConflictInfo {
64
64
  * top-level entry add/remove — deep edits to plugin contents won't
65
65
  * trigger auto-resync, run `agents sync` for that.
66
66
  */
67
- export declare const SHIM_SCHEMA_VERSION = 32;
67
+ export declare const SHIM_SCHEMA_VERSION = 33;
68
68
  /**
69
69
  * Generate the full bash shim script for the given agent. The returned string
70
70
  * is written to ~/.agents/shims/{cliCommand} and made executable.
@@ -271,7 +271,7 @@ async function promptConflictStrategy(conflictInfos) {
271
271
  // v32 — every config-dir pin (claude, grok, opencode, kimi, muse, copilot) yields to
272
272
  // an account-slot launch (AGENTS_EXEC_HOME from `agents run`), so a slot run
273
273
  // reads and writes its own slot home instead of the shared version home.
274
- export const SHIM_SCHEMA_VERSION = 32;
274
+ export const SHIM_SCHEMA_VERSION = 33;
275
275
  /** Internal marker string used to embed the schema version in shim scripts. */
276
276
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
277
277
  function shellQuote(value) {
@@ -306,7 +306,14 @@ function shellQuote(value) {
306
306
  * versions.ts (which reads bare filenames via `fs.readdirSync` and so never
307
307
  * had the full-path-match bug).
308
308
  */
309
- const GROK_RESOLVE_BINARY_FN = `_resolve_grok_binary() {
309
+ const GROK_RESOLVE_BINARY_FN = `_resolve_grok_current() {
310
+ local target
311
+ target=$(readlink -f "$1/bin/grok" 2>/dev/null) || return 0
312
+ [ -f "$target" ] && [ -x "$target" ] || return 0
313
+ [ "$(wc -c < "$target" 2>/dev/null || echo 0)" -ge 1000000 ] || return 0
314
+ printf '%s\\n' "$target"
315
+ }
316
+ _resolve_grok_binary() {
310
317
  local dir="$1" version_hint="$2"
311
318
  local candidate base size mtime
312
319
  for candidate in "$dir"/grok-*; do
@@ -569,12 +576,20 @@ ${GROK_RESOLVE_BINARY_FN}
569
576
  # Check the versioned home first — this is where the binary lands when the
570
577
  # installer runs with GROK_HOME set (i.e. via the shim or a correct
571
578
  # \`agents add grok\`), or when grok self-updates from within the shim.
579
+ # \`bin/grok\` is grok's own current-release pointer, kept by its updater in
580
+ # both layouts (\`bin/grok -> ../downloads/grok-<v>-<arch>\` and the newer
581
+ # \`bin/grok -> grok-<v>\`); \`grok update\` never touches downloads/ in the
582
+ # newer one, so scanning downloads/ alone keeps exec'ing a dead release.
583
+ BINARY=$(_resolve_grok_current "$VERSION_DIR/home/.grok")
572
584
  GROK_DOWNLOADS="$VERSION_DIR/home/.grok/downloads"
573
- if [ -d "$GROK_DOWNLOADS" ]; then
585
+ if [ -z "$BINARY" ] && [ -d "$GROK_DOWNLOADS" ]; then
574
586
  BINARY=$(_resolve_grok_binary "$GROK_DOWNLOADS" "$VERSION")
575
587
  fi
576
588
  # Fall back to the global grok home (binary installed without GROK_HOME set,
577
589
  # e.g. an earlier \`agents add grok@latest\` before this resolution fix).
590
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
591
+ BINARY=$(_resolve_grok_current "$HOME/.grok")
592
+ fi
578
593
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
579
594
  GROK_DOWNLOADS="$HOME/.grok/downloads"
580
595
  if [ -d "$GROK_DOWNLOADS" ]; then
@@ -1300,12 +1315,15 @@ export AGENT_CLI_CREDENTIAL_STORE="file"
1300
1315
  ? `# Grok ships its native binary in the versioned home's .grok/downloads (or,
1301
1316
  # for pre-fix installs, the global ~/.grok/downloads), not node_modules.
1302
1317
  ${GROK_RESOLVE_BINARY_FN}
1318
+ BINARY=$(_resolve_grok_current "${versionDir}/home/.grok")
1303
1319
  GROK_DOWNLOADS="${versionDir}/home/.grok/downloads"
1304
- BINARY=""
1305
- if [ -d "$GROK_DOWNLOADS" ]; then
1320
+ if [ -z "$BINARY" ] && [ -d "$GROK_DOWNLOADS" ]; then
1306
1321
  BINARY=$(_resolve_grok_binary "$GROK_DOWNLOADS" "${version}")
1307
1322
  fi
1308
1323
  # Fall back to the global grok home (binary installed without GROK_HOME set).
1324
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
1325
+ BINARY=$(_resolve_grok_current "$AGENTS_REAL_HOME/.grok")
1326
+ fi
1309
1327
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
1310
1328
  GROK_GLOBAL_DOWNLOADS="$AGENTS_REAL_HOME/.grok/downloads"
1311
1329
  if [ -d "$GROK_GLOBAL_DOWNLOADS" ]; then
@@ -2189,15 +2207,17 @@ export function listShimFileNames() {
2189
2207
  }
2190
2208
  /**
2191
2209
  * Legacy command shims that are wrong even when their baked install is alive.
2192
- * `secrets` (PHNX-3989), `sessions` (PHNX-4012) and `computer` (PHNX-4075): the
2193
- * shim `exec`s `agents <name>`, which is a passthrough to the STANDALONE binary —
2194
- * so the shim re-enters itself (the 1.22.85 fork bomb). Pruned unconditionally.
2210
+ * `secrets` (PHNX-3989), `sessions` (PHNX-4012), `computer` (PHNX-4075) and
2211
+ * `browser` (PHNX-4101): the shim `exec`s `agents <name>`, which is a passthrough
2212
+ * to the STANDALONE binary — so the shim re-enters itself (the 1.22.85 fork
2213
+ * bomb) and, sitting first on PATH, shadows the real `@phnx-labs/<name>-cli`
2214
+ * binary a user types directly. Pruned unconditionally.
2195
2215
  *
2196
- * `computer` is the sharpest of the three, because agents-cli USED to publish a
2197
- * `computer` bin of its own: a machine that installed an older CLI has a real
2198
- * shim on PATH under the name the standalone engine now owns.
2216
+ * `computer` and `browser` are the sharpest, because agents-cli USED to publish
2217
+ * (`computer`) or shim (`browser`) a bin of its own: a machine that installed an
2218
+ * older CLI has a real shim on PATH under the name the standalone engine now owns.
2199
2219
  */
2200
- const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions', 'computer', 'pty']);
2220
+ const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions', 'computer', 'pty', 'browser']);
2201
2221
  /**
2202
2222
  * Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
2203
2223
  * alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
@@ -118,6 +118,15 @@ export declare function getVersionDir(agent: AgentId, version: string): string;
118
118
  * Returns null (fail loud, never guess) when nothing survives the size filter.
119
119
  */
120
120
  export declare function resolveGrokFallbackBinary(downloadsDir: string): string | null;
121
+ /**
122
+ * Grok's own current-release pointer, `<grokHome>/bin/grok`, resolved to the
123
+ * real binary, or null when absent or not a real binary. Grok's updater keeps
124
+ * it current in both of its layouts (`bin/grok -> ../downloads/grok-<v>-<arch>`
125
+ * and the newer `bin/grok -> grok-<v>`, which never touches downloads/), so it
126
+ * is the only location that tracks `grok update`. Mirrors the shim's
127
+ * `_resolve_grok_current`.
128
+ */
129
+ export declare function resolveGrokCurrentBinary(grokHome: string): string | null;
121
130
  /**
122
131
  * Get the binary path for a specific agent version.
123
132
  */
@@ -339,12 +339,36 @@ export function resolveGrokFallbackBinary(downloadsDir) {
339
339
  }
340
340
  return best ? path.join(downloadsDir, best.name) : null;
341
341
  }
342
+ /**
343
+ * Grok's own current-release pointer, `<grokHome>/bin/grok`, resolved to the
344
+ * real binary, or null when absent or not a real binary. Grok's updater keeps
345
+ * it current in both of its layouts (`bin/grok -> ../downloads/grok-<v>-<arch>`
346
+ * and the newer `bin/grok -> grok-<v>`, which never touches downloads/), so it
347
+ * is the only location that tracks `grok update`. Mirrors the shim's
348
+ * `_resolve_grok_current`.
349
+ */
350
+ export function resolveGrokCurrentBinary(grokHome) {
351
+ try {
352
+ const target = fs.realpathSync(path.join(grokHome, 'bin', 'grok'));
353
+ const stat = fs.statSync(target);
354
+ if (!stat.isFile() || stat.size < MIN_GROK_BINARY_BYTES)
355
+ return null;
356
+ fs.accessSync(target, fs.constants.X_OK);
357
+ return target;
358
+ }
359
+ catch {
360
+ return null;
361
+ }
362
+ }
342
363
  /**
343
364
  * Get the binary path for a specific agent version.
344
365
  */
345
366
  export function getBinaryPath(agent, version) {
346
367
  const agentConfig = AGENTS[agent];
347
368
  if (agent === 'grok') {
369
+ const current = resolveGrokCurrentBinary(path.join(getVersionHomePath(agent, version), '.grok'));
370
+ if (current)
371
+ return current;
348
372
  const grokDownloads = path.join(getVersionHomePath(agent, version), '.grok', 'downloads');
349
373
  // The directory token is the stable installation/account label. A
350
374
  // self-updating slot may carry a newer vendor release, whose binary keeps
@@ -6,7 +6,7 @@ import { getBinaryPath, invalidateInstalledVersionsCache, invalidateLiveVersionC
6
6
  import { assertValidRelease, selectUpdateStrategy, } from './strategies.js';
7
7
  import { listInstallations, readInstallation, recordRelease, writeInstallation } from './store.js';
8
8
  import { effectiveUpdatePolicy, isAutoUpdateEnabledForAgent } from './update-policy.js';
9
- import { isInstallationLikelyActive } from './active-check.js';
9
+ import { describeInstallationActivity, formatInUseDeferral } from './active-check.js';
10
10
  import { installationLockTarget, INSTALLATION_LOCK_OPTIONS } from './installation-lock.js';
11
11
  /**
12
12
  * Move one frozen installation to a new vendor release, preserving its identity.
@@ -88,18 +88,21 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
88
88
  return { installation, strategy: strategy.id, fromRelease: installation.releaseVersion, toRelease: target,
89
89
  unchanged: true, deferred: 'Update cancelled or automatic update policy changed.', alsoUpdated: [] };
90
90
  }
91
- if (strategy.transactional && await isInstallationLikelyActive(installation)) {
92
- options.onProgress?.(`${AGENTS[agent].name}@${installation.label} looks active right now (a process or launch lease); `
93
- + `not staging release ${target}.`);
94
- return {
95
- installation,
96
- strategy: strategy.id,
97
- fromRelease: installation.releaseVersion,
98
- toRelease: target,
99
- unchanged: true,
100
- deferred: 'Account home is in use; retry after its sessions finish.',
101
- alsoUpdated: [],
102
- };
91
+ if (strategy.transactional) {
92
+ const activity = await describeInstallationActivity(installation);
93
+ if (activity.active) {
94
+ const name = `${AGENTS[agent].name}@${installation.label}`;
95
+ options.onProgress?.(`${name} looks active right now; not staging release ${target}.`);
96
+ return {
97
+ installation,
98
+ strategy: strategy.id,
99
+ fromRelease: installation.releaseVersion,
100
+ toRelease: target,
101
+ unchanged: true,
102
+ deferred: formatInUseDeferral(name, activity),
103
+ alsoUpdated: [],
104
+ };
105
+ }
103
106
  }
104
107
  let staged = null;
105
108
  try {
@@ -179,7 +182,8 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
179
182
  // identical pre-stage check above for why this cannot be opt-in: a launch
180
183
  // that starts AFTER that first check but before this swap is exactly the
181
184
  // window this closes.
182
- if (options.shouldCancel?.() || (strategy.transactional && await isInstallationLikelyActive(installation))) {
185
+ const lateActivity = strategy.transactional ? await describeInstallationActivity(installation) : null;
186
+ if (options.shouldCancel?.() || lateActivity?.active) {
183
187
  options.onProgress?.(`${AGENTS[agent].name}@${installation.label} looks active now (a process or launch lease appeared while `
184
188
  + `${staged.release} was staging); not committing it.`);
185
189
  return {
@@ -188,7 +192,9 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
188
192
  fromRelease: installation.releaseVersion,
189
193
  toRelease: staged.release,
190
194
  unchanged: true,
191
- deferred: 'Update cancelled or the account home became active.',
195
+ deferred: lateActivity?.active
196
+ ? formatInUseDeferral(`${AGENTS[agent].name}@${installation.label}`, lateActivity)
197
+ : 'Update cancelled while the release was being prepared.',
192
198
  alsoUpdated: [],
193
199
  };
194
200
  }