@phnx-labs/agents-cli 1.21.1 → 1.21.3

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 (111) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +1 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +34 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/doctor.js +5 -2
  8. package/dist/commands/feed.js +28 -19
  9. package/dist/commands/hooks.js +9 -45
  10. package/dist/commands/menubar.js +24 -24
  11. package/dist/commands/message.js +23 -3
  12. package/dist/commands/perf.d.ts +29 -0
  13. package/dist/commands/perf.js +90 -23
  14. package/dist/commands/projects.d.ts +21 -0
  15. package/dist/commands/projects.js +169 -23
  16. package/dist/commands/routines.js +46 -1
  17. package/dist/commands/sessions-picker.js +17 -2
  18. package/dist/commands/sessions.d.ts +5 -0
  19. package/dist/commands/sessions.js +14 -0
  20. package/dist/commands/ssh.js +69 -0
  21. package/dist/commands/trends.d.ts +2 -0
  22. package/dist/commands/trends.js +158 -0
  23. package/dist/commands/usage.d.ts +4 -4
  24. package/dist/commands/view.d.ts +6 -0
  25. package/dist/commands/view.js +90 -45
  26. package/dist/index.js +22 -1
  27. package/dist/lib/agents.js +2 -2
  28. package/dist/lib/analytics/dashboard.d.ts +11 -0
  29. package/dist/lib/analytics/dashboard.js +31 -0
  30. package/dist/lib/analytics/recipes.d.ts +32 -0
  31. package/dist/lib/analytics/recipes.js +316 -0
  32. package/dist/lib/analytics/usage-db.d.ts +84 -0
  33. package/dist/lib/analytics/usage-db.js +301 -0
  34. package/dist/lib/browser/service.js +31 -0
  35. package/dist/lib/cli-resources.d.ts +20 -0
  36. package/dist/lib/cli-resources.js +48 -1
  37. package/dist/lib/computer/dispatch.d.ts +3 -1
  38. package/dist/lib/computer/dispatch.js +10 -2
  39. package/dist/lib/daemon.js +51 -14
  40. package/dist/lib/devices/health-report.d.ts +5 -0
  41. package/dist/lib/devices/health-report.js +3 -0
  42. package/dist/lib/event-stream.d.ts +2 -0
  43. package/dist/lib/event-stream.js +3 -0
  44. package/dist/lib/events.d.ts +3 -1
  45. package/dist/lib/events.js +4 -2
  46. package/dist/lib/feed-broadcast.d.ts +52 -7
  47. package/dist/lib/feed-broadcast.js +125 -18
  48. package/dist/lib/fleet-cache.d.ts +37 -0
  49. package/dist/lib/fleet-cache.js +40 -0
  50. package/dist/lib/fleet-status.d.ts +53 -0
  51. package/dist/lib/fleet-status.js +120 -0
  52. package/dist/lib/friction-heuristics.d.ts +32 -0
  53. package/dist/lib/friction-heuristics.js +47 -0
  54. package/dist/lib/git.d.ts +14 -0
  55. package/dist/lib/git.js +36 -0
  56. package/dist/lib/hooks/cache.js +28 -6
  57. package/dist/lib/hooks/profile.d.ts +8 -0
  58. package/dist/lib/hooks/profile.js +15 -18
  59. package/dist/lib/hooks.js +72 -17
  60. package/dist/lib/linear-cache.d.ts +63 -0
  61. package/dist/lib/linear-cache.js +146 -0
  62. package/dist/lib/linear-project-counts.d.ts +35 -5
  63. package/dist/lib/linear-project-counts.js +61 -16
  64. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  65. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
  66. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  67. package/dist/lib/menubar/install-menubar.d.ts +7 -0
  68. package/dist/lib/menubar/install-menubar.js +36 -6
  69. package/dist/lib/percentile.d.ts +12 -0
  70. package/dist/lib/percentile.js +24 -0
  71. package/dist/lib/perf/db.d.ts +7 -3
  72. package/dist/lib/perf/db.js +37 -19
  73. package/dist/lib/perf/types.d.ts +10 -0
  74. package/dist/lib/plugins.js +12 -1
  75. package/dist/lib/project-doctor.d.ts +36 -0
  76. package/dist/lib/project-doctor.js +45 -0
  77. package/dist/lib/project-import.d.ts +11 -1
  78. package/dist/lib/project-import.js +17 -3
  79. package/dist/lib/project-status.d.ts +25 -5
  80. package/dist/lib/project-status.js +48 -6
  81. package/dist/lib/resources.d.ts +16 -0
  82. package/dist/lib/resources.js +25 -14
  83. package/dist/lib/rotate.d.ts +27 -0
  84. package/dist/lib/rotate.js +44 -17
  85. package/dist/lib/routines.d.ts +16 -0
  86. package/dist/lib/routines.js +26 -0
  87. package/dist/lib/runner.js +41 -0
  88. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  89. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  90. package/dist/lib/secrets/usage-db.d.ts +3 -63
  91. package/dist/lib/secrets/usage-db.js +46 -186
  92. package/dist/lib/session/db.d.ts +17 -1
  93. package/dist/lib/session/db.js +240 -6
  94. package/dist/lib/session/discover.d.ts +31 -0
  95. package/dist/lib/session/discover.js +50 -1
  96. package/dist/lib/session/highlights.d.ts +24 -4
  97. package/dist/lib/session/highlights.js +52 -7
  98. package/dist/lib/session/parse.js +19 -3
  99. package/dist/lib/session/prompt.d.ts +19 -0
  100. package/dist/lib/session/prompt.js +43 -0
  101. package/dist/lib/session/types.d.ts +43 -0
  102. package/dist/lib/startup/command-registry.d.ts +1 -0
  103. package/dist/lib/startup/command-registry.js +2 -0
  104. package/dist/lib/state.d.ts +31 -3
  105. package/dist/lib/state.js +53 -10
  106. package/dist/lib/types.d.ts +22 -4
  107. package/dist/lib/usage-refresh.d.ts +106 -0
  108. package/dist/lib/usage-refresh.js +238 -0
  109. package/dist/lib/usage.d.ts +152 -17
  110. package/dist/lib/usage.js +393 -79
  111. package/package.json +1 -1
@@ -346,6 +346,15 @@ export class BrowserService {
346
346
  conn.tasks.set(taskName, task);
347
347
  await this.saveTaskState(effectiveProfileName, conn.tasks);
348
348
  emit('browser.launch', { profile: effectiveProfileName, task: taskName, pid: conn.pid });
349
+ void import('../analytics/usage-db.js').then(({ recordUsage }) => {
350
+ recordUsage({
351
+ kind: 'browser',
352
+ name: effectiveProfileName,
353
+ event: 'launch',
354
+ source: 'browser',
355
+ meta: { task: taskName },
356
+ });
357
+ }).catch(() => { });
349
358
  // If URL provided, create tab directly (no about:blank)
350
359
  let tabId;
351
360
  if (opts.url && !conn.electron) {
@@ -412,6 +421,15 @@ export class BrowserService {
412
421
  conn.tasks.delete(taskName);
413
422
  await this.saveTaskState(profileName, conn.tasks);
414
423
  emit('browser.close', { profile: profileName, task: taskName });
424
+ void import('../analytics/usage-db.js').then(({ recordUsage }) => {
425
+ recordUsage({
426
+ kind: 'browser',
427
+ name: profileName,
428
+ event: 'close',
429
+ source: 'browser',
430
+ meta: { task: taskName },
431
+ });
432
+ }).catch(() => { });
415
433
  if (conn.forkedFrom && conn.tasks.size === 0) {
416
434
  conn.cdp.close();
417
435
  killChrome(conn.pid);
@@ -468,6 +486,7 @@ export class BrowserService {
468
486
  const sessionId = await this.getSessionId(conn, cdpTargetId);
469
487
  await conn.cdp.send('Page.navigate', { url }, sessionId);
470
488
  await this.saveTaskState(task.profile, conn.tasks);
489
+ emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: currentShortId, created: false });
471
490
  return { tabId: currentShortId, url, created: false };
472
491
  }
473
492
  // No current tab - create one
@@ -482,6 +501,7 @@ export class BrowserService {
482
501
  task.tabs[shortId] = cdpTargetId;
483
502
  task.currentTabId = shortId;
484
503
  await this.saveTaskState(task.profile, conn.tasks);
504
+ emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: shortId, created: true });
485
505
  return { tabId: shortId, url, created: true };
486
506
  }
487
507
  // Chrome: create new tab
@@ -493,6 +513,7 @@ export class BrowserService {
493
513
  task.currentTabId = shortId;
494
514
  this.invalidateTargetCache(conn);
495
515
  await this.saveTaskState(task.profile, conn.tasks);
516
+ emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: shortId, created: true });
496
517
  return { tabId: shortId, url, created: true };
497
518
  }
498
519
  async tabAdd(taskId, url, profileName) {
@@ -687,6 +708,16 @@ export class BrowserService {
687
708
  await fs.promises.writeFile(finalPath, buffer);
688
709
  const dims = (extension === 'png' ? readPngDimensions(buffer) : readJpegDimensions(buffer)) ??
689
710
  { width: 0, height: 0 };
711
+ emit('browser.screenshot', {
712
+ profile: profileName,
713
+ task: task.name,
714
+ tabId: shortId,
715
+ path: finalPath,
716
+ bytes: buffer.length,
717
+ width: dims.width,
718
+ height: dims.height,
719
+ quality,
720
+ });
690
721
  return { path: finalPath, bytes: buffer.length, width: dims.width, height: dims.height };
691
722
  }
692
723
  /**
@@ -90,6 +90,15 @@ export declare function hasCommand(cmd: string): boolean;
90
90
  * shell, never interpolates strings into a command line.
91
91
  */
92
92
  export declare function isCliInstalled(manifest: CliManifest): boolean;
93
+ /**
94
+ * Async, non-blocking sibling of {@link isCliInstalled}. Same dispatch and
95
+ * Windows-shim retry, but over `execFile` so many manifests can be checked
96
+ * concurrently — the fix for RUSH-2136, where `agents doctor --json` ran a dozen+
97
+ * blocking 10s-timeout `spawnSync` checks SERIALLY (measured ~136s on an idle
98
+ * box). `execFile`'s `timeout` still SIGKILLs a wedged check, so a single
99
+ * hanging probe can't stall the whole set past 10s.
100
+ */
101
+ export declare function isCliInstalledAsync(manifest: CliManifest): Promise<boolean>;
93
102
  /**
94
103
  * Pick the first install method whose required host tool is available.
95
104
  * Returns null when none of the declared methods can run on this host.
@@ -151,3 +160,14 @@ export declare function listCliStatus(cwd?: string): {
151
160
  statuses: CliStatus[];
152
161
  errors: CliManifestError[];
153
162
  };
163
+ /**
164
+ * Async sibling of {@link listCliStatus} that probes every manifest CONCURRENTLY
165
+ * (RUSH-2136). The sync version runs each blocking `spawnSync` check one after
166
+ * another, so a dozen host CLIs whose checks are slow serialize into a
167
+ * multi-minute stall on `agents doctor --json`. This awaits them in parallel, so
168
+ * total wall time is the slowest single check (bounded at 10s), not their sum.
169
+ */
170
+ export declare function listCliStatusAsync(cwd?: string): Promise<{
171
+ statuses: CliStatus[];
172
+ errors: CliManifestError[];
173
+ }>;
@@ -18,7 +18,7 @@
18
18
  import * as fs from 'fs';
19
19
  import * as os from 'os';
20
20
  import * as path from 'path';
21
- import { spawnSync } from 'child_process';
21
+ import { spawnSync, execFile } from 'child_process';
22
22
  import * as yaml from 'yaml';
23
23
  import { listResources, resolveResource } from './resources.js';
24
24
  import { composeWin32CommandLine } from './platform/index.js';
@@ -304,6 +304,38 @@ export function isCliInstalled(manifest) {
304
304
  }
305
305
  return false;
306
306
  }
307
+ /**
308
+ * Async, non-blocking sibling of {@link isCliInstalled}. Same dispatch and
309
+ * Windows-shim retry, but over `execFile` so many manifests can be checked
310
+ * concurrently — the fix for RUSH-2136, where `agents doctor --json` ran a dozen+
311
+ * blocking 10s-timeout `spawnSync` checks SERIALLY (measured ~136s on an idle
312
+ * box). `execFile`'s `timeout` still SIGKILLs a wedged check, so a single
313
+ * hanging probe can't stall the whole set past 10s.
314
+ */
315
+ export function isCliInstalledAsync(manifest) {
316
+ const c = manifest.check;
317
+ if (c.kind === 'which') {
318
+ cmdExistsCache.delete(c.cmd);
319
+ return Promise.resolve(hasCommand(c.cmd));
320
+ }
321
+ return new Promise((resolve) => {
322
+ execFile(c.cmd, c.args, { timeout: 10_000 }, (err) => {
323
+ if (!err)
324
+ return resolve(true);
325
+ // A spawn failure (as opposed to a non-zero exit) surfaces as a string
326
+ // errno code (ENOENT/EINVAL); a non-zero exit surfaces as a numeric code.
327
+ // On Windows a `.cmd`/`.bat` shim spawn-fails without a shell — retry once
328
+ // through the shell, exactly as the sync path does.
329
+ const spawnFailed = typeof err.code === 'string';
330
+ if (process.platform === 'win32' && spawnFailed) {
331
+ const line = composeWin32CommandLine(c.cmd, c.args);
332
+ execFile(line, { timeout: 10_000, shell: true }, (retryErr) => resolve(!retryErr));
333
+ return;
334
+ }
335
+ resolve(false);
336
+ });
337
+ });
338
+ }
307
339
  // ─── Method selection ────────────────────────────────────────────────────────
308
340
  /**
309
341
  * Pick the first install method whose required host tool is available.
@@ -550,3 +582,18 @@ export function listCliStatus(cwd) {
550
582
  }));
551
583
  return { statuses, errors };
552
584
  }
585
+ /**
586
+ * Async sibling of {@link listCliStatus} that probes every manifest CONCURRENTLY
587
+ * (RUSH-2136). The sync version runs each blocking `spawnSync` check one after
588
+ * another, so a dozen host CLIs whose checks are slow serialize into a
589
+ * multi-minute stall on `agents doctor --json`. This awaits them in parallel, so
590
+ * total wall time is the slowest single check (bounded at 10s), not their sum.
591
+ */
592
+ export async function listCliStatusAsync(cwd) {
593
+ const { manifests, errors } = listCliManifests(cwd);
594
+ const statuses = await Promise.all(manifests.map(async (manifest) => ({
595
+ manifest,
596
+ installed: await isCliInstalledAsync(manifest),
597
+ })));
598
+ return { statuses, errors };
599
+ }
@@ -3,6 +3,8 @@ import { type AppInfo } from '../../commands/computer-actions.js';
3
3
  import type { VerbDispatcher } from './loop.js';
4
4
  declare function rpcMethodFor(verb: string): string;
5
5
  declare function toRpcParams(verb: string, input: Record<string, unknown>, pid?: number): Record<string, unknown>;
6
- export declare function makeVerbDispatcher(client: ComputerClient): VerbDispatcher;
6
+ export declare function makeVerbDispatcher(client: ComputerClient, context?: {
7
+ host?: string;
8
+ }): VerbDispatcher;
7
9
  export { rpcMethodFor, toRpcParams };
8
10
  export type { AppInfo };
@@ -5,7 +5,7 @@
5
5
  // it names the same RPC methods the explicit `agents computer <verb>` commands
6
6
  // call, so the external-agent verb surface is untouched. The daemon stays the
7
7
  // single authority on permissions and targeting.
8
- import { resolveTargetPidDecision } from '../../commands/computer-actions.js';
8
+ import { resolveTargetPidDecision, emitComputerAction } from '../../commands/computer-actions.js';
9
9
  // CLI verb -> daemon RPC method. Only the rename cases need listing; the rest
10
10
  // pass through by name.
11
11
  const RPC_METHOD = {
@@ -102,7 +102,14 @@ function toRpcParams(verb, input, pid) {
102
102
  // Build a dispatcher bound to a live client. Every call is a single daemon
103
103
  // round-trip; errors are returned (never thrown) so the loop can feed them
104
104
  // back to the model as a tool_result.
105
- export function makeVerbDispatcher(client) {
105
+ //
106
+ // `computer run`'s embedded model loop drives every verb (click, type,
107
+ // screenshot, ...) through THIS single seam rather than the explicit
108
+ // `agents computer <verb>` commands in computer-actions.ts, so it must emit
109
+ // the same `computer.action` event those commands emit — otherwise a session
110
+ // that only used the loop reads back usedComputer=false even though it drove
111
+ // real actions (reviewer-flagged regression on #1864).
112
+ export function makeVerbDispatcher(client, context = {}) {
106
113
  return async (call) => {
107
114
  const verb = call.name;
108
115
  const input = call.input ?? {};
@@ -118,6 +125,7 @@ export function makeVerbDispatcher(client) {
118
125
  const res = await client.call(method, params);
119
126
  if (res.error)
120
127
  return { ok: false, error: `${res.error.code}: ${res.error.message}` };
128
+ emitComputerAction(verb, pid, { bundle: input.bundle, host: context.host });
121
129
  return { ok: true, result: res.result ?? {} };
122
130
  };
123
131
  }
@@ -803,13 +803,17 @@ export async function runDaemon() {
803
803
  };
804
804
  const launchHealthInterval = setInterval(() => { void runLaunchHealthCheck(); }, 6 * 60 * 60_000);
805
805
  const launchHealthKickoff = setTimeout(() => { void runLaunchHealthCheck(); }, 90_000);
806
- // Fleet cache warm: keep the caches that `agents devices list`, `fleet status`,
807
- // and `agents view` read cache-first actually fresh, so a default read never
808
- // has to ssh out. Two cheap refreshes: (1) this host's auth-health verdicts
809
- // (also feeds the `doctor --json` Auth rollup other hosts read), and (2) the
810
- // fleet resource-stats cache (one bounded parallel probe of the tailnet). Both
811
- // best-effort + overlap-guarded like the probes above. ~every 3 min, plus once
812
- // ~60s after startup (staggered off launch).
806
+ // Fleet cache warm: publish THIS host's row for the caches `agents fleet
807
+ // status` / `agents devices list` read. PUBLISH-OWN / READ-UNION (RUSH-2061):
808
+ // each daemon probes only ITSELF and never SSHes another box, so the fleet no
809
+ // longer pays SSH resource probes every 3 minutes (N daemons × N devices)
810
+ // the source of the fan-out storm AND the orphaned-probe pile-up. Two cheap
811
+ // self-only refreshes: (1) this host's auth-health verdicts (also feeds the
812
+ // `doctor --json` Auth rollup other hosts read), and (2) its fleet-status row
813
+ // (local resource probe + live-agent workload). Cross-host rows are unioned on
814
+ // demand by the reader (`agents fleet status`), not pushed by every daemon.
815
+ // Best-effort + overlap-guarded like the probes above; ~every 3 min, once ~60s
816
+ // after startup.
813
817
  let warmingFleetCache = false;
814
818
  const runFleetCacheWarm = async () => {
815
819
  if (warmingFleetCache)
@@ -822,13 +826,9 @@ export async function runDaemon() {
822
826
  const { getCliVersion } = await import('./version.js');
823
827
  const authRows = await probeLocalFleetAuth({ cliVersion: getCliVersion() });
824
828
  writeFleetAuthRows(self, authRows);
825
- const { loadDevices } = await import('./devices/registry.js');
826
- const { planFleetTargets } = await import('./devices/fleet.js');
827
- const { loadFleetStats } = await import('./devices/stats-cache.js');
828
- const reg = await loadDevices();
829
- const probeable = planFleetTargets(reg).filter((t) => !t.skip).map((t) => t.device);
830
- const res = await loadFleetStats(probeable, { forceRefresh: true, selfName: self });
831
- log('INFO', `fleet cache warm: ${authRows.length} auth row(s), ${res.stats.size} device stat(s)`);
829
+ const { publishLocalFleetStatus } = await import('./fleet-status.js');
830
+ const row = await publishLocalFleetStatus(self);
831
+ log('INFO', `fleet cache warm: ${authRows.length} auth row(s), ${row.agents.running} running agent(s) on ${self}`);
832
832
  }
833
833
  catch (err) {
834
834
  log('ERROR', `fleet cache warm failed: ${err.message}`);
@@ -839,6 +839,41 @@ export async function runDaemon() {
839
839
  };
840
840
  const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
841
841
  const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
842
+ // Adaptive usage refresh: keep the usage cache the `agents run` router reads
843
+ // (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
844
+ // This host is the sole writer for its own local accounts. The tick wakes at
845
+ // the 90s floor, but per-account cadence gates the actual live fetches: an
846
+ // account racing toward its 5h cap is polled sooner (down to 90s), an idle one
847
+ // rarely (up to 15min), capped at ~6 provider calls/account/hour and skipped
848
+ // entirely while its provider is under a 429 backoff. Overlap-guarded like the
849
+ // probes above; a box signed into no networked-usage account is a clean no-op.
850
+ let refreshingUsage = false;
851
+ const runUsageRefreshTick = async () => {
852
+ if (refreshingUsage)
853
+ return;
854
+ refreshingUsage = true;
855
+ try {
856
+ const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
857
+ const { writeClaudeUsageCache } = await import('./usage.js');
858
+ const { usageRateLimitedUntil } = await import('./usage-backoff.js');
859
+ const r = await runUsageRefresh({
860
+ listAccounts: buildLocalUsageAccounts,
861
+ writeUsageCache: writeClaudeUsageCache,
862
+ backoffUntil: usageRateLimitedUntil,
863
+ });
864
+ if (r.refreshed > 0 || r.failed > 0) {
865
+ log('INFO', `usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
866
+ }
867
+ }
868
+ catch (err) {
869
+ log('ERROR', `usage refresh failed: ${err.message}`);
870
+ }
871
+ finally {
872
+ refreshingUsage = false;
873
+ }
874
+ };
875
+ const usageRefreshInterval = setInterval(() => { void runUsageRefreshTick(); }, 90_000);
876
+ const usageRefreshKickoff = setTimeout(() => { void runUsageRefreshTick(); }, 30_000);
842
877
  // RUSH-1817: the startup host decision above is one-shot. If a standalone
843
878
  // broker answered agentPing() at daemon start, the daemon declined to host —
844
879
  // but should that standalone later die or crash-loop, nothing takes over and
@@ -928,6 +963,8 @@ export async function runDaemon() {
928
963
  clearTimeout(launchHealthKickoff);
929
964
  clearInterval(fleetCacheInterval);
930
965
  clearTimeout(fleetCacheKickoff);
966
+ clearInterval(usageRefreshInterval);
967
+ clearTimeout(usageRefreshKickoff);
931
968
  clearInterval(brokerSelfHealInterval);
932
969
  hostedBroker?.close();
933
970
  removeDaemonPid();
@@ -2,6 +2,7 @@ import { type DeviceStats } from './health.js';
2
2
  import { type HostAuthSummary } from '../auth-health.js';
3
3
  import type { OnlineState } from './reachability.js';
4
4
  import { type FleetInventory } from './fleet-divergence.js';
5
+ import type { FleetAgentCounts } from '../fleet-status.js';
5
6
  export interface FleetCliStatus {
6
7
  installed: boolean;
7
8
  path: string | null;
@@ -46,6 +47,10 @@ export interface FleetHealthRow {
46
47
  * divergence detection (RUSH-2027). Undefined for an unreachable box or an
47
48
  * older CLI that doesn't emit it. */
48
49
  inventory?: FleetInventory;
50
+ /** This host's live agent workload (running-agent count + per-context / per-
51
+ * agent breakdown), from the fleet-status mirror / read-union (RUSH-2061).
52
+ * Undefined when no row has been published for the host yet. */
53
+ agents?: FleetAgentCounts;
49
54
  }
50
55
  export interface FleetWarning {
51
56
  kind: 'unreachable' | 'drift' | 'cli' | 'version-skew' | 'divergence';
@@ -445,6 +445,9 @@ export function renderFleetSummary(report, opts = {}) {
445
445
  else if (isGenuinelyOffline(row) && row.lastSeen) {
446
446
  marks.push(chalk.gray(`last seen ${formatCheckedAge(Date.parse(row.lastSeen), now)}`));
447
447
  }
448
+ if (row.agents && row.agents.running > 0) {
449
+ marks.push(chalk.cyan(`${row.agents.running} agent${row.agents.running === 1 ? '' : 's'}`));
450
+ }
448
451
  if (isSelf)
449
452
  marks.push(chalk.cyan('← this machine'));
450
453
  const note = marks.length ? ` ${marks.join(' ')}` : '';
@@ -18,6 +18,8 @@ export interface UnifiedQuery {
18
18
  eventTypes?: EventType[];
19
19
  level?: EventLevel;
20
20
  agent?: string;
21
+ /** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
22
+ sessionId?: string;
21
23
  caller?: string;
22
24
  command?: string;
23
25
  module?: string;
@@ -29,6 +29,8 @@ function matches(r, q) {
29
29
  return false;
30
30
  if (q.agent && r.agent !== q.agent)
31
31
  return false;
32
+ if (q.sessionId && r.sessionId !== q.sessionId)
33
+ return false;
32
34
  if (q.caller && r.caller !== q.caller)
33
35
  return false;
34
36
  if (q.command && r.command !== q.command &&
@@ -51,6 +53,7 @@ export function readUnifiedEvents(q = {}) {
51
53
  eventTypes: q.eventTypes,
52
54
  level: q.level,
53
55
  agent: q.agent,
56
+ sessionId: q.sessionId,
54
57
  caller: q.caller,
55
58
  command: q.command,
56
59
  module: q.module,
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { ActorKind } from './actor.js';
15
15
  export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
16
- export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
16
+ export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
17
17
  /** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
18
18
  export declare const EVENT_TYPES: readonly EventType[];
19
19
  /** Runtime guard for an event kind arriving from outside this process. */
@@ -208,6 +208,8 @@ export declare function query(options: {
208
208
  eventTypes?: EventType[];
209
209
  level?: EventLevel;
210
210
  agent?: string;
211
+ /** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
212
+ sessionId?: string;
211
213
  caller?: string;
212
214
  command?: string;
213
215
  module?: string;
@@ -84,6 +84,7 @@ const EVENT_TYPE_TABLE = {
84
84
  'version.install': true, 'version.switch': true, 'version.remove': true,
85
85
  'skill.install': true, 'skill.remove': true,
86
86
  'browser.launch': true, 'browser.close': true, 'browser.navigate': true, 'browser.screenshot': true,
87
+ 'computer.action': true,
87
88
  'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
88
89
  'cloud.dispatch': true, 'cloud.complete': true, 'cloud.cancel': true, 'cloud.message': true,
89
90
  'teams.create': true, 'teams.add': true, 'teams.start': true, 'teams.complete': true, 'teams.disband': true,
@@ -327,7 +328,6 @@ export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.st
327
328
  }
328
329
  return { kind: stdoutIsTTY ? 'terminal' : 'script' };
329
330
  }
330
- // ─── Audit attribution ────────────────────────────────────────────────────────
331
331
  // ─── Core API ─────────────────────────────────────────────────────────────────
332
332
  /**
333
333
  * Emit a structured event to the append-only audit log.
@@ -729,7 +729,7 @@ export function maybeRotate() {
729
729
  * @returns Array of event records
730
730
  */
731
731
  export function query(options) {
732
- const { startDate, endDate = new Date(), eventTypes, level, agent, caller, command, module, limit } = options;
732
+ const { startDate, endDate = new Date(), eventTypes, level, agent, sessionId, caller, command, module, limit } = options;
733
733
  const results = [];
734
734
  if (!fs.existsSync(eventsDir()))
735
735
  return results;
@@ -774,6 +774,8 @@ export function query(options) {
774
774
  continue;
775
775
  if (agent && record.agent !== agent)
776
776
  continue;
777
+ if (sessionId && record.sessionId !== sessionId)
778
+ continue;
777
779
  if (caller && record.caller !== caller)
778
780
  continue;
779
781
  if (command && record.command !== command &&
@@ -1,3 +1,4 @@
1
+ import type { Meta } from './types.js';
1
2
  /** How loudly a post asks to be heard. Ordered — `important` implies milestone. */
2
3
  export type FeedPostLevel = 'milestone' | 'important';
3
4
  /** Parse a `--level` value; anything unrecognized is a usage error, not a default. */
@@ -6,9 +7,20 @@ export interface FeedSinkConfig {
6
7
  /**
7
8
  * argv to run, with `{placeholder}` tokens substituted. First element is the
8
9
  * program; it is spawned directly (no shell), so quoting is not a concern and
9
- * post text can never become shell syntax.
10
+ * post text can never become shell syntax. Mutually exclusive with `channel`
11
+ * — a sink is one shape or the other.
10
12
  */
11
- command: string[];
13
+ command?: string[];
14
+ /**
15
+ * In-process delivery through the same channel-provider registry `agents
16
+ * send`/`agents notify` use — the composed `{message}` body, no argv, no
17
+ * spawn. `'owner'` is the address alias (expands to `notify.owner.{channel,to}`
18
+ * in agents.yaml, same as `agents notify`); any other value is a registered
19
+ * channel name (or a `notify.transports` mapping) and requires `to`.
20
+ */
21
+ channel?: string;
22
+ /** Recipient for a `channel` sink. Required unless `channel` is the `owner` alias. */
23
+ to?: string;
12
24
  /** Lowest post level that reaches this sink. Defaults to `milestone` (all posts). */
13
25
  minLevel?: FeedPostLevel;
14
26
  }
@@ -82,7 +94,14 @@ export declare function blockBroadcastContext(block: {
82
94
  export declare function blockDeliveryFailure(blocked: boolean, outcomes: SinkOutcome[]): string | undefined;
83
95
  export interface PlannedSink {
84
96
  name: string;
85
- argv: string[];
97
+ /** Command sink: argv to spawn (mutually exclusive with `channel`). */
98
+ argv?: string[];
99
+ /** Channel sink: provider channel name, or the `owner` alias. */
100
+ channel?: string;
101
+ /** Channel sink recipient. Unset for the `owner` alias — resolved at delivery. */
102
+ to?: string;
103
+ /** Channel sink body — the composed `{message}` for this post. */
104
+ text?: string;
86
105
  }
87
106
  export interface SinkOutcome {
88
107
  name: string;
@@ -139,11 +158,37 @@ export declare function renderSinkArgv(template: string[], ctx: FeedBroadcastCon
139
158
  /**
140
159
  * Which sinks this post reaches, in config order. Pure — the dry-run listing and
141
160
  * the real fan-out plan through here, so what `--dry-run` shows is what runs.
161
+ *
162
+ * A `channel:` sink is gated by the same `minLevel` rule as a `command:` sink —
163
+ * one level check for both shapes, so a dry-run plan is truthful regardless of
164
+ * which shape an operator's sink uses.
142
165
  */
143
166
  export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefined, ctx: FeedBroadcastContext): PlannedSink[];
144
167
  /**
145
- * Run the planned sinks. Each is a direct spawn with a bounded lifetime; a sink
146
- * that fails or is not installed is reported, never thrown the post is already
147
- * written and must not be undone by a mirror that could not be reached.
168
+ * The effective sink config for a post: the operator's `feed.broadcast`, or
169
+ * when that is unset or empty an implicit fallback straight to
170
+ * `notify.owner`, for a post worth interrupting someone over.
171
+ *
172
+ * Before this, `broadcastPostedEvent`/`broadcastBlock` returned early the
173
+ * moment `feed.broadcast` was empty, even when `notify.owner` was fully
174
+ * configured — so the common case (an operator who set up owner notifications
175
+ * but never wrote a `feed.broadcast` block) produced a `--blocked` post that
176
+ * looked recorded and reached nobody. `agents notify` already treats
177
+ * `notify.owner` as the default human destination; this makes an important
178
+ * feed post/block use that same default instead of requiring a second,
179
+ * redundant config block that says the same thing.
180
+ *
181
+ * The fallback only fires for `important` — a routine `milestone` post stays
182
+ * record-only, matching the `minLevel` contract every declared sink already
183
+ * follows. An operator-declared `feed.broadcast` (any non-empty config)
184
+ * always wins outright; the fallback never layers on top of it.
185
+ */
186
+ export declare function effectiveBroadcastConfig(config: FeedBroadcastConfig | undefined, level: FeedPostLevel, meta: Meta): FeedBroadcastConfig | undefined;
187
+ /**
188
+ * Run the planned sinks. A `command:` sink is a direct spawn with a bounded
189
+ * lifetime; a `channel:` sink delivers in-process. Either way a sink that
190
+ * fails or is not installed/registered is reported, never thrown — the post
191
+ * is already written and must not be undone by a mirror that could not be
192
+ * reached.
148
193
  */
149
- export declare function runFeedBroadcast(planned: PlannedSink[], timeoutMs?: number): SinkOutcome[];
194
+ export declare function runFeedBroadcast(planned: PlannedSink[], meta: Meta, timeoutMs?: number): Promise<SinkOutcome[]>;