@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -24,7 +24,7 @@ import { getRuntimeStateDir, getHelpersDir } from '../state.js';
24
24
  import { getCliVersion, resolveAgentsBin, resolveInstalledLayout } from '../version.js';
25
25
  import { copyAppBundle, withInstallLock } from '../app-bundle-install.js';
26
26
  import { compareVersions } from '../agent-spec/primitives.js';
27
- import { namespacedServiceLabel, serviceManifestHomeEnv } from '../service-manifest.js';
27
+ import { namespacedServiceLabel, serviceManifestHomeEnv, serviceManagerRegistrationAllowed } from '../service-manifest.js';
28
28
  const APP_BUNDLE_NAME = 'MenubarHelper.app';
29
29
  const INSTALL_DIR_NAME = 'agents-cli';
30
30
  const SERVICE_LABEL_BASE = 'com.phnx-labs.agents-menubar';
@@ -333,6 +333,11 @@ ${envXml}
333
333
  * is the only sequence that reliably re-attaches the status item.
334
334
  */
335
335
  export function restartMenubarLaunchAgent(uid, plist, exec = execFileSync) {
336
+ const reg = serviceManagerRegistrationAllowed();
337
+ if (!reg.allowed) {
338
+ process.stderr.write(`[agents] ${reg.reason}\n`);
339
+ return;
340
+ }
336
341
  const serviceTarget = `gui/${uid}/${serviceLabel()}`;
337
342
  const opts = { stdio: ['ignore', 'ignore', 'ignore'] };
338
343
  try {
@@ -348,6 +353,45 @@ export function restartMenubarLaunchAgent(uid, plist, exec = execFileSync) {
348
353
  }
349
354
  catch { /* best effort */ }
350
355
  }
356
+ /**
357
+ * Force a running helper off the binary that was just swapped underneath it.
358
+ *
359
+ * `installMenubarLaunchAgentOnUpgrade()` calls this after a heal actually
360
+ * replaces the on-disk bundle (a version bump or the ad-hoc -> Developer ID
361
+ * transition — see `menubarHealReplacedBundle`). `restartMenubarLaunchAgent`
362
+ * (run moments earlier via `installAndStartService`) already attempts
363
+ * bootout+bootstrap+kickstart, but that targets the GUI launchd domain and
364
+ * fails silently — `launchctl kickstart -k gui/<uid>/<label>` prints "Could not
365
+ * find service ... in domain for user gui" — from a shell with no Aqua session,
366
+ * which is the ordinary case for `agents` invoked from a terminal/tmux/ssh
367
+ * session rather than literally the login item (verified live). When that
368
+ * happens the plist and on-disk bundle are current but the LIVE process is
369
+ * still the old binary, so it requests Accessibility under the OLD code
370
+ * identity and the grant never sticks.
371
+ *
372
+ * `kickstart -k` (force-restart, unlike the bare kickstart already tried) is
373
+ * attempted first since it is the clean launchd-native path when it works. If
374
+ * launchd has no record of the service in this context either, fall back to
375
+ * ending the specific pid(s) already confirmed to be running the installed
376
+ * bundle (`ownProcesses`, resolved by the caller via `liveMenubarProcesses`) —
377
+ * never a broad pkill. The launchd plist has `KeepAlive=true`, so the ended
378
+ * process is relaunched from the swapped binary within seconds.
379
+ */
380
+ export function restartMenubarHelperAfterSwap(uid, ownProcesses, exec = execFileSync, kill = endProcess) {
381
+ const reg = serviceManagerRegistrationAllowed();
382
+ if (!reg.allowed)
383
+ return;
384
+ const target = `gui/${uid}/${serviceLabel()}`;
385
+ const opts = { stdio: ['ignore', 'ignore', 'ignore'] };
386
+ try {
387
+ exec('launchctl', ['kickstart', '-k', target], opts);
388
+ return;
389
+ }
390
+ catch {
391
+ for (const p of ownProcesses)
392
+ kill(p.pid);
393
+ }
394
+ }
351
395
  /**
352
396
  * Install + start the menu-bar helper as a launchd user service (idempotent).
353
397
  * Clears the sticky opt-out, installs the .app, writes the plist, and
@@ -420,6 +464,22 @@ function menubarSetupStale() {
420
464
  execExists: fs.existsSync(installedExecutablePath()),
421
465
  });
422
466
  }
467
+ /**
468
+ * Pure decision (no I/O): did THIS heal actually replace bundle content a live
469
+ * process could be running stale, as opposed to a plist-only repoint?
470
+ *
471
+ * `stale` (version bump or missing exec) and `needsDevIdHeal` (ad-hoc ->
472
+ * Developer ID) both mean the shipped `.app` differs from what is already
473
+ * installed — the exact condition under which a running helper can be left on
474
+ * an old code identity after `enableMenubarService` swaps the bundle. A
475
+ * repoint-only heal (`menubarSetupNeedsRepoint()` alone, same version, same
476
+ * signing identity) does NOT count: RUSH-3005 already owns the churn from
477
+ * mixed-Node-interpreter repoints, and restarting the helper on every one of
478
+ * those would double that churn rather than fix this bug.
479
+ */
480
+ export function menubarHealReplacedBundle(opts) {
481
+ return opts.stale || opts.needsDevIdHeal;
482
+ }
423
483
  /**
424
484
  * Pure re-point decision (no I/O): the plist's baked interpreter/entry no longer
425
485
  * match the install that is now running `agents`. This catches DUAL-INSTALL skew
@@ -435,7 +495,12 @@ export function menubarPlistNeedsRepoint(opts) {
435
495
  return false; // can't resolve the running install — don't churn
436
496
  if (opts.plistEntry !== opts.activeEntry)
437
497
  return true;
438
- if (opts.activeNode && opts.plistNode !== opts.activeNode)
498
+ // The entry path owns the helper. The same installed CLI may be invoked through
499
+ // several compatible Node interpreters (nvm, Homebrew, a parent process's
500
+ // process.execPath). Re-pointing merely because those valid interpreter paths
501
+ // differ makes every invocation replace and relaunch the shared helper. Keep the
502
+ // recorded interpreter until it disappears; then repoint to the active one.
503
+ if (opts.activeNode && (!opts.plistNode || !opts.plistNodeExists))
439
504
  return true;
440
505
  return false;
441
506
  }
@@ -452,9 +517,11 @@ function readPlistEnvValue(key) {
452
517
  }
453
518
  /** True when the installed plist points at a different install than the active one. */
454
519
  function menubarSetupNeedsRepoint() {
520
+ const plistNode = readPlistEnvValue('AGENTS_NODE');
455
521
  return menubarPlistNeedsRepoint({
456
522
  plistEntry: readPlistEnvValue('AGENTS_ENTRY'),
457
- plistNode: readPlistEnvValue('AGENTS_NODE'),
523
+ plistNode,
524
+ plistNodeExists: Boolean(plistNode) && fs.existsSync(plistNode),
458
525
  activeEntry: resolveCliEntry(),
459
526
  activeNode: process.execPath,
460
527
  });
@@ -467,15 +534,21 @@ export function disableMenubarService() {
467
534
  if (!onDarwin())
468
535
  return;
469
536
  const plist = servicePlistPath();
470
- const uid = process.getuid?.() ?? 0;
471
- try {
472
- execFileSync('launchctl', ['bootout', `gui/${uid}/${serviceLabel()}`], { stdio: ['ignore', 'ignore', 'ignore'] });
473
- }
474
- catch {
537
+ const reg = serviceManagerRegistrationAllowed();
538
+ if (reg.allowed) {
539
+ const uid = process.getuid?.() ?? 0;
475
540
  try {
476
- execFileSync('launchctl', ['unload', '-w', plist], { stdio: ['ignore', 'ignore', 'ignore'] });
541
+ execFileSync('launchctl', ['bootout', `gui/${uid}/${serviceLabel()}`], { stdio: ['ignore', 'ignore', 'ignore'] });
542
+ }
543
+ catch {
544
+ try {
545
+ execFileSync('launchctl', ['unload', '-w', plist], { stdio: ['ignore', 'ignore', 'ignore'] });
546
+ }
547
+ catch { /* not loaded */ }
477
548
  }
478
- catch { /* not loaded */ }
549
+ }
550
+ else {
551
+ process.stderr.write(`[agents] ${reg.reason}\n`);
479
552
  }
480
553
  try {
481
554
  fs.unlinkSync(plist);
@@ -639,7 +712,8 @@ export function installMenubarLaunchAgentOnUpgrade() {
639
712
  // installed copy is still ad-hoc while the shipped source is Developer ID
640
713
  // (older heal path; Accessibility re-prompts until the identity is restored).
641
714
  const needsDevIdHeal = installedNeedsDevIdHeal();
642
- if (!(menubarSetupStale() || menubarSetupNeedsRepoint() || needsDevIdHeal))
715
+ const stale = menubarSetupStale();
716
+ if (!(stale || menubarSetupNeedsRepoint() || needsDevIdHeal))
643
717
  return;
644
718
  // ...but a copy that does not own the helper only gets to act on that drift
645
719
  // once per cooldown. Without the gate every coexisting install recopies the
@@ -650,8 +724,22 @@ export function installMenubarLaunchAgentOnUpgrade() {
650
724
  // false without installing when the bundle fails the Gatekeeper check, and
651
725
  // stamping first would spend the shared cooldown on a no-op — locking every
652
726
  // non-owner out for another hour while nothing had been fixed.
653
- if (enableMenubarService({ clearOptOut: false }))
727
+ if (enableMenubarService({ clearOptOut: false })) {
654
728
  stampMenubarHeal();
729
+ // One-time: the ad-hoc -> Developer ID transition leaves a dead TCC row
730
+ // under the old identity (tccutil on zion reset it 11 times across
731
+ // machines that made this jump) — clear it so the fresh grant sticks.
732
+ if (shouldMigrateMenubarTcc({ needsDevIdHeal, alreadyMigrated: menubarTccAlreadyMigrated() })) {
733
+ resetMenubarAccessibilityTcc();
734
+ }
735
+ // Only a REAL content swap (version bump or the Dev-ID transition) can
736
+ // leave a running process on stale code — a plist-only repoint (handled
737
+ // above by menubarSetupNeedsRepoint alone) is RUSH-3005's churn to own,
738
+ // not this heal's to restart on top of.
739
+ if (menubarHealReplacedBundle({ stale, needsDevIdHeal })) {
740
+ restartMenubarHelperAfterSwap(process.getuid?.() ?? 0, liveMenubarProcesses().own);
741
+ }
742
+ }
655
743
  }
656
744
  catch {
657
745
  /* never block startup on the menu bar */
@@ -666,6 +754,46 @@ function installedNeedsDevIdHeal() {
666
754
  return false;
667
755
  return hasDeveloperIdSignature(src);
668
756
  }
757
+ /** Marker written once the one-time ad-hoc -> Developer ID TCC migration has
758
+ * run on this machine, next to the version/heal stamps. */
759
+ function tccMigrationMarkerPath() {
760
+ return path.join(installDir(), '.menubar-tcc-migrated');
761
+ }
762
+ function menubarTccAlreadyMigrated() {
763
+ return fs.existsSync(tccMigrationMarkerPath());
764
+ }
765
+ /**
766
+ * Pure decision (no I/O): run the one-time `tccutil reset Accessibility` only
767
+ * on a real ad-hoc -> Developer ID transition, and only once ever. A machine
768
+ * that was always Developer ID never accumulated a dead TCC row under an
769
+ * ad-hoc identity, so resetting there would just force a needless re-prompt;
770
+ * a machine that already migrated must not be reset again on every heal.
771
+ */
772
+ export function shouldMigrateMenubarTcc(opts) {
773
+ return opts.needsDevIdHeal && !opts.alreadyMigrated;
774
+ }
775
+ /**
776
+ * Reset the stale Accessibility grant TCC recorded against the pre-migration
777
+ * ad-hoc identity, then stamp the marker so this never runs again on this
778
+ * machine. Targets `SERVICE_LABEL_BASE` — the bundle's actual
779
+ * `CFBundleIdentifier`/codesign `--identifier` (menubar/scripts/build.sh) that
780
+ * TCC keys grants to — not the namespaced `serviceLabel()`, which exists only
781
+ * to keep launchd job labels apart under a redirected HOME (RUSH-2639) and is
782
+ * never the app's real identity. Best-effort: a missing/failing `tccutil`
783
+ * (older macOS, a sandboxed test HOME) must never block the heal that just
784
+ * fixed the signing identity itself.
785
+ */
786
+ export function resetMenubarAccessibilityTcc(exec = execFileSync) {
787
+ try {
788
+ exec('tccutil', ['reset', 'Accessibility', SERVICE_LABEL_BASE], { stdio: ['ignore', 'ignore', 'ignore'] });
789
+ }
790
+ catch { /* best effort — a missing/failing tccutil must not block the heal */ }
791
+ try {
792
+ fs.mkdirSync(installDir(), { recursive: true });
793
+ fs.writeFileSync(tccMigrationMarkerPath(), new Date().toISOString());
794
+ }
795
+ catch { /* best effort */ }
796
+ }
669
797
  /**
670
798
  * Decide which live helper processes must be ended so exactly one status item
671
799
  * survives. Pure so the choice is unit-testable without a live menu bar.
@@ -856,3 +984,81 @@ export function getMenubarStatus() {
856
984
  disabledByUser: menubarDisabledByUser(),
857
985
  };
858
986
  }
987
+ /**
988
+ * Pure comparison (no I/O) behind the stale-process check: a helper pid that
989
+ * started before the bundle on disk was last written is still running the
990
+ * PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
991
+ * to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
992
+ * without a live process or filesystem.
993
+ */
994
+ export function isMenubarProcessStaleAgainstBundle(pidStartedAtMs, bundleMtimeMs) {
995
+ return pidStartedAtMs < bundleMtimeMs;
996
+ }
997
+ /** Wall-clock start time of a live pid via `ps`, or null if it can't be read. */
998
+ function pidStartTimeMs(pid) {
999
+ const r = spawnSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
1000
+ stdio: ['ignore', 'pipe', 'ignore'],
1001
+ encoding: 'utf-8',
1002
+ });
1003
+ if (r.status !== 0)
1004
+ return null;
1005
+ const raw = (r.stdout || '').trim();
1006
+ if (!raw)
1007
+ return null;
1008
+ const t = new Date(raw).getTime();
1009
+ return Number.isNaN(t) ? null : t;
1010
+ }
1011
+ /**
1012
+ * `agents menubar doctor` — everything a human needs to tell "why did
1013
+ * Accessibility ask again" apart from "the helper is genuinely broken":
1014
+ * install path, version skew, whether the signing identity is update-stable,
1015
+ * and whether a live process predates the bundle it's supposed to be running.
1016
+ * Read-only: no install, no restart, no TCC reset — `agents menubar setup`
1017
+ * remains the command that fixes what this reports.
1018
+ */
1019
+ export function buildMenubarDoctorReport() {
1020
+ if (!onDarwin()) {
1021
+ return {
1022
+ platform: process.platform,
1023
+ installPath: null,
1024
+ installedVersion: null,
1025
+ currentVersion: getCliVersion(),
1026
+ versionMatches: false,
1027
+ signingIdentity: 'unknown',
1028
+ running: false,
1029
+ staleRunningProcess: [],
1030
+ accessibilityHintNeeded: false,
1031
+ };
1032
+ }
1033
+ const status = getMenubarStatus();
1034
+ const appPath = status.installedApp;
1035
+ const signingIdentity = appPath
1036
+ ? (hasDeveloperIdSignature(appPath) ? 'developer-id' : 'ad-hoc')
1037
+ : 'unknown';
1038
+ let staleRunningProcess = [];
1039
+ if (appPath && status.instances.length > 0) {
1040
+ try {
1041
+ const bundleMtimeMs = fs.statSync(installedExecutablePath()).mtimeMs;
1042
+ staleRunningProcess = status.instances
1043
+ .map((p) => {
1044
+ const startedAt = pidStartTimeMs(p.pid);
1045
+ return startedAt === null
1046
+ ? null
1047
+ : { pid: p.pid, stale: isMenubarProcessStaleAgainstBundle(startedAt, bundleMtimeMs) };
1048
+ })
1049
+ .filter((p) => p !== null);
1050
+ }
1051
+ catch { /* exec vanished mid-check — report no staleness rather than throw */ }
1052
+ }
1053
+ return {
1054
+ platform: process.platform,
1055
+ installPath: appPath,
1056
+ installedVersion: status.installedVersion,
1057
+ currentVersion: status.currentVersion,
1058
+ versionMatches: status.installedVersion === status.currentVersion,
1059
+ signingIdentity,
1060
+ running: status.running,
1061
+ staleRunningProcess,
1062
+ accessibilityHintNeeded: signingIdentity === 'ad-hoc' || staleRunningProcess.some((p) => p.stale),
1063
+ };
1064
+ }
@@ -68,8 +68,8 @@ export type MonitorActionType = 'run' | 'routine' | 'notify' | 'webhook-out';
68
68
  */
69
69
  export interface ActionConfig {
70
70
  type: MonitorActionType;
71
- /** run: which agent to spawn. */
72
- agent?: AgentId;
71
+ /** run: which agent to spawn — a native harness id or a custom harness name (agents harness list). */
72
+ agent?: AgentId | (string & {});
73
73
  /** run: the prompt; `{event}` is replaced with the fired event summary. */
74
74
  prompt?: string;
75
75
  /** run: execution mode (shared with routines). */
@@ -17,6 +17,7 @@ import { safeJoin, isSafeSegmentName } from '../paths.js';
17
17
  import { atomicWriteFileSync } from '../fs-atomic.js';
18
18
  import { machineId, normalizeHost } from '../machine-id.js';
19
19
  import { ALL_AGENT_IDS } from '../agents.js';
20
+ import { isCustomHarnessName } from '../profiles.js';
20
21
  /** Default values applied to every monitor config when fields are omitted. */
21
22
  const MONITOR_DEFAULTS = {
22
23
  enabled: true,
@@ -215,8 +216,8 @@ export function validateMonitor(config) {
215
216
  errors.push(`action has conflicting fields (${populatedAct.join(', ')}); specify exactly one action`);
216
217
  }
217
218
  if (action.type === 'run') {
218
- if (action.agent && !ALL_AGENT_IDS.includes(action.agent)) {
219
- errors.push(`action.agent must be one of: ${ALL_AGENT_IDS.join(', ')}`);
219
+ if (action.agent && !ALL_AGENT_IDS.includes(action.agent) && !isCustomHarnessName(action.agent)) {
220
+ errors.push(`action.agent must be one of: ${ALL_AGENT_IDS.join(', ')}, or a custom harness (agents harness list)`);
220
221
  }
221
222
  if (!action.prompt || typeof action.prompt !== 'string') {
222
223
  errors.push("action.type 'run' requires action.prompt");
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resolve which machine a session transcript originated on.
3
+ *
4
+ * Cross-machine sync mirrors a remote transcript to
5
+ * `backups/<agent>/<machine>/<subdir>/…`. Every other transcript is a live-home
6
+ * file on this box, so the origin is the local machine id.
7
+ *
8
+ * Kept in a leaf module (no session/db imports) so both discovery and the
9
+ * sessions DB upsert path can stamp `machine` without circular deps.
10
+ */
11
+ /** Local machine id, cached for the process lifetime. */
12
+ export declare function localMachineId(): string;
13
+ /**
14
+ * The machine a discovered session originated on.
15
+ * When the path sits under the agent's backups root, the first segment below it
16
+ * is the origin machine id; otherwise it's the local machine.
17
+ */
18
+ export declare function machineForSessionFile(filePath: string, agent: string): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Resolve which machine a session transcript originated on.
3
+ *
4
+ * Cross-machine sync mirrors a remote transcript to
5
+ * `backups/<agent>/<machine>/<subdir>/…`. Every other transcript is a live-home
6
+ * file on this box, so the origin is the local machine id.
7
+ *
8
+ * Kept in a leaf module (no session/db imports) so both discovery and the
9
+ * sessions DB upsert path can stamp `machine` without circular deps.
10
+ */
11
+ import * as path from 'path';
12
+ import { getHistoryDir } from './state.js';
13
+ import { machineId } from './machine-id.js';
14
+ let _localMachineId;
15
+ /** Local machine id, cached for the process lifetime. */
16
+ export function localMachineId() {
17
+ return (_localMachineId ??= machineId());
18
+ }
19
+ /**
20
+ * The machine a discovered session originated on.
21
+ * When the path sits under the agent's backups root, the first segment below it
22
+ * is the origin machine id; otherwise it's the local machine.
23
+ */
24
+ export function machineForSessionFile(filePath, agent) {
25
+ if (!filePath)
26
+ return localMachineId();
27
+ const base = path.join(getHistoryDir(), 'backups', agent) + path.sep;
28
+ if (filePath.startsWith(base)) {
29
+ const seg = filePath.slice(base.length).split(path.sep)[0];
30
+ if (seg)
31
+ return seg;
32
+ }
33
+ return localMachineId();
34
+ }
@@ -8,7 +8,7 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import Database from '../sqlite.js';
10
10
  import { getPerfDbPath, getPerfDir } from '../state.js';
11
- import { localMachineId } from '../session/origin-machine.js';
11
+ import { localMachineId } from '../origin-machine.js';
12
12
  import { resolveProjectKey } from '../project-key.js';
13
13
  import { percentile } from '../percentile.js';
14
14
  import { resolveSpoolPath, shortSessionId, _resetPerfSpoolForTest } from './spool.js';
@@ -9,7 +9,7 @@ import * as fs from 'fs';
9
9
  import * as os from 'os';
10
10
  import * as path from 'path';
11
11
  import { getPerfSpoolPath } from '../state.js';
12
- import { localMachineId } from '../session/origin-machine.js';
12
+ import { localMachineId } from '../origin-machine.js';
13
13
  let _spoolOverride = null;
14
14
  let _disabled = false;
15
15
  /** Test seam — pair with db._resetPerfDbForTest. */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Client for the Rush account layer (`api.prix.dev`) — backs `agents auth` and
3
- * `agents org`. `agents org` maps to `/api/v1/spaces`, not `/api/v1/orgs`: spaces
3
+ * `agents auth space` (canonical) / `agents org` (deprecated alias). `agents auth space`
4
+ * maps to `/api/v1/spaces`, not `/api/v1/orgs`: spaces
4
5
  * already carry the free-tier caps (1 owned space, 3 members) and can exist
5
6
  * standalone with no parent organization, matching a "just want a team" CLI flow
6
7
  * better than the heavier enterprise-tenancy `orgs` routes (domain, SSO). See
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Client for the Rush account layer (`api.prix.dev`) — backs `agents auth` and
3
- * `agents org`. `agents org` maps to `/api/v1/spaces`, not `/api/v1/orgs`: spaces
3
+ * `agents auth space` (canonical) / `agents org` (deprecated alias). `agents auth space`
4
+ * maps to `/api/v1/spaces`, not `/api/v1/orgs`: spaces
4
5
  * already carry the free-tier caps (1 owned space, 3 members) and can exist
5
6
  * standalone with no parent organization, matching a "just want a team" CLI flow
6
7
  * better than the heavier enterprise-tenancy `orgs` routes (domain, SSO). See
@@ -96,6 +96,13 @@ export declare function getProfilePath(name: string): string;
96
96
  export declare function validateProfileName(name: string): void;
97
97
  /** Check whether a profile YAML file exists on disk. */
98
98
  export declare function profileExists(name: string): boolean;
99
+ /**
100
+ * True when `name` is a custom harness (a profile that is not shadowing a
101
+ * native agent id) — the predicate routines/monitors use to accept an
102
+ * `agent:` value that `agents run` will resolve as a profile. A native id
103
+ * always reads as native, matching exec's resolution order.
104
+ */
105
+ export declare function isCustomHarnessName(name: string): boolean;
99
106
  /** Read and parse a profile from disk. Throws if not found or malformed. */
100
107
  export declare function readProfile(name: string): Profile;
101
108
  /** Write a profile to disk atomically (write-to-tmp then rename). */
@@ -9,6 +9,7 @@ import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import * as crypto from 'node:crypto';
11
11
  import * as yaml from 'yaml';
12
+ import { ALL_AGENT_IDS } from './agents.js';
12
13
  import { getUserAgentsDir } from './state.js';
13
14
  import { deleteKeychainToken, getKeychainToken, hasKeychainToken, keychainItemName } from './secrets/profiles.js';
14
15
  import { getPreset } from './profiles-presets.js';
@@ -39,6 +40,17 @@ export function validateProfileName(name) {
39
40
  export function profileExists(name) {
40
41
  return fs.existsSync(profilePath(name));
41
42
  }
43
+ /**
44
+ * True when `name` is a custom harness (a profile that is not shadowing a
45
+ * native agent id) — the predicate routines/monitors use to accept an
46
+ * `agent:` value that `agents run` will resolve as a profile. A native id
47
+ * always reads as native, matching exec's resolution order.
48
+ */
49
+ export function isCustomHarnessName(name) {
50
+ if (!PROFILE_NAME_PATTERN.test(name))
51
+ return false;
52
+ return !ALL_AGENT_IDS.includes(name) && profileExists(name);
53
+ }
42
54
  /** Read and parse a profile from disk. Throws if not found or malformed. */
43
55
  export function readProfile(name) {
44
56
  validateProfileName(name);
@@ -77,7 +89,11 @@ function migrateLegacyProfileAuth(profile, file) {
77
89
  }
78
90
  const migratedAccount = findAccount(accountName);
79
91
  const oldItem = profile.auth.keychainItem;
80
- profile.account = migratedAccount.id;
92
+ // Reference by NAME, not id: profiles sync fleet-wide with `agents repo push`
93
+ // while account ids are minted per-device, so an id ref breaks on every other
94
+ // machine ("Unknown account '<uuid>'"). Names are the portable handle — the
95
+ // registry resolves both, and `accounts rename` already rewrites name refs.
96
+ profile.account = migratedAccount.name;
81
97
  profile.provider = provider;
82
98
  delete profile.auth;
83
99
  delete profile.authOptional;
@@ -477,6 +493,15 @@ export function renameProfile(oldName, newName) {
477
493
  export function resolveProfileEnv(profile) {
478
494
  const env = { ...profile.env };
479
495
  if (profile.account) {
496
+ if (!findAccount(profile.account)) {
497
+ // The commonest cause: the profile synced here via `agents repo push/pull`
498
+ // but its account ref was minted on another device (legacy id refs), so
499
+ // the bare "Unknown account" from the registry gives the user nothing to
500
+ // act on. Name the harness and the repair.
501
+ throw new Error(`Harness '${profile.name}' references account '${profile.account}', which does not exist on this device. ` +
502
+ `Accounts are per-machine; pick one from 'agents accounts list' and repoint with ` +
503
+ `'agents harness edit ${profile.name} --account <name>', or add it with 'agents accounts add'.`);
504
+ }
480
505
  const account = resolveCredentialAccount(profile.account, profile.host.agent, profile.provider);
481
506
  Object.assign(env, account.env);
482
507
  }
@@ -12,7 +12,7 @@ import chalk from 'chalk';
12
12
  import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote, REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator, } from './ssh-exec.js';
13
13
  import { deviceIdentityArgs, sshTargetFor } from './devices/connect.js';
14
14
  import { resolveExplicitTargets } from './devices/resolve-target.js';
15
- import { loadDevices, isControlDevice, isDialableDevice } from './devices/registry.js';
15
+ import { loadDevices, isDialableDevice } from './devices/registry.js';
16
16
  import { remoteShellFor, buildWindowsAgentsCommand, stripClixml } from './hosts/remote-cmd.js';
17
17
  import { machineId, normalizeHost } from './machine-id.js';
18
18
  const REMOTE_TIMEOUT_MS = 12_000;
@@ -117,12 +117,6 @@ export async function gatherRemoteAgentsJson(options, deps = {}) {
117
117
  continue;
118
118
  if (normalizeHost(device.name) === self)
119
119
  continue;
120
- // Control-only devices (a phone/tablet cockpit) drive the fleet but never
121
- // run agents — never dial them, whatever their platform reads as. Keyed on
122
- // role, not platform, so this holds even for a control device that carries
123
- // a real OS value (mirrors the skip in session/remote-list.ts).
124
- if (isControlDevice(device))
125
- continue;
126
120
  if (!['windows', 'linux', 'macos'].includes(device.platform))
127
121
  continue;
128
122
  try {
@@ -29,7 +29,7 @@ export type PlacementTarget = {
29
29
  * `config.devices` is the *firing* allowlist only (which daemon may fire the
30
30
  * job). It is intentionally NOT used as an execution pool filter — otherwise
31
31
  * the double-fire pin (`devices: [self]`) would collapse fleet placement to
32
- * always-local. Control / offline / no-address devices are never chosen.
32
+ * always-local. Offline / no-address devices are never chosen.
33
33
  */
34
34
  export declare function pickFleetDevice(_config?: Pick<JobConfig, 'devices'>, platform?: DevicePlatform): string | null;
35
35
  /**
@@ -23,7 +23,7 @@ import { planFleetTargets } from './devices/fleet.js';
23
23
  * `config.devices` is the *firing* allowlist only (which daemon may fire the
24
24
  * job). It is intentionally NOT used as an execution pool filter — otherwise
25
25
  * the double-fire pin (`devices: [self]`) would collapse fleet placement to
26
- * always-local. Control / offline / no-address devices are never chosen.
26
+ * always-local. Offline / no-address devices are never chosen.
27
27
  */
28
28
  export function pickFleetDevice(_config, platform) {
29
29
  let reg;
@@ -7,6 +7,14 @@
7
7
  * access to explicitly allowed paths.
8
8
  */
9
9
  import type { JobConfig } from './scheduling/routines.js';
10
+ /**
11
+ * Absolute path to this host's `gh` config directory (`hosts.yml` + `config.yml`),
12
+ * or null when the host has never run `gh auth login`. Prefer `GH_CONFIG_DIR` when
13
+ * the parent already pinned one; otherwise `$XDG_CONFIG_HOME/gh` or `~/.config/gh`.
14
+ */
15
+ export declare function resolveHostGhConfigDir(): string | null;
16
+ /** True when this host holds usable GitHub CLI auth (hosts.yml or a token env). */
17
+ export declare function hostHasGhAuth(): boolean;
10
18
  /** Build a restricted environment for a sandboxed process, setting HOME to the overlay. */
11
19
  export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<string, string>): Record<string, string>;
12
20
  /**
@@ -21,6 +29,22 @@ export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<str
21
29
  export declare function getJobHomePath(name: string): string;
22
30
  /** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
23
31
  export declare function prepareJobHome(config: JobConfig): string;
32
+ /**
33
+ * Link this host's `gh` config directory into the disposable overlay so
34
+ * `$HOME/.config/gh` resolves even when `GH_CONFIG_DIR` is unset. Mirrors
35
+ * `generateCursorConfig`: same-host credentials, never copied to another box.
36
+ */
37
+ export declare function linkHostGhConfig(overlayHome: string): void;
38
+ /**
39
+ * Refuse to launch a sandboxed child when THIS host holds GitHub auth but the
40
+ * spawn env would hide it. The RUSH-2860 failure mode was silent: the monitor
41
+ * fire recorded `ok` (spawn succeeded) while every `gh` call inside the child
42
+ * failed. Prefer forwarding (buildSpawnEnv / linkHostGhConfig); this assert is
43
+ * the regression tripwire — if forwarding ever regresses, fail loud at spawn
44
+ * instead of recording a hollow success. No-ops when the host has no gh auth
45
+ * (jobs that do not need GitHub still run).
46
+ */
47
+ export declare function assertSandboxForwardsHostGhAuth(spawnEnv: Record<string, string>): void;
24
48
  /** Link this host's Cursor login and CLI config into the disposable overlay. */
25
49
  export declare function generateCursorConfig(overlayHome: string): void;
26
50
  /** Remove a job's overlay HOME directory entirely. */