agent-relay 3.2.9 → 3.2.11

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 (132) 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 +9183 -698
  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 +11 -9
  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 +2 -2
  19. package/packages/policy/package.json +2 -2
  20. package/packages/sdk/dist/cli-registry.d.ts +42 -0
  21. package/packages/sdk/dist/cli-registry.d.ts.map +1 -0
  22. package/packages/sdk/dist/cli-registry.js +126 -0
  23. package/packages/sdk/dist/cli-registry.js.map +1 -0
  24. package/packages/sdk/dist/cli-resolver.d.ts +30 -0
  25. package/packages/sdk/dist/cli-resolver.d.ts.map +1 -0
  26. package/packages/sdk/dist/cli-resolver.js +132 -0
  27. package/packages/sdk/dist/cli-resolver.js.map +1 -0
  28. package/packages/sdk/dist/index.d.ts +2 -0
  29. package/packages/sdk/dist/index.d.ts.map +1 -1
  30. package/packages/sdk/dist/index.js +2 -0
  31. package/packages/sdk/dist/index.js.map +1 -1
  32. package/packages/sdk/dist/spawn-from-env.d.ts.map +1 -1
  33. package/packages/sdk/dist/spawn-from-env.js +6 -15
  34. package/packages/sdk/dist/spawn-from-env.js.map +1 -1
  35. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.d.ts +2 -0
  36. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.d.ts.map +1 -0
  37. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.js +54 -0
  38. package/packages/sdk/dist/workflows/__tests__/cli-session-collector.test.js.map +1 -0
  39. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.d.ts +2 -0
  40. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.d.ts.map +1 -0
  41. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.js +85 -0
  42. package/packages/sdk/dist/workflows/__tests__/collectors/claude.test.js.map +1 -0
  43. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.d.ts +2 -0
  44. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.d.ts.map +1 -0
  45. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.js +67 -0
  46. package/packages/sdk/dist/workflows/__tests__/collectors/codex.test.js.map +1 -0
  47. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.d.ts +2 -0
  48. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.d.ts.map +1 -0
  49. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.js +119 -0
  50. package/packages/sdk/dist/workflows/__tests__/collectors/opencode.test.js.map +1 -0
  51. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.d.ts +2 -0
  52. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.d.ts.map +1 -0
  53. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +130 -0
  54. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -0
  55. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.d.ts +2 -0
  56. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.d.ts.map +1 -0
  57. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.js +42 -0
  58. package/packages/sdk/dist/workflows/__tests__/step-cwd.test.js.map +1 -0
  59. package/packages/sdk/dist/workflows/builder.d.ts +7 -0
  60. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  61. package/packages/sdk/dist/workflows/builder.js +40 -5
  62. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  63. package/packages/sdk/dist/workflows/cli-session-collector.d.ts +39 -0
  64. package/packages/sdk/dist/workflows/cli-session-collector.d.ts.map +1 -0
  65. package/packages/sdk/dist/workflows/cli-session-collector.js +23 -0
  66. package/packages/sdk/dist/workflows/cli-session-collector.js.map +1 -0
  67. package/packages/sdk/dist/workflows/cli.js +228 -48
  68. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  69. package/packages/sdk/dist/workflows/collectors/claude.d.ts +6 -0
  70. package/packages/sdk/dist/workflows/collectors/claude.d.ts.map +1 -0
  71. package/packages/sdk/dist/workflows/collectors/claude.js +330 -0
  72. package/packages/sdk/dist/workflows/collectors/claude.js.map +1 -0
  73. package/packages/sdk/dist/workflows/collectors/codex.d.ts +18 -0
  74. package/packages/sdk/dist/workflows/collectors/codex.d.ts.map +1 -0
  75. package/packages/sdk/dist/workflows/collectors/codex.js +265 -0
  76. package/packages/sdk/dist/workflows/collectors/codex.js.map +1 -0
  77. package/packages/sdk/dist/workflows/collectors/opencode.d.ts +6 -0
  78. package/packages/sdk/dist/workflows/collectors/opencode.d.ts.map +1 -0
  79. package/packages/sdk/dist/workflows/collectors/opencode.js +204 -0
  80. package/packages/sdk/dist/workflows/collectors/opencode.js.map +1 -0
  81. package/packages/sdk/dist/workflows/default-logger.d.ts +9 -0
  82. package/packages/sdk/dist/workflows/default-logger.d.ts.map +1 -0
  83. package/packages/sdk/dist/workflows/default-logger.js +104 -0
  84. package/packages/sdk/dist/workflows/default-logger.js.map +1 -0
  85. package/packages/sdk/dist/workflows/index.d.ts +4 -0
  86. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  87. package/packages/sdk/dist/workflows/index.js +4 -0
  88. package/packages/sdk/dist/workflows/index.js.map +1 -1
  89. package/packages/sdk/dist/workflows/listr-renderer.d.ts +26 -0
  90. package/packages/sdk/dist/workflows/listr-renderer.d.ts.map +1 -0
  91. package/packages/sdk/dist/workflows/listr-renderer.js +232 -0
  92. package/packages/sdk/dist/workflows/listr-renderer.js.map +1 -0
  93. package/packages/sdk/dist/workflows/run-summary-table.d.ts +4 -0
  94. package/packages/sdk/dist/workflows/run-summary-table.d.ts.map +1 -0
  95. package/packages/sdk/dist/workflows/run-summary-table.js +98 -0
  96. package/packages/sdk/dist/workflows/run-summary-table.js.map +1 -0
  97. package/packages/sdk/dist/workflows/runner.d.ts +12 -1
  98. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  99. package/packages/sdk/dist/workflows/runner.js +107 -71
  100. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  101. package/packages/sdk/dist/workflows/types.d.ts +2 -0
  102. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  103. package/packages/sdk/dist/workflows/types.js.map +1 -1
  104. package/packages/sdk/package.json +4 -2
  105. package/packages/sdk/src/cli-registry.ts +148 -0
  106. package/packages/sdk/src/cli-resolver.ts +155 -0
  107. package/packages/sdk/src/index.ts +2 -0
  108. package/packages/sdk/src/spawn-from-env.ts +6 -17
  109. package/packages/sdk/src/workflows/__tests__/cli-session-collector.test.ts +64 -0
  110. package/packages/sdk/src/workflows/__tests__/collectors/claude.test.ts +104 -0
  111. package/packages/sdk/src/workflows/__tests__/collectors/codex.test.ts +82 -0
  112. package/packages/sdk/src/workflows/__tests__/collectors/opencode.test.ts +178 -0
  113. package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +160 -0
  114. package/packages/sdk/src/workflows/__tests__/step-cwd.test.ts +72 -0
  115. package/packages/sdk/src/workflows/builder.ts +48 -4
  116. package/packages/sdk/src/workflows/cli-session-collector.ts +58 -0
  117. package/packages/sdk/src/workflows/cli.ts +289 -50
  118. package/packages/sdk/src/workflows/collectors/claude.ts +415 -0
  119. package/packages/sdk/src/workflows/collectors/codex.ts +351 -0
  120. package/packages/sdk/src/workflows/collectors/opencode.ts +305 -0
  121. package/packages/sdk/src/workflows/default-logger.ts +120 -0
  122. package/packages/sdk/src/workflows/index.ts +4 -0
  123. package/packages/sdk/src/workflows/listr-renderer.ts +278 -0
  124. package/packages/sdk/src/workflows/run-summary-table.ts +110 -0
  125. package/packages/sdk/src/workflows/runner.ts +138 -71
  126. package/packages/sdk/src/workflows/types.ts +2 -0
  127. package/packages/sdk/vitest.config.ts +1 -1
  128. package/packages/sdk-py/pyproject.toml +1 -1
  129. package/packages/telemetry/package.json +1 -1
  130. package/packages/trajectory/package.json +2 -2
  131. package/packages/user-directory/package.json +2 -2
  132. package/packages/utils/package.json +2 -2
@@ -0,0 +1,120 @@
1
+ import chalk from 'chalk';
2
+ import type { WorkflowEvent, WorkflowEventListener } from './runner.js';
3
+
4
+ export type LogLevel = 'verbose' | 'normal' | 'quiet' | false;
5
+
6
+ const noop: WorkflowEventListener = () => {};
7
+
8
+ /**
9
+ * Create a default event logger that writes workflow progress to the console.
10
+ *
11
+ * @param level - Log verbosity: "verbose" | "normal" (default) | "quiet" | false (no-op)
12
+ */
13
+ export function createDefaultEventLogger(level: LogLevel = 'normal'): WorkflowEventListener {
14
+ if (level === false) return noop;
15
+
16
+ return (event: WorkflowEvent) => {
17
+ switch (event.type) {
18
+ // ── Run lifecycle ──
19
+ case 'run:started':
20
+ if (level !== 'quiet') {
21
+ console.log(chalk.cyan(`[workflow] run ${event.runId.slice(0, 8)}...`));
22
+ }
23
+ break;
24
+
25
+ case 'run:completed':
26
+ console.log(chalk.green(`[workflow] completed`));
27
+ break;
28
+
29
+ case 'run:failed':
30
+ console.log(chalk.red(`[workflow] FAILED: ${event.error}`));
31
+ break;
32
+
33
+ case 'run:cancelled':
34
+ if (level !== 'quiet') {
35
+ console.log(chalk.yellow(`[workflow] cancelled`));
36
+ }
37
+ break;
38
+
39
+ // ── Step lifecycle ──
40
+ case 'step:started':
41
+ if (level !== 'quiet') {
42
+ console.log(chalk.blue(` ● ${event.stepName} — started`));
43
+ }
44
+ break;
45
+
46
+ case 'step:completed':
47
+ if (level !== 'quiet') {
48
+ console.log(chalk.green(` ✓ ${event.stepName} — completed`));
49
+ }
50
+ break;
51
+
52
+ case 'step:failed':
53
+ console.log(chalk.red(` ✗ ${event.stepName} — FAILED: ${event.error}`));
54
+ break;
55
+
56
+ case 'step:skipped':
57
+ if (level !== 'quiet') {
58
+ console.log(chalk.gray(` ○ ${event.stepName} — skipped`));
59
+ }
60
+ break;
61
+
62
+ case 'step:retrying':
63
+ if (level !== 'quiet') {
64
+ console.log(chalk.yellow(` ↻ ${event.stepName} — retrying (attempt ${event.attempt})`));
65
+ }
66
+ break;
67
+
68
+ case 'step:nudged':
69
+ if (level !== 'quiet') {
70
+ console.log(chalk.yellow(` ⚡ ${event.stepName} — nudged (${event.nudgeCount})`));
71
+ }
72
+ break;
73
+
74
+ case 'step:agent-report': {
75
+ if (level !== 'quiet') {
76
+ const r = event.report;
77
+ const parts: string[] = [];
78
+ if (r.model) parts.push(r.model);
79
+ if (r.cost != null) parts.push(`$${r.cost.toFixed(2)}`);
80
+ if (r.tokens) parts.push(`${r.tokens.input}+${r.tokens.output} tokens`);
81
+ parts.push(`${r.errors.length} errors`);
82
+ console.log(chalk.dim(` 📊 ${event.stepName} — ${parts.join(' · ')}`));
83
+ }
84
+ break;
85
+ }
86
+
87
+ // ── Broker-level events (verbose only) ──
88
+ case 'broker:event':
89
+ if (level === 'verbose') {
90
+ console.log(chalk.dim(` [broker] ${JSON.stringify(event.event)}`));
91
+ }
92
+ break;
93
+
94
+ // ── Other events (verbose only) ──
95
+ case 'step:owner-assigned':
96
+ if (level === 'verbose') {
97
+ console.log(chalk.dim(` ${event.stepName} — owner: ${event.ownerName}, specialist: ${event.specialistName}`));
98
+ }
99
+ break;
100
+
101
+ case 'step:review-completed':
102
+ if (level === 'verbose') {
103
+ console.log(chalk.dim(` ${event.stepName} — review: ${event.decision} by ${event.reviewerName}`));
104
+ }
105
+ break;
106
+
107
+ case 'step:owner-timeout':
108
+ if (level !== 'quiet') {
109
+ console.log(chalk.yellow(` ⏱ ${event.stepName} — owner timeout (${event.ownerName})`));
110
+ }
111
+ break;
112
+
113
+ case 'step:force-released':
114
+ if (level === 'verbose') {
115
+ console.log(chalk.dim(` ${event.stepName} — force-released`));
116
+ }
117
+ break;
118
+ }
119
+ };
120
+ }
@@ -1,6 +1,8 @@
1
1
  export * from './types.js';
2
2
  export * from './runner.js';
3
3
  export * from './custom-steps.js';
4
+ export * from './cli-session-collector.js';
5
+ export * from './run-summary-table.js';
4
6
  export {
5
7
  Models,
6
8
  ClaudeModels,
@@ -22,3 +24,5 @@ export * from './state.js';
22
24
  export * from './templates.js';
23
25
  export { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
24
26
  export { formatDryRunReport } from './dry-run-format.js';
27
+ export { createWorkflowRenderer, type WorkflowRenderer } from './listr-renderer.js';
28
+ export { createDefaultEventLogger } from './default-logger.js';
@@ -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
+ }