agent-relay 3.2.8 → 3.2.10

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 (108) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +45361 -23429
  6. package/dist/src/cli/commands/setup.d.ts +8 -0
  7. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  8. package/dist/src/cli/commands/setup.js +42 -0
  9. package/dist/src/cli/commands/setup.js.map +1 -1
  10. package/dist/src/cli/relaycast-mcp.d.ts.map +1 -1
  11. package/dist/src/cli/relaycast-mcp.js +8 -1
  12. package/dist/src/cli/relaycast-mcp.js.map +1 -1
  13. package/package.json +13 -11
  14. package/packages/acp-bridge/package.json +2 -2
  15. package/packages/config/package.json +1 -1
  16. package/packages/hooks/package.json +4 -4
  17. package/packages/memory/package.json +2 -2
  18. package/packages/openclaw/package.json +3 -3
  19. package/packages/policy/package.json +2 -2
  20. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.d.ts +2 -0
  21. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.d.ts.map +1 -0
  22. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.js +54 -0
  23. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.js.map +1 -0
  24. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.d.ts +2 -0
  25. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.d.ts.map +1 -0
  26. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.js +85 -0
  27. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.js.map +1 -0
  28. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.d.ts +2 -0
  29. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.d.ts.map +1 -0
  30. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.js +67 -0
  31. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.js.map +1 -0
  32. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.d.ts +2 -0
  33. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.d.ts.map +1 -0
  34. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.js +119 -0
  35. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.js.map +1 -0
  36. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.d.ts +2 -0
  37. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.d.ts.map +1 -0
  38. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +130 -0
  39. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -0
  40. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.d.ts +2 -0
  41. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.d.ts.map +1 -0
  42. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.js +42 -0
  43. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.js.map +1 -0
  44. package/packages/sdk/dist/workflows/builder.d.ts +2 -0
  45. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  46. package/packages/sdk/dist/workflows/builder.js +4 -0
  47. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  48. package/packages/sdk/dist/workflows/cli-session-collector.d.ts +39 -0
  49. package/packages/sdk/dist/workflows/cli-session-collector.d.ts.map +1 -0
  50. package/packages/sdk/dist/workflows/cli-session-collector.js +23 -0
  51. package/packages/sdk/dist/workflows/cli-session-collector.js.map +1 -0
  52. package/packages/sdk/dist/workflows/cli.js +228 -48
  53. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  54. package/packages/sdk/dist/workflows/collectors/claude.d.ts +6 -0
  55. package/packages/sdk/dist/workflows/collectors/claude.d.ts.map +1 -0
  56. package/packages/sdk/dist/workflows/collectors/claude.js +330 -0
  57. package/packages/sdk/dist/workflows/collectors/claude.js.map +1 -0
  58. package/packages/sdk/dist/workflows/collectors/codex.d.ts +18 -0
  59. package/packages/sdk/dist/workflows/collectors/codex.d.ts.map +1 -0
  60. package/packages/sdk/dist/workflows/collectors/codex.js +265 -0
  61. package/packages/sdk/dist/workflows/collectors/codex.js.map +1 -0
  62. package/packages/sdk/dist/workflows/collectors/opencode.d.ts +6 -0
  63. package/packages/sdk/dist/workflows/collectors/opencode.d.ts.map +1 -0
  64. package/packages/sdk/dist/workflows/collectors/opencode.js +178 -0
  65. package/packages/sdk/dist/workflows/collectors/opencode.js.map +1 -0
  66. package/packages/sdk/dist/workflows/index.d.ts +3 -0
  67. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  68. package/packages/sdk/dist/workflows/index.js +3 -0
  69. package/packages/sdk/dist/workflows/index.js.map +1 -1
  70. package/packages/sdk/dist/workflows/listr-renderer.d.ts +26 -0
  71. package/packages/sdk/dist/workflows/listr-renderer.d.ts.map +1 -0
  72. package/packages/sdk/dist/workflows/listr-renderer.js +232 -0
  73. package/packages/sdk/dist/workflows/listr-renderer.js.map +1 -0
  74. package/packages/sdk/dist/workflows/run-summary-table.d.ts +4 -0
  75. package/packages/sdk/dist/workflows/run-summary-table.d.ts.map +1 -0
  76. package/packages/sdk/dist/workflows/run-summary-table.js +98 -0
  77. package/packages/sdk/dist/workflows/run-summary-table.js.map +1 -0
  78. package/packages/sdk/dist/workflows/runner.d.ts +11 -0
  79. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  80. package/packages/sdk/dist/workflows/runner.js +91 -26
  81. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  82. package/packages/sdk/dist/workflows/types.d.ts +2 -0
  83. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  84. package/packages/sdk/dist/workflows/types.js.map +1 -1
  85. package/packages/sdk/package.json +5 -3
  86. package/packages/sdk/src/workflows/__tests__/cli-session-collector.test.ts +64 -0
  87. package/packages/sdk/src/workflows/__tests__/collectors/claude.test.ts +104 -0
  88. package/packages/sdk/src/workflows/__tests__/collectors/codex.test.ts +82 -0
  89. package/packages/sdk/src/workflows/__tests__/collectors/opencode.test.ts +178 -0
  90. package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +160 -0
  91. package/packages/sdk/src/workflows/__tests__/step-cwd.test.ts +72 -0
  92. package/packages/sdk/src/workflows/builder.ts +4 -0
  93. package/packages/sdk/src/workflows/cli-session-collector.ts +58 -0
  94. package/packages/sdk/src/workflows/cli.ts +289 -50
  95. package/packages/sdk/src/workflows/collectors/claude.ts +415 -0
  96. package/packages/sdk/src/workflows/collectors/codex.ts +351 -0
  97. package/packages/sdk/src/workflows/collectors/opencode.ts +279 -0
  98. package/packages/sdk/src/workflows/index.ts +3 -0
  99. package/packages/sdk/src/workflows/listr-renderer.ts +278 -0
  100. package/packages/sdk/src/workflows/run-summary-table.ts +110 -0
  101. package/packages/sdk/src/workflows/runner.ts +122 -28
  102. package/packages/sdk/src/workflows/types.ts +2 -0
  103. package/packages/sdk/vitest.config.ts +1 -1
  104. package/packages/sdk-py/pyproject.toml +1 -1
  105. package/packages/telemetry/package.json +1 -1
  106. package/packages/trajectory/package.json +2 -2
  107. package/packages/user-directory/package.json +2 -2
  108. package/packages/utils/package.json +2 -2
@@ -0,0 +1,278 @@
1
+ import chalk from 'chalk';
2
+ import type { ListrTask } from 'listr2';
3
+ import type { WorkflowEvent, WorkflowEventListener } from './runner.js';
4
+
5
+ // Filter console.log while listr owns the terminal.
6
+ // Blocks [broker] noise and [workflow HH:MM] timing lines, but lets the
7
+ // observer URL and channel name through so users can track the run.
8
+ function installOutputFilter(): () => void {
9
+ const orig = console.log.bind(console);
10
+ console.log = (...args: unknown[]) => {
11
+ const str = String(args[0] ?? '');
12
+ // Always show the observer URL and channel so users can follow the run
13
+ if (str.includes('Observer:') || str.includes('agentrelay.dev') || str.includes('Channel: wf-')) {
14
+ orig(...args);
15
+ return;
16
+ }
17
+ // Block [broker] lines and [workflow HH:MM] timing lines
18
+ if (/\[broker\]/.test(str) || /\[workflow\s+\d{2}:\d{2}\]/.test(str)) return;
19
+ orig(...args);
20
+ };
21
+ return () => {
22
+ console.log = orig;
23
+ };
24
+ }
25
+
26
+ interface RenderableTask {
27
+ title: string;
28
+ output: string;
29
+ }
30
+
31
+ interface StepHandle {
32
+ resolve: () => void;
33
+ reject: (error: Error) => void;
34
+ setOutput: (text: string) => void;
35
+ markSkipped: () => void;
36
+ }
37
+
38
+ export interface WorkflowRenderer {
39
+ /** Pass this to `.run({ onEvent })` in your TypeScript workflow. */
40
+ onEvent: WorkflowEventListener;
41
+ /** Start the listr renderer. Run this concurrently with your workflow. */
42
+ start: () => Promise<void>;
43
+ /** Restore console.log after the workflow finishes. */
44
+ unmount: () => void;
45
+ }
46
+
47
+ /**
48
+ * Creates a listr2-based renderer for TypeScript workflows.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { workflow, createWorkflowRenderer } from '@agent-relay/sdk/workflows';
53
+ *
54
+ * const renderer = createWorkflowRenderer();
55
+ * const [result] = await Promise.all([
56
+ * workflow('my-workflow').step(...).run({ onEvent: renderer.onEvent }),
57
+ * renderer.start(),
58
+ * ]);
59
+ * renderer.unmount();
60
+ * ```
61
+ */
62
+ export function createWorkflowRenderer(): WorkflowRenderer {
63
+ const stepHandles = new Map<string, StepHandle>();
64
+
65
+ let resolveWorkflow!: () => void;
66
+ let rejectWorkflow!: (error: Error) => void;
67
+ const workflowDone = new Promise<void>((resolve, reject) => {
68
+ resolveWorkflow = resolve;
69
+ rejectWorkflow = reject;
70
+ });
71
+ // Prevent unhandled rejection if run:failed fires before the header task
72
+ // reaches `await workflowDone`.
73
+ workflowDone.catch(() => {});
74
+
75
+ let setHeader: (text: string) => void = () => {};
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ let listr: any = null;
78
+ const pendingAdds: ListrTask[] = [];
79
+
80
+ async function ensureListr(): Promise<any> {
81
+ if (listr) return listr;
82
+ const { Listr } = await import('listr2');
83
+ listr = new (Listr as any)(
84
+ [
85
+ {
86
+ title: chalk.dim('Workflow starting...'),
87
+ task: async (_ctx, task): Promise<void> => {
88
+ setHeader = (text: string): void => {
89
+ task.title = text;
90
+ };
91
+ await workflowDone;
92
+ },
93
+ } as ListrTask,
94
+ ],
95
+ {
96
+ concurrent: true,
97
+ renderer: process.stdout.isTTY ? 'default' : 'verbose',
98
+ rendererOptions: {
99
+ collapseErrors: false,
100
+ showErrorMessage: true,
101
+ },
102
+ },
103
+ );
104
+ for (const task of pendingAdds) listr.add(task);
105
+ pendingAdds.length = 0;
106
+ return listr;
107
+ }
108
+
109
+ const addTask = (task: ListrTask): void => {
110
+ if (listr) listr.add(task);
111
+ else pendingAdds.push(task);
112
+ };
113
+
114
+ const onEvent: WorkflowEventListener = (event: WorkflowEvent) => {
115
+ switch (event.type) {
116
+ case 'run:started': {
117
+ setHeader(chalk.dim(`[workflow] run ${event.runId.slice(0, 8)}...`));
118
+ break;
119
+ }
120
+
121
+ case 'step:started': {
122
+ let resolveStep!: () => void;
123
+ let rejectStep!: (error: Error) => void;
124
+ let taskRef: RenderableTask | null = null;
125
+ let skipped = false;
126
+
127
+ const done = new Promise<void>((resolve, reject) => {
128
+ resolveStep = resolve;
129
+ rejectStep = reject;
130
+ });
131
+ // Prevent unhandled rejection if the step fails before the listr
132
+ // task function has started and reached `await done`.
133
+ done.catch(() => {});
134
+
135
+ stepHandles.set(event.stepName, {
136
+ resolve: resolveStep,
137
+ reject: rejectStep,
138
+ setOutput: (text: string) => {
139
+ if (taskRef) taskRef.output = text;
140
+ },
141
+ markSkipped: () => {
142
+ skipped = true;
143
+ if (taskRef) taskRef.title = chalk.dim(`${event.stepName} (skipped)`);
144
+ },
145
+ });
146
+
147
+ addTask({
148
+ title: chalk.white(event.stepName),
149
+ task: async (_ctx, task): Promise<void> => {
150
+ taskRef = task as RenderableTask;
151
+ if (skipped) taskRef.title = chalk.dim(`${event.stepName} (skipped)`);
152
+ await done;
153
+ },
154
+ rendererOptions: { persistentOutput: true },
155
+ } as ListrTask);
156
+ break;
157
+ }
158
+
159
+ case 'step:owner-assigned': {
160
+ const handle = stepHandles.get(event.stepName);
161
+ if (handle) {
162
+ handle.setOutput(
163
+ chalk.dim(`> Owner: ${event.ownerName}`) +
164
+ (event.specialistName ? chalk.dim(` · specialist: ${event.specialistName}`) : ''),
165
+ );
166
+ }
167
+ break;
168
+ }
169
+
170
+ case 'step:retrying': {
171
+ stepHandles.get(event.stepName)?.setOutput(chalk.yellow(`Retrying (attempt ${event.attempt})`));
172
+ break;
173
+ }
174
+
175
+ case 'step:nudged': {
176
+ stepHandles.get(event.stepName)?.setOutput(chalk.dim(`> Nudge #${event.nudgeCount}`));
177
+ break;
178
+ }
179
+
180
+ case 'step:force-released': {
181
+ stepHandles.get(event.stepName)?.setOutput(chalk.yellow('> Force-released'));
182
+ break;
183
+ }
184
+
185
+ case 'step:review-completed': {
186
+ stepHandles
187
+ .get(event.stepName)
188
+ ?.setOutput(chalk.dim(`> Review: ${event.decision} by ${event.reviewerName}`));
189
+ break;
190
+ }
191
+
192
+ case 'step:owner-timeout': {
193
+ stepHandles
194
+ .get(event.stepName)
195
+ ?.setOutput(chalk.red(`> Owner ${event.ownerName} timed out`));
196
+ break;
197
+ }
198
+
199
+ case 'step:agent-report': {
200
+ const handle = stepHandles.get(event.stepName);
201
+ if (handle) {
202
+ const model = event.report.model ? `:${event.report.model}` : '';
203
+ handle.setOutput(chalk.dim(`> Report collected (${event.report.cli}${model})`));
204
+ }
205
+ break;
206
+ }
207
+
208
+ case 'step:completed': {
209
+ stepHandles.get(event.stepName)?.resolve();
210
+ break;
211
+ }
212
+
213
+ case 'step:skipped': {
214
+ const handle = stepHandles.get(event.stepName);
215
+ if (handle) {
216
+ handle.markSkipped();
217
+ handle.resolve();
218
+ } else {
219
+ // Step was skipped without ever being started (downstream of a failure).
220
+ addTask({
221
+ title: chalk.dim(`${event.stepName} (skipped)`),
222
+ task: async (): Promise<void> => {},
223
+ rendererOptions: { persistentOutput: true },
224
+ } as ListrTask);
225
+ }
226
+ break;
227
+ }
228
+
229
+ case 'step:failed': {
230
+ stepHandles.get(event.stepName)?.reject(new Error(event.error ?? 'Step failed'));
231
+ break;
232
+ }
233
+
234
+ case 'run:completed': {
235
+ setHeader(chalk.green('Workflow completed'));
236
+ resolveWorkflow();
237
+ break;
238
+ }
239
+
240
+ case 'run:failed': {
241
+ setHeader(chalk.red(`Workflow failed: ${event.error ?? 'unknown error'}`));
242
+ rejectWorkflow(new Error(event.error ?? 'Workflow failed'));
243
+ break;
244
+ }
245
+
246
+ case 'run:cancelled': {
247
+ setHeader(chalk.yellow('Workflow cancelled'));
248
+ resolveWorkflow();
249
+ break;
250
+ }
251
+
252
+ case 'broker:event':
253
+ break;
254
+
255
+ default: {
256
+ const _exhaustive: never = event;
257
+ void _exhaustive;
258
+ }
259
+ }
260
+ };
261
+
262
+ let restoreConsole: (() => void) | undefined;
263
+
264
+ return {
265
+ onEvent,
266
+ start: async () => {
267
+ restoreConsole = installOutputFilter();
268
+ const l = await ensureListr();
269
+ return l.run().catch(() => {
270
+ // Step failures are already represented in the workflow result.
271
+ });
272
+ },
273
+ unmount: () => {
274
+ restoreConsole?.();
275
+ restoreConsole = undefined;
276
+ },
277
+ };
278
+ }
@@ -0,0 +1,110 @@
1
+ import type { CliSessionReport } from './cli-session-collector.js';
2
+ import type { StepOutcome } from './trajectory.js';
3
+
4
+ function formatCurrency(value: number | null | undefined): string {
5
+ return typeof value === 'number' ? `$${value.toFixed(2)}` : '--';
6
+ }
7
+
8
+ function formatTokens(report: CliSessionReport | undefined): string {
9
+ if (!report?.tokens) return '--';
10
+ const total = report.tokens.input + report.tokens.output + report.tokens.cacheRead;
11
+ return total.toLocaleString('en-US');
12
+ }
13
+
14
+ function formatDuration(durationMs: number | null | undefined): string {
15
+ if (typeof durationMs !== 'number' || !Number.isFinite(durationMs)) return '--';
16
+ if (durationMs < 1000) return `${durationMs}ms`;
17
+
18
+ const totalSeconds = Math.round(durationMs / 1000);
19
+ const minutes = Math.floor(totalSeconds / 60);
20
+ const seconds = totalSeconds % 60;
21
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
22
+ }
23
+
24
+ function truncate(value: string, length: number): string {
25
+ if (value.length <= length) return value;
26
+ if (length <= 1) return value.slice(0, length);
27
+ return `${value.slice(0, length - 1)}…`;
28
+ }
29
+
30
+ function pad(value: string, width: number, align: 'left' | 'right' = 'left'): string {
31
+ return align === 'right' ? value.padStart(width, ' ') : value.padEnd(width, ' ');
32
+ }
33
+
34
+ function formatErrors(outcome: StepOutcome, report: CliSessionReport | undefined): string {
35
+ const count = report?.errors.length ?? 0;
36
+ if (count === 0) return outcome.status === 'failed' && outcome.error ? '1' : '--';
37
+ if (outcome.status === 'completed') return `${count} (fixed)`;
38
+ return String(count);
39
+ }
40
+
41
+ export function formatRunSummaryTable(
42
+ outcomes: StepOutcome[],
43
+ reports: Map<string, CliSessionReport>
44
+ ): string {
45
+ // Only show the Cost column when at least one report has reliable cost data
46
+ // (currently only OpenCode populates cost; Claude and Codex return null)
47
+ const hasCost = Array.from(reports.values()).some((r) => typeof r.cost === 'number' && r.cost > 0);
48
+
49
+ const headers = hasCost
50
+ ? ['Step', 'Status', 'Model', 'Cost', 'Tokens', 'Duration', 'Errors']
51
+ : ['Step', 'Status', 'Model', 'Tokens', 'Duration', 'Errors'];
52
+ const widths = hasCost
53
+ ? [20, 6, 16, 8, 10, 10, 10]
54
+ : [20, 6, 16, 10, 10, 10];
55
+ const lines: string[] = [];
56
+
57
+ lines.push(headers.map((h, i) => {
58
+ const align = i <= 2 ? 'left' : 'right';
59
+ return pad(h, widths[i], align);
60
+ }).join(' '));
61
+
62
+ let totalCost = 0;
63
+ let totalTokens = 0;
64
+ let totalDurationMs = 0;
65
+
66
+ for (const outcome of outcomes) {
67
+ const report = reports.get(outcome.name);
68
+ const reportDuration = report?.durationMs ?? outcome.durationMs;
69
+ const reportTokens = report?.tokens
70
+ ? report.tokens.input + report.tokens.output + report.tokens.cacheRead
71
+ : 0;
72
+
73
+ if (typeof report?.cost === 'number') totalCost += report.cost;
74
+ totalTokens += reportTokens;
75
+ if (typeof reportDuration === 'number') totalDurationMs += reportDuration;
76
+
77
+ const cols: string[] = [
78
+ pad(truncate(outcome.name, widths[0]), widths[0]),
79
+ pad(outcome.status === 'failed' ? 'FAIL' : outcome.status === 'completed' ? 'pass' : 'skip', widths[1]),
80
+ pad(truncate(report?.model ?? '--', widths[2]), widths[2]),
81
+ ];
82
+ if (hasCost) cols.push(pad(formatCurrency(report?.cost), widths[3], 'right'));
83
+ const tokenIdx = hasCost ? 4 : 3;
84
+ cols.push(pad(formatTokens(report), widths[tokenIdx], 'right'));
85
+ cols.push(pad(formatDuration(reportDuration), widths[tokenIdx + 1], 'right'));
86
+ cols.push(pad(formatErrors(outcome, report), widths[tokenIdx + 2], 'right'));
87
+
88
+ lines.push(cols.join(' '));
89
+
90
+ if (outcome.status === 'failed') {
91
+ const firstError = report?.errors[0];
92
+ if (firstError) {
93
+ lines.push(` └─ Error [turn ${firstError.turn}] ${truncate(firstError.text, 120)}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ const totalLabelWidth = widths[0] + widths[1] + widths[2] + 4;
99
+ lines.push('─'.repeat(lines[0].length));
100
+
101
+ const totalCols: string[] = [pad('Total', totalLabelWidth)];
102
+ if (hasCost) totalCols.push(pad(formatCurrency(totalCost), widths[3], 'right'));
103
+ const tokenIdx = hasCost ? 4 : 3;
104
+ totalCols.push(pad(totalTokens > 0 ? totalTokens.toLocaleString('en-US') : '--', widths[tokenIdx], 'right'));
105
+ totalCols.push(pad(formatDuration(totalDurationMs), widths[tokenIdx + 1], 'right'));
106
+ totalCols.push(pad('', widths[tokenIdx + 2], 'right'));
107
+ lines.push(totalCols.join(' '));
108
+
109
+ return lines.map((line) => ` ${line}`).join('\n');
110
+ }
@@ -19,6 +19,7 @@ import {
19
19
  import type { Dirent, WriteStream } from 'node:fs';
20
20
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
21
21
  import path from 'node:path';
22
+ import chalk from 'chalk';
22
23
 
23
24
  import { parse as parseYaml } from 'yaml';
24
25
  import { stripAnsi as stripAnsiFn } from '../pty.js';
@@ -32,7 +33,9 @@ import {
32
33
  CustomStepsParseError,
33
34
  CustomStepResolutionError,
34
35
  } from './custom-steps.js';
36
+ import { collectCliSession, type CliSessionReport } from './cli-session-collector.js';
35
37
  import { InMemoryWorkflowDb } from './memory-db.js';
38
+ import { formatRunSummaryTable } from './run-summary-table.js';
36
39
  import type {
37
40
  AgentCli,
38
41
  AgentDefinition,
@@ -157,6 +160,7 @@ export type WorkflowEvent =
157
160
  decision: 'approved' | 'rejected';
158
161
  }
159
162
  | { type: 'step:owner-timeout'; runId: string; stepName: string; ownerName: string }
163
+ | { type: 'step:agent-report'; runId: string; stepName: string; report: CliSessionReport }
160
164
  | { type: 'step:failed'; runId: string; stepName: string; error: string; exitCode?: number; exitSignal?: string }
161
165
  | { type: 'step:skipped'; runId: string; stepName: string }
162
166
  | { type: 'step:retrying'; runId: string; stepName: string; attempt: number }
@@ -360,6 +364,8 @@ export class WorkflowRunner {
360
364
  private resolvedPaths = new Map<string, string>();
361
365
  /** Tracks agent names currently assigned as reviewers (ref-counted to handle concurrent usage). */
362
366
  private readonly activeReviewers = new Map<string, number>();
367
+ /** Structured CLI session reports captured during the current run, keyed by step name. */
368
+ private readonly agentReports = new Map<string, CliSessionReport>();
363
369
 
364
370
  constructor(options: WorkflowRunnerOptions = {}) {
365
371
  this.db = options.db ?? new InMemoryWorkflowDb();
@@ -454,6 +460,13 @@ export class WorkflowRunner {
454
460
  return resolved;
455
461
  }
456
462
 
463
+ private resolveEffectiveCwd(step: WorkflowStep, agentDef?: AgentDefinition): string {
464
+ if (step.cwd) {
465
+ return path.resolve(this.cwd, step.cwd);
466
+ }
467
+ return this.resolveStepWorkdir(step) ?? (agentDef ? this.resolveAgentCwd(agentDef) : this.cwd);
468
+ }
469
+
457
470
  private static readonly EVIDENCE_IGNORED_DIRS = new Set([
458
471
  '.git',
459
472
  '.agent-relay',
@@ -990,7 +1003,7 @@ export class WorkflowRunner {
990
1003
  mins > 0
991
1004
  ? `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
992
1005
  : `00:${String(secs).padStart(2, '0')}`;
993
- console.log(`[workflow ${ts}] ${msg}`);
1006
+ console.log(`${chalk.dim.cyan('[workflow')} ${chalk.dim.cyan(ts)}${chalk.dim.cyan(']')} ${msg}`);
994
1007
  }
995
1008
 
996
1009
  // ── Relaycast auto-provisioning ────────────────────────────────────────
@@ -1993,6 +2006,7 @@ export class WorkflowRunner {
1993
2006
  this.runStartTime = Date.now();
1994
2007
  this.runtimeStepAgents.clear();
1995
2008
  this.stepCompletionEvidence.clear();
2009
+ this.agentReports.clear();
1996
2010
 
1997
2011
  this.log(`Starting workflow "${workflow.name}" (${workflow.steps.length} steps)`);
1998
2012
 
@@ -2235,7 +2249,7 @@ export class WorkflowRunner {
2235
2249
 
2236
2250
  // Wire broker stderr to console for observability
2237
2251
  this.unsubBrokerStderr = this.relay.onBrokerStderr((line: string) => {
2238
- console.log(`[broker] ${line}`);
2252
+ console.log(`${chalk.dim.yellow('[broker]')} ${line}`);
2239
2253
  });
2240
2254
 
2241
2255
  if (!relaycastDisabled) {
@@ -2771,7 +2785,7 @@ export class WorkflowRunner {
2771
2785
  });
2772
2786
 
2773
2787
  // Resolve step workdir (named path reference) for deterministic steps
2774
- const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
2788
+ const stepCwd = this.resolveEffectiveCwd(step);
2775
2789
  this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
2776
2790
 
2777
2791
  try {
@@ -3245,6 +3259,9 @@ export class WorkflowRunner {
3245
3259
  let lastExitCode: number | undefined;
3246
3260
  let lastExitSignal: string | undefined;
3247
3261
  let lastCompletionReason: WorkflowStepCompletionReason | undefined;
3262
+ let lastAttemptStartedAt: number | undefined;
3263
+ let lastEffectiveAgentDef: AgentDefinition | undefined;
3264
+ let lastEffectiveCwd: string | undefined;
3248
3265
 
3249
3266
  // OWNER_DECISION: INCOMPLETE_RETRY is enforced here at the attempt-loop level so every
3250
3267
  // interactive execution path shares the same contract:
@@ -3276,6 +3293,7 @@ export class WorkflowRunner {
3276
3293
  }
3277
3294
 
3278
3295
  try {
3296
+ lastAttemptStartedAt = Date.now();
3279
3297
  // Mark step as running
3280
3298
  state.row.status = 'running';
3281
3299
  state.row.error = undefined;
@@ -3334,7 +3352,10 @@ export class WorkflowRunner {
3334
3352
  }
3335
3353
 
3336
3354
  // Apply step-level workdir override to agent definitions if present
3337
- const applyStepWorkdir = (def: AgentDefinition): AgentDefinition => {
3355
+ const applyStepCwd = (def: AgentDefinition): AgentDefinition => {
3356
+ if (step.cwd) {
3357
+ return { ...def, cwd: step.cwd, workdir: undefined };
3358
+ }
3338
3359
  if (step.workdir) {
3339
3360
  const stepWorkdir = this.resolveStepWorkdir(step);
3340
3361
  if (stepWorkdir) {
@@ -3343,9 +3364,11 @@ export class WorkflowRunner {
3343
3364
  }
3344
3365
  return def;
3345
3366
  };
3346
- const effectiveSpecialist = applyStepWorkdir(specialistDef);
3347
- const effectiveOwner = applyStepWorkdir(ownerDef);
3348
- const effectiveReviewer = reviewDef ? applyStepWorkdir(reviewDef) : undefined;
3367
+ const effectiveSpecialist = applyStepCwd(specialistDef);
3368
+ const effectiveOwner = applyStepCwd(ownerDef);
3369
+ const effectiveReviewer = reviewDef ? applyStepCwd(reviewDef) : undefined;
3370
+ lastEffectiveAgentDef = effectiveSpecialist;
3371
+ lastEffectiveCwd = this.resolveAgentCwd(effectiveSpecialist);
3349
3372
  this.beginStepEvidence(
3350
3373
  step.name,
3351
3374
  [
@@ -3518,6 +3541,15 @@ export class WorkflowRunner {
3518
3541
  }
3519
3542
  }
3520
3543
 
3544
+ await this.captureAgentReport(
3545
+ runId,
3546
+ step.name,
3547
+ lastEffectiveAgentDef,
3548
+ lastEffectiveCwd,
3549
+ lastAttemptStartedAt,
3550
+ Date.now()
3551
+ );
3552
+
3521
3553
  // Mark completed
3522
3554
  state.row.status = 'completed';
3523
3555
  state.row.output = combinedOutput;
@@ -3570,6 +3602,14 @@ export class WorkflowRunner {
3570
3602
  typeof step.verification === 'object' && 'value' in step.verification
3571
3603
  ? String(step.verification.value)
3572
3604
  : undefined;
3605
+ await this.captureAgentReport(
3606
+ runId,
3607
+ step.name,
3608
+ lastEffectiveAgentDef,
3609
+ lastEffectiveCwd,
3610
+ lastAttemptStartedAt,
3611
+ Date.now()
3612
+ );
3573
3613
  await this.trajectory?.stepFailed(step, lastError ?? 'Unknown error', maxRetries + 1, maxRetries, {
3574
3614
  agent: agentName,
3575
3615
  nonInteractive,
@@ -4856,7 +4896,7 @@ export class WorkflowRunner {
4856
4896
  const { stdout: output, exitCode, exitSignal } = await new Promise<{ stdout: string; exitCode?: number; exitSignal?: string }>((resolve, reject) => {
4857
4897
  const child = cpSpawn(cmd, args, {
4858
4898
  stdio: ['ignore', 'pipe', 'pipe'],
4859
- cwd: this.resolveAgentCwd(agentDef),
4899
+ cwd: this.resolveEffectiveCwd(step, agentDef),
4860
4900
  env: this.getRelayEnv() ?? { ...process.env },
4861
4901
  });
4862
4902
 
@@ -5728,6 +5768,35 @@ export class WorkflowRunner {
5728
5768
  this.finalizeStepEvidence(state.row.stepName, 'failed', state.row.completedAt, completionReason);
5729
5769
  }
5730
5770
 
5771
+ private async captureAgentReport(
5772
+ runId: string,
5773
+ stepName: string,
5774
+ agentDef: AgentDefinition | undefined,
5775
+ cwd: string | undefined,
5776
+ startedAt: number | undefined,
5777
+ completedAt: number
5778
+ ): Promise<void> {
5779
+ if (!agentDef || !cwd || !startedAt) return;
5780
+
5781
+ try {
5782
+ const report = await collectCliSession({
5783
+ cli: agentDef.cli,
5784
+ cwd,
5785
+ startedAt,
5786
+ completedAt,
5787
+ });
5788
+ if (!report) return;
5789
+
5790
+ this.agentReports.set(stepName, report);
5791
+ this.emit({ type: 'step:agent-report', runId, stepName, report });
5792
+ await this.persistAgentReport(runId, stepName, report);
5793
+ } catch (error) {
5794
+ this.log(
5795
+ `[${stepName}] CLI session collection failed: ${error instanceof Error ? error.message : String(error)}`
5796
+ );
5797
+ }
5798
+ }
5799
+
5731
5800
  private async markDownstreamSkipped(
5732
5801
  failedStepName: string,
5733
5802
  allSteps: WorkflowStep[],
@@ -6003,26 +6072,33 @@ export class WorkflowRunner {
6003
6072
  const skipped = outcomes.filter((o) => o.status === 'skipped');
6004
6073
 
6005
6074
  console.log('');
6006
- console.log('━'.repeat(70));
6007
- console.log(` Workflow "${workflowName}" — ${failed.length === 0 ? 'COMPLETED' : 'FAILED'}`);
6008
- console.log(` ${completed.length} passed, ${failed.length} failed, ${skipped.length} skipped`);
6009
- console.log('━'.repeat(70));
6010
-
6011
- for (const outcome of outcomes) {
6012
- const icon = outcome.status === 'completed' ? '✓' : outcome.status === 'failed' ? '✗' : '⊘';
6013
- const retryNote = outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : '';
6014
- console.log(` ${icon} ${outcome.name} [${outcome.agent}]${retryNote}`);
6015
-
6016
- if (outcome.error) {
6017
- console.log(` Error: ${outcome.error}`);
6018
- }
6075
+ console.log(chalk.dim('━'.repeat(70)));
6076
+ console.log(` Workflow "${workflowName}" — ${failed.length === 0 ? chalk.green('COMPLETED') : chalk.red('FAILED')}`);
6077
+ console.log(
6078
+ ` ${chalk.green(`${completed.length} passed`)}, ${chalk.red(`${failed.length} failed`)}, ${chalk.dim(`${skipped.length} skipped`)}`
6079
+ );
6080
+ console.log(chalk.dim('━'.repeat(70)));
6081
+
6082
+ if (this.agentReports.size > 0) {
6083
+ console.log(formatRunSummaryTable(outcomes, this.agentReports));
6084
+ } else {
6085
+ for (const outcome of outcomes) {
6086
+ const icon =
6087
+ outcome.status === 'completed' ? chalk.green('✓') : outcome.status === 'failed' ? chalk.red('✗') : chalk.dim('⊘');
6088
+ const retryNote = outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : '';
6089
+ console.log(` ${icon} ${outcome.name} [${outcome.agent}]${retryNote}`);
6090
+
6091
+ if (outcome.error) {
6092
+ console.log(` Error: ${outcome.error}`);
6093
+ }
6019
6094
 
6020
- // Extract last meaningful lines from raw PTY output
6021
- if (outcome.output) {
6022
- const excerpt = this.extractOutputExcerpt(outcome.output);
6023
- if (excerpt) {
6024
- for (const line of excerpt.split('\n')) {
6025
- console.log(` ${line}`);
6095
+ // Extract last meaningful lines from raw PTY output
6096
+ if (outcome.output) {
6097
+ const excerpt = this.extractOutputExcerpt(outcome.output);
6098
+ if (excerpt) {
6099
+ for (const line of excerpt.split('\n')) {
6100
+ console.log(` ${line}`);
6101
+ }
6026
6102
  }
6027
6103
  }
6028
6104
  }
@@ -6032,9 +6108,10 @@ export class WorkflowRunner {
6032
6108
  const outputDir = this.getStepOutputDir(runId);
6033
6109
  const logsDir = path.join(this.cwd, '.agent-relay', 'team', 'worker-logs');
6034
6110
  console.log('');
6111
+ console.log(` Run ID: ${runId}`);
6035
6112
  console.log(` Step output: ${outputDir}`);
6036
6113
  console.log(` Agent logs: ${logsDir}`);
6037
- console.log('━'.repeat(70));
6114
+ console.log(chalk.dim('━'.repeat(70)));
6038
6115
  console.log('');
6039
6116
  }
6040
6117
 
@@ -6095,6 +6172,12 @@ export class WorkflowRunner {
6095
6172
  const stepsWithVerification = new Set(steps?.filter((s) => s.verification).map((s) => s.name) ?? []);
6096
6173
  const outcomes: StepOutcome[] = [];
6097
6174
  for (const [name, state] of stepStates) {
6175
+ const startedAtMs = state.row.startedAt ? Date.parse(state.row.startedAt) : Number.NaN;
6176
+ const completedAtMs = state.row.completedAt ? Date.parse(state.row.completedAt) : Number.NaN;
6177
+ const durationMs =
6178
+ Number.isFinite(startedAtMs) && Number.isFinite(completedAtMs)
6179
+ ? Math.max(0, completedAtMs - startedAtMs)
6180
+ : undefined;
6098
6181
  outcomes.push({
6099
6182
  name,
6100
6183
  agent: state.row.agentName ?? 'deterministic',
@@ -6108,6 +6191,7 @@ export class WorkflowRunner {
6108
6191
  output: state.row.output,
6109
6192
  error: state.row.error,
6110
6193
  verificationPassed: state.row.status === 'completed' && stepsWithVerification.has(name),
6194
+ durationMs,
6111
6195
  completionMode: state.row.completionReason
6112
6196
  ? this.buildStepCompletionDecision(name, state.row.completionReason)?.mode
6113
6197
  : undefined,
@@ -6290,6 +6374,16 @@ export class WorkflowRunner {
6290
6374
  this.postToChannel(`**[${stepName}] Output:**\n\`\`\`\n${preview}\n\`\`\``, { stepName });
6291
6375
  }
6292
6376
 
6377
+ private async persistAgentReport(runId: string, stepName: string, report: CliSessionReport): Promise<void> {
6378
+ const reportPath = path.join(this.getStepOutputDir(runId), `${stepName}.report.json`);
6379
+ try {
6380
+ mkdirSync(this.getStepOutputDir(runId), { recursive: true });
6381
+ await writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8');
6382
+ } catch {
6383
+ // Non-critical
6384
+ }
6385
+ }
6386
+
6293
6387
  /** Scan .agent-relay/step-outputs/ for the most recent run directory containing the needed steps. */
6294
6388
  private findMostRecentRunWithSteps(stepNames: Set<string>): string | undefined {
6295
6389
  try {