@phnx-labs/agents-cli 1.20.51 → 1.20.53

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 (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +215 -7
  5. package/dist/commands/cloud.d.ts +3 -0
  6. package/dist/commands/cloud.js +8 -1
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +82 -3
  10. package/dist/commands/factory.js +8 -0
  11. package/dist/commands/feed.d.ts +38 -0
  12. package/dist/commands/feed.js +274 -0
  13. package/dist/commands/logs.d.ts +5 -1
  14. package/dist/commands/logs.js +248 -3
  15. package/dist/commands/mcp.js +7 -0
  16. package/dist/commands/memory.d.ts +9 -0
  17. package/dist/commands/memory.js +164 -0
  18. package/dist/commands/message.d.ts +11 -6
  19. package/dist/commands/message.js +140 -5
  20. package/dist/commands/routines.js +12 -0
  21. package/dist/commands/secrets-migrate.d.ts +2 -1
  22. package/dist/commands/secrets-migrate.js +88 -13
  23. package/dist/commands/secrets.d.ts +22 -0
  24. package/dist/commands/secrets.js +176 -42
  25. package/dist/commands/sessions.js +10 -1
  26. package/dist/commands/teams.js +4 -0
  27. package/dist/commands/worktree.js +4 -2
  28. package/dist/index.js +21 -22
  29. package/dist/lib/agents.js +249 -17
  30. package/dist/lib/answer-router.d.ts +75 -0
  31. package/dist/lib/answer-router.js +149 -0
  32. package/dist/lib/ask-classifier.d.ts +71 -0
  33. package/dist/lib/ask-classifier.js +197 -0
  34. package/dist/lib/browser/login-detection.d.ts +94 -0
  35. package/dist/lib/browser/login-detection.js +274 -0
  36. package/dist/lib/browser/profiles.d.ts +17 -8
  37. package/dist/lib/browser/profiles.js +27 -8
  38. package/dist/lib/browser/secret-ref.d.ts +10 -0
  39. package/dist/lib/browser/secret-ref.js +14 -0
  40. package/dist/lib/browser/service.js +14 -12
  41. package/dist/lib/cloud/antigravity.d.ts +0 -2
  42. package/dist/lib/cloud/antigravity.js +2 -17
  43. package/dist/lib/cloud/codex.js +3 -18
  44. package/dist/lib/cloud/rush.d.ts +15 -0
  45. package/dist/lib/cloud/rush.js +10 -16
  46. package/dist/lib/cloud/stream.js +2 -0
  47. package/dist/lib/cloud/types.d.ts +21 -0
  48. package/dist/lib/cloud/types.js +81 -0
  49. package/dist/lib/crabbox/cli.d.ts +1 -1
  50. package/dist/lib/crabbox/cli.js +12 -2
  51. package/dist/lib/crabbox/lease.d.ts +19 -0
  52. package/dist/lib/crabbox/lease.js +22 -11
  53. package/dist/lib/crabbox/progress.d.ts +62 -0
  54. package/dist/lib/crabbox/progress.js +129 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  56. package/dist/lib/crabbox/runtimes.js +98 -5
  57. package/dist/lib/daemon.d.ts +12 -9
  58. package/dist/lib/daemon.js +32 -17
  59. package/dist/lib/events.d.ts +31 -5
  60. package/dist/lib/events.js +291 -101
  61. package/dist/lib/exec.js +20 -1
  62. package/dist/lib/feed-outcome.d.ts +101 -0
  63. package/dist/lib/feed-outcome.js +244 -0
  64. package/dist/lib/feed-policy.d.ts +30 -0
  65. package/dist/lib/feed-policy.js +133 -0
  66. package/dist/lib/feed.d.ts +180 -0
  67. package/dist/lib/feed.js +627 -0
  68. package/dist/lib/git.d.ts +17 -1
  69. package/dist/lib/git.js +20 -1
  70. package/dist/lib/hooks.js +529 -14
  71. package/dist/lib/hosts/passthrough.d.ts +3 -3
  72. package/dist/lib/hosts/passthrough.js +3 -3
  73. package/dist/lib/mailbox-gc.d.ts +22 -0
  74. package/dist/lib/mailbox-gc.js +161 -0
  75. package/dist/lib/mailbox.d.ts +26 -2
  76. package/dist/lib/mailbox.js +80 -5
  77. package/dist/lib/mcp.js +82 -0
  78. package/dist/lib/memory.d.ts +55 -0
  79. package/dist/lib/memory.js +274 -0
  80. package/dist/lib/notify.d.ts +16 -0
  81. package/dist/lib/notify.js +61 -0
  82. package/dist/lib/operator.d.ts +26 -0
  83. package/dist/lib/operator.js +107 -0
  84. package/dist/lib/plugins.d.ts +35 -0
  85. package/dist/lib/plugins.js +217 -0
  86. package/dist/lib/remote-agents-json.d.ts +14 -0
  87. package/dist/lib/remote-agents-json.js +94 -0
  88. package/dist/lib/resources/mcp.js +44 -0
  89. package/dist/lib/resources/memory.d.ts +15 -0
  90. package/dist/lib/resources/memory.js +46 -0
  91. package/dist/lib/resources/types.d.ts +2 -2
  92. package/dist/lib/rotate.js +2 -0
  93. package/dist/lib/runner.d.ts +43 -0
  94. package/dist/lib/runner.js +323 -74
  95. package/dist/lib/sandbox.js +6 -0
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.d.ts +21 -0
  99. package/dist/lib/secrets/agent.js +63 -1
  100. package/dist/lib/secrets/bundles.d.ts +33 -1
  101. package/dist/lib/secrets/bundles.js +76 -21
  102. package/dist/lib/secrets/icloud-import.d.ts +79 -0
  103. package/dist/lib/secrets/icloud-import.js +203 -0
  104. package/dist/lib/secrets/index.d.ts +142 -2
  105. package/dist/lib/secrets/index.js +695 -30
  106. package/dist/lib/secrets/remote.js +1 -1
  107. package/dist/lib/secrets/sync.js +1 -1
  108. package/dist/lib/session/active.d.ts +18 -0
  109. package/dist/lib/session/active.js +47 -17
  110. package/dist/lib/session/db.d.ts +9 -1
  111. package/dist/lib/session/db.js +18 -3
  112. package/dist/lib/session/discover.d.ts +13 -0
  113. package/dist/lib/session/discover.js +32 -2
  114. package/dist/lib/session/parse.d.ts +8 -0
  115. package/dist/lib/session/parse.js +42 -21
  116. package/dist/lib/session/remote-active.js +8 -89
  117. package/dist/lib/session/state.d.ts +11 -0
  118. package/dist/lib/session/state.js +50 -1
  119. package/dist/lib/session/tail.d.ts +23 -4
  120. package/dist/lib/session/tail.js +34 -16
  121. package/dist/lib/session/throughput.d.ts +30 -0
  122. package/dist/lib/session/throughput.js +86 -0
  123. package/dist/lib/shim-heal.d.ts +12 -3
  124. package/dist/lib/shim-heal.js +12 -6
  125. package/dist/lib/staleness/detectors/subagents.js +57 -3
  126. package/dist/lib/staleness/writers/hooks.js +7 -3
  127. package/dist/lib/staleness/writers/subagents.js +37 -6
  128. package/dist/lib/startup/command-registry.d.ts +2 -0
  129. package/dist/lib/startup/command-registry.js +4 -0
  130. package/dist/lib/state.d.ts +2 -0
  131. package/dist/lib/state.js +25 -8
  132. package/dist/lib/subagents.d.ts +52 -0
  133. package/dist/lib/subagents.js +315 -12
  134. package/dist/lib/teams/agents.js +6 -3
  135. package/dist/lib/teams/worktree.d.ts +8 -0
  136. package/dist/lib/teams/worktree.js +8 -0
  137. package/dist/lib/types.d.ts +20 -2
  138. package/dist/lib/versions.js +38 -48
  139. package/dist/lib/whats-new.d.ts +5 -3
  140. package/dist/lib/whats-new.js +25 -5
  141. package/package.json +4 -3
  142. package/scripts/postinstall.js +61 -1
@@ -327,7 +327,7 @@ export function registerRunCommand(program) {
327
327
  process.exit(1);
328
328
  }
329
329
  const backend = typeof options.lease === 'string' ? options.lease : undefined;
330
- const { detectSignedInRuntimes, pickRuntimes } = await import('../lib/crabbox/runtimes.js');
330
+ const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
331
331
  const { leaseAndRun } = await import('../lib/crabbox/lease.js');
332
332
  const { confirm } = await import('@inquirer/prompts');
333
333
  const detected = await detectSignedInRuntimes();
@@ -344,30 +344,109 @@ export function registerRunCommand(program) {
344
344
  return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
345
345
  })
346
346
  .join(', ');
347
+ // Claude ships its OAuth token (not just the .claude.json config) — name it
348
+ // explicitly so the consent covers the actual credential transferred.
349
+ const whatShips = runtimes.includes('claude') ? 'credentials + Claude OAuth token' : 'credentials';
347
350
  const ok = await confirm({
348
- message: `Copy credentials for ${names} to a disposable cloud box, run there, then destroy it?`,
351
+ message: `Copy ${whatShips} for ${names} to a disposable cloud box, run there, then destroy it?`,
349
352
  default: false,
350
353
  });
351
354
  if (!ok) {
352
355
  console.error(chalk.yellow('Aborted — no credentials pushed, no box leased.'));
353
356
  process.exit(1);
354
357
  }
358
+ // Read the Claude OAuth token from the local Keychain (silent) so it can be
359
+ // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
360
+ // "Not logged in". Consent above already covered this transfer.
361
+ let claudeCredentialsJson = null;
362
+ if (runtimes.includes('claude')) {
363
+ const claudeEmail = detected.find((d) => d.id === 'claude')?.email ?? null;
364
+ claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
365
+ if (!claudeCredentialsJson) {
366
+ console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
367
+ }
368
+ }
369
+ // Progress UI. A self-throttled spinner (NOT ora — see progress.ts) covers
370
+ // the otherwise-silent provisioning + box-setup phases; the agent's own
371
+ // output then prints verbatim. The router splits the crabbox stream at the
372
+ // box-side marker. Rule: only ONE spinner phase is active at a time, and no
373
+ // other output is written to stderr while it spins — so it can never storm.
374
+ const { createLeaseOutputRouter, createSpinner } = await import('../lib/crabbox/progress.js');
375
+ const agentName = agentSpec.split('@')[0];
376
+ const spinner = createSpinner({ stream: process.stderr });
377
+ let warmupTimer;
378
+ const stopTimer = () => { if (warmupTimer) {
379
+ clearInterval(warmupTimer);
380
+ warmupTimer = undefined;
381
+ } };
382
+ const fit = (s) => {
383
+ const w = Math.max(20, (process.stderr.columns || 80) - 24);
384
+ return s.length > w ? s.slice(0, w - 1) + '…' : s;
385
+ };
386
+ const router = createLeaseOutputRouter({
387
+ // Setup lines only ever update spinner text (no direct stderr writes),
388
+ // so the spinner stays the single writer during the setup phase.
389
+ onSetupLine: (line) => spinner.update(`Setting up box — ${fit(line)}`),
390
+ onAgentChunk: (chunk) => {
391
+ // First agent byte: stop the setup spinner, THEN stream to stdout.
392
+ if (spinner.active)
393
+ spinner.stopAndPersist('✔', chalk.gray(`Box provisioned — ${agentName} output:`));
394
+ process.stdout.write(chunk);
395
+ },
396
+ });
355
397
  try {
356
398
  const { exitCode, box, toreDown } = await leaseAndRun({
357
- agent: agentSpec.split('@')[0],
399
+ agent: agentName,
358
400
  prompt,
359
401
  mode: options.mode,
360
402
  model: options.model,
361
403
  backend,
362
404
  runtimes,
363
405
  detected,
406
+ claudeCredentialsJson,
364
407
  secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
365
408
  keep: options.keepBox,
409
+ onData: (chunk) => router.push(chunk),
410
+ onPhase: (phase) => {
411
+ if (phase.kind === 'warmup') {
412
+ const label = `Leasing a ${phase.backend ?? 'hetzner'} box`;
413
+ spinner.start(`${label}…`);
414
+ const t0 = Date.now();
415
+ warmupTimer = setInterval(() => spinner.update(`${label}… (${Math.round((Date.now() - t0) / 1000)}s)`), 1000);
416
+ }
417
+ else if (phase.kind === 'ready') {
418
+ stopTimer();
419
+ spinner.stopAndPersist('✔', `Box ${phase.box.slug} ready${phase.box.ip ? ` (${phase.box.ip})` : ''} · ${Math.round(phase.elapsedMs / 1000)}s`);
420
+ spinner.start('Setting up box…');
421
+ }
422
+ else if (phase.kind === 'teardown') {
423
+ if (spinner.active)
424
+ spinner.stop();
425
+ }
426
+ },
366
427
  });
428
+ router.end();
429
+ stopTimer();
430
+ if (spinner.active)
431
+ spinner.stop();
432
+ // Safety net: if setup failed before the agent ever ran, the setup log
433
+ // was only shown as transient spinner text — surface it so the error is
434
+ // diagnosable.
435
+ if (exitCode !== 0 && !router.sawAgent()) {
436
+ const log = router.setupLines();
437
+ if (log.length)
438
+ process.stderr.write(chalk.dim(log.join('\n')) + '\n');
439
+ }
367
440
  console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop ${box.slug}`));
368
441
  process.exit(exitCode === null ? 1 : exitCode);
369
442
  }
370
443
  catch (err) {
444
+ stopTimer();
445
+ if (spinner.active)
446
+ spinner.stopAndPersist('✖', chalk.red('Lease failed'));
447
+ const log = router.setupLines();
448
+ if (log.length && !router.sawAgent())
449
+ process.stderr.write(chalk.dim(log.join('\n')) + '\n');
371
450
  console.error(chalk.red(err.message));
372
451
  process.exit(1);
373
452
  }
@@ -5,6 +5,7 @@ import * as path from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
7
7
  import { insertTask } from '../lib/cloud/store.js';
8
+ import { emit } from '../lib/events.js';
8
9
  function requireFactoryUrl() {
9
10
  const url = process.env.FACTORY_FLOOR_URL;
10
11
  if (!url) {
@@ -79,6 +80,13 @@ Examples:
79
80
  createdAt: now,
80
81
  updatedAt: now,
81
82
  });
83
+ emit('cloud.dispatch', {
84
+ module: 'factory',
85
+ taskId: result.cloud_execution_id,
86
+ ref,
87
+ linear_identifier: result.linear_identifier,
88
+ status: 'queued',
89
+ });
82
90
  console.log(chalk.green(`Submitted ${result.linear_identifier} (${result.label})`));
83
91
  console.log(` ticket ${result.ticket_id}`);
84
92
  console.log(` execution ${result.cloud_execution_id}`);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `agents feed` -- list open blocks (decisions agents are waiting on).
3
+ *
4
+ * Aggregates block records from the local feed store and, with --host, from
5
+ * reachable remote hosts via SSH passthrough. Each block carries enough
6
+ * identity for `agents message` to route a reply back to the right agent.
7
+ *
8
+ * Default view groups by **outcome** (ticket / PR / worktree / Unassigned) so
9
+ * an operator sees dozens of deliverables, not ~1,100 agents. Pass `--flat`
10
+ * for the legacy per-agent list.
11
+ */
12
+ import type { Command } from 'commander';
13
+ import { type OpenBlock } from '../lib/feed.js';
14
+ import { type OutcomeGroup, type SessionOutcomeHint } from '../lib/feed-outcome.js';
15
+ export declare const FEED_NO_FANOUT_ENV = "AGENTS_FEED_LOCAL";
16
+ export declare function parseRemoteFeed(stdout: string, machine: string): OpenBlock[];
17
+ /** Merge local and remote rows, keeping the first copy of a host/session block. */
18
+ export declare function mergeFeedBlocks(...groups: OpenBlock[][]): OpenBlock[];
19
+ export declare function shouldIncludeLocalFeed(hosts: string[] | undefined, self: string): boolean;
20
+ export declare function remoteFeedHostsToDial(hosts: string[] | undefined, self: string): string[] | undefined;
21
+ /** Human summary line for one outcome rollup. */
22
+ export declare function formatOutcomeHeader(group: OutcomeGroup): string;
23
+ /** Map active sessions into the lightweight hints outcome enrichment needs. */
24
+ export declare function sessionHintsFromActive(sessions: Array<{
25
+ sessionId?: string;
26
+ agentId?: string;
27
+ ticket?: {
28
+ id?: string;
29
+ };
30
+ pr?: {
31
+ url?: string;
32
+ number?: number;
33
+ };
34
+ worktree?: {
35
+ slug?: string;
36
+ };
37
+ }>): SessionOutcomeHint[];
38
+ export declare function registerFeedCommand(program: Command): void;
@@ -0,0 +1,274 @@
1
+ import chalk from 'chalk';
2
+ import { ensureFeedPublishHook, listBlocks, recordNotified } from '../lib/feed.js';
3
+ import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
4
+ import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
5
+ import { machineId, normalizeHost } from '../lib/machine-id.js';
6
+ import { relTime } from '../lib/format.js';
7
+ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
8
+ import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
9
+ import { notifyUrgentBlock } from '../lib/notify.js';
10
+ import { gcMailbox } from '../lib/mailbox-gc.js';
11
+ import { getActiveSessions } from '../lib/session/active.js';
12
+ import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
13
+ export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
14
+ export function parseRemoteFeed(stdout, machine) {
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(stdout);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ if (!Array.isArray(parsed))
23
+ return [];
24
+ const blocks = [];
25
+ for (const item of parsed) {
26
+ if (!item || typeof item !== 'object' || Array.isArray(item))
27
+ continue;
28
+ const block = item;
29
+ if (!block.blockId || !block.sessionId || !block.mailboxId || !block.questions?.length)
30
+ continue;
31
+ blocks.push({ ...block, host: machine });
32
+ }
33
+ return blocks;
34
+ }
35
+ /** Merge local and remote rows, keeping the first copy of a host/session block. */
36
+ export function mergeFeedBlocks(...groups) {
37
+ const byIdentity = new Map();
38
+ for (const block of groups.flat()) {
39
+ const key = `${normalizeHost(block.host)}:${block.blockId}`;
40
+ if (!byIdentity.has(key))
41
+ byIdentity.set(key, block);
42
+ }
43
+ return [...byIdentity.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
44
+ }
45
+ function hostToken(host) {
46
+ return normalizeHost(host.split('@').pop() || host);
47
+ }
48
+ export function shouldIncludeLocalFeed(hosts, self) {
49
+ return !hosts?.length || hosts.some((host) => hostToken(host) === self);
50
+ }
51
+ export function remoteFeedHostsToDial(hosts, self) {
52
+ if (!hosts?.length)
53
+ return undefined;
54
+ return hosts.filter((host) => hostToken(host) !== self);
55
+ }
56
+ function renderBlock(b, localHost, indent = '') {
57
+ const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
58
+ const runtime = chalk.gray(b.runtime);
59
+ const age = chalk.gray(relTime(b.ts));
60
+ const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
61
+ const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
62
+ const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
63
+ console.log(`${indent}${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
64
+ for (const question of b.questions) {
65
+ const header = question.header ? chalk.gray(`[${question.header}] `) : '';
66
+ console.log(`${indent} ${header}${question.text}`);
67
+ if (question.options?.length) {
68
+ for (let i = 0; i < question.options.length; i++) {
69
+ const o = question.options[i];
70
+ const desc = o.description ? chalk.gray(` -- ${o.description}`) : '';
71
+ console.log(`${indent} ${chalk.dim(`${i + 1}.`)} ${o.label}${desc}`);
72
+ }
73
+ }
74
+ }
75
+ if (b.ticket || b.pr || b.worktreeSlug) {
76
+ const meta = [b.ticket, b.pr, b.worktreeSlug].filter(Boolean).join(' ');
77
+ console.log(`${indent} ${chalk.gray(meta)}`);
78
+ }
79
+ if (b.answer) {
80
+ const verified = b.answer.verified ? chalk.green('✓') : chalk.yellow('?');
81
+ const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
82
+ console.log(`${indent} ${chalk.green('answered')} by ${who} ${verified}`);
83
+ }
84
+ if (b.parkedAt) {
85
+ console.log(`${indent} ${chalk.red('hard-parked')} ${relTime(b.parkedAt)}`);
86
+ }
87
+ if (b.defaultedAt) {
88
+ console.log(`${indent} ${chalk.yellow('defaulted')} ${relTime(b.defaultedAt)}`);
89
+ }
90
+ if (b.receipts && b.receipts.length > 0) {
91
+ const latest = b.receipts[b.receipts.length - 1];
92
+ console.log(`${indent} ${chalk.dim('delivery:')} ${latest.status}`);
93
+ }
94
+ if (b.continuedAt) {
95
+ console.log(`${indent} ${chalk.green('continued')} ${relTime(b.continuedAt)}`);
96
+ }
97
+ if (b.notifiedAt) {
98
+ console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
99
+ }
100
+ if (!b.answer && !b.parkedAt) {
101
+ console.log(`${indent} ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
102
+ }
103
+ console.log();
104
+ }
105
+ /** Human summary line for one outcome rollup. */
106
+ export function formatOutcomeHeader(group) {
107
+ const { agents, open, answered, parked } = group.counts;
108
+ const parts = [
109
+ `${agents} agent${agents === 1 ? '' : 's'}`,
110
+ open > 0 ? `${open} needs you` : null,
111
+ answered > 0 ? `${answered} answered` : null,
112
+ parked > 0 ? `${parked} parked` : null,
113
+ ].filter(Boolean);
114
+ return `${group.outcome.label} · ${parts.join(' · ')}`;
115
+ }
116
+ function renderOutcomeGroup(group, localHost) {
117
+ console.log(chalk.bold(formatOutcomeHeader(group)));
118
+ if (isUnambiguousOutcomeAnswer(group) && openBlocksForOutcome(group).length > 1) {
119
+ const ids = openBlocksForOutcome(group).map((b) => b.mailboxId).join(', ');
120
+ console.log(chalk.dim(` same question on ${openBlocksForOutcome(group).length} agents — fan-out safe: ${ids}`));
121
+ }
122
+ for (const b of group.blocks) {
123
+ renderBlock(b, localHost, ' ');
124
+ }
125
+ }
126
+ /** Map active sessions into the lightweight hints outcome enrichment needs. */
127
+ export function sessionHintsFromActive(sessions) {
128
+ return sessions.map((s) => ({
129
+ sessionId: s.sessionId,
130
+ agentId: s.agentId,
131
+ // Same precedence as mailboxIdForActiveSession (agentId ?? sessionId).
132
+ mailboxId: s.agentId ?? s.sessionId,
133
+ ticketId: s.ticket?.id,
134
+ prNumber: s.pr?.number,
135
+ prUrl: s.pr?.url,
136
+ worktreeSlug: s.worktree?.slug,
137
+ }));
138
+ }
139
+ export function registerFeedCommand(program) {
140
+ program
141
+ .command('feed')
142
+ .description('List open blocks -- decisions agents are waiting on (grouped by outcome)')
143
+ .option('--json', 'Output as JSON (each block stamped with its outcome + ask class)')
144
+ .option('--flat', 'List one block per agent instead of grouping by outcome')
145
+ .option('--all', 'Include stalls/FYIs that policy would suppress (default: hide them)')
146
+ .option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
147
+ .option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
148
+ .option('--device <target...>', 'Alias for --host; repeatable')
149
+ .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
150
+ .action(async (opts) => {
151
+ if (opts.device?.length)
152
+ opts.host = [...(opts.host ?? []), ...opts.device];
153
+ const self = machineId();
154
+ const includeLocal = shouldIncludeLocalFeed(opts.host, self);
155
+ const setupWarnings = [];
156
+ if (includeLocal) {
157
+ const hookInstall = ensureFeedPublishHook();
158
+ if (hookInstall.error) {
159
+ setupWarnings.push(hookInstall.error);
160
+ }
161
+ else {
162
+ const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
163
+ import('../lib/hooks.js'),
164
+ import('../lib/versions.js'),
165
+ ]);
166
+ const manifest = parseHookManifest({ warn: false });
167
+ for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
168
+ const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
169
+ if (result.errors.length > 0) {
170
+ setupWarnings.push(`${agent}@${version}: ${result.errors.join('; ')}`);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ // Active sessions feed both the GC sweep and outcome enrichment (ticket/PR).
176
+ let sessions = [];
177
+ if (includeLocal) {
178
+ sessions = await getActiveSessions();
179
+ }
180
+ if (opts.dispatch && includeLocal) {
181
+ // Liveness sweep: drop messages to dead agents and retire stale blocks
182
+ // before we render the feed.
183
+ const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => !!id));
184
+ const gcResult = gcMailbox(activeBoxIds);
185
+ if (gcResult.blocksRemoved > 0 || gcResult.messagesDroppedDead > 0) {
186
+ console.log(chalk.yellow(`gc: ${gcResult.messagesDroppedDead} dead messages, ${gcResult.blocksRemoved} stale blocks removed`));
187
+ }
188
+ }
189
+ const localBlocks = includeLocal ? listBlocks() : [];
190
+ let blocks = localBlocks;
191
+ const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
192
+ if (!forceLocal) {
193
+ const remoteHosts = remoteFeedHostsToDial(opts.host, self);
194
+ if (!opts.host?.length || (remoteHosts && remoteHosts.length > 0)) {
195
+ const remote = await gatherRemoteAgentsJson({
196
+ // Bare --json stays a block array so older peers and scripts keep working.
197
+ args: ['feed', '--json'],
198
+ noFanoutEnv: FEED_NO_FANOUT_ENV,
199
+ hosts: remoteHosts,
200
+ parse: parseRemoteFeed,
201
+ });
202
+ blocks = mergeFeedBlocks(localBlocks, remote.items);
203
+ }
204
+ }
205
+ // Fill missing ticket/PR/worktree from live session meta so outcome keys
206
+ // land even when the publish hook had no deliverable stamp.
207
+ if (sessions.length > 0) {
208
+ blocks = enrichBlocksFromSessions(blocks, sessionHintsFromActive(sessions));
209
+ }
210
+ // Stall suppression (RUSH-1477): auto-answer "should I…?" / "what's next?"
211
+ // so they never render. Applied on every feed read unless --all; --dispatch
212
+ // also applies so unattended ticks drain the stall backlog.
213
+ const filter = filterBlocksForFeed(blocks, {
214
+ apply: !opts.all || opts.dispatch === true,
215
+ });
216
+ if (!opts.all) {
217
+ blocks = filter.surfaced;
218
+ }
219
+ const digest = suppressionDigest(filter);
220
+ if (digest && !opts.json) {
221
+ console.log(chalk.dim(digest));
222
+ }
223
+ if (opts.dispatch) {
224
+ const policy = loadPolicy();
225
+ const now = new Date();
226
+ for (const b of blocks) {
227
+ const result = applyPolicyToBlock(b, policy, now);
228
+ if (result.action !== 'none') {
229
+ console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
230
+ }
231
+ if (isPhoneUrgent(b, policy)) {
232
+ const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
233
+ if (notifyResult.ok && !notifyResult.skipped) {
234
+ recordNotified(b.blockId);
235
+ console.log(`${chalk.green('notified')} ${b.blockId}`);
236
+ }
237
+ else if (notifyResult.error) {
238
+ console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
239
+ }
240
+ }
241
+ }
242
+ }
243
+ for (const warning of setupWarnings) {
244
+ console.error(chalk.yellow(`Feed hook setup warning: ${warning}`));
245
+ }
246
+ if (opts.json) {
247
+ // Always a block array (stamped with outcome + ask class) so remote fan-out
248
+ // and scripts keep a stable contract. Human grouping is text-only.
249
+ const stamped = stampBlockOutcomes(blocks).map((b) => ({
250
+ ...b,
251
+ ask: classifyBlock(b),
252
+ }));
253
+ console.log(JSON.stringify(stamped, null, 2));
254
+ return;
255
+ }
256
+ if (blocks.length === 0) {
257
+ console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
258
+ return;
259
+ }
260
+ if (opts.flat) {
261
+ console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
262
+ for (const b of blocks)
263
+ renderBlock(b, self);
264
+ return;
265
+ }
266
+ const groups = groupBlocksByOutcome(blocks);
267
+ const openOutcomes = groups.filter((g) => g.counts.open > 0).length;
268
+ console.log(chalk.bold(`${groups.length} outcome${groups.length === 1 ? '' : 's'} · ${blocks.length} block${blocks.length === 1 ? '' : 's'}` +
269
+ (openOutcomes > 0 ? ` · ${openOutcomes} need you` : '') +
270
+ ':\n'));
271
+ for (const g of groups)
272
+ renderOutcomeGroup(g, self);
273
+ });
274
+ }
@@ -1,10 +1,14 @@
1
1
  /**
2
- * `agents logs` — unified, discoverable run-log viewer.
2
+ * `agents logs` — unified, discoverable run-log viewer + audit trail.
3
3
  *
4
4
  * Resolves a run across two substrates and shows (or `-f` follows) its log:
5
5
  * - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
6
6
  * - sessions (the local index) → transcript, tailed via the sessions tailer
7
7
  *
8
+ * Subcommands:
9
+ * - `agents logs audit` — read the structured audit/event log
10
+ * - `agents logs stats` — show aggregate audit statistics
11
+ *
8
12
  * Concise by default: a bare `agents logs <id>` prints the same summary digest as
9
13
  * `agents sessions <id>` — cheap for an agent to glance at. The token-heavy full
10
14
  * transcript / raw stdout is opt-in behind `--full` (alias `-m/--markdown`).