@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents devices lease` — manage the disposable cloud boxes used by `agents run --lease`.
3
3
  *
4
- * Today: `agents devices lease gc`, which stops expired + idle "orphan" boxes that are
4
+ * Today: `agents devices lease prune`, which stops expired + idle "orphan" boxes that are
5
5
  * holding a provider's server quota (the cause of the `server_limit` 403 a new
6
6
  * lease hits). Reaping is conservative: only boxes whose lease has expired AND
7
7
  * that have been untouched for a safety window are eligible (see `isReapSafe`),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents devices lease` — manage the disposable cloud boxes used by `agents run --lease`.
3
3
  *
4
- * Today: `agents devices lease gc`, which stops expired + idle "orphan" boxes that are
4
+ * Today: `agents devices lease prune`, which stops expired + idle "orphan" boxes that are
5
5
  * holding a provider's server quota (the cause of the `server_limit` 403 a new
6
6
  * lease hits). Reaping is conservative: only boxes whose lease has expired AND
7
7
  * that have been untouched for a safety window are eligible (see `isReapSafe`),
@@ -256,7 +256,8 @@ export function registerLeaseCommand(devicesCommand) {
256
256
  process.exit(ok ? 0 : 1);
257
257
  });
258
258
  lease
259
- .command('gc')
259
+ .command('prune')
260
+ .alias('gc')
260
261
  .description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
261
262
  .option('--dry-run', 'List reap-safe orphan boxes without stopping any', false)
262
263
  .option('--yes', 'Stop them without the interactive confirm', false)
@@ -269,7 +270,7 @@ export function registerLeaseCommand(devicesCommand) {
269
270
  candidates = reapSafeOrphans(crabboxList(boxOpts), nowSecs);
270
271
  }
271
272
  catch (e) {
272
- console.error(chalk.red(`lease gc: ${e.message}`));
273
+ console.error(chalk.red(`lease prune: ${e.message}`));
273
274
  process.exit(1);
274
275
  return;
275
276
  }
@@ -405,17 +405,18 @@ export function registerMailboxesCommand(program) {
405
405
  renderOverview(boxes, limit, filters);
406
406
  });
407
407
  const gcCmd = cmd
408
- .command('gc')
408
+ .command('prune')
409
+ .alias('gc')
409
410
  .description('Run a liveness sweep: archive pending messages in dead boxes and prune stale consumed mail.')
410
411
  .option('--json', 'Emit the GC result as JSON')
411
412
  .action(async (_opts, command) => runGcCommand({ json: command.optsWithGlobals().json === true }));
412
413
  setHelpSections(gcCmd, {
413
414
  examples: `
414
415
  # One-shot sweep using the live session set as the liveness source
415
- agents mailboxes gc
416
+ agents mailboxes prune
416
417
 
417
418
  # Machine-readable summary (for scripts / monitors)
418
- agents mailboxes gc --json
419
+ agents mailboxes prune --json
419
420
  `,
420
421
  notes: `
421
422
  A box is considered dead when no live session (the same source
@@ -115,7 +115,7 @@ export function registerMenubarCommands(program) {
115
115
  .description('Configure AGI Menu end-to-end: one instance, started at login')
116
116
  .option('--check', 'Report the current state, change nothing')
117
117
  .option('--json', 'Emit machine-readable JSON')
118
- .action((options) => {
118
+ .action(async (options) => {
119
119
  if (options.check) {
120
120
  const s = getMenubarStatus();
121
121
  if (options.json) {
@@ -129,7 +129,7 @@ export function registerMenubarCommands(program) {
129
129
  }
130
130
  if (!options.json && notMac())
131
131
  return;
132
- const r = runMenubarSetup();
132
+ const r = await runMenubarSetup();
133
133
  if (options.json) {
134
134
  process.stdout.write(JSON.stringify(r) + '\n');
135
135
  }
@@ -174,10 +174,17 @@ export function registerMenubarCommands(program) {
174
174
  menubar
175
175
  .command('enable')
176
176
  .description('Install and start AGI Menu (launches at login)')
177
- .action(() => {
177
+ .action(async () => {
178
178
  if (notMac())
179
179
  return;
180
- const ok = enableMenubarService({ clearOptOut: true });
180
+ let ok = false;
181
+ try {
182
+ ok = await enableMenubarService({ clearOptOut: true });
183
+ }
184
+ catch (e) {
185
+ console.log(chalk.red(`Could not enable AGI Menu: ${e.message}`));
186
+ return;
187
+ }
181
188
  if (!ok) {
182
189
  console.log(chalk.red('Could not enable: no AGI Menu bundle ships with this install.'));
183
190
  console.log(chalk.gray(' This build may predate the helper, or be a non-macOS package.'));
@@ -8,7 +8,7 @@
8
8
  * lifecycle state, plan completion, open PRs, and tickets in flight.
9
9
  */
10
10
  import type { Command } from 'commander';
11
- import { type ProjectRepo } from '../lib/projects.js';
11
+ import { type ProjectDef, type ProjectRepo } from '../lib/projects.js';
12
12
  import { type LinearMilestone } from '../lib/linear-project-counts.js';
13
13
  /** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
14
14
  export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
@@ -101,4 +101,19 @@ export declare function formatMilestoneDue(targetDate: string, nowMs: number): s
101
101
  export declare function formatMilestoneLines(milestones: LinearMilestone[], next: LinearMilestone | undefined, nowMs: number, limit: number): string[];
102
102
  /** The `next` card line: what this project is due to hit, and how far along it is. */
103
103
  export declare function formatNextMilestone(ms: LinearMilestone, nowMs: number): string;
104
+ /** Options shared by `status` and `view`. */
105
+ export type ProjectCardOpts = {
106
+ json?: boolean;
107
+ window?: string;
108
+ remote?: boolean;
109
+ /** Scope the fleet fan-out to specific devices; undefined = the whole fleet. */
110
+ deviceFilter?: string[];
111
+ };
112
+ /**
113
+ * Merge `--device a b` (variadic) and `--devices a,b,c` (comma list) into one
114
+ * deduped device filter; undefined when neither was given (= whole fleet).
115
+ */
116
+ export declare function resolveDeviceFilter(device?: string[], devices?: string): string[] | undefined;
117
+ /** Print the YAML-side fields that sit under the shared card in `view` mode. */
118
+ export declare function printProjectDefinition(def: ProjectDef, name: string): void;
104
119
  export declare function registerProjectsCommands(program: Command): void;
@@ -461,6 +461,44 @@ warnWorkspaces = []) {
461
461
  if (!detail)
462
462
  console.log('');
463
463
  }
464
+ /**
465
+ * Merge `--device a b` (variadic) and `--devices a,b,c` (comma list) into one
466
+ * deduped device filter; undefined when neither was given (= whole fleet).
467
+ */
468
+ export function resolveDeviceFilter(device, devices) {
469
+ const merged = [
470
+ ...(device ?? []),
471
+ ...(devices ? devices.split(',').map((s) => s.trim()).filter(Boolean) : []),
472
+ ];
473
+ return merged.length ? [...new Set(merged)] : undefined;
474
+ }
475
+ /** Print the YAML-side fields that sit under the shared card in `view` mode. */
476
+ export function printProjectDefinition(def, name) {
477
+ console.log();
478
+ if (def.root)
479
+ console.log(` ${chalk.dim('root')} ${def.root}`);
480
+ if (def.defaultPath)
481
+ console.log(` ${chalk.dim('path')} ${def.defaultPath}`);
482
+ for (const rp of def.repos ?? []) {
483
+ const where = [rp.subpath ? `subpath ${rp.subpath}` : undefined, rp.path].filter(Boolean).join(' · ');
484
+ console.log(` ${chalk.dim('repo')} ${rp.slug}${where ? chalk.dim(` (${where})`) : ''}`);
485
+ }
486
+ for (const g of def.goals ?? [])
487
+ console.log(` ${chalk.dim('goal')} ${g.objective}${g.measure ? chalk.dim(` · ${g.measure}`) : ''}`);
488
+ for (const c of def.contexts ?? [])
489
+ console.log(` ${chalk.dim('context')} ${chalk.cyan(c.path)} ${chalk.dim('—')} ${c.purpose}`);
490
+ for (const ig of def.integrations ?? []) {
491
+ console.log(` ${chalk.dim(ig.kind.padEnd(8))} ${ig.url}${ig.label ? chalk.dim(` (${ig.label})`) : ''}`);
492
+ }
493
+ if (def.linear?.url || def.linear?.projectId || def.linear?.name) {
494
+ const ref = def.linear.url ?? def.linear.projectId;
495
+ const label = def.linear.name ? chalk.cyan(def.linear.name) : '';
496
+ console.log(` ${chalk.dim('linear')} ${[label, ref && chalk.dim(ref)].filter(Boolean).join(' ')}`);
497
+ }
498
+ for (const d of def.docs ?? [])
499
+ console.log(` ${chalk.dim('doc')} ${d}`);
500
+ console.log(chalk.gray(` ${projectDefPath(name)}`));
501
+ }
464
502
  export function registerProjectsCommands(program) {
465
503
  const projects = program
466
504
  .command('projects')
@@ -476,9 +514,9 @@ export function registerProjectsCommands(program) {
476
514
  agents projects list --with-agents # opt-in local active counts
477
515
  agents projects list --json # machine-readable defs (AGI EXT uses this)
478
516
  echo '{...}' | agents projects save --json # create/update one def from stdin
479
- agents projects rm rush --json # machine-readable delete
517
+ agents projects remove rush --json # machine-readable removal
480
518
  agents projects status # every project, across the whole fleet
481
- agents projects status rush # one project (same body as view/show)
519
+ agents projects status rush # one project (same body as view)
482
520
  agents projects view rush # alias of status <name>
483
521
  agents projects status --device s0 # scope to one device (or --devices a,b,c)
484
522
  agents projects link rush --linear # bind the Linear project (auto-suggest)
@@ -607,44 +645,6 @@ export function registerProjectsCommands(program) {
607
645
  for (const r of def.repos ?? [])
608
646
  console.log(chalk.gray(` dir ${r.path} · ${r.slug}`));
609
647
  });
610
- /** Merge `--device a b` (variadic) and `--devices a,b,c` (comma list) into one
611
- * deduped device filter; undefined when neither was given (= whole fleet). */
612
- function resolveDeviceFilter(device, devices) {
613
- const merged = [
614
- ...(device ?? []),
615
- ...(devices ? devices.split(',').map((s) => s.trim()).filter(Boolean) : []),
616
- ];
617
- return merged.length ? [...new Set(merged)] : undefined;
618
- }
619
- /** Print the YAML-side fields that sit under the shared card in `view` mode. */
620
- function printProjectDefinition(def, name) {
621
- console.log();
622
- if (def.root)
623
- console.log(` ${chalk.dim('root')} ${def.root}`);
624
- if (def.defaultPath)
625
- console.log(` ${chalk.dim('path')} ${def.defaultPath}`);
626
- for (const rp of def.repos ?? []) {
627
- const where = [rp.subpath ? `subpath ${rp.subpath}` : undefined, rp.path].filter(Boolean).join(' · ');
628
- console.log(` ${chalk.dim('repo')} ${rp.slug}${where ? chalk.dim(` (${where})`) : ''}`);
629
- }
630
- for (const g of def.goals ?? [])
631
- console.log(` ${chalk.dim('goal')} ${g.objective}${g.measure ? chalk.dim(` · ${g.measure}`) : ''}`);
632
- for (const c of def.contexts ?? [])
633
- console.log(` ${chalk.dim('context')} ${chalk.cyan(c.path)} ${chalk.dim('—')} ${c.purpose}`);
634
- for (const ig of def.integrations ?? []) {
635
- console.log(` ${chalk.dim(ig.kind.padEnd(8))} ${ig.url}${ig.label ? chalk.dim(` (${ig.label})`) : ''}`);
636
- }
637
- if (def.linear?.url || def.linear?.projectId || def.linear?.name) {
638
- // Lead with the board's own name — the id/url answers "which project",
639
- // but the name is what a reader (or an agent) calls the work.
640
- const ref = def.linear.url ?? def.linear.projectId;
641
- const label = def.linear.name ? chalk.cyan(def.linear.name) : '';
642
- console.log(` ${chalk.dim('linear')} ${[label, ref && chalk.dim(ref)].filter(Boolean).join(' ')}`);
643
- }
644
- for (const d of def.docs ?? [])
645
- console.log(` ${chalk.dim('doc')} ${d}`);
646
- console.log(chalk.gray(` ${projectDefPath(name)}`));
647
- }
648
648
  async function runProjectCard(name, opts, mode) {
649
649
  const detail = mode === 'view';
650
650
  const all = listProjectDefs();
@@ -681,7 +681,7 @@ export function registerProjectsCommands(program) {
681
681
  const [probeRes, activeRes] = await Promise.all([
682
682
  fleetTargets.length > 0
683
683
  ? gatherRemoteAgentsJson({
684
- args: ['projects', 'probe', '--json', ...fleetTargets],
684
+ args: ['projects', 'probe', ...fleetTargets],
685
685
  noFanoutEnv: PROJECTS_NO_FANOUT_ENV,
686
686
  hosts: opts.deviceFilter,
687
687
  parse: parseRemoteProbe,
@@ -765,8 +765,7 @@ export function registerProjectsCommands(program) {
765
765
  projects
766
766
  .command('status [name]')
767
767
  .alias('view')
768
- .alias('show')
769
- .description('Progress card for every project across the whole fleet, or one named project (aliases: view, show). Named form also prints every milestone and the stored definition.')
768
+ .description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition.')
770
769
  .option('--json', 'Machine-readable output')
771
770
  .option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
772
771
  .option('--no-remote', 'Skip the GitHub and Linear lookups; faster, offline')
@@ -774,8 +773,8 @@ export function registerProjectsCommands(program) {
774
773
  .option('--devices <names>', 'Scope fleet status to a comma-separated list of devices')
775
774
  .action(async (name, rawOpts) => {
776
775
  // Named invocation = `view` depth (all milestones + definition). Unnamed
777
- // stays the scannable multi-project rollup. `view`/`show` are commander
778
- // aliases of this same command, so there is only one implementation.
776
+ // stays the scannable multi-project rollup. `view` is a commander alias
777
+ // of this same command, so there is only one implementation.
779
778
  // Fleet is dialled by default; --device/--devices narrows it to a subset.
780
779
  const opts = {
781
780
  json: rawOpts.json,
@@ -806,7 +805,6 @@ export function registerProjectsCommands(program) {
806
805
  projects
807
806
  .command('probe [paths...]', { hidden: true })
808
807
  .description('Probe workspace repos (presence, branch, drift, dirtiness) and print JSON. Answers for this machine only.')
809
- .option('--json', 'Machine-readable output (the only output format)')
810
808
  .action((paths) => {
811
809
  // Never fans out — the recursion guard env is set by the parent fan-out,
812
810
  // and this command has no remote code path either way.
@@ -817,7 +815,6 @@ export function registerProjectsCommands(program) {
817
815
  .command('pull-local', { hidden: true })
818
816
  .description('Fast-forward workspace repos (default-branch only) and print JSON. Answers for this machine only.')
819
817
  .requiredOption('--targets <json>', 'JSON array of {path, expectedSlug} targets, from the orchestrating `pull`')
820
- .option('--json', 'Machine-readable output (the only output format)')
821
818
  .action(async (opts) => {
822
819
  // Never fans out — this is the peer half of the fleet fan-out.
823
820
  //
@@ -1158,9 +1155,9 @@ export function registerProjectsCommands(program) {
1158
1155
  });
1159
1156
  // ---- rm ----
1160
1157
  projects
1161
- .command('rm <name>')
1162
- .alias('remove')
1163
- .description('Delete a project definition. Never touches the repo.')
1158
+ .command('remove <name>')
1159
+ .alias('rm')
1160
+ .description('Remove a project definition. Never touches the repo.')
1164
1161
  .option('--json', 'Machine-readable success / error')
1165
1162
  .action((name, opts) => {
1166
1163
  if (!isSafeProjectName(name)) {
@@ -1172,22 +1169,19 @@ export function registerProjectsCommands(program) {
1172
1169
  }
1173
1170
  process.exit(1);
1174
1171
  }
1175
- if (removeProjectDef(name)) {
1176
- if (opts.json) {
1177
- console.log(JSON.stringify({ ok: true, name, removed: true }));
1178
- }
1179
- else {
1180
- console.log(chalk.green(`Removed project "${name}"`));
1181
- }
1172
+ const removed = removeProjectDef(name);
1173
+ if (opts.json) {
1174
+ console.log(JSON.stringify(removed
1175
+ ? { ok: true, name, removed: true }
1176
+ : { ok: false, name, error: `No project named "${name}"` }));
1177
+ }
1178
+ else if (removed) {
1179
+ console.log(chalk.green(`Removed project "${name}"`));
1182
1180
  }
1183
1181
  else {
1184
- if (opts.json) {
1185
- console.log(JSON.stringify({ ok: false, name, error: `No project named "${name}"` }));
1186
- }
1187
- else {
1188
- console.error(chalk.red(`No project named "${name}".`));
1189
- }
1190
- process.exit(1);
1182
+ console.error(chalk.red(`No project named "${name}".`));
1191
1183
  }
1184
+ if (!removed)
1185
+ process.exit(1);
1192
1186
  });
1193
1187
  }
@@ -102,7 +102,7 @@ export async function runStrictResume(sessionId, prompt, options) {
102
102
  // one re-discovers locally and marks the run AGENTS_FLEET_REMOTE, which
103
103
  // a long-lived resumed session must not inherit.
104
104
  const { runOnPeer } = await import('../lib/session/remote-list.js');
105
- const rc = await runOnPeer(buildResumeRemoteArgs(outcome.session.id, prompt, options), owner, { tty: !!process.stdout.isTTY, env: { [RESUME_PINNED_ENV]: '1' } });
105
+ const rc = await runOnPeer(buildResumeRemoteArgs(outcome.session.id, prompt, options), owner, { tty: !!process.stdout.isTTY, env: { [RESUME_PINNED_ENV]: '1' }, sessionId: outcome.session.id });
106
106
  if (rc === 'no-target') {
107
107
  console.error(chalk.red(`Session ${outcome.session.shortId} lives on ${owner}, which isn't a reachable device right now.`));
108
108
  console.error(chalk.gray(`Register/wake it (agents devices), or run there: agents ssh ${owner}`));
@@ -243,15 +243,15 @@ export function registerRouteCommands(program) {
243
243
  console.log(chalk.green(`Router '${name}': unlinked ${harness} account '${account}'`));
244
244
  });
245
245
  routeCmd
246
- .command('rm <name>')
247
- .alias('remove')
248
- .description('Delete a router.')
246
+ .command('remove <name>')
247
+ .alias('rm')
248
+ .description('Remove a router.')
249
249
  .action((name) => {
250
250
  const source = routerSource(name);
251
251
  if (source === null)
252
252
  die(`Router '${name}' not found.`);
253
253
  if (source !== 'user') {
254
- die(`Router '${name}' resolves from the '${source}' layer, not 'user' -- agents route rm can only remove a user-layer router.`);
254
+ die(`Router '${name}' resolves from the '${source}' layer, not 'user' -- agents route remove can only remove a user-layer router.`);
255
255
  }
256
256
  const existed = deleteRouter(name);
257
257
  if (!existed)
@@ -1281,7 +1281,8 @@ export function registerRoutinesCommands(program) {
1281
1281
  }
1282
1282
  });
1283
1283
  routinesCmd
1284
- .command('cleanup')
1284
+ .command('prune')
1285
+ .alias('cleanup')
1285
1286
  .description('Remove expired one-shot routines that already fired and still have a user-layer YAML file.')
1286
1287
  .option('--dry-run', 'Show routines that would be removed without deleting files')
1287
1288
  .action((options) => {
@@ -1297,7 +1298,7 @@ export function registerRoutinesCommands(program) {
1297
1298
  for (const job of jobs) {
1298
1299
  console.log(` ${chalk.cyan(job.name)} ${chalk.gray(scheduleLabel(job))}`);
1299
1300
  }
1300
- console.log(chalk.gray(`\nDry run. Remove with: agents routines cleanup`));
1301
+ console.log(chalk.gray(`\nDry run. Remove with: agents routines prune`));
1301
1302
  return;
1302
1303
  }
1303
1304
  let removed = 0;
@@ -2,7 +2,7 @@
2
2
  * `agents run --broadcast` — run the same task/prompt across an agent × model
3
3
  * matrix (formerly `agents bench`).
4
4
  *
5
- * Task definitions live under apps/cli/bench/tasks/<id>/task.json.
5
+ * Task definitions live under cli/bench/tasks/<id>/task.json.
6
6
  */
7
7
  export interface BroadcastOpts {
8
8
  listTasks?: boolean;
@@ -2,7 +2,7 @@
2
2
  * `agents run --broadcast` — run the same task/prompt across an agent × model
3
3
  * matrix (formerly `agents bench`).
4
4
  *
5
- * Task definitions live under apps/cli/bench/tasks/<id>/task.json.
5
+ * Task definitions live under cli/bench/tasks/<id>/task.json.
6
6
  */
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
@@ -69,6 +69,26 @@ export declare function buildPreview(session: SessionMeta): string;
69
69
  * machine — far too much for a pane that repaints as the cursor moves. The line
70
70
  * names the command that does report them instead.
71
71
  */
72
+ /**
73
+ * The "what did this session leave running" segments of the Doing line.
74
+ *
75
+ * Prefers a DERIVED count (fresh, from the transcript the caller just parsed)
76
+ * and falls back to the PERSISTED column, which is what lets a remote/unindexed
77
+ * row — rendered from `SessionMeta` alone, with no events — show fan-out at all.
78
+ * That fallback is the whole reason the counts are columns (RUSH-3091/3095).
79
+ *
80
+ * A count of 0 or undefined renders NOTHING. Both are real states — "scanned,
81
+ * none found" and "this harness cannot report it" — and a literal "0 background
82
+ * shells" would assert "nothing is running" for a harness that simply does not
83
+ * record them.
84
+ *
85
+ * Wording is "left behind", not "running": a transcript records a start and
86
+ * never a death (see extractBackgroundShells).
87
+ */
88
+ export declare function formatFanOut(session: SessionMeta, derived?: {
89
+ subAgentCount?: number;
90
+ backgroundShellCount?: number;
91
+ }): string[];
72
92
  export declare function formatTeamLineage(session: SessionMeta): string;
73
93
  /**
74
94
  * The three timing facts the header reports: when the session was created, when
@@ -94,6 +114,8 @@ export interface SessionPreviewDigest {
94
114
  planFile: string;
95
115
  todos?: TodoProgress;
96
116
  subAgentCount: number;
117
+ /** undefined when the harness records no background-shell concept. */
118
+ backgroundShellCount?: number;
97
119
  toolTags: string[];
98
120
  changes: ReturnType<typeof changeCounts>;
99
121
  dirs: string[];
@@ -22,7 +22,7 @@ import { terminalWidth, stringWidth } from '../lib/session/width.js';
22
22
  import { itemPicker } from '../lib/picker.js';
23
23
  import { createMemoryCache } from '../lib/memory-cache.js';
24
24
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
25
- import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills } from '../lib/session/highlights.js';
25
+ import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills, harnessTracksBackgroundShells, isBackgroundShellStart, isSubAgentTool, } from '../lib/session/highlights.js';
26
26
  import { getSessionPlugins, readSessionPreviewCache, writeSessionPreviewCache, readSessionContent, readArchivedSessionPreview } from '../lib/session/db.js';
27
27
  /** A session whose transcript FILE is on another machine (folded in over the
28
28
  * live cross-machine fan-out): its `filePath` is on that peer's disk, so the
@@ -222,6 +222,10 @@ export function sanitizeRemoteDigest(raw) {
222
222
  planFile: str(d.planFile),
223
223
  todos,
224
224
  subAgentCount: num(d.subAgentCount),
225
+ // optNum, not num: a peer's JSON is untrusted, and the undefined-vs-0
226
+ // distinction is load-bearing here — num() would coerce a malformed value
227
+ // to 0, which reads as "scanned, none running".
228
+ backgroundShellCount: optNum(d.backgroundShellCount),
225
229
  toolTags: strList(d.toolTags),
226
230
  changes,
227
231
  dirs: strList(d.dirs),
@@ -472,6 +476,37 @@ function formatHeader(session, events) {
472
476
  * machine — far too much for a pane that repaints as the cursor moves. The line
473
477
  * names the command that does report them instead.
474
478
  */
479
+ /**
480
+ * The "what did this session leave running" segments of the Doing line.
481
+ *
482
+ * Prefers a DERIVED count (fresh, from the transcript the caller just parsed)
483
+ * and falls back to the PERSISTED column, which is what lets a remote/unindexed
484
+ * row — rendered from `SessionMeta` alone, with no events — show fan-out at all.
485
+ * That fallback is the whole reason the counts are columns (RUSH-3091/3095).
486
+ *
487
+ * A count of 0 or undefined renders NOTHING. Both are real states — "scanned,
488
+ * none found" and "this harness cannot report it" — and a literal "0 background
489
+ * shells" would assert "nothing is running" for a harness that simply does not
490
+ * record them.
491
+ *
492
+ * Wording is "left behind", not "running": a transcript records a start and
493
+ * never a death (see extractBackgroundShells).
494
+ */
495
+ export function formatFanOut(session, derived) {
496
+ // DERIVED wins when the caller has one. It is computed from the transcript as
497
+ // it stands right now, whereas the persisted column is only as fresh as the
498
+ // last scan — for a live session those disagree within seconds. The persisted
499
+ // value is the fallback that makes a remote/unindexed row (no events, so no
500
+ // derived value) render at all, which is the whole reason it is a column.
501
+ const subAgents = derived?.subAgentCount ?? session.subAgentCount ?? 0;
502
+ const shells = derived?.backgroundShellCount ?? session.backgroundShellCount ?? 0;
503
+ const out = [];
504
+ if (subAgents > 0)
505
+ out.push(chalk.gray(`${subAgents} sub-agent${subAgents === 1 ? '' : 's'}`));
506
+ if (shells > 0)
507
+ out.push(chalk.gray(`${shells} background shell${shells === 1 ? '' : 's'}`));
508
+ return out;
509
+ }
475
510
  export function formatTeamLineage(session) {
476
511
  const origin = session.teamOrigin;
477
512
  if (origin) {
@@ -505,7 +540,7 @@ function formatMetaOnlyBody(session) {
505
540
  }
506
541
  const compact = formatTodoCompact(session.todos);
507
542
  const teamLine = formatTeamLineage(session);
508
- const doing = [compact ? chalk.white(compact) : '', teamLine].filter(Boolean);
543
+ const doing = [compact ? chalk.white(compact) : '', teamLine, ...formatFanOut(session)].filter(Boolean);
509
544
  if (doing.length) {
510
545
  lines.push(verbLabel('Doing') + doing.join(DOT));
511
546
  }
@@ -630,6 +665,12 @@ export function buildSessionPreviewDigest(events, session) {
630
665
  /** Latest checklist from the transcript (Claude TodoWrite / Codex update_plan). */
631
666
  let latestTodos;
632
667
  let subAgentCount = 0;
668
+ // undefined, not 0, when the harness records no background-shell concept —
669
+ // formatFanOut renders absence for both, but conflating them here would let a
670
+ // later consumer read "0 shells" as a positive "none running" claim.
671
+ let backgroundShellCount = harnessTracksBackgroundShells(session.agent)
672
+ ? 0
673
+ : undefined;
633
674
  const toolTags = new Set();
634
675
  // usedBrowser/usedComputer are computed at scan time from a sessionId-scoped
635
676
  // events-log read (session/db.ts detectToolUsage), NOT a transcript regex —
@@ -658,6 +699,8 @@ export function buildSessionPreviewDigest(events, session) {
658
699
  }
659
700
  if (isSubAgentTool(tool, command))
660
701
  subAgentCount++;
702
+ if (backgroundShellCount !== undefined && isBackgroundShellStart(event))
703
+ backgroundShellCount++;
661
704
  const p = event.path || event.args?.file_path || event.args?.path || '';
662
705
  if (['Read', 'read_file', 'view_file', 'cat_file', 'get_file'].includes(tool) && p) {
663
706
  filesRead.add(p);
@@ -700,6 +743,7 @@ export function buildSessionPreviewDigest(events, session) {
700
743
  planFile,
701
744
  todos,
702
745
  subAgentCount,
746
+ backgroundShellCount,
703
747
  toolTags: [...toolTags],
704
748
  changes: chg,
705
749
  dirs: directoriesTouched(session, events, changes),
@@ -722,7 +766,7 @@ function verbLabel(v) {
722
766
  return chalk.cyan(v.padEnd(VERB_GUTTER)) + ' ';
723
767
  }
724
768
  function formatCompactPreview(digest, session, events) {
725
- const { firstUser, lastAssistant, filesRead, toolCalls, planFile, todos, subAgentCount, toolTags, changes: chg, dirs, repos, artifacts, skills, plugins, hooks, links, errorCount, firstError, toolHistogram: hist, test, } = digest;
769
+ const { firstUser, lastAssistant, filesRead, toolCalls, planFile, todos, subAgentCount, backgroundShellCount, toolTags, changes: chg, dirs, repos, artifacts, skills, plugins, hooks, links, errorCount, firstError, toolHistogram: hist, test, } = digest;
726
770
  const lines = [];
727
771
  const termWidth = process.stdout.columns || 80;
728
772
  const valueWidth = termWidth - VERB_GUTTER - 5;
@@ -740,7 +784,7 @@ function formatCompactPreview(digest, session, events) {
740
784
  const doing = [
741
785
  compact ? chalk.white(compact) : '',
742
786
  teamLine,
743
- subAgentCount ? chalk.gray(`${subAgentCount} sub-agent${subAgentCount === 1 ? '' : 's'}`) : '',
787
+ ...formatFanOut(session, { subAgentCount, backgroundShellCount }),
744
788
  ].filter(Boolean);
745
789
  if (doing.length) {
746
790
  lines.push(verbLabel('Doing') + doing.join(DOT));
@@ -854,11 +898,6 @@ function classifySessionTool(tool, command) {
854
898
  tags.push('computer');
855
899
  return tags;
856
900
  }
857
- function isSubAgentTool(tool, command) {
858
- if (/^(Agent|Task)$/i.test(tool))
859
- return true;
860
- return /\b(?:agents|ag)\b[^\n;&|]*\b(?:run|cloud\s+run|teams\s+(?:add|start))\b/.test(command);
861
- }
862
901
  /**
863
902
  * Unique directories the session touched, compact and human-readable.
864
903
  * Prefer `session.recentDirectoriesTouched` — the scan records it on the row, so
@@ -267,7 +267,7 @@ Three or more selectors, and --tree with more than one selector, fail loud.`,
267
267
  return;
268
268
  }
269
269
  // HTML.
270
- const html = renderLineageHtml(lineage);
270
+ const html = renderLineageHtml(lineage, redact);
271
271
  if (options.output) {
272
272
  fs.writeFileSync(options.output, html, { mode: 0o600 });
273
273
  process.stdout.write(`${path.resolve(options.output)}\n`);
@@ -3853,6 +3853,7 @@ export async function resumeOnOwnerIfRemote(session) {
3853
3853
  const rc = await runOnPeer(['sessions', 'resume', session.id], owner, {
3854
3854
  tty: true,
3855
3855
  env: { [RESUME_PINNED_ENV]: '1' },
3856
+ sessionId: session.id,
3856
3857
  });
3857
3858
  if (rc === 'no-target')
3858
3859
  warnNoPeerTarget(owner, session);
@@ -61,6 +61,27 @@ function applySecretsDefaults(backend, policy) {
61
61
  function backendArgs(backend) {
62
62
  return backend === 'vault' ? ['--synced'] : ['--backend', backend];
63
63
  }
64
+ /**
65
+ * Provision the signed macOS keychain broker on the explicit onboarding path.
66
+ * This is the async, download-capable trigger the hot sync `getKeychainHelperPath()`
67
+ * fails loud toward: with a bundled `.app` it is a local copy (no network); on a
68
+ * tarball that lacks the bundle it fetches the verified release asset. Best-effort
69
+ * — the wizard's saved prefs must survive an offline provision failure; the next
70
+ * secret op re-points here.
71
+ */
72
+ async function maybeProvisionKeychainHelper(backend) {
73
+ if (backend !== 'keychain' || process.platform !== 'darwin')
74
+ return;
75
+ const { ensureKeychainHelperInstalledAsync } = await import('../lib/secrets/install-helper.js');
76
+ try {
77
+ await ensureKeychainHelperInstalledAsync();
78
+ console.log(chalk.gray('keychain helper: installed the signed broker for this version.'));
79
+ }
80
+ catch (err) {
81
+ console.error(chalk.yellow(`keychain helper not installed: ${err.message}\n` +
82
+ 'Re-run `agents setup secrets` once online, or reinstall agents-cli.'));
83
+ }
84
+ }
64
85
  function printBackendNotes(backend) {
65
86
  if (backend === 'keychain') {
66
87
  console.log(chalk.gray('backend: keychain — macOS reads may ask for Touch ID or the device password.'));
@@ -189,6 +210,8 @@ export async function runSecretsSetupWizard(opts = {}) {
189
210
  defaultPolicy: policy,
190
211
  updatedAt: new Date().toISOString(),
191
212
  });
213
+ // Provision the signed keychain broker before any import subprocess reads it.
214
+ await maybeProvisionKeychainHelper(backend);
192
215
  await maybeImportSecrets(importSource, {
193
216
  bundle: opts.bundle,
194
217
  vault: opts.vault,