@phnx-labs/agents-cli 1.20.90 → 1.20.92

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 (112) hide show
  1. package/CHANGELOG.md +276 -0
  2. package/README.md +1 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +72 -6
  5. package/dist/commands/activity.js +198 -49
  6. package/dist/commands/beta.js +1 -0
  7. package/dist/commands/doctor.js +4 -2
  8. package/dist/commands/exec.d.ts +14 -0
  9. package/dist/commands/exec.js +144 -14
  10. package/dist/commands/feed.js +77 -4
  11. package/dist/commands/hooks.js +22 -6
  12. package/dist/commands/perf.d.ts +14 -0
  13. package/dist/commands/perf.js +221 -0
  14. package/dist/commands/projects.d.ts +12 -0
  15. package/dist/commands/projects.js +358 -0
  16. package/dist/commands/routines.js +30 -24
  17. package/dist/commands/secrets.d.ts +43 -4
  18. package/dist/commands/secrets.js +217 -32
  19. package/dist/commands/send.d.ts +5 -1
  20. package/dist/commands/send.js +1 -1
  21. package/dist/commands/sessions-picker.d.ts +15 -0
  22. package/dist/commands/sessions-picker.js +107 -13
  23. package/dist/commands/sessions-resume.d.ts +2 -0
  24. package/dist/commands/sessions-resume.js +9 -1
  25. package/dist/commands/sessions.d.ts +10 -5
  26. package/dist/commands/sessions.js +65 -27
  27. package/dist/index.js +19 -3
  28. package/dist/lib/activity.d.ts +80 -13
  29. package/dist/lib/activity.js +418 -74
  30. package/dist/lib/beta.d.ts +1 -1
  31. package/dist/lib/beta.js +1 -1
  32. package/dist/lib/catchup.d.ts +105 -0
  33. package/dist/lib/catchup.js +160 -0
  34. package/dist/lib/channels/providers/desktop.d.ts +49 -0
  35. package/dist/lib/channels/providers/desktop.js +132 -0
  36. package/dist/lib/channels/providers/index.js +2 -0
  37. package/dist/lib/daemon.js +74 -13
  38. package/dist/lib/devices/registry.d.ts +14 -0
  39. package/dist/lib/devices/registry.js +37 -0
  40. package/dist/lib/events.d.ts +12 -0
  41. package/dist/lib/events.js +122 -9
  42. package/dist/lib/exec.js +10 -0
  43. package/dist/lib/feed-broadcast.d.ts +47 -0
  44. package/dist/lib/feed-broadcast.js +65 -1
  45. package/dist/lib/feed-post.d.ts +10 -0
  46. package/dist/lib/feed-post.js +9 -3
  47. package/dist/lib/feed.d.ts +47 -1
  48. package/dist/lib/feed.js +38 -0
  49. package/dist/lib/hooks/cache.d.ts +2 -0
  50. package/dist/lib/hooks/cache.js +24 -4
  51. package/dist/lib/hosts/remote-cmd.js +4 -0
  52. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  53. package/dist/lib/menubar/install-menubar.d.ts +14 -4
  54. package/dist/lib/menubar/install-menubar.js +20 -6
  55. package/dist/lib/overdue.d.ts +14 -0
  56. package/dist/lib/overdue.js +37 -1
  57. package/dist/lib/perf/db.d.ts +25 -0
  58. package/dist/lib/perf/db.js +290 -0
  59. package/dist/lib/perf/spool.d.ts +18 -0
  60. package/dist/lib/perf/spool.js +79 -0
  61. package/dist/lib/perf/types.d.ts +45 -0
  62. package/dist/lib/perf/types.js +2 -0
  63. package/dist/lib/project-key.d.ts +44 -0
  64. package/dist/lib/project-key.js +79 -0
  65. package/dist/lib/project-root.js +16 -0
  66. package/dist/lib/project-status.d.ts +69 -0
  67. package/dist/lib/project-status.js +101 -0
  68. package/dist/lib/projects.d.ts +138 -0
  69. package/dist/lib/projects.js +301 -0
  70. package/dist/lib/remote-agents-json.d.ts +9 -0
  71. package/dist/lib/remote-agents-json.js +11 -5
  72. package/dist/lib/routines-project.js +6 -0
  73. package/dist/lib/routines.d.ts +30 -1
  74. package/dist/lib/routines.js +11 -0
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/list-filter.d.ts +94 -0
  78. package/dist/lib/secrets/list-filter.js +245 -0
  79. package/dist/lib/session/bash-command.d.ts +53 -0
  80. package/dist/lib/session/bash-command.js +364 -0
  81. package/dist/lib/session/digest.d.ts +13 -0
  82. package/dist/lib/session/digest.js +48 -1
  83. package/dist/lib/session/discover.d.ts +1 -2
  84. package/dist/lib/session/discover.js +7 -24
  85. package/dist/lib/session/highlights.d.ts +82 -0
  86. package/dist/lib/session/highlights.js +251 -0
  87. package/dist/lib/session/parse.js +23 -1
  88. package/dist/lib/session/relative-time.d.ts +37 -0
  89. package/dist/lib/session/relative-time.js +96 -8
  90. package/dist/lib/session/remote-list.js +5 -2
  91. package/dist/lib/session/render.d.ts +9 -9
  92. package/dist/lib/session/render.js +112 -73
  93. package/dist/lib/session/types.d.ts +4 -1
  94. package/dist/lib/ssh-exec.d.ts +6 -0
  95. package/dist/lib/ssh-exec.js +10 -1
  96. package/dist/lib/startup/command-registry.d.ts +2 -0
  97. package/dist/lib/startup/command-registry.js +4 -0
  98. package/dist/lib/state.d.ts +11 -0
  99. package/dist/lib/state.js +16 -0
  100. package/dist/lib/terminal/backends/index.d.ts +10 -2
  101. package/dist/lib/terminal/backends/index.js +14 -2
  102. package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
  103. package/dist/lib/terminal/backends/terminal-app.js +73 -0
  104. package/dist/lib/terminal/index.d.ts +2 -1
  105. package/dist/lib/terminal/index.js +2 -1
  106. package/dist/lib/terminal/preferred.d.ts +89 -0
  107. package/dist/lib/terminal/preferred.js +87 -0
  108. package/dist/lib/terminal/run-surface.d.ts +82 -0
  109. package/dist/lib/terminal/run-surface.js +146 -0
  110. package/dist/lib/terminal/types.d.ts +1 -1
  111. package/dist/lib/types.d.ts +1 -1
  112. package/package.json +4 -1
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
- import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
2
+ import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed.js';
3
3
  import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/activity.js';
4
4
  import { postFeedStatus } from '../lib/feed-post.js';
5
- import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, } from '../lib/feed-broadcast.js';
5
+ import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
6
6
  import { getSessionById } from '../lib/session/db.js';
7
7
  import { readMeta } from '../lib/state.js';
8
8
  import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
@@ -238,6 +238,9 @@ export function registerFeedCommand(program) {
238
238
  .option('--session <id>', 'Session id escape hatch (default: auto from env / pid registry)')
239
239
  .option('--attach <path-or-url...>', 'Attach an artifact (local file or URL); repeatable')
240
240
  .option('--level <level>', 'How loudly to broadcast: milestone (default) or important. Configured sinks with minLevel: important only fire on the latter.', 'milestone')
241
+ .option('--blocked', 'You are STUCK and need the user. Opens an answerable block and always broadcasts at important — do not also pass --level.')
242
+ .option('--option <label...>', 'With --blocked: an answer the user can pick; repeatable')
243
+ .option('--default <answer>', 'With --blocked: a safe default policy may apply if nobody answers in time')
241
244
  .option('--json', 'Emit the written event as JSON')
242
245
  .addHelpText('after', `
243
246
  Examples:
@@ -249,6 +252,17 @@ Examples:
249
252
  # Worth interrupting someone over — reaches sinks gated on minLevel: important:
250
253
  agents feed post "release blocked: npm token expired" --level important
251
254
 
255
+ # You are STUCK and cannot proceed. Opens an answerable block that stays in
256
+ # 'agents feed' until someone resolves it, and always reaches the owner —
257
+ # do NOT also pass --level:
258
+ agents feed post "force-push denied by git-guard on PR #1749" --blocked
259
+ agents feed post "publish to npm or wait for review?" --blocked --option publish --option wait
260
+ agents feed post "delete the stale preview env?" --blocked --default "leave it"
261
+
262
+ # Exhaust self-serve FIRST. A block is for what you genuinely cannot do:
263
+ # a credential only the user holds, a decision only they can make, an
264
+ # approval only they can give. Not "should I do the obvious next step?".
265
+
252
266
  # Outside a run, pass the session explicitly:
253
267
  agents feed post "manual note" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
254
268
 
@@ -268,22 +282,66 @@ docs/06-observability.md.
268
282
  session: opts?.session ?? cmd?.opts?.()?.session,
269
283
  attach: opts?.attach ?? cmd?.opts?.()?.attach,
270
284
  level: opts?.level ?? cmd?.opts?.()?.level,
285
+ blocked: Boolean(opts?.blocked ?? cmd?.opts?.()?.blocked),
286
+ option: opts?.option ?? cmd?.opts?.()?.option,
287
+ default: opts?.default ?? cmd?.opts?.()?.default,
271
288
  json: Boolean(opts?.json ?? cmd?.opts?.()?.json ?? cmd?.parent?.opts?.()?.json),
272
289
  };
273
290
  try {
274
- const level = parseFeedPostLevel(flags.level);
291
+ // Blocked is a state, not a volume: it always broadcasts at `important`,
292
+ // so an agent has exactly one thing to say. Passing both is a usage
293
+ // error rather than a silent override -- an agent that thinks it chose
294
+ // the level should not be quietly ignored.
295
+ if (flags.blocked && flags.level && flags.level !== 'milestone') {
296
+ throw new Error('--blocked already broadcasts at important; drop --level.');
297
+ }
298
+ if (!flags.blocked && (flags.option?.length || flags.default)) {
299
+ throw new Error('--option/--default only apply with --blocked.');
300
+ }
301
+ const level = flags.blocked ? 'important' : parseFeedPostLevel(flags.level);
275
302
  const { event } = postFeedStatus({
276
303
  text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
277
304
  sessionId: flags.session,
278
305
  attach: flags.attach,
306
+ blocked: flags.blocked,
279
307
  });
280
- const outcomes = broadcastPostedEvent(event, level);
308
+ // A blocked post lands in BOTH stores: the event in the shared activity
309
+ // stream (what happened) and an OpenBlock in the ledger (what is still
310
+ // open). The ledger is what makes it answerable and clearable -- without
311
+ // it the ask would scroll away like any other update.
312
+ let outcomes;
313
+ if (flags.blocked) {
314
+ const block = buildDeclaredBlock(event, {
315
+ text: event.detail ?? '',
316
+ options: flags.option,
317
+ safeDefault: flags.default,
318
+ });
319
+ publishBlock(block);
320
+ outcomes = broadcastBlock(block, { project: event.project, agent: event.agent });
321
+ }
322
+ else {
323
+ outcomes = broadcastPostedEvent(event, level);
324
+ }
325
+ // Fail loud when a block reached nobody. This is computed BEFORE the
326
+ // --json early return: a machine caller is exactly the one that reads the
327
+ // exit code, so returning 0 there while a human gets 1 would make the
328
+ // undelivered block invisible to the caller most likely to act on it —
329
+ // reintroducing, behind a flag, the silent failure this exists to remove.
330
+ // One sink failing among several stays a warning: the channels are
331
+ // redundant by design.
332
+ const undelivered = blockDeliveryFailure(flags.blocked, outcomes);
333
+ if (undelivered)
334
+ process.exitCode = 1;
281
335
  if (flags.json) {
282
336
  console.log(JSON.stringify(outcomes.length ? { ...event, broadcast: outcomes } : event, null, 2));
337
+ if (undelivered)
338
+ console.error(chalk.red(undelivered));
283
339
  return;
284
340
  }
285
341
  console.log(formatProgressUpdate(event));
286
342
  reportBroadcast(outcomes);
343
+ if (undelivered)
344
+ console.error(chalk.red(undelivered));
287
345
  }
288
346
  catch (err) {
289
347
  console.error(chalk.red(err.message));
@@ -516,6 +574,21 @@ function broadcastPostedEvent(event, level) {
516
574
  });
517
575
  return runFeedBroadcast(planned);
518
576
  }
577
+ /**
578
+ * Mirror a declared block to the same sinks a post reaches.
579
+ *
580
+ * Blocks previously never broadcast at all: `broadcastPostedEvent` ran only for
581
+ * `feed post`, while every `publishBlock` call wrote to the ledger and stopped
582
+ * there — so a "needs you" record was durable and invisible at the same time.
583
+ */
584
+ function broadcastBlock(block, extras) {
585
+ const config = readMeta().feed?.broadcast;
586
+ if (!config || Object.keys(config).length === 0)
587
+ return [];
588
+ const ticket = getSessionById(block.sessionId)?.ticketId;
589
+ const ctx = blockBroadcastContext({ ...block, ticket: block.ticket ?? ticket }, extras);
590
+ return runFeedBroadcast(planFeedBroadcast(config, ctx));
591
+ }
519
592
  /** One line per sink that ran. Silent when nothing is configured. */
520
593
  function reportBroadcast(outcomes) {
521
594
  for (const o of outcomes) {
@@ -603,30 +603,46 @@ Examples:
603
603
  .option('--warn-ms <n>', 'p99 threshold above which a hook is flagged as slow', '2000')
604
604
  .option('--json', 'Emit raw JSON rows instead of the table')
605
605
  .addHelpText('after', `
606
- Shows aggregated stats for every hook that emitted a hook.fire event into
607
- ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl. Only hooks with \`cache:\` in
608
- their manifest are instrumented today the generated shim writes the events.
606
+ Shows aggregated stats for every hook that fired through a generated shim.
607
+ Primary source: disposable SQLite warehouse ~/.agents/.cache/perf/perf.db
608
+ (same data as \`agents perf hooks\`). Falls back to the legacy daily JSONL under
609
+ ~/.agents/.cache/logs/ when the warehouse is empty.
609
610
 
610
611
  Examples:
611
612
  agents hooks profile # last 7 days, table form
612
613
  agents hooks profile --days 30 # roll up the full month
613
614
  agents hooks profile --json | jq # pipe somewhere
615
+ agents perf hooks # same rollup under the perf surface
614
616
 
615
617
  A hook whose p99 exceeds --warn-ms gets flagged in the cache column. Add
616
618
  'cache: 5m' or 'cache: 5m-bg' to its hooks.yaml entry to fix it.
617
619
  `)
618
620
  .action(async (options) => {
619
621
  const { aggregateHookProfile, loadHookFireEvents, formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS } = await import('../lib/hooks/profile.js');
622
+ const { aggregateSamples } = await import('../lib/perf/db.js');
620
623
  const days = Math.max(1, parseInt(options.days, 10) || 7);
621
624
  const warnMs = Math.max(0, parseInt(options.warnMs, 10) || DEFAULT_SLOW_HOOK_WARN_MS);
622
- const rows = aggregateHookProfile(loadHookFireEvents(days));
625
+ // Prefer the indexed warehouse; fall back to legacy JSONL for pre-warehouse shims.
626
+ const fromDb = aggregateSamples({ days, kinds: ['hook.fire'] }).map((r) => ({
627
+ hook: r.label,
628
+ n: r.n,
629
+ p50Ms: r.p50Ms,
630
+ p99Ms: r.p99Ms,
631
+ meanMs: r.meanMs,
632
+ maxMs: r.maxMs,
633
+ cacheHitPct: r.cacheHitPct ?? 0,
634
+ cacheStalePct: r.cacheStalePct ?? 0,
635
+ cacheMissPct: r.cacheMissPct ?? 0,
636
+ errorCount: r.errorCount ?? 0,
637
+ }));
638
+ const rows = fromDb.length > 0 ? fromDb : aggregateHookProfile(loadHookFireEvents(days));
623
639
  if (options.json) {
624
640
  console.log(JSON.stringify(rows, null, 2));
625
641
  return;
626
642
  }
627
643
  if (rows.length === 0) {
628
- console.log(chalk.gray(`No hook.fire events in the last ${days} day${days === 1 ? '' : 's'}.`));
629
- console.log(chalk.gray('Add \'cache: 5m\' to a hook in hooks.yaml to start collecting stats.'));
644
+ console.log(chalk.gray(`No hook.fire samples in the last ${days} day${days === 1 ? '' : 's'}.`));
645
+ console.log(chalk.gray('Add \'cache: 5m\' to a hook (or matches:) so a shim instruments it, then resync.'));
630
646
  return;
631
647
  }
632
648
  const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 30 };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `agents perf` — latency rollups over the disposable perf SQLite warehouse.
3
+ *
4
+ * 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
+ *
10
+ * Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
11
+ * no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
12
+ */
13
+ import type { Command } from 'commander';
14
+ export declare function registerPerfCommand(program: Command): void;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * `agents perf` — latency rollups over the disposable perf SQLite warehouse.
3
+ *
4
+ * 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
+ *
10
+ * Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
11
+ * no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
12
+ */
13
+ import chalk from 'chalk';
14
+ import { aggregateSamples, perfDbPath, } from '../lib/perf/db.js';
15
+ import { formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS, loadHookFireEvents, aggregateHookProfile, } from '../lib/hooks/profile.js';
16
+ function parseDays(raw) {
17
+ const n = parseInt(raw ?? '7', 10);
18
+ return Number.isFinite(n) && n > 0 ? n : 7;
19
+ }
20
+ function parseWarnMs(raw, fallback) {
21
+ const n = parseInt(raw ?? String(fallback), 10);
22
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
23
+ }
24
+ function parseLimit(raw, fallback) {
25
+ const n = parseInt(raw ?? String(fallback), 10);
26
+ return Number.isFinite(n) && n > 0 ? n : fallback;
27
+ }
28
+ /** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
29
+ function asHookRows(rows) {
30
+ return rows.map((r) => ({
31
+ hook: r.label,
32
+ n: r.n,
33
+ p50Ms: r.p50Ms,
34
+ p99Ms: r.p99Ms,
35
+ meanMs: r.meanMs,
36
+ maxMs: r.maxMs,
37
+ cacheHitPct: r.cacheHitPct ?? 0,
38
+ cacheStalePct: r.cacheStalePct ?? 0,
39
+ cacheMissPct: r.cacheMissPct ?? 0,
40
+ errorCount: r.errorCount ?? 0,
41
+ }));
42
+ }
43
+ function printTable(headers, widths, lines, slowFlags) {
44
+ const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
45
+ const header = headers.map((h, i) => pad(h, widths[i])).join(' ');
46
+ console.log(chalk.bold(header));
47
+ console.log(chalk.gray('─'.repeat(header.length)));
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const line = lines[i].map((c, j) => pad(c, widths[j])).join(' ');
50
+ console.log(slowFlags[i] ? chalk.yellow(line) : line);
51
+ }
52
+ }
53
+ function renderHookTable(rows, warnMs) {
54
+ if (rows.length === 0) {
55
+ console.log(chalk.gray('No hook timing samples yet.'));
56
+ console.log(chalk.gray(`Warehouse: ${perfDbPath()}`));
57
+ console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
58
+ return;
59
+ }
60
+ const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 28 };
61
+ const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
62
+ const header = [
63
+ pad('HOOK', widths.hook),
64
+ pad('N', widths.n),
65
+ pad('P50', widths.p50),
66
+ pad('P99', widths.p99),
67
+ pad('MEAN', widths.mean),
68
+ pad('MAX', widths.max),
69
+ pad('CACHE', widths.cache),
70
+ ].join(' ');
71
+ console.log(chalk.bold(header));
72
+ console.log(chalk.gray('─'.repeat(header.length)));
73
+ for (const r of rows) {
74
+ const slow = r.p99Ms > warnMs;
75
+ const cacheCol = formatCacheColumn(r);
76
+ const warning = slow && r.cacheHitPct + r.cacheStalePct === 0 ? ' ← add cache: 5m' : '';
77
+ const line = [
78
+ pad(r.hook, widths.hook),
79
+ pad(String(r.n), widths.n),
80
+ pad(formatMs(r.p50Ms), widths.p50),
81
+ pad(formatMs(r.p99Ms), widths.p99),
82
+ pad(formatMs(r.meanMs), widths.mean),
83
+ pad(formatMs(r.maxMs), widths.max),
84
+ pad(cacheCol, widths.cache),
85
+ ].join(' ') + warning;
86
+ console.log(slow ? chalk.yellow(line) : line);
87
+ }
88
+ }
89
+ function renderLabelTable(title, rows, warnMs, limit) {
90
+ const sliced = rows.slice(0, limit);
91
+ if (sliced.length === 0) {
92
+ console.log(chalk.gray(`No ${title} samples yet.`));
93
+ return;
94
+ }
95
+ const widths = [40, 5, 7, 7, 7, 7];
96
+ printTable(['LABEL', 'N', 'P50', 'P99', 'MEAN', 'MAX'], widths, sliced.map((r) => [
97
+ r.label,
98
+ String(r.n),
99
+ formatMs(r.p50Ms),
100
+ formatMs(r.p99Ms),
101
+ formatMs(r.meanMs),
102
+ formatMs(r.maxMs),
103
+ ]), sliced.map((r) => r.p99Ms > warnMs));
104
+ }
105
+ /**
106
+ * Prefer SQLite samples; fall back to the legacy daily JSONL so existing
107
+ * instrumentation still surfaces until shims are resynced.
108
+ */
109
+ function loadHookProfile(days) {
110
+ const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'] }));
111
+ if (fromDb.length > 0)
112
+ return fromDb;
113
+ return aggregateHookProfile(loadHookFireEvents(days));
114
+ }
115
+ function hooksAction(opts) {
116
+ const days = parseDays(opts.days);
117
+ const warnMs = parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS);
118
+ const rows = loadHookProfile(days);
119
+ if (opts.json) {
120
+ console.log(JSON.stringify(rows, null, 2));
121
+ return;
122
+ }
123
+ renderHookTable(rows, warnMs);
124
+ }
125
+ function commandsAction(opts) {
126
+ const days = parseDays(opts.days);
127
+ const warnMs = parseWarnMs(opts.warnMs, 500);
128
+ const limit = parseLimit(opts.limit, 40);
129
+ const rows = aggregateSamples({ days, kinds: ['command.end'] });
130
+ if (opts.json) {
131
+ console.log(JSON.stringify(rows.slice(0, limit), null, 2));
132
+ return;
133
+ }
134
+ renderLabelTable('command', rows, warnMs, limit);
135
+ }
136
+ function runAction(opts) {
137
+ const days = parseDays(opts.days);
138
+ const warnMs = parseWarnMs(opts.warnMs, 60_000);
139
+ const limit = parseLimit(opts.limit, 40);
140
+ const rows = aggregateSamples({ days, kinds: ['perf.timing'] });
141
+ if (opts.json) {
142
+ console.log(JSON.stringify(rows.slice(0, limit), null, 2));
143
+ return;
144
+ }
145
+ renderLabelTable('run/timing', rows, warnMs, limit);
146
+ }
147
+ function summaryAction(opts) {
148
+ const days = parseDays(opts.days);
149
+ if (opts.json) {
150
+ console.log(JSON.stringify({
151
+ days,
152
+ warehouse: perfDbPath(),
153
+ hooks: loadHookProfile(days),
154
+ commands: aggregateSamples({ days, kinds: ['command.end'] }).slice(0, 20),
155
+ run: aggregateSamples({ days, kinds: ['perf.timing'] }).slice(0, 20),
156
+ }, null, 2));
157
+ return;
158
+ }
159
+ console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}`));
160
+ console.log(chalk.gray(`warehouse: ${perfDbPath()} (disposable; soft-join sessions via session_id/agent/machine)`));
161
+ console.log('');
162
+ console.log(chalk.bold('Commands (slowest by p99)'));
163
+ renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'] }), parseWarnMs(opts.warnMs, 500), 12);
164
+ console.log('');
165
+ console.log(chalk.bold('Hooks'));
166
+ renderHookTable(loadHookProfile(days), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
167
+ console.log('');
168
+ console.log(chalk.bold('Runs (perf.timing)'));
169
+ renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'] }), parseWarnMs(opts.warnMs, 60_000), 12);
170
+ }
171
+ function attachSharedOptions(cmd) {
172
+ return cmd
173
+ .option('--days <n>', 'Days of samples to include', '7')
174
+ .option('--warn-ms <n>', 'p99 above this is highlighted')
175
+ .option('--limit <n>', 'Max rows in the table', '40')
176
+ .option('--json', 'Emit JSON instead of a table');
177
+ }
178
+ /**
179
+ * Commander binds a flag declared on both parent and child to the *parent*.
180
+ * Merge so `agents perf commands --json` still sees json:true on the leaf.
181
+ */
182
+ function leafOpts(cmd) {
183
+ const parent = cmd.parent && typeof cmd.parent.opts === 'function'
184
+ ? cmd.parent.opts()
185
+ : {};
186
+ return { ...parent, ...cmd.opts() };
187
+ }
188
+ export function registerPerfCommand(program) {
189
+ const perf = program
190
+ .command('perf')
191
+ .description('Latency rollups from the disposable perf warehouse (hooks, commands, runs)')
192
+ .addHelpText('after', `
193
+ The warehouse is SQLite at ~/.agents/.cache/perf/perf.db — safe to delete.
194
+ Identity columns reuse sessions/events string shapes (session_id, agent, machine)
195
+ for soft cross-reference; there are no foreign keys.
196
+
197
+ Examples:
198
+ agents perf # summary: commands + hooks + runs
199
+ agents perf hooks # per-hook p50/p99 + cache hit rate
200
+ agents perf commands --days 30 # slowest CLI entrypoints
201
+ agents perf run --json # agent.run timings as JSON
202
+ agents perf hooks --warn-ms 500
203
+ `);
204
+ // Options live on the parent so `agents perf --json` and
205
+ // `agents perf commands --json` both work (see leafOpts).
206
+ attachSharedOptions(perf).action(function summary() {
207
+ summaryAction(this.opts());
208
+ });
209
+ perf.command('hooks').description('Per-hook timing + cache stats')
210
+ .action(function hooks() {
211
+ hooksAction(leafOpts(this));
212
+ });
213
+ perf.command('commands').description('Slowest CLI command paths (command.end samples)')
214
+ .action(function commands() {
215
+ commandsAction(leafOpts(this));
216
+ });
217
+ perf.command('run').description('agent.run / perf.timing label rollups')
218
+ .action(function run() {
219
+ runAction(leafOpts(this));
220
+ });
221
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `agents projects` — named, multi-repo projects and the progress
3
+ * rollup. Definitions live in `~/.agents/projects/<name>.yaml` (see
4
+ * `lib/projects.ts`); this registers the command tree over them. Beta-gated on
5
+ * `isBetaEnabled('projects')`, mirroring `agents factory`.
6
+ *
7
+ * The headline is `status`: instead of the vague per-agent activity line, it
8
+ * rolls every session up by project (matched on cwd) into one card — agents by
9
+ * lifecycle state, plan completion, open PRs, and tickets in flight.
10
+ */
11
+ import type { Command } from 'commander';
12
+ export declare function registerProjectsCommands(program: Command): void;