@phnx-labs/agents-cli 1.22.56 → 1.22.58

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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +4 -4
  3. package/dist/bootstrap.js +11 -2
  4. package/dist/cli/command-registry.d.ts +0 -1
  5. package/dist/cli/command-registry.js +0 -3
  6. package/dist/commands/accounts.js +7 -3
  7. package/dist/commands/apply.js +10 -2
  8. package/dist/commands/exec.js +1 -1
  9. package/dist/commands/fork.d.ts +23 -10
  10. package/dist/commands/fork.js +115 -58
  11. package/dist/commands/hooks.js +4 -4
  12. package/dist/commands/insights.d.ts +7 -5
  13. package/dist/commands/insights.js +16 -9
  14. package/dist/commands/monitors.js +11 -0
  15. package/dist/commands/perf.d.ts +16 -7
  16. package/dist/commands/perf.js +29 -20
  17. package/dist/commands/prune.js +5 -3
  18. package/dist/commands/routines.d.ts +8 -0
  19. package/dist/commands/routines.js +57 -3
  20. package/dist/commands/rules.js +1 -1
  21. package/dist/commands/sessions-picker.d.ts +11 -0
  22. package/dist/commands/sessions-picker.js +16 -0
  23. package/dist/commands/sessions.js +1 -0
  24. package/dist/commands/share.d.ts +14 -0
  25. package/dist/commands/share.js +43 -2
  26. package/dist/commands/ssh.js +24 -14
  27. package/dist/commands/status.js +1 -1
  28. package/dist/commands/sync.js +83 -7
  29. package/dist/commands/traces.js +7 -0
  30. package/dist/commands/trash.d.ts +2 -2
  31. package/dist/commands/trash.js +2 -6
  32. package/dist/commands/versions.d.ts +2 -2
  33. package/dist/commands/versions.js +1 -10
  34. package/dist/commands/view.d.ts +2 -2
  35. package/dist/commands/view.js +7 -6
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +14 -0
  38. package/dist/lib/account-registry.d.ts +5 -1
  39. package/dist/lib/account-registry.js +47 -14
  40. package/dist/lib/accounting/capacity.d.ts +18 -7
  41. package/dist/lib/accounting/capacity.js +19 -8
  42. package/dist/lib/accounting/usage-ingest.d.ts +1 -0
  43. package/dist/lib/accounting/usage-ingest.js +75 -0
  44. package/dist/lib/accounting/usage-sync.d.ts +97 -0
  45. package/dist/lib/accounting/usage-sync.js +203 -0
  46. package/dist/lib/accounting/usage.d.ts +48 -2
  47. package/dist/lib/accounting/usage.js +79 -2
  48. package/dist/lib/agent-spec/agents.js +1 -1
  49. package/dist/lib/analytics/mix-commands.d.ts +8 -7
  50. package/dist/lib/analytics/mix-commands.js +50 -73
  51. package/dist/lib/auth-mint.d.ts +11 -1
  52. package/dist/lib/auth-mint.js +21 -6
  53. package/dist/lib/browser/ipc.d.ts +8 -0
  54. package/dist/lib/browser/ipc.js +87 -0
  55. package/dist/lib/browser/service.d.ts +19 -0
  56. package/dist/lib/browser/service.js +96 -11
  57. package/dist/lib/browser/sessions-list.js +10 -1
  58. package/dist/lib/daemon/daemon.js +5 -0
  59. package/dist/lib/daemon/runner.d.ts +3 -0
  60. package/dist/lib/daemon/runner.js +95 -53
  61. package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
  62. package/dist/lib/daemon/usage-sync-service.js +42 -0
  63. package/dist/lib/daemon-services.d.ts +1 -1
  64. package/dist/lib/daemon-services.js +5 -0
  65. package/dist/lib/device-config.d.ts +17 -6
  66. package/dist/lib/device-config.js +25 -11
  67. package/dist/lib/devices/connect.d.ts +17 -8
  68. package/dist/lib/devices/connect.js +31 -14
  69. package/dist/lib/devices/pool.d.ts +4 -3
  70. package/dist/lib/devices/pool.js +13 -5
  71. package/dist/lib/doctor-diff.js +77 -7
  72. package/dist/lib/exec.d.ts +6 -41
  73. package/dist/lib/exec.js +6 -41
  74. package/dist/lib/fleet/manifest.d.ts +17 -0
  75. package/dist/lib/fleet/manifest.js +26 -0
  76. package/dist/lib/git.d.ts +13 -1
  77. package/dist/lib/git.js +36 -7
  78. package/dist/lib/harness/adapter.d.ts +7 -7
  79. package/dist/lib/harness/adapters/claude.js +3 -2
  80. package/dist/lib/hooks/install.d.ts +27 -11
  81. package/dist/lib/hooks/install.js +42 -17
  82. package/dist/lib/hosts/reconnect.d.ts +52 -203
  83. package/dist/lib/hosts/reconnect.js +64 -284
  84. package/dist/lib/hosts/remote-cmd.d.ts +9 -0
  85. package/dist/lib/hosts/remote-cmd.js +22 -0
  86. package/dist/lib/installations/migrate.d.ts +6 -120
  87. package/dist/lib/installations/migrate.js +27 -259
  88. package/dist/lib/installations/shims.d.ts +13 -95
  89. package/dist/lib/installations/shims.js +22 -139
  90. package/dist/lib/installations/store.js +1 -1
  91. package/dist/lib/installations/versions.d.ts +26 -133
  92. package/dist/lib/installations/versions.js +41 -204
  93. package/dist/lib/perf/db.d.ts +1 -1
  94. package/dist/lib/perf/db.js +1 -1
  95. package/dist/lib/plugins/skills.d.ts +8 -1
  96. package/dist/lib/plugins/skills.js +18 -2
  97. package/dist/lib/refresh.d.ts +9 -0
  98. package/dist/lib/refresh.js +3 -1
  99. package/dist/lib/routine-readiness.d.ts +15 -1
  100. package/dist/lib/routine-readiness.js +41 -0
  101. package/dist/lib/sandbox.d.ts +4 -1
  102. package/dist/lib/sandbox.js +30 -1
  103. package/dist/lib/secrets/agent.d.ts +80 -225
  104. package/dist/lib/secrets/agent.js +139 -401
  105. package/dist/lib/secrets/bundles.d.ts +73 -222
  106. package/dist/lib/secrets/bundles.js +168 -467
  107. package/dist/lib/secrets/reaper.d.ts +28 -70
  108. package/dist/lib/secrets/reaper.js +30 -85
  109. package/dist/lib/secrets/remote.d.ts +42 -129
  110. package/dist/lib/secrets/remote.js +55 -173
  111. package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
  112. package/dist/lib/self-heal/checks/install-staging.js +96 -0
  113. package/dist/lib/self-heal/registry.js +2 -0
  114. package/dist/lib/self-heal/types.d.ts +1 -1
  115. package/dist/lib/self-update.d.ts +23 -0
  116. package/dist/lib/self-update.js +50 -0
  117. package/dist/lib/session/active.d.ts +16 -32
  118. package/dist/lib/session/active.js +10 -68
  119. package/dist/lib/session/db.d.ts +24 -36
  120. package/dist/lib/session/db.js +143 -44
  121. package/dist/lib/session/discover.d.ts +6 -58
  122. package/dist/lib/session/discover.js +5 -43
  123. package/dist/lib/session/fork.d.ts +45 -26
  124. package/dist/lib/session/fork.js +32 -95
  125. package/dist/lib/session/parse.d.ts +1 -19
  126. package/dist/lib/session/parse.js +2 -15
  127. package/dist/lib/session/tool-calls.d.ts +43 -1
  128. package/dist/lib/session/tool-calls.js +74 -44
  129. package/dist/lib/session/tool-store.d.ts +33 -2
  130. package/dist/lib/session/tool-store.js +56 -3
  131. package/dist/lib/staleness/writers/sources.d.ts +5 -0
  132. package/dist/lib/staleness/writers/sources.js +2 -1
  133. package/dist/lib/startup/command-registry.d.ts +8 -2
  134. package/dist/lib/startup/command-registry.js +12 -4
  135. package/dist/lib/sync-status.d.ts +22 -0
  136. package/dist/lib/sync-status.js +27 -0
  137. package/dist/lib/sync-umbrella.d.ts +9 -0
  138. package/dist/lib/sync-umbrella.js +21 -2
  139. package/dist/lib/traces/insights.d.ts +47 -14
  140. package/dist/lib/traces/insights.js +92 -21
  141. package/dist/lib/traces/phenotype.d.ts +23 -3
  142. package/dist/lib/traces/phenotype.js +72 -24
  143. package/dist/lib/traces/sync.d.ts +15 -0
  144. package/dist/lib/traces/sync.js +104 -19
  145. package/dist/lib/traces/worker-template.js +154 -1
  146. package/package.json +1 -1
@@ -7,20 +7,21 @@
7
7
  * rhythm, edits) — split by Claude account by default
8
8
  * agents insights mix COUNTERS (sessions index + usage.db recipes:
9
9
  * harness/model mix, token ratios, secrets, browser)
10
- * agents insights <recipe> One baked mix recipe (harness-mix, tools-per-session, …)
10
+ * agents insights mix <recipe> One baked mix recipe (harness-mix, tools-per-session, …)
11
11
  * agents insights query Raw usage.db rows
12
12
  *
13
- * Sibling observe verbs (stay separate — different questions):
13
+ * Sibling observe verbs under this same group (different questions):
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 insights perf latency (hooks, CLI commands, agent.run) — not popularity
17
18
  * agents view live quota headroom (per account, with auth state)
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
  *
21
21
  * Why mix lives here (not a second top-level `trends`): two abstract "analytics"
22
22
  * nouns taught agents and humans to guess. One verb, two engines — cheap SQL mix
23
- * vs transcript facets. Latency stays on `perf` so it is never confused with mix.
23
+ * vs transcript facets. `perf` (latency) is also nested here now (PHNX-3391) —
24
+ * performance is an insight, kept a distinct sub-verb so it is never confused with mix.
24
25
  *
25
26
  * Modelled on Claude Code's `/insights`, with the difference that motivated it: that
26
27
  * command reads one account's directory, while `balanced` rotation sprays sessions
@@ -31,7 +32,8 @@
31
32
  * the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
32
33
  * `claude -p`.
33
34
  *
34
- * Former top-level `agents trends` is `agents insights mix` / `agents insights trends`.
35
+ * Former top-level `agents trends` is `agents insights mix` (one section:
36
+ * `agents insights mix <recipe>`; `--list` names them).
35
37
  */
36
38
  import type { Command } from 'commander';
37
39
  export declare function registerInsightsCommand(program: Command): void;
@@ -7,20 +7,21 @@
7
7
  * rhythm, edits) — split by Claude account by default
8
8
  * agents insights mix COUNTERS (sessions index + usage.db recipes:
9
9
  * harness/model mix, token ratios, secrets, browser)
10
- * agents insights <recipe> One baked mix recipe (harness-mix, tools-per-session, …)
10
+ * agents insights mix <recipe> One baked mix recipe (harness-mix, tools-per-session, …)
11
11
  * agents insights query Raw usage.db rows
12
12
  *
13
- * Sibling observe verbs (stay separate — different questions):
13
+ * Sibling observe verbs under this same group (different questions):
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 insights perf latency (hooks, CLI commands, agent.run) — not popularity
17
18
  * agents view live quota headroom (per account, with auth state)
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
  *
21
21
  * Why mix lives here (not a second top-level `trends`): two abstract "analytics"
22
22
  * nouns taught agents and humans to guess. One verb, two engines — cheap SQL mix
23
- * vs transcript facets. Latency stays on `perf` so it is never confused with mix.
23
+ * vs transcript facets. `perf` (latency) is also nested here now (PHNX-3391) —
24
+ * performance is an insight, kept a distinct sub-verb so it is never confused with mix.
24
25
  *
25
26
  * Modelled on Claude Code's `/insights`, with the difference that motivated it: that
26
27
  * command reads one account's directory, while `balanced` rotation sprays sessions
@@ -31,7 +32,8 @@
31
32
  * the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
32
33
  * `claude -p`.
33
34
  *
34
- * Former top-level `agents trends` is `agents insights mix` / `agents insights trends`.
35
+ * Former top-level `agents trends` is `agents insights mix` (one section:
36
+ * `agents insights mix <recipe>`; `--list` names them).
35
37
  */
36
38
  import * as fs from 'fs';
37
39
  import chalk from 'chalk';
@@ -47,6 +49,7 @@ import { formatUsd } from '../lib/pricing/index.js';
47
49
  import { formatDuration } from '../lib/session/render.js';
48
50
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
49
51
  import { registerMixCommands } from '../lib/analytics/mix-commands.js';
52
+ import { registerPerfSubcommand } from './perf.js';
50
53
  import { registerCostCommand } from './cost.js';
51
54
  import { registerOutputCommand } from './output.js';
52
55
  const execFileAsync = promisify(execFile);
@@ -570,7 +573,7 @@ function configureInsightsCommand(cmd) {
570
573
  # Counter mix board (harness/model/token/secrets recipes) — former agents trends
571
574
  agents insights mix
572
575
  agents insights mix --days 30
573
- agents insights harness-mix --json
576
+ agents insights mix harness-mix --json
574
577
  agents insights query --kind secret --days 7
575
578
 
576
579
  # One account only, all of its history
@@ -586,10 +589,10 @@ function configureInsightsCommand(cmd) {
586
589
  Two paths under one verb:
587
590
  bare \`agents insights\` — transcript behaviour (tools, friction, rhythm, by account)
588
591
  \`agents insights mix\` — cheap counters from sessions.db + usage.db
589
- Latency is \`agents perf\` (not mix). Quota is \`agents view\`. Spend is
592
+ Latency is \`agents insights perf\` (not mix). Quota is \`agents view\`. Spend is
590
593
  \`agents insights cost\`; shipped output is \`agents insights output\`. Skill/slash popularity
591
- is \`agents sessions stats\`. Former top-level \`agents trends\` is \`agents insights mix\`
592
- (also \`agents insights trends\`).
594
+ is \`agents sessions stats\`. Former top-level \`agents trends\` is \`agents insights mix\`.
595
+ One recipe is \`agents insights mix <recipe>\` (e.g. \`harness-mix\`); \`--list\` names them.
593
596
 
594
597
  The behavioural report parses in-scope transcripts once and caches facets; later runs
595
598
  re-read only files that changed. \`--refresh\` forces a full re-read.
@@ -606,6 +609,10 @@ export function registerInsightsCommand(program) {
606
609
  configureInsightsCommand(cmd);
607
610
  registerCostCommand(cmd);
608
611
  registerOutputCommand(cmd);
612
+ // Latency rollups (former top-level `agents perf` → `agents insights perf`,
613
+ // PHNX-3391). A sibling observe verb of cost/output, so it lives on the
614
+ // top-level insights group beside them, not under `sessions insights`.
615
+ registerPerfSubcommand(cmd);
609
616
  }
610
617
  export function registerSessionsInsightsCommand(sessions) {
611
618
  configureInsightsCommand(sessions.command('insights'));
@@ -139,6 +139,13 @@ function livenessLabel(monitor, state, liveness) {
139
139
  if (liveness.lastError) {
140
140
  return chalk.red(`checked ${liveness.checkCount}x · error: ${liveness.lastError.replace(/\s+/g, ' ').slice(0, 60)}`);
141
141
  }
142
+ const latestFire = listFires(monitor.name).at(-1);
143
+ if (latestFire) {
144
+ const outcome = resolveFireOutcome(monitor.name, latestFire);
145
+ if (!outcome.ok) {
146
+ return chalk.red(`ACTION FAILED ${formatRelativeTime(latestFire.firedAt)}`) + chalk.gray(` · checked ${liveness.checkCount}x`);
147
+ }
148
+ }
142
149
  if (state?.lastFiredAt) {
143
150
  return chalk.green(`fired ${formatRelativeTime(state.lastFiredAt)}`) + chalk.gray(` · checked ${liveness.checkCount}x`);
144
151
  }
@@ -629,6 +636,8 @@ export function registerMonitorsCommands(program) {
629
636
  const payload = monitors.map((m) => {
630
637
  const state = readState(m.name);
631
638
  const liveness = readLiveness(m.name);
639
+ const latestFire = listFires(m.name).at(-1);
640
+ const latestOutcome = latestFire ? resolveFireOutcome(m.name, latestFire) : null;
632
641
  return {
633
642
  name: m.name,
634
643
  enabled: m.enabled,
@@ -650,6 +659,8 @@ export function registerMonitorsCommands(program) {
650
659
  lastError: liveness?.lastError ?? null,
651
660
  consecutiveErrors: liveness?.consecutiveErrors ?? 0,
652
661
  stalled: isStalled(m, liveness),
662
+ lastActionStatus: latestOutcome?.runStatus ?? null,
663
+ lastActionFailed: latestOutcome ? !latestOutcome.ok : false,
653
664
  };
654
665
  });
655
666
  stdoutJson(payload);
@@ -1,12 +1,13 @@
1
1
  /**
2
- * `agents perf` — latency rollups over the disposable perf SQLite warehouse.
2
+ * `agents insights perf` — latency rollups over the disposable perf SQLite
3
+ * warehouse (former top-level `agents perf`, moved under insights in PHNX-3391).
3
4
  *
4
5
  * Subcommands:
5
- * agents perf multi-section summary (commands + hooks + runs)
6
- * agents perf hooks per-hook p50/p99 + cache hit rates
7
- * agents perf commands slowest CLI command paths (from command.end)
8
- * agents perf run agent.run / perf.timing labels
9
- * agents perf friction sessions stuck repeatedly hitting the same guard
6
+ * agents insights perf multi-section summary (commands + hooks + runs)
7
+ * agents insights perf hooks per-hook p50/p99 + cache hit rates
8
+ * agents insights perf commands slowest CLI command paths (from command.end)
9
+ * agents insights perf run agent.run / perf.timing labels
10
+ * agents insights perf friction sessions stuck repeatedly hitting the same guard
10
11
  *
11
12
  * Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
12
13
  * no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
@@ -49,5 +50,13 @@ export declare function loadHookProfile(days: number, project?: string): HookPro
49
50
  * exit 2 — see lib/friction-heuristics.ts for the grouping.
50
51
  */
51
52
  export declare function frictionAction(opts: PerfGlobalOpts): void;
52
- export declare function registerPerfCommand(program: Command): void;
53
+ /**
54
+ * Attach the `perf` latency-rollup command under a parent (the top-level
55
+ * `insights` group). Performance is an insight, not a top-level noun — `agents
56
+ * perf` was retired in favour of `agents insights perf` (PHNX-3391). It is a
57
+ * sibling of `agents insights cost` / `output`, registered in
58
+ * `registerInsightsCommand` beside them (top-level insights only, not
59
+ * `sessions insights`).
60
+ */
61
+ export declare function registerPerfSubcommand(parent: Command): void;
53
62
  export {};
@@ -1,12 +1,13 @@
1
1
  /**
2
- * `agents perf` — latency rollups over the disposable perf SQLite warehouse.
2
+ * `agents insights perf` — latency rollups over the disposable perf SQLite
3
+ * warehouse (former top-level `agents perf`, moved under insights in PHNX-3391).
3
4
  *
4
5
  * Subcommands:
5
- * agents perf multi-section summary (commands + hooks + runs)
6
- * agents perf hooks per-hook p50/p99 + cache hit rates
7
- * agents perf commands slowest CLI command paths (from command.end)
8
- * agents perf run agent.run / perf.timing labels
9
- * agents perf friction sessions stuck repeatedly hitting the same guard
6
+ * agents insights perf multi-section summary (commands + hooks + runs)
7
+ * agents insights perf hooks per-hook p50/p99 + cache hit rates
8
+ * agents insights perf commands slowest CLI command paths (from command.end)
9
+ * agents insights perf run agent.run / perf.timing labels
10
+ * agents insights perf friction sessions stuck repeatedly hitting the same guard
10
11
  *
11
12
  * Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
12
13
  * no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
@@ -190,7 +191,7 @@ export function frictionAction(opts) {
190
191
  // --cwd flag — so silently accepting the flag here would look like it
191
192
  // filtered when it did nothing. Fail loud instead of no-op.
192
193
  if (opts.project) {
193
- console.error(chalk.red("agents perf friction does not support --project yet — friction events carry no cwd to filter on."));
194
+ console.error(chalk.red("agents insights perf friction does not support --project yet — friction events carry no cwd to filter on."));
194
195
  process.exitCode = 1;
195
196
  return;
196
197
  }
@@ -224,7 +225,7 @@ function summaryAction(opts) {
224
225
  }, null, 2));
225
226
  return;
226
227
  }
227
- console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}${project ? ` — project ${project}` : ''}`));
228
+ console.log(chalk.bold(`agents insights perf — last ${days} day${days === 1 ? '' : 's'}${project ? ` — project ${project}` : ''}`));
228
229
  console.log(chalk.gray(`warehouse: ${perfDbPath()} (disposable; soft-join sessions via session_id/agent/machine)`));
229
230
  console.log('');
230
231
  console.log(chalk.bold('Commands (slowest by p99)'));
@@ -246,7 +247,7 @@ function attachSharedOptions(cmd) {
246
247
  }
247
248
  /**
248
249
  * Commander binds a flag declared on both parent and child to the *parent*.
249
- * Merge so `agents perf commands --json` still sees json:true on the leaf.
250
+ * Merge so `agents insights perf commands --json` still sees json:true on the leaf.
250
251
  */
251
252
  function leafOpts(cmd) {
252
253
  const parent = cmd.parent && typeof cmd.parent.opts === 'function'
@@ -254,8 +255,16 @@ function leafOpts(cmd) {
254
255
  : {};
255
256
  return { ...parent, ...cmd.opts() };
256
257
  }
257
- export function registerPerfCommand(program) {
258
- const perf = program
258
+ /**
259
+ * Attach the `perf` latency-rollup command under a parent (the top-level
260
+ * `insights` group). Performance is an insight, not a top-level noun — `agents
261
+ * perf` was retired in favour of `agents insights perf` (PHNX-3391). It is a
262
+ * sibling of `agents insights cost` / `output`, registered in
263
+ * `registerInsightsCommand` beside them (top-level insights only, not
264
+ * `sessions insights`).
265
+ */
266
+ export function registerPerfSubcommand(parent) {
267
+ const perf = parent
259
268
  .command('perf')
260
269
  .description('Latency rollups from the disposable perf warehouse (hooks, commands, runs)')
261
270
  .addHelpText('after', `
@@ -264,16 +273,16 @@ Identity columns reuse sessions/events string shapes (session_id, agent, machine
264
273
  for soft cross-reference; there are no foreign keys.
265
274
 
266
275
  Examples:
267
- agents perf # summary: commands + hooks + runs
268
- agents perf hooks # per-hook p50/p95/p99 + cache hit rate
269
- agents perf commands --days 30 # slowest CLI entrypoints
270
- agents perf run --json # agent.run timings as JSON
271
- agents perf hooks --warn-ms 500
272
- agents perf hooks --project agents-cli # scope to one repo's samples
273
- agents perf friction # sessions stuck retrying the same guard block
276
+ agents insights perf # summary: commands + hooks + runs
277
+ agents insights perf hooks # per-hook p50/p95/p99 + cache hit rate
278
+ agents insights perf commands --days 30 # slowest CLI entrypoints
279
+ agents insights perf run --json # agent.run timings as JSON
280
+ agents insights perf hooks --warn-ms 500
281
+ agents insights perf hooks --project agents-cli # scope to one repo's samples
282
+ agents insights perf friction # sessions stuck retrying the same guard block
274
283
  `);
275
- // Options live on the parent so `agents perf --json` and
276
- // `agents perf commands --json` both work (see leafOpts).
284
+ // Options live on the parent so `agents insights perf --json` and
285
+ // `agents insights perf commands --json` both work (see leafOpts).
277
286
  attachSharedOptions(perf).action(function summary() {
278
287
  summaryAction(this.opts());
279
288
  });
@@ -66,9 +66,11 @@ function collectOrphans(types, all) {
66
66
  }
67
67
  if (types.includes('hooks')) {
68
68
  for (const { agent, version } of scopePairs(iterHooksCapableVersions(), all)) {
69
- // Orphan hooks = scripts present in the version home that no
70
- // agents.yaml/hooks.yaml entry registers, so they never fire. Same
71
- // definition the doctor overview reports.
69
+ // Orphan hooks = scripts present in the version home but absent from
70
+ // every configured source (user + system + extras) — genuinely dead
71
+ // files, not merely unregistered ones (sync copies helper/test scripts
72
+ // that no manifest entry declares). Same definition the doctor overview
73
+ // reports (PHNX-2693).
72
74
  const orphans = listUnmanagedHooksInVersionHome(agent, version);
73
75
  if (orphans.length > 0) {
74
76
  groups.push({ type: 'hooks', agent, version, orphans });
@@ -46,5 +46,13 @@ export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: De
46
46
  */
47
47
  export declare function groupRoutineJobsByProject(jobs: JobConfig[], knownProjectNames: Set<string>): RoutineListGroup[];
48
48
  export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
49
+ /**
50
+ * The short, human reason a run did not simply complete — for inline display in
51
+ * the list/detail so "why did it fail" needs no dig into the run dir. Prefers the
52
+ * concrete `errorMessage` (which carries `auth_failed: …`, OAuth-revoked, timeouts),
53
+ * then the readiness block for a `blocked` run, then the mapped skip reason. Returns
54
+ * null for a healthy (`completed`/`running`) run, which needs no annotation.
55
+ */
56
+ export declare function runFailureReason(run: RunMeta): string | null;
49
57
  /** Register the `agents routines` command tree. */
50
58
  export declare function registerRoutinesCommands(program: Command): void;
@@ -245,12 +245,17 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = tru
245
245
  : lastStatus === 'missed' ? chalk.magenta
246
246
  : chalk.gray;
247
247
  const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
248
+ // Append the concrete reason a routine did not complete (auth_failed, wedged,
249
+ // blocked, missed) right in the Last Status cell, so the list answers "why"
250
+ // without a drill-in. Only for non-completed local runs; peer rows stay blank.
251
+ const reason = latestRun ? runFailureReason(latestRun) : null;
252
+ const reasonTag = reason ? chalk.gray(` — ${reason}`) : '';
248
253
  const agentLabelPadded = job.command
249
254
  ? chalk.magenta('command'.padEnd(10))
250
255
  : job.workflow
251
256
  ? chalk.magenta(`wf:${job.workflow}`.padEnd(10))
252
257
  : (job.agent || '').padEnd(10);
253
- console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}`);
258
+ console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}${reasonTag}`);
254
259
  }
255
260
  }
256
261
  function parseRoutineTrigger(options) {
@@ -622,6 +627,43 @@ function routineMatchesQuery(job, q) {
622
627
  .toLowerCase()
623
628
  .includes(q);
624
629
  }
630
+ /** Friendly one-liners for the claim a `skipped` run lost. */
631
+ const SKIP_REASON_LABEL = {
632
+ active_run: 'wedged: a prior run is still active',
633
+ duplicate_slot: 'duplicate slot (already fired)',
634
+ wrong_owner: 'pinned to another device',
635
+ };
636
+ /**
637
+ * The short, human reason a run did not simply complete — for inline display in
638
+ * the list/detail so "why did it fail" needs no dig into the run dir. Prefers the
639
+ * concrete `errorMessage` (which carries `auth_failed: …`, OAuth-revoked, timeouts),
640
+ * then the readiness block for a `blocked` run, then the mapped skip reason. Returns
641
+ * null for a healthy (`completed`/`running`) run, which needs no annotation.
642
+ */
643
+ export function runFailureReason(run) {
644
+ if (run.status === 'completed' || run.status === 'running')
645
+ return null;
646
+ const compact = (s) => {
647
+ const one = s.replace(/\s+/g, ' ').trim();
648
+ return one.length > 80 ? one.slice(0, 79) + '…' : one;
649
+ };
650
+ if (run.errorMessage)
651
+ return compact(run.errorMessage);
652
+ if (run.status === 'blocked' && run.readiness) {
653
+ return compact(run.readiness.message || run.readiness.code);
654
+ }
655
+ if (run.skipReason)
656
+ return SKIP_REASON_LABEL[run.skipReason];
657
+ if (run.status === 'missed')
658
+ return 'scheduler was not running when it came due';
659
+ // The most common failure shape: a command/agent body that exited nonzero with
660
+ // no structured errorMessage (the cause is in stdout). Naming the exit code is
661
+ // still more than the bare status word, and tells the reader it ran and threw.
662
+ if ((run.status === 'failed' || run.status === 'timeout') && run.exitCode !== null && run.exitCode !== undefined) {
663
+ return `exit ${run.exitCode}`;
664
+ }
665
+ return null;
666
+ }
625
667
  /** One compact routine row for the browser list: name · kind · schedule · next · last. */
626
668
  function routineBrowserRow(job, scheduler, overdueSet, now) {
627
669
  const kind = job.command ? 'command' : job.workflow ? `wf:${job.workflow}` : job.agent ?? '?';
@@ -685,7 +727,12 @@ function buildRoutineDetail(job, scheduler, now) {
685
727
  ? chalk.red(run.status)
686
728
  : chalk.yellow(run.status);
687
729
  const dur = run.completedAt ? ` ${formatRunDuration(run.startedAt, run.completedAt)}` : '';
688
- lines.push(` ${run.startedAt} ${status}${dur}`);
730
+ // Surface WHY a run did not complete, inline, so "looking at status" does not
731
+ // require digging into the run dir. auth_failed / OAuth-revoked, a wedged
732
+ // active-run skip, or a readiness block all live on the RunMeta already.
733
+ const reason = runFailureReason(run);
734
+ const why = reason ? chalk.gray(` — ${reason}`) : '';
735
+ lines.push(` ${run.startedAt} ${status}${dur}${why}`);
689
736
  }
690
737
  }
691
738
  // 4. Stats
@@ -1533,7 +1580,9 @@ export function registerRoutinesCommands(program) {
1533
1580
  : run.status === 'failed'
1534
1581
  ? chalk.red(run.status)
1535
1582
  : chalk.yellow(run.status);
1536
- console.log(` ${run.runId} ${status} ${run.startedAt}`);
1583
+ const reason = runFailureReason(run);
1584
+ const why = reason ? chalk.gray(` — ${reason}`) : '';
1585
+ console.log(` ${run.runId} ${status} ${run.startedAt}${why}`);
1537
1586
  }
1538
1587
  });
1539
1588
  routinesCmd
@@ -1856,6 +1905,11 @@ export function registerRoutinesCommands(program) {
1856
1905
  chalk.gray(` ${run.startedAt}`) +
1857
1906
  chalk.gray(formatRunDuration(run.startedAt, run.completedAt)) +
1858
1907
  (run.exitCode !== null && run.exitCode !== undefined ? chalk.gray(` exit ${run.exitCode}`) : ''));
1908
+ // The structured reason (auth_failed, blocked readiness, wedged skip) — the
1909
+ // report/stdout tail below often buries or omits it, so name it up front.
1910
+ const logsReason = runFailureReason(run);
1911
+ if (logsReason)
1912
+ console.log(chalk.red('reason: ') + chalk.gray(logsReason));
1859
1913
  console.log(chalk.gray('─'.repeat(60)));
1860
1914
  const reportPath = path.join(getRunDir(name, runId), 'report.md');
1861
1915
  if (fs.existsSync(reportPath)) {
@@ -538,7 +538,7 @@ Examples:
538
538
  }
539
539
  const version = resolveVersionAlias(agentId, rawVersion);
540
540
  if (!version) {
541
- console.log(chalk.red(`Pass a version: ${agentId}@<version>. Try 'default' or 'agents list ${agentId}'.`));
541
+ console.log(chalk.red(`Pass a version: ${agentId}@<version>. Try 'default' or 'agents view ${agentId}'.`));
542
542
  process.exit(1);
543
543
  }
544
544
  const installed = listInstalledVersions(agentId);
@@ -1,6 +1,7 @@
1
1
  import type { SessionEvent, SessionMeta, TodoProgress } from '../lib/session/types.js';
2
2
  import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
3
3
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
4
+ import type { FileChange } from '../lib/session/digest.js';
4
5
  import { extractArtifacts, extractHooks, extractLinks, extractSkills } from '../lib/session/highlights.js';
5
6
  /**
6
7
  * Compact checklist tally for list rows and previews (RUSH-2045).
@@ -118,6 +119,16 @@ export interface SessionPreviewDigest {
118
119
  backgroundShellCount?: number;
119
120
  toolTags: string[];
120
121
  changes: ReturnType<typeof changeCounts>;
122
+ /**
123
+ * The per-file source paths behind `changes`. `changes` keeps the roll-up
124
+ * counts every existing consumer already reads; `changedFiles` carries the
125
+ * real path + op the CLI computed at scan time (via classifyFileChanges) and
126
+ * used to discard — kept so a consumer that renders a per-file diff list (the
127
+ * AGI EXT Fleet detail panel, PHNX-2973) has the paths, not just the totals.
128
+ * Capped so a session that rewrote thousands of files can't bloat the cached
129
+ * digest / JSON payload; the `changes` counts stay the true totals.
130
+ */
131
+ changedFiles: FileChange[];
121
132
  dirs: string[];
122
133
  repos: string[];
123
134
  artifacts: ReturnType<typeof extractArtifacts>;
@@ -229,6 +229,14 @@ export function sanitizeRemoteDigest(raw) {
229
229
  backgroundShellCount: optNum(d.backgroundShellCount),
230
230
  toolTags: strList(d.toolTags),
231
231
  changes,
232
+ // Per-file paths from an untrusted peer: keep only well-formed {path, op}
233
+ // entries (path scrubbed of terminal escapes, op a known FileOp), bounded
234
+ // like the local build so a hostile peer can't flood the pane.
235
+ changedFiles: objList(d.changedFiles, (f) => {
236
+ const path = str(f.path);
237
+ const op = f.op === 'created' || f.op === 'modified' || f.op === 'deleted' ? f.op : undefined;
238
+ return path && op ? { path, op } : undefined;
239
+ }).slice(0, CHANGED_FILES_MAX),
232
240
  dirs: strList(d.dirs),
233
241
  repos: strList(d.repos),
234
242
  artifacts: objList(d.artifacts, (a) => {
@@ -655,6 +663,10 @@ const LAST_RESPONSE_MAX_LINES = 15;
655
663
  const LAST_RESPONSE_MAX_LINES_WITH_TODOS = 8;
656
664
  const TODOS_MAX_ITEMS = 5;
657
665
  const DIRS_TOUCHED_MAX = 5;
666
+ // Upper bound on the per-file `changedFiles` list carried on the digest. High
667
+ // enough to cover any real session's edits, low enough that a runaway rewrite
668
+ // can't bloat the cached JSON. The `changes` counts stay the true totals.
669
+ const CHANGED_FILES_MAX = 200;
658
670
  /** Fold a harness-normalized event stream into the stable preview data model. */
659
671
  export function buildSessionPreviewDigest(events, session) {
660
672
  let firstUser = '';
@@ -747,6 +759,10 @@ export function buildSessionPreviewDigest(events, session) {
747
759
  backgroundShellCount,
748
760
  toolTags: [...toolTags],
749
761
  changes: chg,
762
+ // Full per-file list the picker used to collapse to `chg` and throw away.
763
+ // Bounded so a mass-rewrite session can't blow up the cached digest; the
764
+ // counts above remain the true totals.
765
+ changedFiles: changes.slice(0, CHANGED_FILES_MAX),
750
766
  dirs: directoriesTouched(session, events, changes),
751
767
  repos: extractRepos(events, session.cwd),
752
768
  artifacts: extractArtifacts(changes),
@@ -2061,6 +2061,7 @@ export async function renderSessionPreview(query, scope) {
2061
2061
  tokenCount: session.tokenCount,
2062
2062
  costUsd: session.costUsd,
2063
2063
  label: session.label,
2064
+ topic: session.topic,
2064
2065
  ticketId: session.ticketId,
2065
2066
  prUrl: session.prUrl,
2066
2067
  },
@@ -250,6 +250,17 @@ export interface ShareUpdateResult {
250
250
  templateHash: string;
251
251
  baseUrl: string;
252
252
  workerName: string;
253
+ /** True when this was a `--check` run: nothing was deployed, the fields below
254
+ * just report whether a deploy WOULD be needed. Used by the release train to
255
+ * decide during preflight — before it verifies deploy creds — whether this
256
+ * release even changes the Worker. */
257
+ checked?: boolean;
258
+ /** Whether the current `worker-template.ts` render differs from what the
259
+ * endpoint last deployed (or `--force`/an endpoint with no recorded hash). */
260
+ deployNeeded?: boolean;
261
+ /** The template hash the endpoint last deployed, per local config. Undefined
262
+ * on an endpoint provisioned before the hash field existed. */
263
+ deployedHash?: string;
253
264
  }
254
265
  /** Re-deploy the Worker script on an ALREADY-provisioned endpoint to match the
255
266
  * current `worker-template.ts`. Reuses the existing account/worker/bucket and
@@ -264,6 +275,9 @@ export declare function runShareUpdate(opts?: {
264
275
  account?: string;
265
276
  token?: string;
266
277
  force?: boolean;
278
+ /** Report whether a deploy is needed and return without deploying. Needs no
279
+ * Cloudflare credentials — change detection is a pure local render+hash. */
280
+ check?: boolean;
267
281
  request?: CloudflareRequester;
268
282
  /** Bind PHOENIX_ID_BASE even when the configured hostname is not the managed domain. */
269
283
  managed?: boolean;
@@ -820,6 +820,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
820
820
  .option('--account <id>', 'Cloudflare account id override (default: the configured endpoint\'s account)')
821
821
  .option('--token <t>', 'Cloudflare API token (else read from --bundle)')
822
822
  .option('--force', 're-deploy even if the deployed template already matches')
823
+ .option('--check', 'report whether a deploy is needed and exit without deploying (needs no Cloudflare credentials)')
823
824
  // Named --update-json, not --json: `share <file>` (the parent) already
824
825
  // owns --json for its own publish-time result. Commander resolves an
825
826
  // option's long name against the WHOLE ancestor chain, so a same-named
@@ -832,6 +833,12 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
832
833
  console.log(JSON.stringify(result, null, 2));
833
834
  return;
834
835
  }
836
+ if (result.checked) {
837
+ console.log(result.deployNeeded
838
+ ? chalk.yellow(`Worker '${result.workerName}' template is outdated — deploy needed (current render ${result.templateHash.slice(0, 12)}…).`)
839
+ : chalk.dim(`Worker '${result.workerName}' already matches the current template — no deploy needed.`));
840
+ return;
841
+ }
835
842
  if (result.updated) {
836
843
  console.log(chalk.green(`Worker '${result.workerName}' updated → template ${result.templateHash.slice(0, 12)}…`));
837
844
  }
@@ -851,11 +858,18 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
851
858
 
852
859
  # Force a re-deploy even though the template hash already matches
853
860
  agents artifacts share update --force
861
+
862
+ # Report whether a deploy is due without deploying (no Cloudflare creds needed)
863
+ agents artifacts share update --check --update-json
854
864
  `,
855
865
  notes: `
856
866
  Reuses the existing account/worker/bucket from 'agents artifacts share status' and the
857
867
  existing write token — it never re-provisions a bucket, touches routes, or
858
868
  regenerates the token. See 'agents artifacts share status' for whether an update is due.
869
+
870
+ --check renders the current template and compares its hash to what the endpoint last
871
+ deployed, then exits — deploying nothing and reading no Cloudflare credentials. The
872
+ release train uses it to decide, before publish, whether a release changes the Worker.
859
873
  `,
860
874
  });
861
875
  shareCmd
@@ -1222,6 +1236,27 @@ export async function runShareUpdate(opts = {}) {
1222
1236
  if (!cfg) {
1223
1237
  throw new Error("Not configured. Run 'agents artifacts setup' (to provision) or 'agents artifacts share join' first.");
1224
1238
  }
1239
+ // Change detection is a pure local computation (render + sha256), so it needs
1240
+ // no Cloudflare credentials. The release train calls this in `--check` mode
1241
+ // during preflight — BEFORE it has verified the deploy creds — to decide
1242
+ // whether this release changes the Worker at all. `shareTemplateStatus` is the
1243
+ // same comparator `agents artifacts share status` uses; 'unknown' (an endpoint
1244
+ // with no recorded hash) deploys, mirroring `updateWorker`'s undefined-previous
1245
+ // behavior.
1246
+ const worker = renderWorkerBundle();
1247
+ const templateHash = hashWorkerScript(worker.script);
1248
+ const deployNeeded = opts.force === true || shareTemplateStatus(cfg) !== 'current';
1249
+ if (opts.check) {
1250
+ return {
1251
+ updated: false,
1252
+ checked: true,
1253
+ deployNeeded,
1254
+ templateHash,
1255
+ deployedHash: cfg.templateHash,
1256
+ baseUrl: cfg.baseUrl,
1257
+ workerName: cfg.workerName,
1258
+ };
1259
+ }
1225
1260
  const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle ?? DEFAULT_CF_BUNDLE, {
1226
1261
  apiToken: opts.token,
1227
1262
  accountId: opts.account,
@@ -1231,7 +1266,6 @@ export async function runShareUpdate(opts = {}) {
1231
1266
  throw new Error("Share endpoint has no Cloudflare account id — `agents artifacts share update` cannot call the API. Pass --account <id>, or re-run 'agents artifacts share join'.");
1232
1267
  }
1233
1268
  const writeToken = readWriteToken();
1234
- const worker = renderWorkerBundle();
1235
1269
  const phoenixIdBase = phoenixIdBaseForDeploy({ managed: opts.managed }, cfg);
1236
1270
  const provisionOpts = {
1237
1271
  ...(opts.request ? { request: opts.request } : {}),
@@ -1242,7 +1276,14 @@ export async function runShareUpdate(opts = {}) {
1242
1276
  if (!result.skipped) {
1243
1277
  writeShareConfig({ ...cfg, accountId, templateHash: result.templateHash });
1244
1278
  }
1245
- return { updated: !result.skipped, templateHash: result.templateHash, baseUrl: cfg.baseUrl, workerName: cfg.workerName };
1279
+ return {
1280
+ updated: !result.skipped,
1281
+ templateHash: result.templateHash,
1282
+ deployNeeded,
1283
+ deployedHash: cfg.templateHash,
1284
+ baseUrl: cfg.baseUrl,
1285
+ workerName: cfg.workerName,
1286
+ };
1246
1287
  }
1247
1288
  function cleanHostname(domain) {
1248
1289
  const raw = domain?.trim().replace(/\/+$/, '');