@phnx-labs/agents-cli 1.20.52 → 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 (107) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +3 -0
  5. package/dist/commands/cloud.js +2 -1
  6. package/dist/commands/exec.js +65 -1
  7. package/dist/commands/feed.d.ts +29 -0
  8. package/dist/commands/feed.js +237 -32
  9. package/dist/commands/memory.d.ts +9 -0
  10. package/dist/commands/memory.js +164 -0
  11. package/dist/commands/message.d.ts +11 -6
  12. package/dist/commands/message.js +140 -5
  13. package/dist/commands/routines.js +12 -0
  14. package/dist/commands/secrets-migrate.d.ts +2 -1
  15. package/dist/commands/secrets-migrate.js +88 -13
  16. package/dist/commands/secrets.js +4 -1
  17. package/dist/commands/sessions.js +10 -1
  18. package/dist/commands/worktree.js +4 -2
  19. package/dist/index.js +16 -21
  20. package/dist/lib/agents.js +249 -17
  21. package/dist/lib/answer-router.d.ts +75 -0
  22. package/dist/lib/answer-router.js +149 -0
  23. package/dist/lib/ask-classifier.d.ts +71 -0
  24. package/dist/lib/ask-classifier.js +197 -0
  25. package/dist/lib/cloud/antigravity.d.ts +0 -2
  26. package/dist/lib/cloud/antigravity.js +2 -17
  27. package/dist/lib/cloud/codex.js +3 -18
  28. package/dist/lib/cloud/rush.js +3 -15
  29. package/dist/lib/cloud/stream.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +21 -0
  31. package/dist/lib/cloud/types.js +81 -0
  32. package/dist/lib/crabbox/cli.d.ts +1 -1
  33. package/dist/lib/crabbox/cli.js +12 -2
  34. package/dist/lib/crabbox/lease.d.ts +13 -0
  35. package/dist/lib/crabbox/lease.js +11 -2
  36. package/dist/lib/crabbox/progress.d.ts +62 -0
  37. package/dist/lib/crabbox/progress.js +129 -0
  38. package/dist/lib/events.js +4 -1
  39. package/dist/lib/exec.js +19 -1
  40. package/dist/lib/feed-outcome.d.ts +101 -0
  41. package/dist/lib/feed-outcome.js +244 -0
  42. package/dist/lib/feed-policy.d.ts +30 -0
  43. package/dist/lib/feed-policy.js +133 -0
  44. package/dist/lib/feed.d.ts +127 -3
  45. package/dist/lib/feed.js +416 -40
  46. package/dist/lib/git.d.ts +17 -1
  47. package/dist/lib/git.js +20 -1
  48. package/dist/lib/hooks.js +522 -12
  49. package/dist/lib/hosts/passthrough.d.ts +3 -3
  50. package/dist/lib/hosts/passthrough.js +3 -4
  51. package/dist/lib/mailbox-gc.d.ts +22 -0
  52. package/dist/lib/mailbox-gc.js +161 -0
  53. package/dist/lib/mailbox.d.ts +26 -2
  54. package/dist/lib/mailbox.js +80 -5
  55. package/dist/lib/mcp.js +82 -0
  56. package/dist/lib/memory.d.ts +55 -0
  57. package/dist/lib/memory.js +274 -0
  58. package/dist/lib/notify.d.ts +16 -0
  59. package/dist/lib/notify.js +61 -0
  60. package/dist/lib/operator.d.ts +26 -0
  61. package/dist/lib/operator.js +107 -0
  62. package/dist/lib/plugins.d.ts +35 -0
  63. package/dist/lib/plugins.js +217 -0
  64. package/dist/lib/remote-agents-json.d.ts +14 -0
  65. package/dist/lib/remote-agents-json.js +94 -0
  66. package/dist/lib/resources/mcp.js +44 -0
  67. package/dist/lib/resources/memory.d.ts +15 -0
  68. package/dist/lib/resources/memory.js +46 -0
  69. package/dist/lib/resources/types.d.ts +2 -2
  70. package/dist/lib/runner.d.ts +43 -0
  71. package/dist/lib/runner.js +323 -74
  72. package/dist/lib/sandbox.js +6 -0
  73. package/dist/lib/secrets/bundles.js +38 -13
  74. package/dist/lib/secrets/icloud-import.d.ts +12 -3
  75. package/dist/lib/secrets/icloud-import.js +37 -7
  76. package/dist/lib/secrets/index.d.ts +106 -2
  77. package/dist/lib/secrets/index.js +603 -28
  78. package/dist/lib/session/active.d.ts +18 -0
  79. package/dist/lib/session/active.js +47 -17
  80. package/dist/lib/session/db.d.ts +9 -1
  81. package/dist/lib/session/db.js +18 -3
  82. package/dist/lib/session/discover.d.ts +13 -0
  83. package/dist/lib/session/discover.js +31 -0
  84. package/dist/lib/session/parse.d.ts +8 -0
  85. package/dist/lib/session/parse.js +42 -21
  86. package/dist/lib/session/remote-active.js +8 -89
  87. package/dist/lib/session/state.d.ts +11 -0
  88. package/dist/lib/session/state.js +37 -0
  89. package/dist/lib/session/tail.d.ts +23 -4
  90. package/dist/lib/session/tail.js +34 -16
  91. package/dist/lib/session/throughput.d.ts +30 -0
  92. package/dist/lib/session/throughput.js +86 -0
  93. package/dist/lib/shim-heal.d.ts +12 -3
  94. package/dist/lib/shim-heal.js +12 -6
  95. package/dist/lib/staleness/detectors/subagents.js +57 -3
  96. package/dist/lib/staleness/writers/hooks.js +7 -3
  97. package/dist/lib/staleness/writers/subagents.js +37 -6
  98. package/dist/lib/startup/command-registry.d.ts +1 -0
  99. package/dist/lib/startup/command-registry.js +2 -0
  100. package/dist/lib/subagents.d.ts +52 -0
  101. package/dist/lib/subagents.js +315 -12
  102. package/dist/lib/teams/worktree.d.ts +8 -0
  103. package/dist/lib/teams/worktree.js +8 -0
  104. package/dist/lib/types.d.ts +10 -2
  105. package/dist/lib/versions.js +38 -48
  106. package/package.json +4 -3
  107. package/scripts/postinstall.js +61 -1
@@ -5,6 +5,87 @@
5
5
  * Factory) implement, plus the shared task and event types that flow through
6
6
  * the dispatch pipeline.
7
7
  */
8
+ /**
9
+ * Normalize a provider's raw wire status into the canonical `CloudTaskStatus`.
10
+ *
11
+ * Each cloud backend speaks its own status vocabulary and had its own copy of
12
+ * this mapping, which had drifted. This dispatches per provider while keeping
13
+ * provider-specific defaults explicit:
14
+ * - `rush` — switch over the Factory Floor's known strings; includes
15
+ * `allocating` and stopped/resumable `idle` states, has no
16
+ * `queued`, default `running`.
17
+ * - `codex` — substring match on the lowercased CLI status; default
18
+ * `running`.
19
+ * - `antigravity` — substring match on the (possibly `undefined`) Interactions
20
+ * API status; default `completed` (its synchronous response
21
+ * is terminal), and `undefined`-safe.
22
+ *
23
+ * Factory's `mapResultStatus` is structurally different (it maps a droid *exit*
24
+ * result, not a lifecycle string) and deliberately stays in `factory.ts`.
25
+ */
26
+ export function normalizeProviderStatus(provider, wireStatus) {
27
+ switch (provider) {
28
+ case 'rush':
29
+ return normalizeRushStatus(wireStatus ?? '');
30
+ case 'codex':
31
+ return normalizeCodexStatus(wireStatus ?? '');
32
+ case 'antigravity':
33
+ return normalizeAntigravityStatus(wireStatus);
34
+ }
35
+ }
36
+ /** Rush Factory Floor status → canonical enum. Default `running`; no `queued`. */
37
+ function normalizeRushStatus(s) {
38
+ switch (s) {
39
+ case 'allocating': return 'allocating';
40
+ case 'running': return 'running';
41
+ case 'idle':
42
+ case 'paused':
43
+ case 'needs_review': return 'idle';
44
+ case 'input_required': return 'input_required';
45
+ case 'completed': return 'completed';
46
+ case 'failed': return 'failed';
47
+ case 'cancelled': return 'cancelled';
48
+ default: return 'running';
49
+ }
50
+ }
51
+ /** Codex Cloud CLI status → canonical enum. Substring match; default `running`. */
52
+ function normalizeCodexStatus(s) {
53
+ const lower = s.toLowerCase();
54
+ if (lower.includes('queued') || lower.includes('pending'))
55
+ return 'queued';
56
+ if (lower.includes('running') || lower.includes('in_progress'))
57
+ return 'running';
58
+ if (lower.includes('idle') || lower.includes('paused') || lower.includes('needs_review'))
59
+ return 'idle';
60
+ if (lower.includes('completed') || lower.includes('succeeded') || lower.includes('success'))
61
+ return 'completed';
62
+ if (lower.includes('failed') || lower.includes('error'))
63
+ return 'failed';
64
+ if (lower.includes('cancelled') || lower.includes('canceled'))
65
+ return 'cancelled';
66
+ return 'running';
67
+ }
68
+ /**
69
+ * Antigravity Interactions API status → canonical enum. Substring match,
70
+ * `undefined`-safe; default `completed` because the synchronous response is
71
+ * already terminal.
72
+ */
73
+ function normalizeAntigravityStatus(s) {
74
+ const lower = (s ?? '').toLowerCase();
75
+ if (lower.includes('queue') || lower.includes('pending'))
76
+ return 'queued';
77
+ if (lower.includes('run') || lower.includes('progress'))
78
+ return 'running';
79
+ if (lower.includes('idle') || lower.includes('paused') || lower.includes('needs_review'))
80
+ return 'idle';
81
+ if (lower.includes('complete') || lower.includes('success'))
82
+ return 'completed';
83
+ if (lower.includes('fail') || lower.includes('error'))
84
+ return 'failed';
85
+ if (lower.includes('cancel'))
86
+ return 'cancelled';
87
+ return 'completed';
88
+ }
8
89
  /** Maximum images allowed per dispatch (matches Cursor Background Agents). */
9
90
  export const MAX_IMAGES_PER_DISPATCH = 5;
10
91
  /**
@@ -62,7 +62,7 @@ export interface WarmupOptions extends CrabboxOptions {
62
62
  * created even if warmup's stdout format changes — the new lease id is the one
63
63
  * that wasn't present before.
64
64
  */
65
- export declare function crabboxWarmup(opts?: WarmupOptions): CrabboxBox;
65
+ export declare function crabboxWarmup(opts?: WarmupOptions): Promise<CrabboxBox>;
66
66
  /**
67
67
  * Poll until the box reports ready, or throw after timeoutMs.
68
68
  * `sleep` is injectable so tests don't wall-clock wait.
@@ -87,7 +87,7 @@ export function crabboxFind(slug, opts = {}) {
87
87
  * created even if warmup's stdout format changes — the new lease id is the one
88
88
  * that wasn't present before.
89
89
  */
90
- export function crabboxWarmup(opts = {}) {
90
+ export async function crabboxWarmup(opts = {}) {
91
91
  findCrabbox();
92
92
  const env = crabboxEnv(opts);
93
93
  const before = new Set(crabboxList(opts).map((b) => b.lease));
@@ -100,7 +100,17 @@ export function crabboxWarmup(opts = {}) {
100
100
  args.push('--provider', opts.provider);
101
101
  if (opts.code)
102
102
  args.push('--code');
103
- const r = spawnSync('crabbox', args, { encoding: 'utf-8', env, stdio: ['ignore', 'pipe', 'pipe'] });
103
+ // Async spawn (not spawnSync): provisioning takes 30-90s and a blocking call
104
+ // would freeze any caller's progress spinner. Output is captured, not streamed.
105
+ const r = await new Promise((resolve) => {
106
+ const proc = spawn('crabbox', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
107
+ let stdout = '';
108
+ let stderr = '';
109
+ proc.stdout.on('data', (c) => (stdout += c.toString('utf-8')));
110
+ proc.stderr.on('data', (c) => (stderr += c.toString('utf-8')));
111
+ proc.on('error', () => resolve({ status: null, stdout, stderr }));
112
+ proc.on('close', (code) => resolve({ status: code, stdout, stderr }));
113
+ });
104
114
  if (r.status !== 0) {
105
115
  const detail = (r.stderr || r.stdout || '').trim();
106
116
  throw new Error(`crabbox warmup failed: ${detail || 'unknown error'}. ` +
@@ -9,6 +9,17 @@
9
9
  import type { AgentId } from '../types.js';
10
10
  import { type CrabboxBox } from './cli.js';
11
11
  import { type DetectedRuntime } from './runtimes.js';
12
+ /** Phase signal for a lease run, so the command layer can drive a progress UI. */
13
+ export type LeasePhase = {
14
+ kind: 'warmup';
15
+ backend?: string;
16
+ } | {
17
+ kind: 'ready';
18
+ box: CrabboxBox;
19
+ elapsedMs: number;
20
+ } | {
21
+ kind: 'teardown';
22
+ };
12
23
  export interface LeaseRunOptions {
13
24
  agent: string;
14
25
  prompt: string;
@@ -24,6 +35,8 @@ export interface LeaseRunOptions {
24
35
  /** Secrets bundle providing crabbox's provider token. */
25
36
  secretsBundle?: string;
26
37
  onData?: (s: string) => void;
38
+ /** Progress phases (warmup → ready → teardown) for a command-layer spinner. */
39
+ onPhase?: (phase: LeasePhase) => void;
27
40
  /** Keep the box after the run instead of stopping it. */
28
41
  keep?: boolean;
29
42
  /**
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { crabboxWarmup, crabboxWaitReady, crabboxRunScript, crabboxStop } from './cli.js';
10
10
  import { buildCredentialScript, CLAUDE_TOKEN_REMOTE } from './runtimes.js';
11
+ import { LEASE_AGENT_MARKER } from './progress.js';
11
12
  /** POSIX single-quote for safe embedding in the generated bootstrap script. */
12
13
  function q(s) {
13
14
  return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -68,6 +69,9 @@ export function buildBootstrapScript(opts) {
68
69
  ENSURE_AGENTS_CLI,
69
70
  installRuntimes,
70
71
  credScript,
72
+ // Marker on its own line: the command layer shows everything before this as
73
+ // setup progress and everything after (the agent's output) verbatim.
74
+ `echo ${q(LEASE_AGENT_MARKER)}`,
71
75
  `${runParts.join(' ')}`,
72
76
  'rc=$?',
73
77
  shred,
@@ -77,13 +81,16 @@ export function buildBootstrapScript(opts) {
77
81
  .join('\n');
78
82
  }
79
83
  export async function leaseAndRun(opts) {
80
- const box = crabboxWarmup({
84
+ const startedAt = Date.now();
85
+ opts.onPhase?.({ kind: 'warmup', backend: opts.backend });
86
+ const box = await crabboxWarmup({
81
87
  class: opts.boxClass,
82
88
  profile: opts.profile,
83
89
  provider: opts.backend,
84
90
  secretsBundle: opts.secretsBundle,
85
91
  });
86
92
  await crabboxWaitReady(box.slug, { secretsBundle: opts.secretsBundle });
93
+ opts.onPhase?.({ kind: 'ready', box, elapsedMs: Date.now() - startedAt });
87
94
  const script = buildBootstrapScript(opts);
88
95
  let exitCode = null;
89
96
  let toreDown = false;
@@ -96,8 +103,10 @@ export async function leaseAndRun(opts) {
96
103
  finally {
97
104
  // Always attempt teardown (bounds credential lifetime to the run) unless the
98
105
  // caller explicitly asked to keep the box.
99
- if (!opts.keep)
106
+ if (!opts.keep) {
107
+ opts.onPhase?.({ kind: 'teardown' });
100
108
  toreDown = crabboxStop(box.slug, { secretsBundle: opts.secretsBundle });
109
+ }
101
110
  }
102
111
  return { box, exitCode, toreDown };
103
112
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Progress routing for `agents run --lease`.
3
+ *
4
+ * The box-side bootstrap emits `LEASE_AGENT_MARKER` on its own line right before
5
+ * `agents run`. Everything the crabbox run streams BEFORE that marker is setup
6
+ * noise (sync bytes, node/agents install, `agents setup`) — shown as spinner
7
+ * text and captured for a failure dump. Everything AFTER is the agent's own
8
+ * output — printed through verbatim. The marker line itself is swallowed.
9
+ */
10
+ /** Sentinel echoed on the box right before `agents run`. Distinctive + collision-proof. */
11
+ export declare const LEASE_AGENT_MARKER = "___AGENTS_LEASE_AGENT_OUTPUT_b1f4c2___";
12
+ export interface Spinner {
13
+ /** Begin animating a phase (renders `frame text` on one line, TTY only). */
14
+ start(text: string): void;
15
+ /** Change the text shown; rendered on the next throttled tick (no write itself). */
16
+ update(text: string): void;
17
+ /** Finalize the current line with a symbol (e.g. ✔) and a newline. */
18
+ stopAndPersist(symbol: string, text: string): void;
19
+ /** Clear the current animated line without persisting anything. */
20
+ stop(): void;
21
+ /** True while a phase is active. */
22
+ readonly active: boolean;
23
+ }
24
+ /**
25
+ * A deliberately minimal, self-throttled spinner. Unlike `ora`, it does NOT hook
26
+ * the stream's `write` to re-render on external output — it writes exactly one
27
+ * short line per fixed tick and nowhere else, so it is structurally incapable of
28
+ * a re-render feedback loop (the failure mode that made `ora` blow up when a
29
+ * lease streamed output past a live spinner). On a non-TTY it prints each phase
30
+ * label once and stays silent on `update`, so piped/CI output never floods.
31
+ *
32
+ * Only ever run ONE phase at a time, and never stream other output to the same
33
+ * stream while a phase is active — stop it first.
34
+ */
35
+ export declare function createSpinner(opts?: {
36
+ stream?: {
37
+ write(s: string): unknown;
38
+ isTTY?: boolean;
39
+ };
40
+ enabled?: boolean;
41
+ intervalMs?: number;
42
+ }): Spinner;
43
+ export interface LeaseOutputRouter {
44
+ /** Feed a raw chunk of the crabbox run's combined stdout/stderr. */
45
+ push(chunk: string): void;
46
+ /** Flush any buffered partial line (call once the stream closes). */
47
+ end(): void;
48
+ /** True once the agent-output marker has been seen. */
49
+ sawAgent(): boolean;
50
+ /** The setup lines seen so far (for a failure dump). */
51
+ setupLines(): string[];
52
+ }
53
+ /**
54
+ * Split the crabbox run stream at `LEASE_AGENT_MARKER`. `onSetupLine` fires for
55
+ * each complete non-empty line before the marker; `onAgentChunk` fires with raw
56
+ * text after it (streamed promptly, not line-buffered, so agent output is live).
57
+ */
58
+ export declare function createLeaseOutputRouter(cb: {
59
+ onSetupLine: (line: string) => void;
60
+ onAgentChunk: (chunk: string) => void;
61
+ marker?: string;
62
+ }): LeaseOutputRouter;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Progress routing for `agents run --lease`.
3
+ *
4
+ * The box-side bootstrap emits `LEASE_AGENT_MARKER` on its own line right before
5
+ * `agents run`. Everything the crabbox run streams BEFORE that marker is setup
6
+ * noise (sync bytes, node/agents install, `agents setup`) — shown as spinner
7
+ * text and captured for a failure dump. Everything AFTER is the agent's own
8
+ * output — printed through verbatim. The marker line itself is swallowed.
9
+ */
10
+ /** Sentinel echoed on the box right before `agents run`. Distinctive + collision-proof. */
11
+ export const LEASE_AGENT_MARKER = '___AGENTS_LEASE_AGENT_OUTPUT_b1f4c2___';
12
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
+ /**
14
+ * A deliberately minimal, self-throttled spinner. Unlike `ora`, it does NOT hook
15
+ * the stream's `write` to re-render on external output — it writes exactly one
16
+ * short line per fixed tick and nowhere else, so it is structurally incapable of
17
+ * a re-render feedback loop (the failure mode that made `ora` blow up when a
18
+ * lease streamed output past a live spinner). On a non-TTY it prints each phase
19
+ * label once and stays silent on `update`, so piped/CI output never floods.
20
+ *
21
+ * Only ever run ONE phase at a time, and never stream other output to the same
22
+ * stream while a phase is active — stop it first.
23
+ */
24
+ export function createSpinner(opts = {}) {
25
+ const stream = opts.stream ?? process.stderr;
26
+ const enabled = opts.enabled ?? !!stream.isTTY;
27
+ const intervalMs = opts.intervalMs ?? 120;
28
+ const CLEAR = '\r\u001b[2K';
29
+ let text = '';
30
+ let frame = 0;
31
+ let timer;
32
+ let running = false;
33
+ const write = (s) => stream.write(s);
34
+ return {
35
+ start(t) {
36
+ text = t;
37
+ running = true;
38
+ if (!enabled) {
39
+ write(`${t}\n`);
40
+ return;
41
+ }
42
+ if (timer)
43
+ return;
44
+ frame = 0;
45
+ write(`${CLEAR}${SPINNER_FRAMES[0]} ${text}`);
46
+ timer = setInterval(() => {
47
+ frame = (frame + 1) % SPINNER_FRAMES.length;
48
+ write(`${CLEAR}${SPINNER_FRAMES[frame]} ${text}`);
49
+ }, intervalMs);
50
+ },
51
+ update(t) {
52
+ text = t; // next tick renders it; on a non-TTY we stay silent (no flood)
53
+ },
54
+ stopAndPersist(symbol, t) {
55
+ if (timer) {
56
+ clearInterval(timer);
57
+ timer = undefined;
58
+ }
59
+ running = false;
60
+ write(enabled ? `${CLEAR}${symbol} ${t}\n` : `${symbol} ${t}\n`);
61
+ },
62
+ stop() {
63
+ if (timer) {
64
+ clearInterval(timer);
65
+ timer = undefined;
66
+ if (enabled)
67
+ write(CLEAR);
68
+ }
69
+ running = false;
70
+ },
71
+ get active() {
72
+ return running;
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Split the crabbox run stream at `LEASE_AGENT_MARKER`. `onSetupLine` fires for
78
+ * each complete non-empty line before the marker; `onAgentChunk` fires with raw
79
+ * text after it (streamed promptly, not line-buffered, so agent output is live).
80
+ */
81
+ export function createLeaseOutputRouter(cb) {
82
+ const marker = cb.marker ?? LEASE_AGENT_MARKER;
83
+ let seen = false;
84
+ let buf = '';
85
+ const setup = [];
86
+ const emitLine = (line) => {
87
+ const t = line.replace(/\r$/, '');
88
+ if (t.trim()) {
89
+ setup.push(t);
90
+ cb.onSetupLine(t);
91
+ }
92
+ };
93
+ return {
94
+ push(chunk) {
95
+ if (seen) {
96
+ cb.onAgentChunk(chunk);
97
+ return;
98
+ }
99
+ buf += chunk;
100
+ let idx;
101
+ while ((idx = buf.indexOf('\n')) !== -1) {
102
+ const line = buf.slice(0, idx);
103
+ buf = buf.slice(idx + 1);
104
+ if (line.includes(marker)) {
105
+ seen = true;
106
+ // Anything already buffered past the marker is agent output.
107
+ if (buf) {
108
+ cb.onAgentChunk(buf);
109
+ buf = '';
110
+ }
111
+ return;
112
+ }
113
+ emitLine(line);
114
+ }
115
+ // A trailing partial line stays in `buf` — it may be the marker forming.
116
+ },
117
+ end() {
118
+ if (!seen && buf)
119
+ emitLine(buf);
120
+ buf = '';
121
+ },
122
+ sawAgent() {
123
+ return seen;
124
+ },
125
+ setupLines() {
126
+ return setup;
127
+ },
128
+ };
129
+ }
@@ -22,9 +22,12 @@ import { getUserAgentsDir } from './state.js';
22
22
  // ─── Constants ────────────────────────────────────────────────────────────────
23
23
  // Resolved lazily: events.ts is imported transitively by most CLI surfaces, and
24
24
  // import itself must stay side-effect free. Tests may override the exact path.
25
+ // AGENTS_EVENTS_PATH redirects the sink — a test seam like AGENTS_SECRETS_AGENT_DIR:
26
+ // unlike _resetForTest it survives a bare reset AND propagates to CLI subprocesses
27
+ // a test spawns, so fixture events can never land in the user's real log (#910).
25
28
  let _eventsPath;
26
29
  function eventsPath() {
27
- return (_eventsPath ??= path.join(getUserAgentsDir(), 'events.jsonl'));
30
+ return (_eventsPath ??= process.env.AGENTS_EVENTS_PATH || path.join(getUserAgentsDir(), 'events.jsonl'));
28
31
  }
29
32
  function eventsDir() {
30
33
  return path.dirname(eventsPath());
package/dist/lib/exec.js CHANGED
@@ -398,7 +398,10 @@ export const AGENT_COMMANDS = {
398
398
  modelFlag: '--model',
399
399
  },
400
400
  kiro: {
401
- base: ['kiro-cli'],
401
+ // Standalone hooks live under ~/.kiro/hooks/*.json and only fire on the
402
+ // v3 engine (opt-in via --v3; see https://kiro.dev/docs/cli/v3/hooks/).
403
+ // Without this flag agents-cli would write v3 hook files that never run.
404
+ base: ['kiro-cli', '--v3'],
402
405
  promptFlag: 'positional',
403
406
  modeFlags: {
404
407
  // kiro-cli has no permission flags — edit is the default behavior.
@@ -471,6 +474,21 @@ export const AGENT_COMMANDS = {
471
474
  jsonFlags: ['-o', 'stream-json'],
472
475
  modelFlag: '-m',
473
476
  },
477
+ hermes: {
478
+ base: ['hermes', 'chat'],
479
+ promptFlag: 'positional',
480
+ modeFlags: {
481
+ edit: [],
482
+ },
483
+ modelFlag: '--model',
484
+ },
485
+ forge: {
486
+ base: ['forge'],
487
+ promptFlag: 'positional',
488
+ modeFlags: {
489
+ edit: [],
490
+ },
491
+ },
474
492
  };
475
493
  /**
476
494
  * Whether `agent` has a native resume form (Tier 1). Derived solely from the
@@ -0,0 +1,101 @@
1
+ import type { OpenBlock } from './feed.js';
2
+ export type OutcomeKind = 'ticket' | 'pr' | 'worktree' | 'unassigned';
3
+ /** Stable attribution of a block (or agent) to one deliverable. */
4
+ export interface OutcomeRef {
5
+ /** Map/JSON key, e.g. `ticket:RUSH-1125`, `pr:#534`, `worktree:headless-secrets`, `unassigned`. */
6
+ key: string;
7
+ kind: OutcomeKind;
8
+ /** Human header label, e.g. `RUSH-1125`, `PR#534`, `headless-secrets`, `Unassigned`. */
9
+ label: string;
10
+ }
11
+ /**
12
+ * Signals used to derive an outcome. All optional — callers pass whatever they
13
+ * already know (block fields, session meta, free-text questions).
14
+ */
15
+ export interface OutcomeSignals {
16
+ ticket?: string | null;
17
+ pr?: string | null;
18
+ worktreeSlug?: string | null;
19
+ /** Branch name — scanned for `rush-1125`-style ticket slugs. */
20
+ branch?: string | null;
21
+ /** Epic / team label when no ticket/PR/worktree is known. */
22
+ epic?: string | null;
23
+ /** Free text (question headers + bodies) scanned for ticket/PR refs. */
24
+ text?: string | null;
25
+ }
26
+ /**
27
+ * Normalize a PR ref.
28
+ * - Full GitHub URL → `owner/repo#N` (repo identity included)
29
+ * - Bare `#123` / `PR#123` → `#123` (no repo known)
30
+ */
31
+ export declare function normalizePrRef(raw: string | null | undefined): string | undefined;
32
+ /** `https://github.com/owner/repo/pull/N` → `owner/repo`. */
33
+ export declare function repoFromPrUrl(url: string | null | undefined): string | undefined;
34
+ /** Canonical ticket id (uppercase team key). */
35
+ export declare function normalizeTicketRef(raw: string | null | undefined): string | undefined;
36
+ /**
37
+ * Derive the single outcome a block/agent belongs to.
38
+ * Exactly one outcome per call — never ambiguous, never empty.
39
+ */
40
+ export declare function deriveOutcome(signals: OutcomeSignals): OutcomeRef;
41
+ /** Join question headers + bodies into one scan target for ticket/PR detection. */
42
+ export declare function blockScanText(block: Pick<OpenBlock, 'questions'>): string;
43
+ /** Outcome for a stored open block (uses stamped fields + question text). */
44
+ export declare function outcomeForBlock(block: OpenBlock): OutcomeRef;
45
+ /** Per-outcome rollup used by the default `agents feed` view. */
46
+ export interface OutcomeGroup {
47
+ outcome: OutcomeRef;
48
+ blocks: OpenBlock[];
49
+ counts: {
50
+ /** Distinct mailbox ids under this outcome. */
51
+ agents: number;
52
+ /** Still open: unanswered and not hard-parked. */
53
+ open: number;
54
+ answered: number;
55
+ parked: number;
56
+ };
57
+ }
58
+ /**
59
+ * Collapse blocks under their outcome. Order: needs-you outcomes first
60
+ * (open count desc), then by label. Unassigned always last among ties of 0 open.
61
+ */
62
+ export declare function groupBlocksByOutcome(blocks: OpenBlock[]): OutcomeGroup[];
63
+ /**
64
+ * Stamp each block with its derived outcome for JSON consumers.
65
+ * Does not mutate the input records.
66
+ */
67
+ export declare function stampBlockOutcomes(blocks: OpenBlock[]): Array<OpenBlock & {
68
+ outcome: OutcomeRef;
69
+ }>;
70
+ /**
71
+ * True when every still-open block under the outcome asks the same question
72
+ * (same cluster of header+text). Fan-out answers are only safe when this holds
73
+ * — otherwise the operator must pick a specific agent.
74
+ */
75
+ export declare function isUnambiguousOutcomeAnswer(group: OutcomeGroup): boolean;
76
+ /** Still-open blocks under an outcome (candidates for a fan-out reply). */
77
+ export declare function openBlocksForOutcome(group: OutcomeGroup): OpenBlock[];
78
+ /**
79
+ * Lightweight session signals used to fill missing ticket/PR/worktree on a
80
+ * block at list time (the publish hook may not have had them yet).
81
+ */
82
+ export interface SessionOutcomeHint {
83
+ sessionId?: string | null;
84
+ agentId?: string | null;
85
+ mailboxId?: string | null;
86
+ ticketId?: string | null;
87
+ prNumber?: number | null;
88
+ prUrl?: string | null;
89
+ worktreeSlug?: string | null;
90
+ branch?: string | null;
91
+ }
92
+ /**
93
+ * Overlay session meta onto a block when the block itself is missing ticket/PR/
94
+ * worktree. Never overwrites a field the block already carries.
95
+ */
96
+ export declare function enrichBlockFromSession(block: OpenBlock, hint: SessionOutcomeHint): OpenBlock;
97
+ /**
98
+ * Build a mailboxId → session-hint index and enrich every block. Pure.
99
+ * Matching order: mailboxId, then sessionId, then agentId.
100
+ */
101
+ export declare function enrichBlocksFromSessions(blocks: OpenBlock[], sessions: SessionOutcomeHint[]): OpenBlock[];