@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -14,6 +14,7 @@ import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection }
14
14
  import { getUserAgentsDir, readMeta } from '../lib/state.js';
15
15
  import { parseLoopInterval } from '../lib/loop.js';
16
16
  import { AGENTS, resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
17
+ import { parseAgentVersionSpec } from '../lib/agent-spec/agents.js';
17
18
  import { recordDispatchedRun } from '../lib/audit/log.js';
18
19
  import { maybeShowStarNudge } from '../lib/star-nudge.js';
19
20
  import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
@@ -416,7 +417,7 @@ async function handleTerminalHandoff(agentSpec, options, prompt) {
416
417
  // isValidAgent / profileExists / resolveWorkflowRef chain below), so this must
417
418
  // accept all three. Gating on the agent table alone rejected every profile —
418
419
  // the whole Kimi/DeepSeek/Qwen/GLM path — for `--terminal` runs only.
419
- const rawTarget = parseRunAccountPickerRequest(agentSpec).normalizedAgentSpec.split('@')[0];
420
+ const rawTarget = parseRunAccountPickerRequest(agentSpec).normalizedAgentSpec.split('#')[0].split('@')[0];
420
421
  const knownAgent = resolveAgentName(rawTarget);
421
422
  const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
422
423
  import('../lib/profiles.js'),
@@ -490,7 +491,7 @@ export function registerRunCommand(program) {
490
491
  const runCmd = program
491
492
  .command('run [agent] [prompt]')
492
493
  .description('Execute an agent. Pass a prompt for headless runs; omit it to launch the agent interactively. With --broadcast, run the same prompt/task across an agent × model matrix.')
493
- .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (smart classifier auto-approves safe ops, prompts for risky), skip (bypass all permission prompts). Omitted Codex mode defaults to safe writable edit; other harnesses default to plan. \'full\' accepted as alias for skip.', 'plan')
494
+ .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (more autonomous than edit, mechanism per-harness: smart classifier auto-approves safe ops and still prompts for risky ones on Claude/Copilot; approval_policy=never over the edit sandbox on Codex, which never prompts), skip (bypass all permission prompts). Omitted Codex mode defaults to safe writable edit; other harnesses default to plan. \'full\' accepted as alias for skip.', 'plan')
494
495
  .option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto (claude and codex only)', 'auto')
495
496
  .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete model id; tiers resolve per harness+version to a supported model')
496
497
  .option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
@@ -517,14 +518,14 @@ export function registerRunCommand(program) {
517
518
  .option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
518
519
  .option('--terminal [backend]', "Open this run in a real terminal tab instead of here. Without a value the terminal is detected from your live sessions (`agents sessions --active` host), so it lands where you already work — Ghostty for a Ghostty user, iTerm for an iTerm user. Name one to force it: iterm | ghostty | terminal | tmux | vscodium-agent. This is how the menu bar's New Session opens.")
519
520
  .option('--verbose', 'Show detailed execution logs')
520
- .option('--raw', 'Interactive runs on macOS/Linux launch inside a shared tmux session (for %pane addressing + re-attach). Pass --raw to spawn the agent directly instead. Also disabled by AGENTS_NO_TMUX=1.')
521
- .option('--no-tmux', 'Spawn the agent directly instead of wrapping it in the shared tmux session. Same effect as --raw / AGENTS_NO_TMUX=1. Use this to see the agent\'s full startup output when a launch is failing; to turn the wrap off for every run on this machine, set `agents config set devices.<name>.tmux off`.')
522
- .option('--disable-tmux', 'Alias for --no-tmux.')
521
+ .option('--raw', 'Keep this interactive run direct when the device has opted into tmux wrapping. A no-op under the default tmux-off configuration; equivalent to AGENTS_NO_TMUX=1.')
522
+ .option('--no-tmux', 'Keep this run direct when tmux wrapping is enabled for the device. Same effect as --raw / AGENTS_NO_TMUX=1; it is a no-op under the default tmux-off configuration.')
523
+ .option('--disable-tmux', 'Compatibility alias for --no-tmux; a no-op when tmux wrapping is already off.')
523
524
  .option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
524
525
  .option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,antigravity). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
525
526
  .option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
526
527
  .option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then balanced (spreads load across healthy accounts and skips any that are rate-limited). (Legacy `rotate` accepted as alias for `balanced`.)')
527
- .option('--account <name>', 'Use this durable provider credential for the run')
528
+ .option('--account <label>', 'Use this labeled native login or durable provider credential for the run')
528
529
  .option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for claude via @zed-industries/claude-code-acp adapter. Unified event stream; emits ndjson when --json.')
529
530
  .option('-y, --yes', 'Skip the interactive budget-confirm prompt (require_confirm_over). Never skips a hard budget block.', false)
530
531
  .option('--loop', 'Re-inject the prompt/entrypoint each iteration until a stop condition (issue #332). Guards (--max-iterations, --budget, --until) are enforced outside the agent. Writes a checkpoint after every iteration for --resume-checkpoint.')
@@ -534,7 +535,7 @@ export function registerRunCommand(program) {
534
535
  .option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
535
536
  .option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
536
537
  .option('--where <spec>', 'Where this run\'s body executes (one placement door): local | device:<name> | auto | lease[:backend] | cloud[:provider]. Expands to --device/--lease/--cloud. Do not combine with those flags. See docs/00-concepts.md#placement.')
537
- .option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, or user@host. Pass "auto" to pick the least-loaded reachable device where the requested agent is installed and signed in, keeping the run local when no remote is better. Same as --where device:<name>. See `agents devices`.')
538
+ .option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, or user@host. Pass "auto" to pick the least-loaded reachable device where the requested agent is installed and signed in, keeping the run local when no remote is better, or "interactive" for the machine pinned as interactive.host (the box a human is sitting at). Same as --where device:<name>. See `agents devices`.')
538
539
  .option('--remote-cwd <dir>', "Explicit device working directory for --device runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
539
540
  .option('--no-follow', 'With --device, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
540
541
  .option('--any', 'With --device <cap> (a capability tag), pick any matching device instead of erroring when several match.')
@@ -636,7 +637,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
636
637
  Modes (not every agent supports every mode — run \`agents modes <agent>\`):
637
638
  plan read-only investigation; no writes, no shell side-effects
638
639
  edit may edit files; prompts for shell / risky operations
639
- auto smart classifier auto-approves safe ops, prompts for risky (claude, copilot)
640
+ auto more autonomous than edit; the mechanism is per-harness --
641
+ claude, copilot: smart classifier auto-approves safe ops and
642
+ STILL PROMPTS for risky ones
643
+ codex: approval_policy=never over the edit sandbox; never
644
+ prompts at all, and a denied command fails instead
640
645
  skip bypass every permission prompt (dangerously-skip-permissions)
641
646
  Legacy 'full' is silently rewritten to 'skip'.
642
647
  List per-harness support + native flags: agents modes · agents modes claude
@@ -829,7 +834,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
829
834
  process.exit(1);
830
835
  }
831
836
  // Hard-deprecated harnesses cannot be run — point the user at the successor.
832
- const runBaseAgentName = normalizedAgentSpec.split('@')[0];
837
+ const runBaseAgentName = normalizedAgentSpec.split('#')[0].split('@')[0];
833
838
  const runBaseAgentId = resolveAgentName(runBaseAgentName);
834
839
  const { profileExists: runProfileExists } = await import('../lib/profiles.js');
835
840
  if (runBaseAgentId && !runProfileExists(runBaseAgentName) && isAgentHardDeprecated(runBaseAgentId)) {
@@ -849,7 +854,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
849
854
  }
850
855
  // `agents run auto`: the reserved harness keyword — full-auto dispatch
851
856
  // (host affinity → cross-harness balance → account balance, RUSH-2132).
852
- if (normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD && normalizedAgentSpec !== RUN_AUTO_KEYWORD) {
857
+ if (normalizedAgentSpec.split('#')[0].split('@')[0] === RUN_AUTO_KEYWORD && normalizedAgentSpec !== RUN_AUTO_KEYWORD) {
853
858
  console.error(chalk.red(`agents run auto picks the harness itself — a @version pin does not apply. ` +
854
859
  `Pin a concrete harness instead: agents run <harness>@<version>.`));
855
860
  process.exit(1);
@@ -896,7 +901,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
896
901
  process.exit(1);
897
902
  }
898
903
  resolvedResumeSource = outcome.session;
899
- const [requestedAgent, requestedVersion] = normalizedAgentSpec.split('@');
904
+ const [requestedAgent, requestedVersion] = normalizedAgentSpec.split('#')[0].split('@');
900
905
  if (!autoHarnessRequested && requestedAgent !== resolvedResumeSource.agent) {
901
906
  console.error(chalk.red(`Session ${resolvedResumeSource.shortId} belongs to ${resolvedResumeSource.agent}, not ${requestedAgent}. ` +
902
907
  `Use: agents sessions resume ${resolvedResumeSource.id}`));
@@ -982,9 +987,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
982
987
  accountPickerRequested,
983
988
  // `run auto` selects its harness after placement, so do not filter
984
989
  // candidates against an arbitrary proxy harness at this stage.
985
- agent: normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD
990
+ agent: normalizedAgentSpec.split('#')[0].split('@')[0] === RUN_AUTO_KEYWORD
986
991
  ? undefined
987
- : (resolveAgentName(normalizedAgentSpec.split('@')[0]) ?? undefined),
992
+ : (resolveAgentName(normalizedAgentSpec.split('#')[0].split('@')[0]) ?? undefined),
988
993
  });
989
994
  if (!options.quiet && result.deprecationSmart) {
990
995
  process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
@@ -1137,7 +1142,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1137
1142
  const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/accounting/rotate.js');
1138
1143
  const { profileExists, readProfile, resolveProfileEnv } = await import('../lib/profiles.js');
1139
1144
  const detected = await detectSignedInRuntimes();
1140
- const [agentName, rawLeaseVersion] = normalizedAgentSpec.split('@');
1145
+ const [agentName, rawLeaseVersion] = normalizedAgentSpec.split('#')[0].split('@');
1141
1146
  let runtime = null;
1142
1147
  let credentialRuntimes = [];
1143
1148
  let dispatchProfile;
@@ -1398,7 +1403,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1398
1403
  throw e;
1399
1404
  }
1400
1405
  try {
1401
- const [runAgent, rawRunVersion] = normalizedAgentSpec.split('@');
1406
+ const [runAgent, rawRunVersion] = normalizedAgentSpec.split('#')[0].split('@');
1402
1407
  // Forward the explicit @version pin verbatim. Resolving aliases like
1403
1408
  // @latest locally would check local installs, but the remote host may
1404
1409
  // have versions the laptop does not. The remote agents CLI resolves
@@ -1754,12 +1759,33 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1754
1759
  import('../lib/share/config.js'),
1755
1760
  ]);
1756
1761
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
1757
- // Parse agent@version
1758
- const [rawAgent, rawVersion] = normalizedAgentSpec.split('@');
1762
+ // Parse agent@version#label. The label selects native auth without pinning
1763
+ // the binary version; --account remains the equivalent flag form.
1764
+ const labelParts = normalizedAgentSpec.split('#');
1765
+ if (labelParts.length > 2 || labelParts[1] === '') {
1766
+ console.error(chalk.red(`Invalid account label in '${normalizedAgentSpec}'.`));
1767
+ process.exit(1);
1768
+ }
1769
+ const [rawAgent, rawVersion] = labelParts[0].split('@');
1770
+ const specAccountLabel = labelParts[1];
1771
+ if (resolveAgentName(rawAgent)) {
1772
+ const parsed = parseAgentVersionSpec(normalizedAgentSpec);
1773
+ if ('error' in parsed) {
1774
+ console.error(chalk.red(parsed.error));
1775
+ process.exit(1);
1776
+ }
1777
+ }
1778
+ if (specAccountLabel && options.account && specAccountLabel !== options.account) {
1779
+ console.error(chalk.red(`Account '${specAccountLabel}' from the agent spec conflicts with --account '${options.account}'.`));
1780
+ process.exit(1);
1781
+ }
1782
+ if (specAccountLabel)
1783
+ options.account = specAccountLabel;
1759
1784
  let agent;
1760
1785
  let version = rawVersion || undefined;
1761
1786
  let profileEnv;
1762
1787
  let accountEnv;
1788
+ let accountConfigVersion;
1763
1789
  let profileProvider;
1764
1790
  let fromProfile = false;
1765
1791
  let profileFallbackModel;
@@ -2126,29 +2152,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2126
2152
  console.error(chalk.red(`Account '${spawnAccount.name}' is a device-local ${spawnAccount.agent} login and cannot be forwarded to '${remoteTarget}'. Sign in on that device and name the login there.`));
2127
2153
  process.exit(1);
2128
2154
  }
2129
- if (version) {
2130
- // A pinned version must actually be signed in as the named identity.
2131
- const { getAccountInfo } = await import('../lib/agents.js');
2132
- const info = await getAccountInfo(agent, getVersionHomePath(agent, version));
2133
- const liveKey = info.accountKey ?? info.email?.toLowerCase() ?? null;
2134
- if (!info.signedIn || liveKey !== spawnAccount.identityKey) {
2135
- console.error(chalk.red(`Account '${spawnAccount.name}' names a specific ${spawnAccount.agent} identity, but ${agent}@${version} ${info.signedIn ? 'is signed in as a different identity' : 'is not signed in'}. Sign in as that identity, or re-name the account.`));
2136
- process.exit(1);
2137
- }
2155
+ const { CONFIG_ENV_ISOLATED_AGENTS } = await import('../lib/installations/shims.js');
2156
+ if (!CONFIG_ENV_ISOLATED_AGENTS.includes(agent)) {
2157
+ console.error(chalk.red(`${agent} cannot select native accounts independently of its installed version.`));
2158
+ process.exit(1);
2138
2159
  }
2139
- else {
2140
- // No version pinned: locate the installed version currently signed in
2141
- // as this identity and pin the run to it — do NOT assume the global
2142
- // default holds it (the account may live on another installed copy).
2143
- const { resolveAccountVersion } = await import('../lib/accounting/rotate.js');
2144
- const matched = await resolveAccountVersion(agent, spawnAccount.identityKey);
2145
- if (!matched) {
2146
- console.error(chalk.red(`No installed ${spawnAccount.agent} version is signed in as the identity named by account '${spawnAccount.name}'. Sign in as that identity, or attach a different account.`));
2147
- process.exit(1);
2148
- }
2149
- version = matched;
2160
+ const { resolveAccountVersion } = await import('../lib/accounting/rotate.js');
2161
+ accountConfigVersion = await resolveAccountVersion(agent, spawnAccount.identityKey) ?? undefined;
2162
+ if (!accountConfigVersion) {
2163
+ console.error(chalk.red(`No installed ${spawnAccount.agent} version is signed in as the identity labeled '${spawnAccount.name}'. Sign in as that identity, or label a different account.`));
2164
+ process.exit(1);
2150
2165
  }
2151
- // Native identity confirmed live; the harness reads it from its home.
2166
+ if (!options.quiet)
2167
+ process.stderr.write(chalk.gray(`[agents] account '${spawnAccount.name}' · ${agent} auth from ${accountConfigVersion}\n`));
2152
2168
  }
2153
2169
  else {
2154
2170
  accountEnv = spawnAccount.env;
@@ -2670,6 +2686,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2670
2686
  const execOptions = {
2671
2687
  agent,
2672
2688
  version,
2689
+ configVersion: accountConfigVersion,
2673
2690
  prompt,
2674
2691
  interactive: options.interactive || forceInteractive,
2675
2692
  mode: requestedMode,
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerFeedWatchCommand(parent: Command): void;
@@ -0,0 +1,28 @@
1
+ import { machineId } from '../lib/machine-id.js';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { watchFleetFeed, watchLocalFeed } from '../lib/feed/watch.js';
4
+ export function registerFeedWatchCommand(parent) {
5
+ const command = parent.command('watch').description('Stream the canonical agent, attention, and activity projection as NDJSON')
6
+ .option('--json', 'Emit versioned NDJSON envelopes').option('--local', 'Watch only this machine');
7
+ setHelpSections(command, { examples: `agents feed watch --json\nagents feed watch --json --local`, notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect.' });
8
+ command.action(async (_opts, invoked) => {
9
+ const opts = invoked.optsWithGlobals();
10
+ if (!opts.json)
11
+ invoked.error("error: required option '--json' not specified");
12
+ const controller = new AbortController();
13
+ const stop = () => controller.abort();
14
+ const emit = (event) => process.stdout.write(`${JSON.stringify(event)}\n`);
15
+ process.once('SIGINT', stop);
16
+ process.once('SIGTERM', stop);
17
+ try {
18
+ if (opts.local)
19
+ await watchLocalFeed({ scope: machineId(), signal: controller.signal, emit });
20
+ else
21
+ await watchFleetFeed({ signal: controller.signal, emit });
22
+ }
23
+ finally {
24
+ process.off('SIGINT', stop);
25
+ process.off('SIGTERM', stop);
26
+ }
27
+ });
28
+ }
@@ -13,6 +13,8 @@ import { relTime } from '../lib/format.js';
13
13
  import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
14
14
  import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
15
15
  import { notifyUrgentBlock } from '../lib/notify.js';
16
+ import { registerFeedWatchCommand } from './feed-watch.js';
17
+ import { claimAndRouteAttentionAnswer, forwardFeedAnswer } from '../lib/feed/answer.js';
16
18
  import { gcMailbox } from '../lib/mailbox-gc.js';
17
19
  import { isValidMailboxId } from '../lib/mailbox.js';
18
20
  import { getActiveSessions } from '../lib/session/active.js';
@@ -323,6 +325,31 @@ export function registerFeedCommand(program) {
323
325
  .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
324
326
  .option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
325
327
  .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task');
328
+ registerFeedWatchCommand(feed);
329
+ feed.command('answer <attention-key>')
330
+ .description('Atomically claim and deliver one answer to an open attention item')
331
+ .option('--choice <choice-id>', 'Stable choice id from the attention item')
332
+ .option('--text <answer>', 'Free-text answer')
333
+ .option('--as <operator>', 'Verified operator id for high-consequence answers')
334
+ .option('--json', 'Emit the delivery result as JSON')
335
+ .action(async (attentionKey, opts, invoked) => {
336
+ try {
337
+ const ownerHost = attentionKey.slice(0, attentionKey.indexOf('/'));
338
+ const result = ownerHost && ownerHost !== machineId()
339
+ ? await forwardFeedAnswer({ host: ownerHost, attentionKey, choiceId: opts.choice, text: opts.text, operatorId: opts.as })
340
+ : await claimAndRouteAttentionAnswer({
341
+ attentionKey, choiceId: opts.choice, text: opts.text,
342
+ operator: { id: opts.as, verified: Boolean(opts.as), label: opts.as },
343
+ });
344
+ if (opts.json || invoked.parent?.opts()?.json)
345
+ console.log(JSON.stringify(result));
346
+ else
347
+ console.log(result.status === 'delivered' ? `Delivered ${result.receipt.msgId}.` : `Already answered (${result.receipt.at}).`);
348
+ }
349
+ catch (error) {
350
+ invoked.error(`error: ${error instanceof Error ? error.message : String(error)}`);
351
+ }
352
+ });
326
353
  feed
327
354
  .command('post')
328
355
  .description('Post a status update to the fleet activity stream (for agents)')
@@ -3,10 +3,10 @@
3
3
  * pre-filled with version + OS + agent inventory; falls back to printing the
4
4
  * URL when no browser is available.
5
5
  */
6
- import { spawnSync } from 'node:child_process';
7
6
  import { arch, platform, release } from 'node:os';
8
7
  import { createRequire } from 'node:module';
9
8
  import chalk from 'chalk';
9
+ import { showUrl } from '../lib/open-url.js';
10
10
  const REPO = 'phnx-labs/agents-cli';
11
11
  const DISCUSSION_BASE = `https://github.com/${REPO}/discussions/new`;
12
12
  const ISSUE_BASE = `https://github.com/${REPO}/issues/new`;
@@ -20,22 +20,6 @@ function readCliVersion() {
20
20
  return 'unknown';
21
21
  }
22
22
  }
23
- function openInBrowser(url) {
24
- const openers = process.platform === 'darwin'
25
- ? [['open', [url]]]
26
- : process.platform === 'win32'
27
- ? [['cmd', ['/c', 'start', '""', url]]]
28
- : [
29
- ['xdg-open', [url]],
30
- ['gnome-open', [url]]
31
- ];
32
- for (const [cmd, args] of openers) {
33
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
34
- if (r.status === 0)
35
- return true;
36
- }
37
- return false;
38
- }
39
23
  function buildPrefill(kind, summary) {
40
24
  const version = readCliVersion();
41
25
  const os = `${platform()} ${release()} (${arch()})`;
@@ -69,7 +53,7 @@ export function registerFeedbackCommand(program) {
69
53
  .option('-i, --idea', 'File as a feature idea (Discussions → Ideas)')
70
54
  .option('-q, --question', 'Ask a question (Discussions → Q&A)')
71
55
  .option('--print', 'Print the URL instead of opening it')
72
- .action((summary, opts) => {
56
+ .action(async (summary, opts) => {
73
57
  const kind = opts.bug ? 'bug' : opts.idea ? 'idea' : 'question';
74
58
  const summaryText = (summary ?? []).join(' ').trim();
75
59
  const { url } = buildPrefill(kind, summaryText);
@@ -77,7 +61,7 @@ export function registerFeedbackCommand(program) {
77
61
  console.log(url);
78
62
  return;
79
63
  }
80
- const opened = openInBrowser(url);
64
+ const opened = (await showUrl(url)).via !== 'none';
81
65
  if (opened) {
82
66
  console.log(chalk.dim(`Opened ${kind} form in your browser:\n ${url}`));
83
67
  }
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * agents insights cost what you spent ($ and duration)
16
16
  * agents insights output what shipped (burn vs PRs and commits)
17
- * agents usage live quota headroom
17
+ * agents view live quota headroom (per account, with auth state)
18
18
  * agents perf latency (hooks, CLI commands, agent.run) — not popularity
19
19
  * agents sessions stats which skills/slash-commands were explicitly invoked
20
20
  *
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * agents insights cost what you spent ($ and duration)
16
16
  * agents insights output what shipped (burn vs PRs and commits)
17
- * agents usage live quota headroom
17
+ * agents view live quota headroom (per account, with auth state)
18
18
  * agents perf latency (hooks, CLI commands, agent.run) — not popularity
19
19
  * agents sessions stats which skills/slash-commands were explicitly invoked
20
20
  *
@@ -577,7 +577,7 @@ function configureInsightsCommand(cmd) {
577
577
  Two paths under one verb:
578
578
  bare \`agents insights\` — transcript behaviour (tools, friction, rhythm, by account)
579
579
  \`agents insights mix\` — cheap counters from sessions.db + usage.db
580
- Latency is \`agents perf\` (not mix). Quota is \`agents usage\`. Spend is
580
+ Latency is \`agents perf\` (not mix). Quota is \`agents view\`. Spend is
581
581
  \`agents insights cost\`; shipped output is \`agents insights output\`. Skill/slash popularity
582
582
  is \`agents sessions stats\`. Former top-level \`agents trends\` is \`agents insights mix\`
583
583
  (also \`agents insights trends\`).
@@ -8,7 +8,7 @@
8
8
  * so a box a concurrent run just reused is never stopped.
9
9
  */
10
10
  import chalk from 'chalk';
11
- import { openUrl } from '../lib/open-url.js';
11
+ import { showUrl } from '../lib/open-url.js';
12
12
  import { crabboxList, crabboxStop, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
13
13
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
14
14
  import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
@@ -94,7 +94,7 @@ async function captureTailscaleAuthKey() {
94
94
  const { password } = await import('@inquirer/prompts');
95
95
  let key;
96
96
  try {
97
- openUrl(TAILSCALE_KEYS_URL);
97
+ await showUrl(TAILSCALE_KEYS_URL);
98
98
  key = (await password({ message: 'Paste a Tailscale auth key (blank to skip):', mask: true })).trim();
99
99
  }
100
100
  catch (e) {
@@ -155,7 +155,7 @@ export async function runLeaseSetup(opts = {}) {
155
155
  console.error(chalk.bold('\nSet up leasing (Hetzner) — one time (~30s):'));
156
156
  console.error(chalk.dim('Opening the Hetzner console. Create/select a project, then Security → API Tokens →'));
157
157
  console.error(chalk.dim('Generate a token with Read & Write permission, and copy it.\n'));
158
- openUrl(HETZNER_CONSOLE_URL);
158
+ await showUrl(HETZNER_CONSOLE_URL);
159
159
  const { password } = await import('@inquirer/prompts');
160
160
  const ora = (await import('ora')).default;
161
161
  try {
@@ -904,7 +904,7 @@ export function registerRoutinesCommands(program) {
904
904
  .option('--workflow <name>', 'Run an installed workflow (~/.agents/workflows/<name>) via `agents run`. Mutually exclusive with --agent.')
905
905
  .option('--command <sh>', 'Run a plain shell command directly (no agent, no auth, no sandbox) — for deterministic housekeeping routines. Mutually exclusive with --agent and --workflow; --prompt is not used.')
906
906
  .option('-p, --prompt <prompt>', 'Task instruction for the agent')
907
- .option('-m, --mode <mode>', "Execution mode: plan (read-only), edit (can write files), auto (smart classifier, the default), or skip (bypass all permission prompts). 'full' accepted as alias for skip.", 'auto')
907
+ .option('-m, --mode <mode>', "Execution mode: plan (read-only), edit (can write files), auto (the default; more autonomous than edit, mechanism per-harness — see `agents modes <agent>`), or skip (bypass all permission prompts). 'full' accepted as alias for skip.", 'auto')
908
908
  .option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto', 'auto')
909
909
  .option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
910
910
  .option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
@@ -11,7 +11,8 @@ export interface SessionsPickerCommandSpec<TRow, TOpts extends SessionsPickerGat
11
11
  * selector) falls through to the flat printer. Computer has no `--open`.
12
12
  */
13
13
  requireOpenUndefined?: boolean;
14
- runFlat: (opts: TOpts) => void;
14
+ /** May be async — the browser spec's flat path awaits an artifact open. */
15
+ runFlat: (opts: TOpts) => void | Promise<void>;
15
16
  buildRows: (opts: TOpts) => TRow[];
16
17
  emptyMessage: (opts: TOpts) => string;
17
18
  message: string;
@@ -44,7 +44,7 @@ export function createSessionsPickerCommand(spec) {
44
44
  shouldOpen,
45
45
  run: async (opts) => {
46
46
  if (!shouldOpen(opts, isInteractiveTerminal())) {
47
- spec.runFlat(opts);
47
+ await spec.runFlat(opts);
48
48
  return;
49
49
  }
50
50
  const rows = spec.buildRows(opts);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `agents sessions stop <id>` — end a live agent session outright: stop its
3
+ * interactive process and tear down its tmux/mux session, so the session becomes
4
+ * done/closed rather than an orphaned idle background process.
5
+ *
6
+ * The lifecycle sibling of `detach` that does NOT resume headless. `detach`
7
+ * backgrounds a still-wanted agent (stop the terminal, keep it working); `stop`
8
+ * is for when the work is over — the agent's editor tab was closed, or you want
9
+ * it gone. Both reuse the same live-session resolution and the same
10
+ * `stopInteractive` teardown (tmux `kill-session`, else SIGTERM→SIGKILL the pid),
11
+ * so a session killed here reaps its helpers exactly as `detach` does before it
12
+ * resumes.
13
+ *
14
+ * The primary caller is AGI EXT: when a user genuinely closes an agent tab
15
+ * (Cmd+W), the extension runs this so the underlying agent + its mux are shut
16
+ * down instead of being left running detached (the "Cmd+W orphans an idle
17
+ * session" bug). A window RELOAD does NOT call this — the extension distinguishes
18
+ * the two via `terminal.exitStatus.reason` and only a real user close tears down.
19
+ */
20
+ import type { Command } from 'commander';
21
+ export declare function registerSessionsStopCommand(program: Command): void;
22
+ export declare function stopSessionAction(id: string, opts?: {
23
+ local?: boolean;
24
+ }): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import chalk from 'chalk';
2
+ import { gatherLiveTargets } from './go.js';
3
+ import { resolveDetachTarget, resolveOne } from './detach-core.js';
4
+ import { stopInteractive } from './detach.js';
5
+ import { runOnPeer } from '../lib/session/remote-list.js';
6
+ import { setHelpSections } from '../lib/help.js';
7
+ export function registerSessionsStopCommand(program) {
8
+ const cmd = program
9
+ .command('stop')
10
+ .argument('<id>', 'Short or full id of the live session to stop')
11
+ .option('--local', 'Only this machine (skip the cross-host sweep)')
12
+ .description('Stop a live agent outright — end its process and tear down its tmux/mux session')
13
+ .action(async (id, opts) => {
14
+ await stopSessionAction(id, opts);
15
+ });
16
+ setHelpSections(cmd, {
17
+ examples: `
18
+ # Stop a live session by a short id prefix
19
+ agents sessions stop 4b2f1a9c
20
+
21
+ # Only look on this machine (skip the fleet sweep)
22
+ agents sessions stop 4b2f1a9c --local
23
+ `,
24
+ notes: `
25
+ stop ENDS the session; it does not background it. To keep an agent working
26
+ unattended instead, use \`agents sessions detach <id>\`, and bring it back
27
+ with \`agents sessions resume <id>\`.
28
+
29
+ A session that lives on another machine is stopped THERE over SSH — its pid
30
+ and tmux socket only mean something where it actually runs.
31
+ `,
32
+ });
33
+ }
34
+ export async function stopSessionAction(id, opts = {}) {
35
+ const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true });
36
+ const resolved = resolveOne(activeById, id);
37
+ if ('error' in resolved) {
38
+ console.error(chalk.red(resolved.error));
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ const s = resolved;
43
+ const target = resolveDetachTarget(s, self);
44
+ // Cloud/team/id-less sessions have their own lifecycles — refuse rather than
45
+ // half-stopping one from here (same boundary `detach` enforces).
46
+ if (target.kind === 'refuse') {
47
+ console.error(chalk.red(target.reason));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ const short = target.sessionId.slice(0, 8);
52
+ // A session on another host: its pid/tmux socket only mean something where it
53
+ // runs, so stop it THERE over SSH — never kill locally. Mirrors detach/focus.
54
+ if (target.kind === 'remote') {
55
+ console.log(chalk.gray(`${short} lives on ${target.machine} — stopping it there over SSH…`));
56
+ const rc = await runOnPeer(['sessions', 'stop', target.sessionId, '--local'], target.machine);
57
+ if (rc === 'no-target') {
58
+ console.error(chalk.red(`Can't reach ${target.machine} to stop ${short}.`));
59
+ process.exitCode = 1;
60
+ }
61
+ return;
62
+ }
63
+ // Local: end the interactive process and tear down its tmux/mux session.
64
+ await stopInteractive(s);
65
+ console.log(chalk.green(`■ Stopped ${s.kind} ${short}`) + chalk.gray(' — process ended, session closed.'));
66
+ }
@@ -0,0 +1,82 @@
1
+ import type { Command } from 'commander';
2
+ import type { SessionTrajectory } from '../lib/session/trajectory.js';
3
+ import type { TrajectoryComparison, TrajectoryDivergence, TrajectorySummary } from '../lib/session/trajectory-compare.js';
4
+ import type { TrajectoryStep } from '../lib/session/trajectory.js';
5
+ import type { LineageEdge, LineageNode, SessionLineage } from '../lib/session/trajectory-lineage.js';
6
+ /** Versioned envelope emitted by `--json` — the stable contract for consumers. */
7
+ export declare const SESSIONS_TRACE_SCHEMA_VERSION = 1;
8
+ /** The `diff` block of a `layout: 'compare'` envelope — everything but the two full trajectories, which already ride `sessions`. */
9
+ export interface SessionsTraceDiffEnvelope {
10
+ divergence?: TrajectoryDivergence;
11
+ added: TrajectoryStep[];
12
+ removed: TrajectoryStep[];
13
+ summaryA: TrajectorySummary;
14
+ summaryB: TrajectorySummary;
15
+ truncatedA: number;
16
+ truncatedB: number;
17
+ }
18
+ /** The `lineage` block of a `layout: 'lineage'` envelope — the delegation graph. */
19
+ export interface SessionsTraceLineageEnvelope {
20
+ rootId: string;
21
+ nodes: LineageNode[];
22
+ edges: LineageEdge[];
23
+ teams: string[];
24
+ unresolvedParentIds: string[];
25
+ }
26
+ /** The `--json` envelope: the versioned contract for the ext / triaging agents. */
27
+ export interface SessionsTraceEnvelope {
28
+ schemaVersion: typeof SESSIONS_TRACE_SCHEMA_VERSION;
29
+ kind: 'sessions-trace';
30
+ layout: 'single' | 'compare' | 'lineage';
31
+ sessions: SessionTrajectory[];
32
+ diff?: SessionsTraceDiffEnvelope;
33
+ lineage?: SessionsTraceLineageEnvelope;
34
+ }
35
+ /** Build the versioned `--json` envelope for a single-session trajectory. */
36
+ export declare function buildTraceEnvelope(models: SessionTrajectory[]): SessionsTraceEnvelope;
37
+ /** Build the versioned `--json` envelope for a two-session compare. */
38
+ export declare function buildCompareTraceEnvelope(cmp: TrajectoryComparison): SessionsTraceEnvelope;
39
+ /**
40
+ * Build the versioned `--json` envelope for a lineage. `sessions` carries the
41
+ * ROOT's trajectory only: the graph's own numbers come from the indexed session
42
+ * rows, so a consumer pays one transcript parse instead of one per teammate —
43
+ * `agents sessions trace <child>` is how you get a child's full trajectory.
44
+ */
45
+ export declare function buildLineageTraceEnvelope(lineage: SessionLineage, rootTrajectory: SessionTrajectory): SessionsTraceEnvelope;
46
+ interface TraceOptions {
47
+ html?: boolean;
48
+ text?: boolean;
49
+ json?: boolean;
50
+ output?: string;
51
+ open?: boolean;
52
+ errorsOnly?: boolean;
53
+ redact?: boolean;
54
+ all?: boolean;
55
+ since?: string;
56
+ limit?: string;
57
+ agent?: string;
58
+ compare?: boolean;
59
+ tree?: boolean;
60
+ }
61
+ export type RenderFormat = 'html' | 'text' | 'json';
62
+ /**
63
+ * Pick the rendering by audience: explicit `--html/--text/--json` win; otherwise
64
+ * a person at a TTY gets the visual HTML and a piped/headless caller (an agent)
65
+ * gets the compact text trajectory.
66
+ */
67
+ export declare function chooseFormat(options: TraceOptions, isTTY: boolean): RenderFormat;
68
+ /**
69
+ * Decide the trace layout from what the user TYPED (`selectorCount`) vs what
70
+ * those selectors RESOLVED to (`resolvedCount`). Keyed on selector count so a
71
+ * single content-search selector that happens to match two sessions never
72
+ * silently becomes a compare — every unsupported combination fails loud with a
73
+ * clear message. Pure, so the boundaries are unit-tested without the command.
74
+ */
75
+ export declare function decideTraceLayout(options: Pick<TraceOptions, 'tree' | 'compare'>, selectorCount: number, resolvedCount: number): 'single' | 'compare' | 'lineage';
76
+ /** Attach the trace behaviour to a command node (canonical or top-level alias). */
77
+ export declare function configureTraceCommand(cmd: Command): Command;
78
+ /** Canonical `agents sessions trace <selectors...>`. */
79
+ export declare function registerSessionsTraceCommand(sessionsCmd: Command): void;
80
+ /** Top-level alias `agents trace <selectors...>` (mirrors `agents insights`). */
81
+ export declare function registerTraceCommand(program: Command): void;
82
+ export {};