@phnx-labs/agents-cli 1.22.52 → 1.22.54

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 (180) hide show
  1. package/CHANGELOG.md +336 -0
  2. package/README.md +42 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +220 -174
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +26 -17
  16. package/dist/commands/fleet-capture.js +7 -0
  17. package/dist/commands/focus.d.ts +1 -0
  18. package/dist/commands/focus.js +4 -2
  19. package/dist/commands/go.d.ts +5 -4
  20. package/dist/commands/go.js +8 -7
  21. package/dist/commands/insights.js +9 -0
  22. package/dist/commands/monitors.js +85 -30
  23. package/dist/commands/output.js +8 -2
  24. package/dist/commands/repo.js +18 -0
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions-inject.js +8 -3
  27. package/dist/commands/sessions-picker.js +2 -1
  28. package/dist/commands/sessions.d.ts +20 -12
  29. package/dist/commands/sessions.js +94 -40
  30. package/dist/commands/setup-accounts.d.ts +8 -0
  31. package/dist/commands/setup-accounts.js +47 -0
  32. package/dist/commands/setup.d.ts +1 -1
  33. package/dist/commands/setup.js +11 -2
  34. package/dist/commands/share.d.ts +52 -3
  35. package/dist/commands/share.js +262 -18
  36. package/dist/commands/ssh.d.ts +7 -0
  37. package/dist/commands/ssh.js +53 -14
  38. package/dist/commands/status.js +14 -0
  39. package/dist/commands/sync.js +44 -0
  40. package/dist/commands/view.d.ts +3 -1
  41. package/dist/commands/view.js +5 -4
  42. package/dist/lib/account-registry.d.ts +15 -5
  43. package/dist/lib/account-registry.js +165 -53
  44. package/dist/lib/accounting/rotate.d.ts +20 -6
  45. package/dist/lib/accounting/rotate.js +38 -7
  46. package/dist/lib/accounting/usage.d.ts +37 -1
  47. package/dist/lib/accounting/usage.js +71 -6
  48. package/dist/lib/agent-spec/agents.d.ts +5 -2
  49. package/dist/lib/agent-spec/agents.js +25 -7
  50. package/dist/lib/analytics/mix-commands.js +12 -6
  51. package/dist/lib/answer-router.js +2 -1
  52. package/dist/lib/auth-mint.d.ts +150 -0
  53. package/dist/lib/auth-mint.js +434 -0
  54. package/dist/lib/browser/profiles.d.ts +18 -0
  55. package/dist/lib/browser/profiles.js +26 -1
  56. package/dist/lib/browser/registry.d.ts +44 -14
  57. package/dist/lib/browser/registry.js +141 -45
  58. package/dist/lib/browser/remote-control.d.ts +9 -7
  59. package/dist/lib/browser/remote-control.js +9 -7
  60. package/dist/lib/claude-account-token.d.ts +10 -0
  61. package/dist/lib/claude-account-token.js +14 -4
  62. package/dist/lib/config-drift.d.ts +37 -0
  63. package/dist/lib/config-drift.js +72 -0
  64. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  65. package/dist/lib/daemon/auth-sync-service.js +34 -0
  66. package/dist/lib/daemon/daemon.js +30 -4
  67. package/dist/lib/daemon/runner.js +10 -2
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/device-config.d.ts +3 -3
  71. package/dist/lib/device-config.js +8 -7
  72. package/dist/lib/devices/config-migration.js +147 -1
  73. package/dist/lib/devices/connect.d.ts +26 -0
  74. package/dist/lib/devices/connect.js +48 -1
  75. package/dist/lib/devices/device-docs.d.ts +35 -0
  76. package/dist/lib/devices/device-docs.js +163 -0
  77. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  78. package/dist/lib/devices/discovery-policy.js +31 -21
  79. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  80. package/dist/lib/devices/doctor-findings.js +19 -1
  81. package/dist/lib/devices/registry.d.ts +11 -5
  82. package/dist/lib/devices/registry.js +46 -18
  83. package/dist/lib/exec.d.ts +88 -30
  84. package/dist/lib/exec.js +138 -34
  85. package/dist/lib/feed/feed.d.ts +10 -2
  86. package/dist/lib/feed/feed.js +35 -2
  87. package/dist/lib/feed-broadcast.js +1 -1
  88. package/dist/lib/fleet/apply.d.ts +11 -0
  89. package/dist/lib/fleet/apply.js +23 -3
  90. package/dist/lib/fleet/auth-sync.js +5 -3
  91. package/dist/lib/help.d.ts +9 -0
  92. package/dist/lib/help.js +29 -1
  93. package/dist/lib/hosts/dispatch.d.ts +4 -3
  94. package/dist/lib/hosts/dispatch.js +12 -8
  95. package/dist/lib/hosts/passthrough.d.ts +1 -10
  96. package/dist/lib/hosts/passthrough.js +1 -13
  97. package/dist/lib/hosts/providers/local.d.ts +9 -3
  98. package/dist/lib/hosts/providers/local.js +23 -12
  99. package/dist/lib/hosts/reconnect.d.ts +7 -4
  100. package/dist/lib/hosts/reconnect.js +29 -25
  101. package/dist/lib/hosts/registry.js +4 -1
  102. package/dist/lib/hosts/remote-os.js +3 -1
  103. package/dist/lib/installations/versions.js +9 -1
  104. package/dist/lib/linux-userns.d.ts +58 -0
  105. package/dist/lib/linux-userns.js +116 -0
  106. package/dist/lib/memory.d.ts +26 -0
  107. package/dist/lib/memory.js +80 -1
  108. package/dist/lib/monitors/config.d.ts +11 -0
  109. package/dist/lib/monitors/config.js +8 -0
  110. package/dist/lib/monitors/engine.js +8 -1
  111. package/dist/lib/monitors/state.d.ts +37 -1
  112. package/dist/lib/monitors/state.js +79 -4
  113. package/dist/lib/permissions-registry.d.ts +2 -0
  114. package/dist/lib/permissions-registry.js +116 -14
  115. package/dist/lib/permissions.d.ts +5 -3
  116. package/dist/lib/permissions.js +25 -27
  117. package/dist/lib/profiles.d.ts +8 -7
  118. package/dist/lib/profiles.js +12 -0
  119. package/dist/lib/project-key.d.ts +9 -0
  120. package/dist/lib/project-key.js +11 -0
  121. package/dist/lib/secrets/bundles.d.ts +35 -0
  122. package/dist/lib/secrets/bundles.js +78 -1
  123. package/dist/lib/secrets/push.d.ts +3 -8
  124. package/dist/lib/secrets/push.js +18 -14
  125. package/dist/lib/secrets/remote.d.ts +9 -18
  126. package/dist/lib/secrets/remote.js +11 -26
  127. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  128. package/dist/lib/secrets/reserved-sync.js +129 -0
  129. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  130. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  131. package/dist/lib/self-heal/registry.js +4 -0
  132. package/dist/lib/self-heal/types.d.ts +1 -1
  133. package/dist/lib/session/active.d.ts +10 -1
  134. package/dist/lib/session/active.js +8 -5
  135. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  136. package/dist/lib/session/actor-sidecar.js +2 -0
  137. package/dist/lib/session/db.d.ts +39 -4
  138. package/dist/lib/session/db.js +168 -31
  139. package/dist/lib/session/discover.d.ts +32 -4
  140. package/dist/lib/session/discover.js +126 -37
  141. package/dist/lib/session/insights.d.ts +14 -0
  142. package/dist/lib/session/insights.js +25 -2
  143. package/dist/lib/session/linear.js +1 -1
  144. package/dist/lib/session/live-metadata.js +1 -0
  145. package/dist/lib/session/pid-registry.d.ts +7 -0
  146. package/dist/lib/session/prompt.d.ts +15 -0
  147. package/dist/lib/session/prompt.js +21 -0
  148. package/dist/lib/session/shell-programs.d.ts +17 -0
  149. package/dist/lib/session/shell-programs.js +21 -0
  150. package/dist/lib/session/state.js +2 -1
  151. package/dist/lib/session/stream-render.js +2 -1
  152. package/dist/lib/session/tool-calls.js +2 -5
  153. package/dist/lib/session/trajectory-html.js +2 -1
  154. package/dist/lib/session/trajectory.js +3 -12
  155. package/dist/lib/session/types.d.ts +25 -0
  156. package/dist/lib/session/types.js +10 -0
  157. package/dist/lib/share/publish.d.ts +53 -5
  158. package/dist/lib/share/publish.js +99 -17
  159. package/dist/lib/share/worker-template.js +594 -64
  160. package/dist/lib/startup/root-command.js +2 -1
  161. package/dist/lib/state.d.ts +24 -0
  162. package/dist/lib/state.js +318 -54
  163. package/dist/lib/sync-status.d.ts +4 -0
  164. package/dist/lib/sync-status.js +3 -0
  165. package/dist/lib/terminal/resolve.d.ts +7 -0
  166. package/dist/lib/terminal/resolve.js +41 -2
  167. package/dist/lib/traces/classify.js +24 -19
  168. package/dist/lib/traces/insights.d.ts +67 -0
  169. package/dist/lib/traces/insights.js +178 -0
  170. package/dist/lib/traces/phenotype.d.ts +67 -0
  171. package/dist/lib/traces/phenotype.js +437 -0
  172. package/dist/lib/traces/segments.d.ts +133 -0
  173. package/dist/lib/traces/segments.js +301 -0
  174. package/dist/lib/traces/sync.d.ts +33 -0
  175. package/dist/lib/traces/sync.js +11 -2
  176. package/dist/lib/types.d.ts +47 -1
  177. package/dist/lib/usage-refresh.js +2 -1
  178. package/dist/lib/view-types.d.ts +2 -0
  179. package/dist/lib/watchdog/runner.js +18 -4
  180. package/package.json +2 -1
@@ -24,6 +24,7 @@ import { IS_WINDOWS } from '../lib/platform/index.js';
24
24
  import { safeJoin } from '../lib/paths.js';
25
25
  import { machineId, normalizeHost } from '../lib/machine-id.js';
26
26
  import { loadDevices } from '../lib/devices/registry.js';
27
+ import { assertDaemonEnabled } from '../lib/device-config.js';
27
28
  import { setHelpSections } from '../lib/help.js';
28
29
  import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
29
30
  function stdoutJson(payload) {
@@ -52,6 +53,26 @@ function sourceLabel(source) {
52
53
  return source.type;
53
54
  }
54
55
  }
56
+ /** Warn when a run/routine action has no postcondition — completed-exit-0 still records as ok (PHNX-2842). */
57
+ function warnMissingPostcondition(action) {
58
+ if ((action.type === 'run' || action.type === 'routine') && !action.postcondition) {
59
+ stderrLine(chalk.yellow(` Note: ${action.type} has no --postcondition — a completed agent that did nothing will still record as ok. Add --postcondition '<cmd>' that exits 0 when the intended effect happened.`));
60
+ }
61
+ }
62
+ /** Reconciled one-line outcome for a fire (ok / no effect / failed). */
63
+ function fireOutcomeDisplay(name, f) {
64
+ const rec = resolveFireOutcome(name, f);
65
+ if (rec.ok)
66
+ return { label: chalk.green('ok'), note: '' };
67
+ if (rec.effect === 'none') {
68
+ const note = rec.error ? chalk.gray(` ${rec.error}`) : '';
69
+ return { label: chalk.yellow('no effect'), note };
70
+ }
71
+ const corrected = f.ok !== false && rec.runStatus
72
+ ? chalk.yellow(` (run ${rec.runStatus})`)
73
+ : '';
74
+ return { label: chalk.red('failed') + corrected, note: '' };
75
+ }
55
76
  /** A one-line human label for a monitor's action. */
56
77
  function actionLabel(action) {
57
78
  switch (action.type) {
@@ -123,12 +144,26 @@ function livenessLabel(monitor, state, liveness) {
123
144
  }
124
145
  return chalk.gray(`checked ${liveness.checkCount}x · last ${checkedAgo} · no match yet`);
125
146
  }
126
- /** Start or reload the background daemon so a newly-added monitor is watched. */
147
+ /**
148
+ * Start or reload the background daemon so a newly-added monitor is watched.
149
+ * Returns false when auto-start was refused (`daemon.enabled=false`); the add
150
+ * itself already succeeded — the monitor is config and stays valid fleet-wide.
151
+ * The refusal names the setting and the fix. Mirrors `ensureSchedulerRunning`
152
+ * in commands/routines.ts (PHNX-2637).
153
+ */
127
154
  function ensureDaemonRunning() {
155
+ try {
156
+ assertDaemonEnabled();
157
+ }
158
+ catch (err) {
159
+ // Loud stated skip, on stderr so --json stdout stays clean.
160
+ stderrLine(chalk.yellow(err.message));
161
+ return false;
162
+ }
128
163
  if (isDaemonRunning()) {
129
164
  signalDaemonReload();
130
165
  stderrLine(chalk.gray('Daemon reloaded'));
131
- return;
166
+ return true;
132
167
  }
133
168
  const result = startDaemon();
134
169
  if (result.pid) {
@@ -138,6 +173,7 @@ function ensureDaemonRunning() {
138
173
  else {
139
174
  stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents routines start'));
140
175
  }
176
+ return true;
141
177
  }
142
178
  /**
143
179
  * Assert the postcondition of `add`: the engine actually picked the monitor up.
@@ -272,10 +308,16 @@ function buildAction(options) {
272
308
  action.effort = options.effort;
273
309
  if (options.actionTimeout)
274
310
  action.timeout = options.actionTimeout;
311
+ if (options.postcondition)
312
+ action.postcondition = options.postcondition;
313
+ chosen.push(action);
314
+ }
315
+ if (options.routine) {
316
+ const action = { type: 'routine', routine: options.routine };
317
+ if (options.postcondition)
318
+ action.postcondition = options.postcondition;
275
319
  chosen.push(action);
276
320
  }
277
- if (options.routine)
278
- chosen.push({ type: 'routine', routine: options.routine });
279
321
  if (options.notify !== undefined) {
280
322
  // --notify may be a bare flag (notify the owner) or carry a channel that
281
323
  // overrides notify.owner.channel. Left unset, the send resolves the owner
@@ -293,6 +335,10 @@ function buildAction(options) {
293
335
  stderrLine(chalk.red(`Exactly one action is allowed; got ${chosen.map((c) => c.type).join(', ')}`));
294
336
  process.exit(1);
295
337
  }
338
+ if (options.postcondition && chosen[0].type !== 'run' && chosen[0].type !== 'routine') {
339
+ stderrLine(chalk.red('--postcondition only applies to --run or --routine'));
340
+ process.exit(1);
341
+ }
296
342
  return chosen[0];
297
343
  }
298
344
  /** Interactive monitor picker. Returns the selected name or null on cancel/empty. */
@@ -379,7 +425,7 @@ async function guardAgainstDuplicateMonitor(config, force) {
379
425
  export function registerMonitorsCommands(program) {
380
426
  const monitorsCmd = program
381
427
  .command('monitors')
382
- .description('Durable event-triggered watchers: watch a source, detect a change, fire an action. The daemon auto-starts on first add.');
428
+ .description('Durable event-triggered watchers: watch a source, detect a change, fire an action. The daemon auto-starts on first add unless daemon.enabled is false.');
383
429
  setHelpSections(monitorsCmd, {
384
430
  examples: `
385
431
  # CI went red → triage it (poll a command, diff, fire an agent)
@@ -388,6 +434,12 @@ export function registerMonitorsCommands(program) {
388
434
  --run claude --prompt 'CI failed on #1119: {event}. Diagnose and fix.' \\
389
435
  --device yosemite-s0
390
436
 
437
+ # Merge-on-green: the fire is ok only if the PR actually merged
438
+ agents monitors add merge-1682 \\
439
+ --poll 'gh pr view 1682 --json state --jq .state' 2m --match OPEN \\
440
+ --run claude --prompt 'Rebase-merge #1682: {event}' \\
441
+ --postcondition 'gh pr view 1682 --json state --jq .state | grep -qx MERGED'
442
+
391
443
  # SSL cert issued → notify (poll an HTTPS endpoint every 8h)
392
444
  agents monitors add cert-issued \\
393
445
  --poll-http 'https://secure.ssl.com/team/.../co-ec1l5dgjofa' 8h \\
@@ -406,10 +458,14 @@ export function registerMonitorsCommands(program) {
406
458
  - SOURCE (--watch, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
407
459
  - CONDITION (--on-change [default], --match <re>, --every; --dedupe-key)
408
460
  - ACTION (--run <agent> --prompt, --routine, --notify, --webhook-out)
461
+ --postcondition <cmd> on --run/--routine asserts the effect
462
+ happened (exit 0) after the agent settles; otherwise the
463
+ fire records as "no effect", not ok.
409
464
 
410
465
  The fired event is injected into a run/routine prompt as {event}.
411
466
  Pin the single OWNER device with --device (exactly-once). The daemon (shared
412
- with routines) auto-starts on first add; manage it with 'agents routines start|stop'.
467
+ with routines) auto-starts on first add unless daemon.enabled is false on this
468
+ device; manage it with 'agents daemon start|stop' (or 'agents routines start|stop').
413
469
 
414
470
  v1 evaluates poll sources (command, poll, poll-http, file, device). Push
415
471
  sources (ws, webhook) are accepted but delivered through a receiver wired in
@@ -419,7 +475,7 @@ export function registerMonitorsCommands(program) {
419
475
  // ─── add ────────────────────────────────────────────────────────────────────
420
476
  monitorsCmd
421
477
  .command('add [nameOrPath]')
422
- .description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon.')
478
+ .description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon unless daemon.enabled is false.')
423
479
  // SOURCE
424
480
  .option('--watch <cmd>', 'Run a shell command; its stdout is the observation')
425
481
  .option('--poll <cmd...>', 'Re-run a command every interval: --poll "<cmd>" <interval> (e.g. 30s)')
@@ -445,6 +501,7 @@ export function registerMonitorsCommands(program) {
445
501
  .option('--mode <mode>', 'Execution mode for --run: plan, edit, auto, or skip')
446
502
  .option('--effort <effort>', 'Reasoning effort for --run: low | medium | high | xhigh | max | auto')
447
503
  .option('--action-timeout <t>', 'Kill the --run action if it runs longer than this (e.g. 10m)')
504
+ .option('--postcondition <cmd>', 'Shell command that must exit 0 after a --run/--routine action settles; otherwise the fire records as no-effect, not ok. {event} is replaced with the fired event summary')
448
505
  .option('--routine <name>', 'Fire an existing routine on change')
449
506
  .option('--notify [channel]', 'Notify the owner (notify.owner); [channel] overrides the owner channel')
450
507
  .option('--webhook-out <url>', 'POST the event to this URL')
@@ -481,8 +538,9 @@ export function registerMonitorsCommands(program) {
481
538
  await guardAgainstDuplicateMonitor(config, options.force === true);
482
539
  writeMonitor(config);
483
540
  console.log(chalk.green(`Monitor '${name}' added`));
484
- ensureDaemonRunning();
485
- await assertEnginePickup(config);
541
+ warnMissingPostcondition(config.action);
542
+ if (ensureDaemonRunning())
543
+ await assertEnginePickup(config);
486
544
  return;
487
545
  }
488
546
  if (!nameOrPath) {
@@ -556,8 +614,9 @@ export function registerMonitorsCommands(program) {
556
614
  writeMonitor(config);
557
615
  console.log(chalk.green(`Monitor '${nameOrPath}' added`));
558
616
  console.log(chalk.gray(` ${sourceLabel(source)} → [${condition.mode}] → ${actionLabel(action)} · owner: ${ownerLabel(config)}`));
559
- ensureDaemonRunning();
560
- await assertEnginePickup(config);
617
+ warnMissingPostcondition(action);
618
+ if (ensureDaemonRunning())
619
+ await assertEnginePickup(config);
561
620
  });
562
621
  // ─── list ────────────────────────────────────────────────────────────────────
563
622
  monitorsCmd
@@ -641,7 +700,7 @@ export function registerMonitorsCommands(program) {
641
700
  state,
642
701
  liveness,
643
702
  stalled: isStalled(monitor, liveness),
644
- recentFires,
703
+ recentFires: recentFires.map((f) => ({ ...f, reconciled: resolveFireOutcome(name, f) })),
645
704
  });
646
705
  return;
647
706
  }
@@ -671,9 +730,11 @@ export function registerMonitorsCommands(program) {
671
730
  if (recentFires.length > 0) {
672
731
  console.log(chalk.bold('\nRecent fires'));
673
732
  for (const f of recentFires) {
674
- // Reconciled against the run's real status — see `runs`, above (RUSH-2690).
675
- const { ok } = resolveFireOutcome(name, f);
676
- console.log(` ${chalk.gray(f.firedAt)} ${f.action ?? '?'} ${ok ? chalk.green('ok') : chalk.red('failed')}`);
733
+ // Reconciled against the run's real status + postcondition (RUSH-2690, PHNX-2842).
734
+ const { label, note } = fireOutcomeDisplay(name, f);
735
+ console.log(` ${chalk.gray(f.firedAt)} ${f.action ?? '?'} ${label}`);
736
+ if (note)
737
+ console.log(note);
677
738
  }
678
739
  }
679
740
  });
@@ -847,23 +908,17 @@ export function registerMonitorsCommands(program) {
847
908
  }
848
909
  console.log(chalk.bold(`Fire history: ${name}\n`));
849
910
  for (const f of fires.slice(-20)) {
850
- // Reconcile against the run's REAL, current status (RUSH-2690) rather
851
- // than trusting the frozen `ok` written at fire time — `dispatchAction`
852
- // only sees a synchronous 'running' snapshot before the run has
853
- // actually settled, so a run that later fails/times out/never produces
854
- // output would otherwise read `ok` here forever while `agents monitors
855
- // logs` (which reads the run record fresh) already shows the truth.
856
- const { ok, runStatus } = resolveFireOutcome(name, f);
857
- const outcome = ok ? chalk.green('ok') : chalk.red('failed');
858
- // Surface the divergence explicitly when the frozen write disagreed
859
- // with the reconciled read — the exact "fire says ok, run says
860
- // otherwise" symptom RUSH-2690 reported.
861
- const corrected = f.ok !== false && !ok && runStatus
862
- ? chalk.yellow(` (run ${runStatus})`)
863
- : '';
911
+ // Reconcile against the run's REAL, current status (RUSH-2690) and a
912
+ // declared postcondition (PHNX-2842) rather than trusting the frozen
913
+ // `ok` written at fire time — `dispatchAction` only sees a synchronous
914
+ // 'running' snapshot before the run has actually settled, and a
915
+ // `completed` agent that did nothing used to read `ok` here forever.
916
+ const { label, note } = fireOutcomeDisplay(name, f);
864
917
  const runRef = f.runId ? chalk.gray(` run ${f.runId}`) : '';
865
- console.log(` ${f.firedAt} ${(f.action ?? '?').padEnd(12)} ${outcome}${corrected}${runRef}`);
918
+ console.log(` ${f.firedAt} ${(f.action ?? '?').padEnd(12)} ${label}${runRef}`);
866
919
  console.log(chalk.gray(` ${f.summary.slice(0, 100)}`));
920
+ if (note)
921
+ console.log(note);
867
922
  }
868
923
  });
869
924
  // ─── pause / resume ───────────────────────────────────────────────────────────
@@ -42,8 +42,14 @@ The burn is split into input / cache-read / cache-write where the harness record
42
42
  (Claude/Codex/Gemini/Droid). --pricing no-cache reprices cached tokens at the input rate,
43
43
  so you can see what caching is saving. --json always carries both actual and no-cache costs.
44
44
  `)
45
- .action(async (options) => {
46
- await outputAction(options);
45
+ // Read opts via optsWithGlobals(): `--json`/`--since`/`--by` collide by name
46
+ // with the `insights` parent's own options, so commander binds them to the
47
+ // parent at parse time and the leaf's plain opts() never sees them. Merging
48
+ // ancestor opts is what the sibling `insights mix` recipes already do
49
+ // (mix-commands.ts) — without it every flag on this command is silently
50
+ // dropped (e.g. `agents insights output --json` printed the human table).
51
+ .action(async (_options, command) => {
52
+ await outputAction(command.optsWithGlobals());
47
53
  });
48
54
  }
49
55
  function resolveGroup(by) {
@@ -1052,6 +1052,14 @@ export function registerRepoCommands(program) {
1052
1052
  ].filter(Boolean);
1053
1053
  if (parts.length > 0)
1054
1054
  console.log(chalk.gray(`Device policy: ${parts.join(' · ')}`));
1055
+ const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
1056
+ const auth = syncReservedAuthBundle();
1057
+ if (auth.pushed.length > 0) {
1058
+ console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
1059
+ }
1060
+ for (const err of auth.errors) {
1061
+ console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
1062
+ }
1055
1063
  }
1056
1064
  });
1057
1065
  repoCmd
@@ -1091,6 +1099,16 @@ export function registerRepoCommands(program) {
1091
1099
  const result = await commitAndPush(t.dir, options.message);
1092
1100
  if (result.success) {
1093
1101
  spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
1102
+ if (t.alias === 'user') {
1103
+ const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
1104
+ const auth = syncReservedAuthBundle();
1105
+ if (auth.pushed.length > 0) {
1106
+ console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
1107
+ }
1108
+ for (const err of auth.errors) {
1109
+ console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
1110
+ }
1111
+ }
1094
1112
  }
1095
1113
  else {
1096
1114
  spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon/daemon.js';
18
18
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } from '../lib/secrets/remote.js';
19
19
  import { resolveBundleForPush, pushResolvedBundleToHost, bundleEnvToDotenv } from '../lib/secrets/push.js';
20
20
  export { bundleEnvToDotenv };
21
- import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, isAgentInvocationContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
21
+ import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, isAgentInvocationContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, assertFileBundleDecryptable, isReservedBundleName, ReservedBundleWrongBackendError, SECRET_TYPES, } from '../lib/secrets/bundles.js';
22
22
  import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
23
23
  import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
24
24
  import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
@@ -846,7 +846,12 @@ export function renderExpiringCol(b, now = Date.now()) {
846
846
  * exists to describe. `--force` already means "overwrite what is there".
847
847
  */
848
848
  export function resolveImportBundle(name, backendOpt, synced = false, force = false) {
849
- const requestedBackend = synced ? 'vault' : resolveBackendOpt(backendOpt);
849
+ if (isReservedBundleName(name) && (synced || (backendOpt && backendOpt !== 'file'))) {
850
+ throw new ReservedBundleWrongBackendError(name, synced ? 'vault' : resolveBackendOpt(backendOpt));
851
+ }
852
+ const requestedBackend = isReservedBundleName(name)
853
+ ? 'file'
854
+ : (synced ? 'vault' : resolveBackendOpt(backendOpt));
850
855
  if (bundleExists(name)) {
851
856
  // readBundleIfDecryptable nulls ONLY on BundleUndecryptableError; a locked
852
857
  // keychain or logged-out vault still throws, so a recoverable state can
@@ -892,6 +897,9 @@ function applyEnvToBundle(bundle, env, opts) {
892
897
  else {
893
898
  writeBundle(bundle);
894
899
  }
900
+ if (added > 0) {
901
+ assertFileBundleDecryptable(bundle.name, Object.keys(env).filter((k) => k in bundle.vars));
902
+ }
895
903
  return { added, skipped };
896
904
  }
897
905
  /** Register the `agents secrets` command tree. */
@@ -1581,7 +1589,16 @@ export function registerSecretsCommands(program) {
1581
1589
  // inherits the configured default (`hold`) instead of being pinned.
1582
1590
  const policyOpt = opts.policy ?? opts.tier;
1583
1591
  const policy = policyOpt ? parsePolicyOpt(policyOpt) : undefined;
1584
- const backend = opts.synced ? 'vault' : resolveBackendOpt(opts.backend);
1592
+ let backend = opts.synced ? 'vault' : resolveBackendOpt(opts.backend);
1593
+ if (isReservedBundleName(resolvedName)) {
1594
+ // Reserved `auth` is file-backed by construction. Omitting --backend
1595
+ // still forces file even when agents.yaml defaults to keychain; an
1596
+ // explicit non-file backend fails loud (SEC-GAP-3).
1597
+ if (opts.synced || (opts.backend && opts.backend !== 'file')) {
1598
+ throw new ReservedBundleWrongBackendError(resolvedName, backend);
1599
+ }
1600
+ backend = 'file';
1601
+ }
1585
1602
  if (bundleExists(resolvedName) && !opts.force) {
1586
1603
  console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
1587
1604
  process.exit(1);
@@ -2134,7 +2151,7 @@ Examples:
2134
2151
  .option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
2135
2152
  .option('--vault <name>', '1Password vault name (used with --to-1password)')
2136
2153
  .option('--device <target...>', 'Push the bundle over SSH to this device (host alias or user@host); repeatable for multiple machines')
2137
- .option('--remote-backend <backend>', 'Backend for the bundle on the remote device (with --device): keychain (default) or file. file is headless-readable via the remote\'s machine-local key; it forwards AGENTS_SECRETS_PASSPHRASE over stdin only if set (opt-in).', 'keychain')
2154
+ .option('--remote-backend <backend>', 'Backend for the bundle on the remote device (with --device): keychain (default) or file. file is headless-readable via the remote\'s machine-local key and never forwards AGENTS_SECRETS_PASSPHRASE.', 'keychain')
2138
2155
  .option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --device)')
2139
2156
  .option('--to-file <path>', `Write the bundle as an AES-256-GCM encrypted offline file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of import --from-file)`)
2140
2157
  .action(async (bundleName, opts) => {
@@ -2170,15 +2187,18 @@ Examples:
2170
2187
  console.error(chalk.red("--remote-backend vault is not supported; use 'keychain' or 'file'."));
2171
2188
  process.exit(1);
2172
2189
  }
2173
- const remoteBackend = parsedBackend;
2174
- // For a file-backed remote bundle a passphrase is OPTIONAL. The file
2175
- // store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
2176
- // unset the remote `import --backend file` auto-provisions the remote's
2177
- // own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
2178
- // are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
2179
- // the operator opts in by setting it. Forcing a shared passphrase would
2180
- // defeat headless reads, so we no longer require one.
2181
- const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
2190
+ let remoteBackend = parsedBackend;
2191
+ if (isReservedBundleName(resolvedBundleName)) {
2192
+ if (opts.remoteBackend && opts.remoteBackend !== 'file') {
2193
+ throw new ReservedBundleWrongBackendError(resolvedBundleName, parsedBackend);
2194
+ }
2195
+ remoteBackend = 'file';
2196
+ }
2197
+ // File-backend export NEVER forwards AGENTS_SECRETS_PASSPHRASE
2198
+ // (PHNX-2371). Forwarding it keys the remote ciphertext to a secret
2199
+ // the destination daemon does not hold, so later headless reads fail
2200
+ // while import still prints "Imported N key(s)". The remote
2201
+ // auto-provisions its own machine-local key instead.
2182
2202
  // Resolve ONCE for N hosts — reading a bundle can prompt, and doing it
2183
2203
  // per host would prompt per host.
2184
2204
  //
@@ -2197,7 +2217,6 @@ Examples:
2197
2217
  const out = pushResolvedBundleToHost(resolvedForPush, resolvedBundleName, host, {
2198
2218
  remoteBackend,
2199
2219
  force: opts.force,
2200
- passphrase: remotePassphrase,
2201
2220
  operation: 'ssh export',
2202
2221
  });
2203
2222
  if (!out.ok) {
@@ -26,7 +26,11 @@ async function resolveTarget(sessionId) {
26
26
  const resolution = resolveInjectTargetForSession(match);
27
27
  if (!resolution.addressable) {
28
28
  // Surface the resolver's precise reason (host/rail), not a generic "not tmux".
29
- return { target: null, reason: `Session "${sessionId}": ${resolution.reason}` };
29
+ return {
30
+ target: null,
31
+ reason: `Session "${sessionId}": ${resolution.reason}`,
32
+ hint: resolution.hint,
33
+ };
30
34
  }
31
35
  return { target: resolution.target };
32
36
  }
@@ -43,10 +47,11 @@ async function runInject(sessionId, text, options) {
43
47
  else {
44
48
  const resolved = await resolveTarget(sessionId);
45
49
  if (!resolved.target) {
50
+ const message = resolved.hint ? `${resolved.reason}\n${resolved.hint}` : resolved.reason;
46
51
  if (options.json)
47
- console.log(JSON.stringify({ ok: false, error: resolved.reason }));
52
+ console.log(JSON.stringify({ ok: false, error: message }));
48
53
  else
49
- console.error(chalk.red(resolved.reason));
54
+ console.error(chalk.red(message));
50
55
  process.exit(1);
51
56
  }
52
57
  target = resolved.target;
@@ -9,6 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import chalk from 'chalk';
11
11
  import { truncate, humanDuration } from '../lib/format.js';
12
+ import { sessionDisplayAgent } from '../lib/session/types.js';
12
13
  import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
13
14
  import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
14
15
  import { safeTeamText } from '../lib/session/team-filter.js';
@@ -404,7 +405,7 @@ function formatHeader(session, events) {
404
405
  const { createdAgo, lastActiveAgo, duration } = extractTiming(session, events);
405
406
  // Line 1: Agent v version · shortId · model · account
406
407
  const line1 = [];
407
- line1.push(chalk.gray(`${displayAgent(session.agent)}${session.version ? ` v${session.version}` : ''}`));
408
+ line1.push(chalk.gray(`${displayAgent(sessionDisplayAgent(session))}${session.version ? ` v${session.version}` : ''}`));
408
409
  if (session.shortId)
409
410
  line1.push(chalk.dim(session.shortId));
410
411
  if (model)
@@ -676,7 +676,7 @@ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?:
676
676
  * so it stays fixed across the picker's re-renders within a single run.
677
677
  */
678
678
  export declare function formatPickerTip(sessions: SessionMeta[]): string;
679
- export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string): Promise<PickedSession | null>;
679
+ export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string, scope?: SessionSearchScope): Promise<PickedSession | null>;
680
680
  /**
681
681
  * Row-id prefix for a live session whose agent has not reported a session id yet
682
682
  * (a booting harness, a queued teammate). The browser lists these so nothing live
@@ -736,6 +736,16 @@ interface AgentFilter {
736
736
  * alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
737
737
  export declare function resolveSessionAgentName(name: string): SessionAgentId | null;
738
738
  export declare function parseAgentFilter(agentName?: string): AgentFilter;
739
+ /**
740
+ * Explicit `--agent` / `--project` / `--routine` flags. Distinct from the
741
+ * listing pool (cwd-scoped, default-capped) that PHNX-2767 hydrates past:
742
+ * these flags MUST still exclude FTS hits after that union.
743
+ */
744
+ export type SessionSearchScope = {
745
+ agent?: string;
746
+ project?: string;
747
+ routine?: boolean | string;
748
+ };
739
749
  /**
740
750
  * How a `sessions <query>` argument was resolved against the pool.
741
751
  *
@@ -759,29 +769,27 @@ export interface SessionQueryResolution {
759
769
  */
760
770
  export declare function resolveSessionQuery(pool: SessionMeta[], query: string, options?: {
761
771
  indexFallback?: boolean;
772
+ scope?: SessionSearchScope;
762
773
  }): SessionQueryResolution;
763
774
  /** Honest result of a fleet sweep that found nothing: it says what the sweep
764
775
  * actually did — how many peers answered, which were unreachable — instead of
765
776
  * dead-ending on "search with --device <host>" after the search already ran. */
766
777
  export declare function fleetNotFoundMessage(query: string, deviceCount: number, unreachable: string[]): string[];
767
778
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
768
- export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
779
+ export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined, scope?: SessionSearchScope): SessionMeta[];
769
780
  /**
770
781
  * Narrow a session list by --project and --agent before search resolution.
771
782
  * Without this, a query like "scoped search" could match sessions in BOTH
772
783
  * the project you specified AND elsewhere, producing an ambiguity error
773
784
  * even though the user already pointed at the correct scope.
785
+ *
786
+ * PHNX-2767 hydrates FTS hits missing from the listing pool *after* this
787
+ * runs, so callers that search must also pass the same scope into
788
+ * `filterSessionsByQuery` / `scopedContentIndex` or the union
789
+ * reintroduces the rows this function just dropped.
774
790
  */
775
- export declare function applyScopeFilters(sessions: SessionMeta[], scope: {
776
- agent?: string;
777
- project?: string;
778
- routine?: boolean | string;
779
- }): SessionMeta[];
780
- export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): {
781
- agent?: string;
782
- project?: string;
783
- routine?: boolean | string;
784
- };
791
+ export declare function applyScopeFilters(sessions: SessionMeta[], scope: SessionSearchScope): SessionMeta[];
792
+ export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): SessionSearchScope;
785
793
  /**
786
794
  * Whether a missed local id lookup should widen to a cross-machine sweep.
787
795
  *