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,72 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import path from 'node:path';
3
+
4
+ vi.mock('@relaycast/sdk', () => ({
5
+ RelayCast: vi.fn(),
6
+ RelayError: class RelayError extends Error {},
7
+ }));
8
+
9
+ vi.mock('../../relay.js', () => ({
10
+ AgentRelay: vi.fn(),
11
+ }));
12
+
13
+ const { WorkflowRunner } = await import('../runner.js');
14
+
15
+ describe('WorkflowRunner step cwd resolution', () => {
16
+ it('prefers step.cwd over agent.cwd and runner cwd', () => {
17
+ const runnerRoot = '/runner-root';
18
+ const runner = new WorkflowRunner({ cwd: runnerRoot });
19
+
20
+ const resolved = (runner as any).resolveEffectiveCwd(
21
+ { name: 'generate', agent: 'worker', task: 'Generate', cwd: 'steps/generate' },
22
+ { name: 'worker', cli: 'claude', cwd: 'agents/worker' },
23
+ );
24
+
25
+ expect(resolved).toBe(path.resolve(runnerRoot, 'steps/generate'));
26
+ });
27
+
28
+ it('respects step.cwd for deterministic steps', () => {
29
+ const runnerRoot = '/runner-root';
30
+ const runner = new WorkflowRunner({ cwd: runnerRoot });
31
+
32
+ const resolved = (runner as any).resolveEffectiveCwd({
33
+ name: 'scaffold',
34
+ type: 'deterministic',
35
+ command: 'mkdir -p out',
36
+ cwd: 'deterministic/setup',
37
+ });
38
+
39
+ expect(resolved).toBe(path.resolve(runnerRoot, 'deterministic/setup'));
40
+ });
41
+
42
+ it('falls back through step.cwd to step.workdir to agent.cwd to runner.cwd', () => {
43
+ const runnerRoot = '/runner-root';
44
+ const namedPath = '/named/workdir';
45
+ const runner = new WorkflowRunner({ cwd: runnerRoot });
46
+ (runner as any).resolvedPaths.set('generated', namedPath);
47
+
48
+ const agentDef = { name: 'worker', cli: 'claude', cwd: 'agents/worker' } as const;
49
+
50
+ expect(
51
+ (runner as any).resolveEffectiveCwd(
52
+ { name: 's1', agent: 'worker', task: 'Do work', cwd: 'steps/explicit', workdir: 'generated' },
53
+ agentDef,
54
+ ),
55
+ ).toBe(path.resolve(runnerRoot, 'steps/explicit'));
56
+
57
+ expect(
58
+ (runner as any).resolveEffectiveCwd(
59
+ { name: 's2', agent: 'worker', task: 'Do work', workdir: 'generated' },
60
+ agentDef,
61
+ ),
62
+ ).toBe(namedPath);
63
+
64
+ expect(
65
+ (runner as any).resolveEffectiveCwd({ name: 's3', agent: 'worker', task: 'Do work' }, agentDef),
66
+ ).toBe(path.resolve(runnerRoot, 'agents/worker'));
67
+
68
+ expect(
69
+ (runner as any).resolveEffectiveCwd({ name: 's4', type: 'deterministic', command: 'pwd' }),
70
+ ).toBe(runnerRoot);
71
+ });
72
+ });
@@ -47,6 +47,7 @@ export interface AgentOptions {
47
47
  export interface AgentStepOptions {
48
48
  agent: string;
49
49
  task: string;
50
+ cwd?: string;
50
51
  dependsOn?: string[];
51
52
  verification?: VerificationCheck;
52
53
  timeoutMs?: number;
@@ -57,6 +58,7 @@ export interface AgentStepOptions {
57
58
  export interface DeterministicStepOptions {
58
59
  type: 'deterministic';
59
60
  command: string;
61
+ cwd?: string;
60
62
  /** Capture stdout as step output for downstream steps. Default: true. */
61
63
  captureOutput?: boolean;
62
64
  /** Fail if command exit code is non-zero. Default: true. */
@@ -256,6 +258,7 @@ export class WorkflowBuilder {
256
258
  }
257
259
  step.type = 'deterministic';
258
260
  step.command = options.command;
261
+ if (options.cwd !== undefined) step.cwd = options.cwd;
259
262
  if (options.captureOutput !== undefined) step.captureOutput = options.captureOutput;
260
263
  if (options.failOnError !== undefined) step.failOnError = options.failOnError;
261
264
  if (options.dependsOn !== undefined) step.dependsOn = options.dependsOn;
@@ -280,6 +283,7 @@ export class WorkflowBuilder {
280
283
  }
281
284
  step.agent = agentOpts.agent;
282
285
  step.task = agentOpts.task;
286
+ if (agentOpts.cwd !== undefined) step.cwd = agentOpts.cwd;
283
287
  if (agentOpts.dependsOn !== undefined) step.dependsOn = agentOpts.dependsOn;
284
288
  if (agentOpts.verification !== undefined) step.verification = agentOpts.verification;
285
289
  if (agentOpts.timeoutMs !== undefined) step.timeoutMs = agentOpts.timeoutMs;
@@ -0,0 +1,58 @@
1
+ import type { AgentCli } from './types.js';
2
+ import { ClaudeCodeCollector } from './collectors/claude.js';
3
+ import { CodexCollector } from './collectors/codex.js';
4
+ import { OpenCodeCollector } from './collectors/opencode.js';
5
+
6
+ export interface CliSessionReport {
7
+ cli: AgentCli;
8
+ sessionId: string | null;
9
+ model: string | null;
10
+ provider: string | null;
11
+ durationMs: number | null;
12
+ cost: number | null;
13
+ tokens: {
14
+ input: number;
15
+ output: number;
16
+ cacheRead: number;
17
+ } | null;
18
+ turns: number;
19
+ toolCalls: { name: string; count: number }[];
20
+ errors: { turn: number; text: string }[];
21
+ finalStatus: 'completed' | 'failed' | 'unknown';
22
+ summary: string | null;
23
+ raw?: object;
24
+ }
25
+
26
+ export interface CliSessionQuery {
27
+ cli: AgentCli;
28
+ cwd: string;
29
+ startedAt: number;
30
+ completedAt: number;
31
+ }
32
+
33
+ export interface CliSessionCollector {
34
+ canCollect(): boolean;
35
+ collect(query: CliSessionQuery): Promise<CliSessionReport | null>;
36
+ }
37
+
38
+ export function createCollector(cli: AgentCli): CliSessionCollector | null {
39
+ switch (cli) {
40
+ case 'opencode':
41
+ return new OpenCodeCollector();
42
+ case 'claude':
43
+ return new ClaudeCodeCollector();
44
+ case 'codex':
45
+ return new CodexCollector();
46
+ default:
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export async function collectCliSession(query: CliSessionQuery): Promise<CliSessionReport | null> {
52
+ const collector = createCollector(query.cli);
53
+ if (!collector || !collector.canCollect()) {
54
+ return null;
55
+ }
56
+
57
+ return collector.collect(query);
58
+ }
@@ -10,6 +10,8 @@
10
10
  */
11
11
 
12
12
  import path from 'node:path';
13
+ import chalk from 'chalk';
14
+
13
15
  import type { WorkflowEvent } from './runner.js';
14
16
  import { WorkflowRunner } from './runner.js';
15
17
  import { JsonFileWorkflowDb } from './file-db.js';
@@ -41,43 +43,261 @@ Examples:
41
43
  );
42
44
  }
43
45
 
44
- function formatEvent(event: WorkflowEvent): string {
45
- switch (event.type) {
46
- case 'run:started':
47
- return `[run] started (${event.runId})`;
48
- case 'run:completed':
49
- return `[run] completed`;
50
- case 'run:failed':
51
- return `[run] failed: ${event.error}`;
52
- case 'run:cancelled':
53
- return `[run] cancelled`;
54
- case 'step:started':
55
- return `[step] ${event.stepName} started`;
56
- case 'step:owner-assigned':
57
- return `[step] ${event.stepName} owner=${event.ownerName} specialist=${event.specialistName}`;
58
- case 'step:completed':
59
- return `[step] ${event.stepName} completed`;
60
- case 'step:review-completed':
61
- return `[step] ${event.stepName} review ${event.decision} by ${event.reviewerName}`;
62
- case 'step:owner-timeout':
63
- return `[step] ${event.stepName} owner ${event.ownerName} timed out`;
64
- case 'step:failed':
65
- return `[step] ${event.stepName} failed: ${event.error}`;
66
- case 'step:skipped':
67
- return `[step] ${event.stepName} skipped`;
68
- case 'step:retrying':
69
- return `[step] ${event.stepName} retrying (attempt ${event.attempt})`;
70
- case 'step:nudged':
71
- return `[step] ${event.stepName} nudged (nudge #${event.nudgeCount})`;
72
- case 'step:force-released':
73
- return `[step] ${event.stepName} force-released`;
74
- case 'broker:event':
75
- return `[broker] ${event.event.kind}`;
76
- default: {
77
- const _exhaustive: never = event;
78
- return `[unknown event] ${(_exhaustive as WorkflowEvent).type}`;
46
+ type RunnerConfig = Awaited<ReturnType<WorkflowRunner['parseYamlFile']>>;
47
+
48
+ type RunnerResult = Awaited<ReturnType<WorkflowRunner['execute']>>;
49
+
50
+ type ExecuteOptions = {
51
+ startFrom: string;
52
+ previousRunId?: string;
53
+ };
54
+
55
+ interface RenderableTask {
56
+ output?: string;
57
+ title: string;
58
+ }
59
+
60
+ interface StepHandle {
61
+ resolve: () => void;
62
+ reject: (error: Error) => void;
63
+ setOutput: (text: string) => void;
64
+ markSkipped: () => void;
65
+ }
66
+
67
+ // Filter [broker] and [workflow HH:MM] noise while listr owns the terminal,
68
+ // but let the observer URL and channel name through.
69
+ function installOutputFilter(): () => void {
70
+ const orig = console.log.bind(console);
71
+ console.log = (...args: unknown[]) => {
72
+ const str = String(args[0] ?? '');
73
+ if (str.includes('Observer:') || str.includes('agentrelay.dev') || str.includes('Channel: wf-')) {
74
+ orig(...args);
75
+ return;
79
76
  }
80
- }
77
+ if (/\[broker\]/.test(str) || /\[workflow\s+\d{2}:\d{2}\]/.test(str)) return;
78
+ orig(...args);
79
+ };
80
+ return () => { console.log = orig; };
81
+ }
82
+
83
+ async function runWithListr(
84
+ runner: WorkflowRunner,
85
+ config: RunnerConfig,
86
+ workflowName: string | undefined,
87
+ executeOptions: ExecuteOptions | undefined,
88
+ ): Promise<RunnerResult> {
89
+ const stepHandles = new Map<string, StepHandle>();
90
+ const restoreConsole = installOutputFilter();
91
+
92
+ let resolveWorkflow!: () => void;
93
+ let rejectWorkflow!: (error: Error) => void;
94
+ const workflowDone = new Promise<void>((resolve, reject) => {
95
+ resolveWorkflow = resolve;
96
+ rejectWorkflow = reject;
97
+ });
98
+ workflowDone.catch(() => {});
99
+
100
+ let setHeader: (text: string) => void = () => {};
101
+
102
+ const { Listr } = await import('listr2');
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const listr = new (Listr as any)(
105
+ [
106
+ {
107
+ title: chalk.dim('Workflow starting...'),
108
+ task: async (_ctx: unknown, task: any): Promise<void> => {
109
+ setHeader = (text: string): void => {
110
+ task.title = text;
111
+ };
112
+ await workflowDone;
113
+ },
114
+ },
115
+ ],
116
+ {
117
+ concurrent: true,
118
+ renderer: process.stdout.isTTY ? 'default' : 'verbose',
119
+ rendererOptions: {
120
+ collapseErrors: false,
121
+ showErrorMessage: true,
122
+ },
123
+ },
124
+ );
125
+
126
+ runner.on((event: WorkflowEvent) => {
127
+ switch (event.type) {
128
+ case 'run:started': {
129
+ setHeader(chalk.dim(`[workflow] run ${event.runId.slice(0, 8)}...`));
130
+ break;
131
+ }
132
+
133
+ case 'step:started': {
134
+ let resolveStep!: () => void;
135
+ let rejectStep!: (error: Error) => void;
136
+ let taskRef: RenderableTask | null = null;
137
+ let skipped = false;
138
+
139
+ const done = new Promise<void>((resolve, reject) => {
140
+ resolveStep = resolve;
141
+ rejectStep = reject;
142
+ });
143
+ done.catch(() => {});
144
+
145
+ stepHandles.set(event.stepName, {
146
+ resolve: resolveStep,
147
+ reject: rejectStep,
148
+ setOutput: (text: string) => {
149
+ if (taskRef) {
150
+ taskRef.output = text;
151
+ }
152
+ },
153
+ markSkipped: () => {
154
+ skipped = true;
155
+ if (taskRef) {
156
+ taskRef.title = chalk.dim(`${event.stepName} (skipped)`);
157
+ }
158
+ },
159
+ });
160
+
161
+ listr.add({
162
+ title: chalk.white(event.stepName),
163
+ task: async (_ctx: unknown, task: any): Promise<void> => {
164
+ taskRef = task as RenderableTask;
165
+ if (skipped) {
166
+ taskRef.title = chalk.dim(`${event.stepName} (skipped)`);
167
+ }
168
+ await done;
169
+ },
170
+ rendererOptions: {
171
+ persistentOutput: true,
172
+ },
173
+ });
174
+ break;
175
+ }
176
+
177
+ case 'step:owner-assigned': {
178
+ const handle = stepHandles.get(event.stepName);
179
+ if (handle) {
180
+ handle.setOutput(
181
+ chalk.dim(`> Owner: ${event.ownerName}`) +
182
+ (event.specialistName ? chalk.dim(` - specialist: ${event.specialistName}`) : '')
183
+ );
184
+ }
185
+ break;
186
+ }
187
+
188
+ case 'step:retrying': {
189
+ const handle = stepHandles.get(event.stepName);
190
+ if (handle) {
191
+ handle.setOutput(chalk.yellow(`Retrying (attempt ${event.attempt})`));
192
+ }
193
+ break;
194
+ }
195
+
196
+ case 'step:nudged': {
197
+ const handle = stepHandles.get(event.stepName);
198
+ if (handle) {
199
+ handle.setOutput(chalk.dim(`> Nudge #${event.nudgeCount}`));
200
+ }
201
+ break;
202
+ }
203
+
204
+ case 'step:force-released': {
205
+ const handle = stepHandles.get(event.stepName);
206
+ if (handle) {
207
+ handle.setOutput(chalk.yellow('> Force-released'));
208
+ }
209
+ break;
210
+ }
211
+
212
+ case 'step:review-completed': {
213
+ const handle = stepHandles.get(event.stepName);
214
+ if (handle) {
215
+ handle.setOutput(chalk.dim(`> Review: ${event.decision} by ${event.reviewerName}`));
216
+ }
217
+ break;
218
+ }
219
+
220
+ case 'step:owner-timeout': {
221
+ const handle = stepHandles.get(event.stepName);
222
+ if (handle) {
223
+ handle.setOutput(chalk.red(`> Owner ${event.ownerName} timed out`));
224
+ }
225
+ break;
226
+ }
227
+
228
+ case 'step:agent-report': {
229
+ const handle = stepHandles.get(event.stepName);
230
+ if (handle) {
231
+ const model = event.report.model ? `:${event.report.model}` : '';
232
+ handle.setOutput(chalk.dim(`> Report collected (${event.report.cli}${model})`));
233
+ }
234
+ break;
235
+ }
236
+
237
+ case 'step:completed': {
238
+ stepHandles.get(event.stepName)?.resolve();
239
+ break;
240
+ }
241
+
242
+ case 'step:skipped': {
243
+ const handle = stepHandles.get(event.stepName);
244
+ if (handle) {
245
+ handle.markSkipped();
246
+ handle.resolve();
247
+ } else {
248
+ // Step was skipped without ever being started (downstream of a failure).
249
+ // Add an already-resolved task so it shows in the listr output.
250
+ listr.add({
251
+ title: chalk.dim(`${event.stepName} (skipped)`),
252
+ task: async (): Promise<void> => {},
253
+ rendererOptions: { persistentOutput: true },
254
+ });
255
+ }
256
+ break;
257
+ }
258
+
259
+ case 'step:failed': {
260
+ stepHandles.get(event.stepName)?.reject(new Error(event.error ?? 'Step failed'));
261
+ break;
262
+ }
263
+
264
+ case 'run:completed': {
265
+ setHeader(chalk.green('Workflow completed'));
266
+ resolveWorkflow();
267
+ break;
268
+ }
269
+
270
+ case 'run:failed': {
271
+ setHeader(chalk.red(`Workflow failed: ${event.error}`));
272
+ rejectWorkflow(new Error(event.error ?? 'Workflow failed'));
273
+ break;
274
+ }
275
+
276
+ case 'run:cancelled': {
277
+ setHeader(chalk.yellow('Workflow cancelled'));
278
+ resolveWorkflow();
279
+ break;
280
+ }
281
+
282
+ case 'broker:event':
283
+ break;
284
+
285
+ default: {
286
+ const _exhaustive: never = event;
287
+ void _exhaustive;
288
+ }
289
+ }
290
+ });
291
+
292
+ const [result] = await Promise.all([
293
+ runner.execute(config, workflowName, undefined, executeOptions),
294
+ listr.run().catch(() => {
295
+ // Step failures are already represented in runner result.
296
+ }),
297
+ ]);
298
+
299
+ restoreConsole();
300
+ return result;
81
301
  }
82
302
 
83
303
  async function main(): Promise<void> {
@@ -96,6 +316,7 @@ async function main(): Promise<void> {
96
316
  `[workflow] warning: cannot write to ${dbPath} — run state will not be persisted (--resume unavailable)`
97
317
  );
98
318
  }
319
+
99
320
  const runner = new WorkflowRunner({ db: fileDb });
100
321
  let shuttingDown = false;
101
322
  const shutdown = async (signal: string): Promise<void> => {
@@ -113,17 +334,37 @@ async function main(): Promise<void> {
113
334
  if (resumeIdx !== -1) {
114
335
  const runId = args[resumeIdx + 1];
115
336
  if (!runId) {
116
- console.error('Error: --resume requires a run ID');
337
+ console.error(chalk.red('Error: --resume requires a run ID'));
117
338
  process.exit(1);
118
339
  }
119
- console.log(`Resuming run ${runId}...`);
120
- runner.on((event) => console.log(formatEvent(event)));
340
+
341
+ console.log(chalk.dim(`Resuming run ${runId}...`));
342
+ runner.on((event: WorkflowEvent) => {
343
+ const ts = new Date().toISOString().slice(11, 19);
344
+ switch (event.type) {
345
+ case 'step:started':
346
+ console.log(chalk.dim(`[${ts}]`), chalk.white(event.stepName), chalk.dim('started'));
347
+ break;
348
+ case 'step:completed':
349
+ console.log(chalk.dim(`[${ts}]`), chalk.green('✔'), event.stepName);
350
+ break;
351
+ case 'step:failed':
352
+ console.log(chalk.dim(`[${ts}]`), chalk.red('✗'), event.stepName, chalk.red(event.error ?? ''));
353
+ break;
354
+ case 'step:skipped':
355
+ console.log(chalk.dim(`[${ts}]`), chalk.dim('⊘'), chalk.dim(event.stepName));
356
+ break;
357
+ default:
358
+ break;
359
+ }
360
+ });
121
361
  const result = await runner.resume(runId);
362
+
122
363
  if (result.status === 'completed') {
123
- console.log(`\nWorkflow completed successfully.`);
364
+ console.log(chalk.green('\nWorkflow completed successfully.'));
124
365
  process.exit(0);
125
366
  } else {
126
- console.error(`\nWorkflow ${result.status}${result.error ? `: ${result.error}` : ''}`);
367
+ console.error(chalk.red(`\nWorkflow ${result.status}${result.error ? `: ${result.error}` : ''}`));
127
368
  process.exit(1);
128
369
  }
129
370
  return;
@@ -151,18 +392,17 @@ async function main(): Promise<void> {
151
392
  }
152
393
 
153
394
  const isValidate = args.includes('--validate');
154
-
155
- console.log(`Running workflow from ${yamlPath}...`);
156
-
157
395
  const isDryRun = !!process.env.DRY_RUN;
158
396
 
159
397
  const config = await runner.parseYamlFile(yamlPath);
398
+
160
399
  if (isValidate) {
161
400
  const { validateWorkflow, formatValidationReport } = await import('./validator.js');
162
401
  const issues = validateWorkflow(config);
163
402
  console.log(formatValidationReport(issues, yamlPath));
164
- process.exit(issues.some((i) => i.severity === 'error') ? 1 : 0);
403
+ process.exit(issues.some((issue) => issue.severity === 'error') ? 1 : 0);
165
404
  }
405
+
166
406
  if (isDryRun) {
167
407
  const { formatDryRunReport } = await import('./dry-run-format.js');
168
408
  const report = runner.dryRun(config, workflowName);
@@ -170,20 +410,19 @@ async function main(): Promise<void> {
170
410
  process.exit(report.valid ? 0 : 1);
171
411
  }
172
412
 
173
- runner.on((event) => console.log(formatEvent(event)));
174
413
  const executeOptions = startFromStep ? { startFrom: startFromStep, previousRunId } : undefined;
175
- const result = await runner.execute(config, workflowName, undefined, executeOptions);
414
+ const result = await runWithListr(runner, config, workflowName, executeOptions);
176
415
 
177
416
  if (result.status === 'completed') {
178
- console.log(`\nWorkflow completed successfully.`);
417
+ console.log(chalk.green('\nWorkflow completed successfully.'));
179
418
  process.exit(0);
180
419
  } else {
181
- console.error(`\nWorkflow ${result.status}${result.error ? `: ${result.error}` : ''}`);
420
+ console.error(chalk.red(`\nWorkflow ${result.status}${result.error ? `: ${result.error}` : ''}`));
182
421
  process.exit(1);
183
422
  }
184
423
  }
185
424
 
186
425
  main().catch((err: Error) => {
187
- console.error(`Error: ${err.message}`);
426
+ console.error(chalk.red(`Error: ${err.message}`));
188
427
  process.exit(1);
189
428
  });