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,82 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+
7
+ import { CodexCollector } from '../../collectors/codex.js';
8
+
9
+ const tempDirs: string[] = [];
10
+
11
+ function makeTempDir(prefix: string): string {
12
+ const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
13
+ tempDirs.push(dir);
14
+ return dir;
15
+ }
16
+
17
+ function createCodexFixture(tempDir: string, cwd: string, createdAtSeconds: number) {
18
+ const statePath = path.join(tempDir, 'state_5.sqlite');
19
+ const historyPath = path.join(tempDir, 'history.jsonl');
20
+ const db = new DatabaseSync(statePath);
21
+
22
+ db.exec(`
23
+ CREATE TABLE threads (
24
+ id TEXT PRIMARY KEY,
25
+ cwd TEXT,
26
+ model_provider TEXT,
27
+ tokens_used INTEGER,
28
+ created_at INTEGER,
29
+ updated_at INTEGER
30
+ );
31
+ CREATE TABLE logs (
32
+ thread_id TEXT,
33
+ ts INTEGER,
34
+ level TEXT,
35
+ message TEXT,
36
+ line INTEGER
37
+ );
38
+ `);
39
+
40
+ db.prepare(
41
+ 'INSERT INTO threads (id, cwd, model_provider, tokens_used, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
42
+ ).run('thread-1', cwd, 'openai/gpt-5', 321, createdAtSeconds, createdAtSeconds + 3);
43
+ db.prepare(
44
+ 'INSERT INTO logs (thread_id, ts, level, message, line) VALUES (?, ?, ?, ?, ?)',
45
+ ).run('thread-1', createdAtSeconds + 1, 'error', 'Command failed: bad exit code', 12);
46
+ db.close();
47
+
48
+ writeFileSync(historyPath, `${JSON.stringify({ session_id: 'thread-1', ts: createdAtSeconds, text: 'history' })}\n`);
49
+
50
+ return { statePath, historyPath };
51
+ }
52
+
53
+ afterEach(() => {
54
+ while (tempDirs.length > 0) {
55
+ rmSync(tempDirs.pop()!, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ describe('CodexCollector', () => {
60
+ it('matches by cwd and time window and extracts errors from logs', async () => {
61
+ const tempDir = makeTempDir('codex-fixture-');
62
+ const cwd = '/repo/codex-project';
63
+ const createdAtSeconds = 100;
64
+ const { statePath, historyPath } = createCodexFixture(tempDir, cwd, createdAtSeconds);
65
+ const collector = new CodexCollector({ statePath, historyPath });
66
+
67
+ const report = await collector.collect({
68
+ cli: 'codex',
69
+ cwd,
70
+ startedAt: 100_000,
71
+ completedAt: 105_000,
72
+ });
73
+
74
+ expect(report).not.toBeNull();
75
+ expect(report?.sessionId).toBe('thread-1');
76
+ expect(report?.provider).toBe('openai');
77
+ expect(report?.model).toBe('gpt-5');
78
+ expect(report?.tokens).toEqual({ input: 321, output: 0, cacheRead: 0 });
79
+ expect(report?.errors).toEqual([{ turn: 1, text: 'Command failed: bad exit code' }]);
80
+ expect(report?.finalStatus).toBe('failed');
81
+ });
82
+ });
@@ -0,0 +1,178 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+
7
+ const tempDirs: string[] = [];
8
+ const originalHome = process.env.HOME;
9
+
10
+ function makeTempDir(prefix: string): string {
11
+ const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
12
+ tempDirs.push(dir);
13
+ return dir;
14
+ }
15
+
16
+ function createOpenCodeFixture(homeDir: string, cwd: string, sessionCreatedAt: number): string {
17
+ const dbDir = path.join(homeDir, '.local', 'share', 'opencode');
18
+ mkdirSync(dbDir, { recursive: true });
19
+ const dbPath = path.join(dbDir, 'opencode.db');
20
+ const db = new DatabaseSync(dbPath);
21
+
22
+ db.exec(`
23
+ CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER);
24
+ CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
25
+ CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);
26
+ `);
27
+
28
+ const insertSession = db.prepare('INSERT INTO session (id, directory, time_created) VALUES (?, ?, ?)');
29
+ const insertMessage = db.prepare('INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)');
30
+ const insertPart = db.prepare('INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)');
31
+
32
+ insertSession.run('session-1', cwd, sessionCreatedAt);
33
+ insertSession.run('session-2', '/other/project', sessionCreatedAt + 1000);
34
+
35
+ insertMessage.run(
36
+ 'msg-1',
37
+ 'session-1',
38
+ sessionCreatedAt + 10,
39
+ JSON.stringify({ role: 'user', tokens: { input: 10, output: 0, cache: { read: 1 } } }),
40
+ );
41
+ insertMessage.run(
42
+ 'msg-2',
43
+ 'session-1',
44
+ sessionCreatedAt + 20,
45
+ JSON.stringify({
46
+ role: 'assistant',
47
+ modelID: 'gpt-5',
48
+ providerID: 'openai',
49
+ finish: 'error',
50
+ cost: 1.25,
51
+ tokens: { input: 15, output: 20, cache: { read: 4 } },
52
+ }),
53
+ );
54
+ insertMessage.run(
55
+ 'msg-other',
56
+ 'session-2',
57
+ sessionCreatedAt + 30,
58
+ JSON.stringify({ role: 'assistant', modelID: 'ignore-me', providerID: 'other', finish: 'completed' }),
59
+ );
60
+
61
+ insertPart.run(
62
+ 'part-1',
63
+ 'msg-1',
64
+ 'session-1',
65
+ sessionCreatedAt + 11,
66
+ JSON.stringify({ type: 'text', text: 'Planning work' }),
67
+ );
68
+ insertPart.run(
69
+ 'part-2',
70
+ 'msg-2',
71
+ 'session-1',
72
+ sessionCreatedAt + 21,
73
+ JSON.stringify({ type: 'tool_call', name: 'write_file' }),
74
+ );
75
+ insertPart.run(
76
+ 'part-3',
77
+ 'msg-2',
78
+ 'session-1',
79
+ sessionCreatedAt + 22,
80
+ JSON.stringify({ type: 'text', text: 'Error: database locked\nCleanup afterwards' }),
81
+ );
82
+ insertPart.run(
83
+ 'part-4',
84
+ 'msg-2',
85
+ 'session-1',
86
+ sessionCreatedAt + 23,
87
+ JSON.stringify({ type: 'text', text: 'Completed summary output' }),
88
+ );
89
+
90
+ db.close();
91
+ return dbPath;
92
+ }
93
+
94
+ async function importCollectorWithHome(homeDir: string) {
95
+ process.env.HOME = homeDir;
96
+ vi.resetModules();
97
+ vi.doMock('node:module', () => ({
98
+ createRequire: () => (id: string) => {
99
+ if (id !== 'better-sqlite3') {
100
+ throw new Error(`Unexpected module request: ${id}`);
101
+ }
102
+
103
+ return class BetterSqliteCompat {
104
+ private readonly db: DatabaseSync;
105
+
106
+ constructor(filename: string) {
107
+ this.db = new DatabaseSync(filename, { open: true, readOnly: true });
108
+ }
109
+
110
+ prepare(sql: string) {
111
+ const statement = this.db.prepare(sql);
112
+ return {
113
+ get<T>(params?: unknown): T | undefined {
114
+ return statement.get(params as never) as T | undefined;
115
+ },
116
+ all<T>(params?: unknown): T[] {
117
+ return statement.all(params as never) as T[];
118
+ },
119
+ };
120
+ }
121
+
122
+ pragma(_source: string) {
123
+ return undefined;
124
+ }
125
+
126
+ close() {
127
+ this.db.close();
128
+ }
129
+ };
130
+ },
131
+ }));
132
+ const module = await import('../../collectors/opencode.js');
133
+ return module.OpenCodeCollector;
134
+ }
135
+
136
+ afterEach(() => {
137
+ vi.resetModules();
138
+ process.env.HOME = originalHome;
139
+ while (tempDirs.length > 0) {
140
+ rmSync(tempDirs.pop()!, { recursive: true, force: true });
141
+ }
142
+ });
143
+
144
+ describe('OpenCodeCollector', () => {
145
+ it('matches by directory and time window, aggregates tokens, and extracts errors', async () => {
146
+ const homeDir = makeTempDir('opencode-home-');
147
+ const cwd = path.join(homeDir, 'workspace');
148
+ const sessionCreatedAt = 10_000;
149
+ createOpenCodeFixture(homeDir, cwd, sessionCreatedAt);
150
+ const OpenCodeCollector = await importCollectorWithHome(homeDir);
151
+
152
+ const collector = new OpenCodeCollector();
153
+ const report = await collector.collect({
154
+ cli: 'opencode',
155
+ cwd,
156
+ startedAt: sessionCreatedAt + 100,
157
+ completedAt: sessionCreatedAt + 500,
158
+ });
159
+
160
+ expect(report).not.toBeNull();
161
+ expect(report?.sessionId).toBe('session-1');
162
+ expect(report?.model).toBe('gpt-5');
163
+ expect(report?.provider).toBe('openai');
164
+ expect(report?.tokens).toEqual({ input: 25, output: 20, cacheRead: 5 });
165
+ expect(report?.cost).toBe(1.25);
166
+ expect(report?.toolCalls).toEqual([{ name: 'write_file', count: 1 }]);
167
+ expect(report?.errors).toEqual([{ turn: 3, text: 'Error: database locked' }]);
168
+ expect(report?.finalStatus).toBe('failed');
169
+ expect(report?.summary).toBe('Completed summary output');
170
+ });
171
+
172
+ it('returns false from canCollect when the database is missing', async () => {
173
+ const homeDir = makeTempDir('opencode-missing-home-');
174
+ const OpenCodeCollector = await importCollectorWithHome(homeDir);
175
+
176
+ expect(new OpenCodeCollector().canCollect()).toBe(false);
177
+ });
178
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { formatRunSummaryTable } from '../run-summary-table.js';
4
+
5
+ vi.mock('@relaycast/sdk', () => ({
6
+ RelayCast: vi.fn(),
7
+ RelayError: class RelayError extends Error {},
8
+ }));
9
+
10
+ vi.mock('../../relay.js', () => ({
11
+ AgentRelay: vi.fn(),
12
+ }));
13
+
14
+ const { WorkflowRunner } = await import('../runner.js');
15
+
16
+ describe('formatRunSummaryTable', () => {
17
+ it('renders all-passing steps', () => {
18
+ const output = formatRunSummaryTable(
19
+ [
20
+ { name: 'plan', agent: 'lead', status: 'completed', attempts: 1, durationMs: 1_000 },
21
+ { name: 'implement', agent: 'worker', status: 'completed', attempts: 1, durationMs: 2_000 },
22
+ ],
23
+ new Map([
24
+ [
25
+ 'plan',
26
+ {
27
+ cli: 'claude',
28
+ sessionId: 's1',
29
+ model: 'claude-sonnet-4',
30
+ provider: 'anthropic',
31
+ durationMs: 1_200,
32
+ cost: 0.75,
33
+ tokens: { input: 100, output: 50, cacheRead: 10 },
34
+ turns: 2,
35
+ toolCalls: [],
36
+ errors: [],
37
+ finalStatus: 'completed',
38
+ summary: 'planned',
39
+ },
40
+ ],
41
+ [
42
+ 'implement',
43
+ {
44
+ cli: 'codex',
45
+ sessionId: 's2',
46
+ model: 'gpt-5',
47
+ provider: 'openai',
48
+ durationMs: 3_400,
49
+ cost: 1.25,
50
+ tokens: { input: 300, output: 90, cacheRead: 20 },
51
+ turns: 4,
52
+ toolCalls: [],
53
+ errors: [{ turn: 2, text: 'Error: recovered after retry' }],
54
+ finalStatus: 'completed',
55
+ summary: 'implemented',
56
+ },
57
+ ],
58
+ ]),
59
+ );
60
+
61
+ expect(output).toMatchInlineSnapshot(`
62
+ " Step Status Model Cost Tokens Duration Errors
63
+ plan pass claude-sonnet-4 $0.75 160 1s --
64
+ implement pass gpt-5 $1.25 410 3s 1 (fixed)
65
+ ────────────────────────────────────────────────────────────────────────────────────────────
66
+ Total $2.00 570 5s "
67
+ `);
68
+ });
69
+
70
+ it('renders a failed step with the first error line', () => {
71
+ const output = formatRunSummaryTable(
72
+ [{ name: 'broken-step', agent: 'worker', status: 'failed', attempts: 1, durationMs: 1_500, error: 'boom' }],
73
+ new Map([
74
+ [
75
+ 'broken-step',
76
+ {
77
+ cli: 'opencode',
78
+ sessionId: 's3',
79
+ model: 'gpt-5',
80
+ provider: 'openai',
81
+ durationMs: 1_500,
82
+ cost: 0.01,
83
+ tokens: { input: 10, output: 5, cacheRead: 0 },
84
+ turns: 1,
85
+ toolCalls: [],
86
+ errors: [{ turn: 1, text: 'Error: database locked' }],
87
+ finalStatus: 'failed',
88
+ summary: null,
89
+ },
90
+ ],
91
+ ]),
92
+ );
93
+
94
+ expect(output).toContain('broken-step FAIL');
95
+ expect(output).toContain(' └─ Error [turn 1] Error: database locked');
96
+ });
97
+
98
+ it('renders deterministic steps without reports using placeholder columns', () => {
99
+ const output = formatRunSummaryTable(
100
+ [{ name: 'lint', agent: 'shell', status: 'completed', attempts: 1, durationMs: 900 }],
101
+ new Map(),
102
+ );
103
+
104
+ expect(output).toContain('lint pass --');
105
+ expect(output).toContain('--');
106
+ // No reports means no cost column
107
+ expect(output).not.toContain('Cost');
108
+ });
109
+
110
+ it('hides Cost column when no report has reliable cost data', () => {
111
+ const output = formatRunSummaryTable(
112
+ [
113
+ { name: 'gen-code', agent: 'worker', status: 'completed', attempts: 1, durationMs: 5_000 },
114
+ ],
115
+ new Map([
116
+ [
117
+ 'gen-code',
118
+ {
119
+ cli: 'claude',
120
+ sessionId: 's1',
121
+ model: 'claude-sonnet-4',
122
+ provider: 'anthropic',
123
+ durationMs: 5_000,
124
+ cost: null,
125
+ tokens: { input: 200, output: 80, cacheRead: 0 },
126
+ turns: 3,
127
+ toolCalls: [],
128
+ errors: [],
129
+ finalStatus: 'completed',
130
+ summary: 'done',
131
+ },
132
+ ],
133
+ ]),
134
+ );
135
+
136
+ expect(output).not.toContain('Cost');
137
+ expect(output).toContain('Tokens');
138
+ expect(output).toContain('280');
139
+ });
140
+ });
141
+
142
+ describe('WorkflowRunner logRunSummary', () => {
143
+ it('falls back to the legacy summary format when no reports exist', () => {
144
+ const runner = new WorkflowRunner({ cwd: '/tmp/workflow-runner' });
145
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
146
+
147
+ (runner as any).logRunSummary(
148
+ 'sample-workflow',
149
+ [{ name: 'lint', agent: 'shell', status: 'completed', attempts: 1, output: 'ok' }],
150
+ 'run-1',
151
+ );
152
+
153
+ const combined = logSpy.mock.calls.flat().join('\n');
154
+ expect(combined).toContain('Workflow "sample-workflow"');
155
+ expect(combined).toContain('✓ lint [shell]');
156
+ expect(combined).not.toContain('Step Status');
157
+
158
+ logSpy.mockRestore();
159
+ });
160
+ });
@@ -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
+ });
@@ -22,6 +22,7 @@ import type {
22
22
  } from './types.js';
23
23
  import { WorkflowRunner, type WorkflowEventListener, type VariableContext, type StepExecutor } from './runner.js';
24
24
  import { formatDryRunReport } from './dry-run-format.js';
25
+ import { createDefaultEventLogger, type LogLevel } from './default-logger.js';
25
26
 
26
27
  // ── Option types for the builder API ────────────────────────────────────────
27
28
 
@@ -47,6 +48,7 @@ export interface AgentOptions {
47
48
  export interface AgentStepOptions {
48
49
  agent: string;
49
50
  task: string;
51
+ cwd?: string;
50
52
  dependsOn?: string[];
51
53
  verification?: VerificationCheck;
52
54
  timeoutMs?: number;
@@ -57,6 +59,7 @@ export interface AgentStepOptions {
57
59
  export interface DeterministicStepOptions {
58
60
  type: 'deterministic';
59
61
  command: string;
62
+ cwd?: string;
60
63
  /** Capture stdout as step output for downstream steps. Default: true. */
61
64
  captureOutput?: boolean;
62
65
  /** Fail if command exit code is non-zero. Default: true. */
@@ -104,6 +107,10 @@ export interface WorkflowRunOptions {
104
107
  startFrom?: string;
105
108
  /** Previous run ID whose cached outputs are used with startFrom. */
106
109
  previousRunId?: string;
110
+ /** Console log verbosity: "verbose" | "normal" (default) | "quiet" | false (silent). */
111
+ logLevel?: LogLevel;
112
+ /** Renderer: "listr" for listr2 UI, "default" for console logger, false to disable. */
113
+ renderer?: 'listr' | 'default' | false;
107
114
  }
108
115
 
109
116
  // ── WorkflowBuilder ─────────────────────────────────────────────────────────
@@ -256,6 +263,7 @@ export class WorkflowBuilder {
256
263
  }
257
264
  step.type = 'deterministic';
258
265
  step.command = options.command;
266
+ if (options.cwd !== undefined) step.cwd = options.cwd;
259
267
  if (options.captureOutput !== undefined) step.captureOutput = options.captureOutput;
260
268
  if (options.failOnError !== undefined) step.failOnError = options.failOnError;
261
269
  if (options.dependsOn !== undefined) step.dependsOn = options.dependsOn;
@@ -280,6 +288,7 @@ export class WorkflowBuilder {
280
288
  }
281
289
  step.agent = agentOpts.agent;
282
290
  step.task = agentOpts.task;
291
+ if (agentOpts.cwd !== undefined) step.cwd = agentOpts.cwd;
283
292
  if (agentOpts.dependsOn !== undefined) step.dependsOn = agentOpts.dependsOn;
284
293
  if (agentOpts.verification !== undefined) step.verification = agentOpts.verification;
285
294
  if (agentOpts.timeoutMs !== undefined) step.timeoutMs = agentOpts.timeoutMs;
@@ -329,7 +338,11 @@ export class WorkflowBuilder {
329
338
  if (this._timeoutMs !== undefined) config.swarm.timeoutMs = this._timeoutMs;
330
339
  if (this._channel !== undefined) config.swarm.channel = this._channel;
331
340
  if (this._idleNudge !== undefined) config.swarm.idleNudge = this._idleNudge;
332
- if (this._errorHandling !== undefined) config.errorHandling = this._errorHandling;
341
+ config.errorHandling = this._errorHandling ?? {
342
+ strategy: 'retry',
343
+ maxRetries: 2,
344
+ retryDelayMs: 10_000,
345
+ };
333
346
  if (this._coordination !== undefined) config.coordination = this._coordination;
334
347
  if (this._state !== undefined) config.state = this._state;
335
348
  if (this._trajectories !== undefined) config.trajectories = this._trajectories;
@@ -363,15 +376,23 @@ export class WorkflowBuilder {
363
376
  return report;
364
377
  }
365
378
 
379
+ // Wire up default console logger unless explicitly disabled
380
+ // renderer: "listr" owns the terminal — skip console logger to avoid garbled output
381
+ // renderer: false implies no output at all
382
+ const logLevel = options.renderer === 'listr' || options.renderer === false
383
+ ? false
384
+ : (options.logLevel ?? 'normal');
385
+ if (logLevel !== false) {
386
+ runner.on(createDefaultEventLogger(logLevel));
387
+ }
388
+
389
+ // Wire up user-provided event handler (additive — does not replace the default logger)
366
390
  if (options.onEvent) {
367
391
  runner.on(options.onEvent);
368
392
  }
369
393
 
370
394
  // Auto-detect RESUME_RUN_ID env var for resuming failed runs
371
395
  const resumeRunId = process.env.RESUME_RUN_ID;
372
- if (resumeRunId) {
373
- return runner.resume(resumeRunId, options.vars);
374
- }
375
396
 
376
397
  const startFrom = this._startFrom ?? options.startFrom ?? process.env.START_FROM;
377
398
  const previousRunId = this._previousRunId ?? options.previousRunId ?? process.env.PREVIOUS_RUN_ID;
@@ -379,6 +400,29 @@ export class WorkflowBuilder {
379
400
  ? { startFrom, previousRunId }
380
401
  : undefined;
381
402
 
403
+ // If listr renderer requested, wire it up and run concurrently
404
+ // Must be set up BEFORE the resume check so resume runs also get event output
405
+ if (options.renderer === 'listr') {
406
+ const { createWorkflowRenderer } = await import('./listr-renderer.js');
407
+ const renderer = createWorkflowRenderer();
408
+ runner.on(renderer.onEvent);
409
+
410
+ const runPromise = resumeRunId
411
+ ? runner.resume(resumeRunId, options.vars)
412
+ : runner.execute(config, options.workflow, options.vars, executeOptions);
413
+
414
+ try {
415
+ const [result] = await Promise.all([runPromise, renderer.start()]);
416
+ return result;
417
+ } finally {
418
+ renderer.unmount();
419
+ }
420
+ }
421
+
422
+ if (resumeRunId) {
423
+ return runner.resume(resumeRunId, options.vars);
424
+ }
425
+
382
426
  return runner.execute(config, options.workflow, options.vars, executeOptions);
383
427
  }
384
428
  }
@@ -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
+ }