@hyperdrive.bot/bmad-workflow 1.0.4 → 1.0.7
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.
- package/dist/models/provider.d.ts +1 -1
- package/dist/models/provider.js +5 -0
- package/dist/services/agents/agent-runner-factory.js +5 -1
- package/dist/services/agents/index.d.ts +1 -0
- package/dist/services/agents/index.js +1 -0
- package/dist/services/agents/opencode-agent-runner.d.ts +92 -0
- package/dist/services/agents/opencode-agent-runner.js +392 -0
- package/dist/services/orchestration/task-decomposition-service.js +79 -126
- package/dist/utils/shared-flags.js +2 -2
- package/package.json +2 -1
package/dist/models/provider.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ClaudeAgentRunner } from './claude-agent-runner.js';
|
|
7
7
|
import { GeminiAgentRunner } from './gemini-agent-runner.js';
|
|
8
|
+
import { OpenCodeAgentRunner } from './opencode-agent-runner.js';
|
|
8
9
|
/**
|
|
9
10
|
* Create an AI provider runner for the specified provider
|
|
10
11
|
*
|
|
@@ -28,6 +29,9 @@ export function createAgentRunner(provider, logger) {
|
|
|
28
29
|
case 'gemini': {
|
|
29
30
|
return new GeminiAgentRunner(logger);
|
|
30
31
|
}
|
|
32
|
+
case 'opencode': {
|
|
33
|
+
return new OpenCodeAgentRunner(logger);
|
|
34
|
+
}
|
|
31
35
|
default: {
|
|
32
36
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
33
37
|
}
|
|
@@ -40,5 +44,5 @@ export function createAgentRunner(provider, logger) {
|
|
|
40
44
|
* @returns true if the provider is supported
|
|
41
45
|
*/
|
|
42
46
|
export function isProviderSupported(provider) {
|
|
43
|
-
return provider === 'claude' || provider === 'gemini';
|
|
47
|
+
return provider === 'claude' || provider === 'gemini' || provider === 'opencode';
|
|
44
48
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCodeAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Open Code CLI process spawning with comprehensive error handling,
|
|
5
|
+
* logging, and timeout management for reliable AI agent execution.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
import type { AgentOptions, AgentResult } from '../../models/index.js';
|
|
9
|
+
import type { AIProvider } from '../../models/provider.js';
|
|
10
|
+
import type { AIProviderRunner } from './agent-runner.js';
|
|
11
|
+
/**
|
|
12
|
+
* OpenCodeAgentRunner service for executing Open Code AI agents
|
|
13
|
+
*
|
|
14
|
+
* Spawns Open Code CLI processes to execute AI agents with specified prompts.
|
|
15
|
+
* Handles timeout, error collection, result formatting, and process cleanup.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const logger = createLogger({ namespace: 'agent-runner' })
|
|
20
|
+
* const runner = new OpenCodeAgentRunner(logger)
|
|
21
|
+
* const result = await runner.runAgent({
|
|
22
|
+
* prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
|
|
23
|
+
* agentType: 'architect',
|
|
24
|
+
* timeout: 300000
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare class OpenCodeAgentRunner implements AIProviderRunner {
|
|
29
|
+
readonly provider: AIProvider;
|
|
30
|
+
private readonly config;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
/**
|
|
33
|
+
* Create a new OpenCodeAgentRunner instance
|
|
34
|
+
*
|
|
35
|
+
* @param logger - Logger instance for structured logging
|
|
36
|
+
*/
|
|
37
|
+
constructor(logger: pino.Logger);
|
|
38
|
+
/**
|
|
39
|
+
* Get the count of active processes
|
|
40
|
+
* (Useful for testing and monitoring purposes)
|
|
41
|
+
*
|
|
42
|
+
* @returns Number of currently active child processes
|
|
43
|
+
*/
|
|
44
|
+
getActiveProcessCount(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Execute an Open Code AI agent with the specified prompt and options
|
|
47
|
+
*
|
|
48
|
+
* This method spawns an Open Code CLI process, captures output, handles errors,
|
|
49
|
+
* and enforces timeouts. It never throws exceptions - all errors are returned
|
|
50
|
+
* as typed AgentResult objects.
|
|
51
|
+
*
|
|
52
|
+
* @param prompt - The prompt to execute with the agent
|
|
53
|
+
* @param options - Agent execution options (without prompt)
|
|
54
|
+
* @returns AgentResult with success status, output, errors, and metadata
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const result = await runner.runAgent('@.bmad-core/agents/architect.md Create epic for user auth', {
|
|
59
|
+
* agentType: 'architect',
|
|
60
|
+
* timeout: 300000
|
|
61
|
+
* })
|
|
62
|
+
*
|
|
63
|
+
* if (result.success) {
|
|
64
|
+
* console.log('Output:', result.output)
|
|
65
|
+
* } else {
|
|
66
|
+
* console.error('Error:', result.errors)
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
runAgent(prompt: string, options: Omit<AgentOptions, 'prompt'>): Promise<AgentResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Parse JSON output from Open Code CLI
|
|
73
|
+
*
|
|
74
|
+
* Open Code's --format json outputs structured events.
|
|
75
|
+
* This method extracts the actual response content.
|
|
76
|
+
*
|
|
77
|
+
* @param stdoutData - Raw stdout from Open Code CLI
|
|
78
|
+
* @returns Extracted content string
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
private parseJsonOutput;
|
|
82
|
+
/**
|
|
83
|
+
* Build detailed error context for debugging process failures
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
private buildErrorContext;
|
|
87
|
+
/**
|
|
88
|
+
* Invoke onResponse callback and return result
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
private returnWithCallback;
|
|
92
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCodeAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Open Code CLI process spawning with comprehensive error handling,
|
|
5
|
+
* logging, and timeout management for reliable AI agent execution.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
8
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
import { PROVIDER_CONFIGS } from '../../models/provider.js';
|
|
14
|
+
/**
|
|
15
|
+
* Track active child processes for cleanup on SIGINT
|
|
16
|
+
*/
|
|
17
|
+
const activeProcesses = new Set();
|
|
18
|
+
/**
|
|
19
|
+
* Track if SIGINT handler has been registered
|
|
20
|
+
*/
|
|
21
|
+
let sigintHandlerRegistered = false;
|
|
22
|
+
/**
|
|
23
|
+
* Register SIGINT handler for graceful process cleanup
|
|
24
|
+
*/
|
|
25
|
+
const registerSigintHandler = (logger) => {
|
|
26
|
+
if (sigintHandlerRegistered)
|
|
27
|
+
return;
|
|
28
|
+
process.on('SIGINT', () => {
|
|
29
|
+
logger.info({
|
|
30
|
+
activeProcessCount: activeProcesses.size,
|
|
31
|
+
}, 'Received SIGINT, cleaning up active processes');
|
|
32
|
+
// Kill all active child processes
|
|
33
|
+
for (const childProcess of activeProcesses) {
|
|
34
|
+
try {
|
|
35
|
+
childProcess.kill('SIGTERM');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error({
|
|
39
|
+
error: error.message,
|
|
40
|
+
}, 'Error killing child process');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Clear the set
|
|
44
|
+
activeProcesses.clear();
|
|
45
|
+
// Exit the main process
|
|
46
|
+
process.exit(130); // 130 = 128 + SIGINT signal number (2)
|
|
47
|
+
});
|
|
48
|
+
sigintHandlerRegistered = true;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* OpenCodeAgentRunner service for executing Open Code AI agents
|
|
52
|
+
*
|
|
53
|
+
* Spawns Open Code CLI processes to execute AI agents with specified prompts.
|
|
54
|
+
* Handles timeout, error collection, result formatting, and process cleanup.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const logger = createLogger({ namespace: 'agent-runner' })
|
|
59
|
+
* const runner = new OpenCodeAgentRunner(logger)
|
|
60
|
+
* const result = await runner.runAgent({
|
|
61
|
+
* prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
|
|
62
|
+
* agentType: 'architect',
|
|
63
|
+
* timeout: 300000
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class OpenCodeAgentRunner {
|
|
68
|
+
provider = 'opencode';
|
|
69
|
+
config = PROVIDER_CONFIGS.opencode;
|
|
70
|
+
logger;
|
|
71
|
+
/**
|
|
72
|
+
* Create a new OpenCodeAgentRunner instance
|
|
73
|
+
*
|
|
74
|
+
* @param logger - Logger instance for structured logging
|
|
75
|
+
*/
|
|
76
|
+
constructor(logger) {
|
|
77
|
+
this.logger = logger;
|
|
78
|
+
registerSigintHandler(logger);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the count of active processes
|
|
82
|
+
* (Useful for testing and monitoring purposes)
|
|
83
|
+
*
|
|
84
|
+
* @returns Number of currently active child processes
|
|
85
|
+
*/
|
|
86
|
+
getActiveProcessCount() {
|
|
87
|
+
return activeProcesses.size;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Execute an Open Code AI agent with the specified prompt and options
|
|
91
|
+
*
|
|
92
|
+
* This method spawns an Open Code CLI process, captures output, handles errors,
|
|
93
|
+
* and enforces timeouts. It never throws exceptions - all errors are returned
|
|
94
|
+
* as typed AgentResult objects.
|
|
95
|
+
*
|
|
96
|
+
* @param prompt - The prompt to execute with the agent
|
|
97
|
+
* @param options - Agent execution options (without prompt)
|
|
98
|
+
* @returns AgentResult with success status, output, errors, and metadata
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const result = await runner.runAgent('@.bmad-core/agents/architect.md Create epic for user auth', {
|
|
103
|
+
* agentType: 'architect',
|
|
104
|
+
* timeout: 300000
|
|
105
|
+
* })
|
|
106
|
+
*
|
|
107
|
+
* if (result.success) {
|
|
108
|
+
* console.log('Output:', result.output)
|
|
109
|
+
* } else {
|
|
110
|
+
* console.error('Error:', result.errors)
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
async runAgent(prompt, options) {
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
const timeout = options.timeout ?? 1_800_000; // Default 30 minutes
|
|
117
|
+
// Input validation
|
|
118
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
119
|
+
return this.returnWithCallback({
|
|
120
|
+
agentType: options.agentType,
|
|
121
|
+
duration: Date.now() - startTime,
|
|
122
|
+
errors: 'Prompt is required and cannot be empty',
|
|
123
|
+
exitCode: -1,
|
|
124
|
+
output: '',
|
|
125
|
+
success: false,
|
|
126
|
+
}, options.onResponse);
|
|
127
|
+
}
|
|
128
|
+
// Log execution start with metadata
|
|
129
|
+
this.logger.info({
|
|
130
|
+
agentType: options.agentType,
|
|
131
|
+
promptLength: prompt.length,
|
|
132
|
+
references: options.references?.length ?? 0,
|
|
133
|
+
timeout,
|
|
134
|
+
}, 'Executing Open Code agent');
|
|
135
|
+
// Invoke onPrompt callback if provided
|
|
136
|
+
if (options.onPrompt) {
|
|
137
|
+
try {
|
|
138
|
+
// Create options object without callbacks for the callback
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
140
|
+
const { onPrompt, onResponse, ...callbackOptions } = options;
|
|
141
|
+
await onPrompt(prompt, callbackOptions);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.logger.warn({ error: error.message }, 'Error in onPrompt callback');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// For large prompts or prompts with special characters, use a temp file
|
|
148
|
+
// This avoids shell escaping issues
|
|
149
|
+
let tempDir = null;
|
|
150
|
+
let tempFile = null;
|
|
151
|
+
let stdoutData = '';
|
|
152
|
+
let stderrData = '';
|
|
153
|
+
try {
|
|
154
|
+
// Create temp directory and file
|
|
155
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencode-prompt-'));
|
|
156
|
+
tempFile = join(tempDir, 'prompt.txt');
|
|
157
|
+
await writeFile(tempFile, prompt, 'utf8');
|
|
158
|
+
// Build command with --file flag (Open Code specific)
|
|
159
|
+
// Format: opencode run --format json --file "${tempFile}"
|
|
160
|
+
const flags = this.config.flags.join(' ');
|
|
161
|
+
const command = `${this.config.command} ${flags} --file "${tempFile}"`;
|
|
162
|
+
// Log the command being executed
|
|
163
|
+
this.logger.info({
|
|
164
|
+
command,
|
|
165
|
+
promptLength: prompt.length,
|
|
166
|
+
tempFile,
|
|
167
|
+
}, 'Executing command with temp file');
|
|
168
|
+
// Use exec for better shell compatibility
|
|
169
|
+
const { stderr, stdout } = await execAsync(command, {
|
|
170
|
+
env: process.env,
|
|
171
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
172
|
+
shell: process.env.SHELL || '/bin/bash',
|
|
173
|
+
timeout,
|
|
174
|
+
});
|
|
175
|
+
stdoutData = stdout;
|
|
176
|
+
stderrData = stderr;
|
|
177
|
+
const duration = Date.now() - startTime;
|
|
178
|
+
this.logger.info({
|
|
179
|
+
duration,
|
|
180
|
+
stderrLength: stderrData.length,
|
|
181
|
+
stdoutLength: stdoutData.length,
|
|
182
|
+
}, 'Open Code CLI process completed successfully');
|
|
183
|
+
// Parse JSON output from Open Code
|
|
184
|
+
const parsedOutput = this.parseJsonOutput(stdoutData);
|
|
185
|
+
const result = {
|
|
186
|
+
agentType: options.agentType,
|
|
187
|
+
duration,
|
|
188
|
+
errors: stderrData,
|
|
189
|
+
exitCode: 0,
|
|
190
|
+
output: parsedOutput,
|
|
191
|
+
success: true,
|
|
192
|
+
};
|
|
193
|
+
return this.returnWithCallback(result, options.onResponse);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
// Handle exec errors (includes timeout, non-zero exit, etc.)
|
|
197
|
+
const duration = Date.now() - startTime;
|
|
198
|
+
// Extract stdout/stderr from exec error if available
|
|
199
|
+
const execError = error;
|
|
200
|
+
if (execError.stdout)
|
|
201
|
+
stdoutData = execError.stdout;
|
|
202
|
+
if (execError.stderr)
|
|
203
|
+
stderrData = execError.stderr;
|
|
204
|
+
const exitCode = execError.code ?? -1;
|
|
205
|
+
const isTimeout = execError.killed === true && execError.signal === 'SIGTERM';
|
|
206
|
+
const wasKilled = execError.killed === true;
|
|
207
|
+
const signal = execError.signal ?? null;
|
|
208
|
+
// Build detailed error context for debugging
|
|
209
|
+
const errorContext = this.buildErrorContext({
|
|
210
|
+
cmd: execError.cmd,
|
|
211
|
+
duration,
|
|
212
|
+
exitCode,
|
|
213
|
+
isTimeout,
|
|
214
|
+
signal,
|
|
215
|
+
stderrData,
|
|
216
|
+
stdoutData,
|
|
217
|
+
wasKilled,
|
|
218
|
+
});
|
|
219
|
+
this.logger.error({
|
|
220
|
+
cmd: execError.cmd,
|
|
221
|
+
duration,
|
|
222
|
+
error: execError.message,
|
|
223
|
+
exitCode,
|
|
224
|
+
isTimeout,
|
|
225
|
+
signal,
|
|
226
|
+
stderrLength: stderrData.length,
|
|
227
|
+
stdoutLength: stdoutData.length,
|
|
228
|
+
wasKilled,
|
|
229
|
+
}, isTimeout ? 'Open Code CLI process timeout' : 'Open Code CLI process error');
|
|
230
|
+
return this.returnWithCallback({
|
|
231
|
+
agentType: options.agentType,
|
|
232
|
+
duration,
|
|
233
|
+
errors: errorContext,
|
|
234
|
+
exitCode: isTimeout ? 124 : exitCode,
|
|
235
|
+
output: stdoutData,
|
|
236
|
+
success: false,
|
|
237
|
+
}, options.onResponse);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
// Clean up temp file and directory
|
|
241
|
+
if (tempDir) {
|
|
242
|
+
try {
|
|
243
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
244
|
+
this.logger.debug({ tempDir }, 'Cleaned up temp directory');
|
|
245
|
+
}
|
|
246
|
+
catch (cleanupError) {
|
|
247
|
+
this.logger.warn({ error: cleanupError.message, tempDir }, 'Failed to clean up temp directory');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Parse JSON output from Open Code CLI
|
|
254
|
+
*
|
|
255
|
+
* Open Code's --format json outputs structured events.
|
|
256
|
+
* This method extracts the actual response content.
|
|
257
|
+
*
|
|
258
|
+
* @param stdoutData - Raw stdout from Open Code CLI
|
|
259
|
+
* @returns Extracted content string
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
parseJsonOutput(stdoutData) {
|
|
263
|
+
if (!stdoutData || stdoutData.trim().length === 0) {
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(stdoutData);
|
|
268
|
+
// Try common JSON response structures
|
|
269
|
+
// Open Code may output different structures depending on version
|
|
270
|
+
if (typeof parsed === 'string') {
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
273
|
+
if (parsed.content) {
|
|
274
|
+
return String(parsed.content);
|
|
275
|
+
}
|
|
276
|
+
if (parsed.message) {
|
|
277
|
+
return String(parsed.message);
|
|
278
|
+
}
|
|
279
|
+
if (parsed.response) {
|
|
280
|
+
return String(parsed.response);
|
|
281
|
+
}
|
|
282
|
+
if (parsed.output) {
|
|
283
|
+
return String(parsed.output);
|
|
284
|
+
}
|
|
285
|
+
if (parsed.text) {
|
|
286
|
+
return String(parsed.text);
|
|
287
|
+
}
|
|
288
|
+
// If we have a result array, try to extract content
|
|
289
|
+
if (Array.isArray(parsed)) {
|
|
290
|
+
const messages = parsed
|
|
291
|
+
.filter((item) => item.type === 'message' || item.content || item.text)
|
|
292
|
+
.map((item) => item.content || item.text || item.message || '')
|
|
293
|
+
.filter(Boolean);
|
|
294
|
+
if (messages.length > 0) {
|
|
295
|
+
return messages.join('\n');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Fallback: stringify the parsed object
|
|
299
|
+
this.logger.debug({ parsedKeys: Object.keys(parsed) }, 'Unknown JSON structure, returning stringified');
|
|
300
|
+
return JSON.stringify(parsed, null, 2);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// JSON parsing failed, return raw output
|
|
304
|
+
this.logger.debug({ stdoutLength: stdoutData.length }, 'JSON parsing failed, returning raw output');
|
|
305
|
+
return stdoutData;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Build detailed error context for debugging process failures
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
buildErrorContext(info) {
|
|
313
|
+
const lines = [];
|
|
314
|
+
// Determine the type of failure
|
|
315
|
+
if (info.isTimeout) {
|
|
316
|
+
lines.push(`Process timeout after ${info.duration}ms`);
|
|
317
|
+
}
|
|
318
|
+
else if (info.wasKilled) {
|
|
319
|
+
lines.push('Process was killed externally');
|
|
320
|
+
// Explain common exit codes
|
|
321
|
+
switch (info.exitCode) {
|
|
322
|
+
case 130: {
|
|
323
|
+
lines.push('Exit code 130 = SIGINT (user interrupt, Ctrl+C)');
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 137: {
|
|
327
|
+
lines.push('Exit code 137 = SIGKILL (process forcefully killed)', 'Possible causes:', ' - System OOM killer', ' - Exceeded memory limits', ' - Docker/container killed the process');
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 143: {
|
|
331
|
+
lines.push('Exit code 143 = SIGTERM (process terminated)', 'Possible causes:', ' - System OOM killer terminated the process', ' - Another process sent SIGTERM', ' - Parent process cleanup', ' - Docker/container resource limits');
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
// No default
|
|
335
|
+
}
|
|
336
|
+
if (info.signal) {
|
|
337
|
+
lines.push(`Signal received: ${info.signal}`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
lines.push('No signal information available (signal: null)');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
lines.push(`Process exited with code ${info.exitCode}`);
|
|
345
|
+
}
|
|
346
|
+
// Add duration context
|
|
347
|
+
lines.push(`Duration: ${(info.duration / 1000).toFixed(2)}s`);
|
|
348
|
+
// Add output context
|
|
349
|
+
if (info.stdoutData.length > 0) {
|
|
350
|
+
lines.push(`Stdout (${info.stdoutData.length} chars):`);
|
|
351
|
+
// Truncate if too long, show last 500 chars which are often most relevant
|
|
352
|
+
const truncatedStdout = info.stdoutData.length > 500 ? `...[truncated]...\n${info.stdoutData.slice(-500)}` : info.stdoutData;
|
|
353
|
+
lines.push(truncatedStdout);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
lines.push('Stdout: (empty - no output captured)');
|
|
357
|
+
}
|
|
358
|
+
if (info.stderrData.length > 0) {
|
|
359
|
+
lines.push(`Stderr (${info.stderrData.length} chars):`);
|
|
360
|
+
const truncatedStderr = info.stderrData.length > 500 ? `...[truncated]...\n${info.stderrData.slice(-500)}` : info.stderrData;
|
|
361
|
+
lines.push(truncatedStderr);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
lines.push('Stderr: (empty - no error output)');
|
|
365
|
+
}
|
|
366
|
+
// Add command for reference (truncate if too long)
|
|
367
|
+
if (info.cmd) {
|
|
368
|
+
const truncatedCmd = info.cmd.length > 200 ? `${info.cmd.slice(0, 200)}...[truncated]` : info.cmd;
|
|
369
|
+
lines.push(`Command: ${truncatedCmd}`);
|
|
370
|
+
}
|
|
371
|
+
// Add troubleshooting suggestions for empty output
|
|
372
|
+
if (info.stdoutData.length === 0 && info.stderrData.length === 0) {
|
|
373
|
+
lines.push('', 'Troubleshooting (no output captured):', ' 1. Check system resources (memory, CPU)', ' 2. Verify Open Code CLI is properly installed: `opencode --version`', ' 3. Check for Open Code API issues', ' 4. Review system logs: `dmesg | tail -50` (for OOM killer)', ' 5. Try running the command manually to see output');
|
|
374
|
+
}
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Invoke onResponse callback and return result
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
async returnWithCallback(result, onResponse) {
|
|
382
|
+
if (onResponse) {
|
|
383
|
+
try {
|
|
384
|
+
await onResponse(result);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
this.logger.warn({ error: error.message }, 'Error in onResponse callback');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -159,70 +159,38 @@ ${options.contextFiles.map((f) => `- ${f}`).join('\n')}
|
|
|
159
159
|
`;
|
|
160
160
|
}
|
|
161
161
|
// Add output format instructions (varies based on story format mode)
|
|
162
|
-
prompt += options.storyFormat ? `## OUTPUT FORMAT - STORY MODE
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
\`\`\`yaml
|
|
162
|
+
prompt += options.storyFormat ? `## OUTPUT FORMAT - STORY MODE - CRITICAL INSTRUCTIONS
|
|
163
|
+
|
|
164
|
+
**YOUR RESPONSE MUST BE PURE YAML ONLY.**
|
|
165
|
+
- Do NOT write any explanation, summary, or commentary
|
|
166
|
+
- Do NOT use markdown code fences
|
|
167
|
+
- Start your response DIRECTLY with "masterPrompt: |"
|
|
168
|
+
|
|
169
|
+
Each task becomes a BMAD story with user story format, acceptance criteria, and tasks breakdown.
|
|
170
|
+
|
|
171
|
+
The YAML structure MUST be:
|
|
172
|
+
|
|
174
173
|
masterPrompt: |
|
|
175
|
-
A reusable prompt template
|
|
176
|
-
Include best practices
|
|
174
|
+
A reusable prompt template for all stories.
|
|
175
|
+
Include best practices and coding standards.
|
|
177
176
|
|
|
178
177
|
tasks:
|
|
179
178
|
- id: ${options.storyPrefix}-001
|
|
180
179
|
title: "Clear, specific story title"
|
|
181
180
|
description: "High-level description of what this story delivers"
|
|
182
181
|
estimatedMinutes: 10
|
|
183
|
-
dependencies: []
|
|
182
|
+
dependencies: []
|
|
184
183
|
parallelizable: true
|
|
185
184
|
agentType: "dev"
|
|
186
|
-
targetFiles:
|
|
185
|
+
targetFiles:
|
|
187
186
|
- "path/to/file.js"
|
|
188
187
|
prompt: |
|
|
189
|
-
Create a story following BMAD story format:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
## Story
|
|
197
|
-
**As a** [role/persona],
|
|
198
|
-
**I want** [capability/feature],
|
|
199
|
-
**so that** [benefit/value]
|
|
200
|
-
|
|
201
|
-
## Acceptance Criteria
|
|
202
|
-
1. [Specific, testable criterion]
|
|
203
|
-
2. [Another criterion]
|
|
204
|
-
3. [Another criterion]
|
|
205
|
-
|
|
206
|
-
## Tasks / Subtasks
|
|
207
|
-
- [ ] Task 1: [Description] (AC: #1)
|
|
208
|
-
- [ ] Subtask 1.1: [Specific action]
|
|
209
|
-
- [ ] Subtask 1.2: [Specific action]
|
|
210
|
-
- [ ] Task 2: [Description] (AC: #2)
|
|
211
|
-
- [ ] Subtask 2.1: [Specific action]
|
|
212
|
-
|
|
213
|
-
## Dev Notes
|
|
214
|
-
[Relevant architecture info, integration points, file locations]
|
|
215
|
-
|
|
216
|
-
### Testing
|
|
217
|
-
- Test file location: [path]
|
|
218
|
-
- Test standards: [requirements]
|
|
219
|
-
- Testing frameworks: [tools being used]
|
|
220
|
-
- Specific requirements: [any special test needs]
|
|
221
|
-
|
|
222
|
-
## Change Log
|
|
223
|
-
| Date | Version | Description | Author |
|
|
224
|
-
|------|---------|-------------|--------|
|
|
225
|
-
| [Date] | 1.0 | Story created | AI Agent |
|
|
188
|
+
Create a story following BMAD story format with:
|
|
189
|
+
- Status: Draft
|
|
190
|
+
- User story (As a... I want... so that...)
|
|
191
|
+
- Acceptance Criteria (numbered, testable)
|
|
192
|
+
- Tasks/Subtasks with AC references
|
|
193
|
+
- Dev Notes with testing info
|
|
226
194
|
outputFile: "${sessionDir}/stories/${options.storyPrefix}-001.md"
|
|
227
195
|
|
|
228
196
|
- id: ${options.storyPrefix}-002
|
|
@@ -233,40 +201,25 @@ tasks:
|
|
|
233
201
|
parallelizable: false
|
|
234
202
|
agentType: "dev"
|
|
235
203
|
prompt: |
|
|
236
|
-
|
|
204
|
+
Story-formatted prompt
|
|
237
205
|
outputFile: "${sessionDir}/stories/${options.storyPrefix}-002.md"
|
|
238
206
|
|
|
239
|
-
|
|
240
|
-
|
|
207
|
+
## STORY MODE RULES
|
|
208
|
+
- Task IDs: ${options.storyPrefix}-001, ${options.storyPrefix}-002, etc.
|
|
209
|
+
- Output files: ${sessionDir}/stories/
|
|
210
|
+
${options.perFile ? '- **CRITICAL**: Create one story per file (per-file mode is ON)' : ''}
|
|
241
211
|
|
|
242
|
-
**
|
|
243
|
-
|
|
244
|
-
- Output files MUST go to stories/ directory
|
|
245
|
-
- Prompts MUST generate full story structure (not simple task outputs)
|
|
246
|
-
- Each story must have user story format, acceptance criteria, and tasks breakdown
|
|
212
|
+
**REMEMBER: Your ENTIRE response must be valid YAML starting with "masterPrompt: |" - NO other text!**
|
|
213
|
+
` : `## OUTPUT FORMAT - CRITICAL INSTRUCTIONS
|
|
247
214
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
5. Stories with the same dependencies can run in parallel (if parallelizable: true)
|
|
254
|
-
6. All file paths in outputFile should use the session directory: ${sessionDir}/stories/
|
|
255
|
-
${options.perFile ? '7. **CRITICAL**: Create one story per file for the main work (per-file mode is ON)' : ''}
|
|
215
|
+
**YOUR RESPONSE MUST BE PURE YAML ONLY.**
|
|
216
|
+
- Do NOT write any explanation, summary, or commentary
|
|
217
|
+
- Do NOT use markdown code fences (\`\`\`yaml or \`\`\`)
|
|
218
|
+
- Do NOT write "Here's the task decomposition..." or similar
|
|
219
|
+
- Start your response DIRECTLY with "masterPrompt: |"
|
|
256
220
|
|
|
257
|
-
|
|
221
|
+
The YAML structure MUST be:
|
|
258
222
|
|
|
259
|
-
Sequential:
|
|
260
|
-
${options.storyPrefix}-001 (deps: []) → ${options.storyPrefix}-002 (deps: [${options.storyPrefix}-001])
|
|
261
|
-
|
|
262
|
-
Parallel then join:
|
|
263
|
-
${options.storyPrefix}-001 (deps: []) → [${options.storyPrefix}-002, ${options.storyPrefix}-003] → ${options.storyPrefix}-004
|
|
264
|
-
|
|
265
|
-
Now, decompose the goal into stories. Output ONLY the YAML structure.
|
|
266
|
-
` : `## OUTPUT FORMAT
|
|
267
|
-
You MUST output ONLY valid YAML in the following structure. DO NOT include markdown code fences or any other text.
|
|
268
|
-
|
|
269
|
-
\`\`\`yaml
|
|
270
223
|
masterPrompt: |
|
|
271
224
|
A reusable prompt template that applies to all tasks.
|
|
272
225
|
Include best practices, coding standards, and common guidelines.
|
|
@@ -276,52 +229,39 @@ tasks:
|
|
|
276
229
|
title: "Clear, specific task title"
|
|
277
230
|
description: "Detailed description of what this task accomplishes"
|
|
278
231
|
estimatedMinutes: 10
|
|
279
|
-
dependencies: []
|
|
280
|
-
parallelizable: true
|
|
281
|
-
agentType: "dev"
|
|
282
|
-
targetFiles:
|
|
232
|
+
dependencies: []
|
|
233
|
+
parallelizable: true
|
|
234
|
+
agentType: "dev"
|
|
235
|
+
targetFiles:
|
|
283
236
|
- "path/to/file.js"
|
|
284
237
|
prompt: |
|
|
285
238
|
Specific prompt for the agent to execute this task.
|
|
286
|
-
Include
|
|
287
|
-
- What to do
|
|
288
|
-
- Where to do it (file paths)
|
|
289
|
-
- Expected outcome
|
|
290
|
-
- Any validation steps
|
|
239
|
+
Include what to do, where to do it, expected outcome, validation steps.
|
|
291
240
|
outputFile: "${sessionDir}/outputs/task-001-output.md"
|
|
292
241
|
|
|
293
242
|
- id: task-002
|
|
294
243
|
title: "Next task"
|
|
295
244
|
description: "Task description"
|
|
296
245
|
estimatedMinutes: 5
|
|
297
|
-
dependencies: ["task-001"]
|
|
246
|
+
dependencies: ["task-001"]
|
|
298
247
|
parallelizable: false
|
|
299
248
|
agentType: "dev"
|
|
300
249
|
prompt: |
|
|
301
250
|
Task-specific prompt here
|
|
302
251
|
outputFile: "${sessionDir}/outputs/task-002-output.md"
|
|
303
252
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
## IMPORTANT RULES
|
|
308
|
-
1. Each task ID must be unique
|
|
253
|
+
## YAML RULES
|
|
254
|
+
1. Each task ID must be unique (task-001, task-002, etc.)
|
|
309
255
|
2. Dependencies must reference valid task IDs
|
|
310
|
-
3.
|
|
311
|
-
4.
|
|
312
|
-
5.
|
|
313
|
-
6. All file paths in outputFile should use the session directory: ${sessionDir}/outputs/
|
|
314
|
-
${options.perFile ? '7. **CRITICAL**: Create one task per file for the main work (per-file mode is ON)' : ''}
|
|
315
|
-
|
|
316
|
-
## EXAMPLE DEPENDENCY PATTERNS
|
|
317
|
-
|
|
318
|
-
Sequential:
|
|
319
|
-
task-001 (deps: []) → task-002 (deps: [task-001]) → task-003 (deps: [task-002])
|
|
256
|
+
3. No circular dependencies
|
|
257
|
+
4. Use outputFile path: ${sessionDir}/outputs/
|
|
258
|
+
${options.perFile ? '5. **CRITICAL**: Create one task per file (per-file mode is ON)' : ''}
|
|
320
259
|
|
|
321
|
-
|
|
322
|
-
|
|
260
|
+
## DEPENDENCY PATTERNS
|
|
261
|
+
- Sequential: task-001 → task-002 (deps: [task-001])
|
|
262
|
+
- Parallel: task-002, task-003 both depend on task-001, then task-004 depends on both
|
|
323
263
|
|
|
324
|
-
|
|
264
|
+
**REMEMBER: Your ENTIRE response must be valid YAML starting with "masterPrompt: |" - NO other text!**
|
|
325
265
|
`;
|
|
326
266
|
return prompt;
|
|
327
267
|
}
|
|
@@ -418,15 +358,25 @@ Now, decompose the goal into tasks. Output ONLY the YAML structure.
|
|
|
418
358
|
startIndex = lines.findIndex((line) => line.trim().startsWith('tasks:'));
|
|
419
359
|
}
|
|
420
360
|
if (startIndex !== -1) {
|
|
421
|
-
//
|
|
361
|
+
// For YAML task graphs, we expect the content to continue until the end
|
|
362
|
+
// since ## headers inside multiline strings (prompt: |) are valid YAML content.
|
|
363
|
+
// Only stop if we see clear non-YAML markers at the start of a line (no indentation).
|
|
422
364
|
let endIndex = lines.length;
|
|
423
365
|
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
424
|
-
const line = lines[i]
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
366
|
+
const line = lines[i];
|
|
367
|
+
const trimmedLine = line.trim();
|
|
368
|
+
// Only stop at lines that are:
|
|
369
|
+
// 1. Not indented (start at column 0)
|
|
370
|
+
// 2. Look like markdown headers or prose (not YAML keys)
|
|
371
|
+
// 3. Are not empty lines (which are valid in YAML)
|
|
372
|
+
if (line.length > 0 && line[0] !== ' ' && line[0] !== '\t') {
|
|
373
|
+
// Line is not indented - check if it's a YAML key or non-YAML content
|
|
374
|
+
const isYamlKey = /^[a-zA-Z_][a-zA-Z0-9_]*:/.test(trimmedLine) || trimmedLine.startsWith('-');
|
|
375
|
+
if (!isYamlKey && !trimmedLine.startsWith('#')) {
|
|
376
|
+
// Non-indented, non-YAML content (like "Here's the summary...")
|
|
377
|
+
endIndex = i;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
430
380
|
}
|
|
431
381
|
}
|
|
432
382
|
const yamlContent = lines.slice(startIndex, endIndex).join('\n');
|
|
@@ -535,21 +485,24 @@ Now, decompose the goal into tasks. Output ONLY the YAML structure.
|
|
|
535
485
|
catch (error) {
|
|
536
486
|
this.logger.warn({ error: error.message }, 'YAML validation failed, asking Claude to fix it');
|
|
537
487
|
// YAML is invalid, ask Claude to fix it
|
|
538
|
-
const fixPrompt = `
|
|
488
|
+
const fixPrompt = `FIX THIS YAML - OUTPUT ONLY THE FIXED YAML, NOTHING ELSE.
|
|
489
|
+
|
|
490
|
+
**CRITICAL: Your response must be PURE YAML only. Start directly with "masterPrompt: |"**
|
|
491
|
+
- Do NOT write any explanation
|
|
492
|
+
- Do NOT use markdown code fences
|
|
493
|
+
- Do NOT say "Here's the fixed YAML" or anything similar
|
|
539
494
|
|
|
540
|
-
ERROR: ${error.message}
|
|
495
|
+
ERROR TO FIX: ${error.message}
|
|
541
496
|
|
|
542
|
-
|
|
497
|
+
YAML TO FIX:
|
|
543
498
|
${output}
|
|
544
499
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
4. No syntax errors remain
|
|
500
|
+
RULES:
|
|
501
|
+
- Use 2 spaces for indentation
|
|
502
|
+
- Quote strings with special characters
|
|
503
|
+
- Ensure valid YAML syntax
|
|
550
504
|
|
|
551
|
-
|
|
552
|
-
`;
|
|
505
|
+
YOUR RESPONSE MUST START WITH "masterPrompt: |" - NO OTHER TEXT ALLOWED!`;
|
|
553
506
|
this.logger.info('Asking Claude to fix YAML errors');
|
|
554
507
|
const fixResult = await this.agentRunner.runAgent(fixPrompt, {
|
|
555
508
|
agentType: 'architect',
|
|
@@ -32,9 +32,9 @@ export const agentFlags = {
|
|
|
32
32
|
}),
|
|
33
33
|
provider: Flags.string({
|
|
34
34
|
default: 'claude',
|
|
35
|
-
description: 'AI provider to use (claude or
|
|
35
|
+
description: 'AI provider to use (claude, gemini, or opencode). Defaults to claude.',
|
|
36
36
|
helpGroup: 'Agent Customization',
|
|
37
|
-
options: ['claude', 'gemini'],
|
|
37
|
+
options: ['claude', 'gemini', 'opencode'],
|
|
38
38
|
}),
|
|
39
39
|
task: Flags.string({
|
|
40
40
|
description: 'Override which task command to execute (e.g., develop-story, draft, review-implementation). Defaults to command-appropriate task.',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperdrive.bot/bmad-workflow",
|
|
3
3
|
"description": "AI-driven development workflow orchestration CLI for BMAD projects",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.7",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DevSquad",
|
|
7
7
|
"email": "marcelo@devsquad.email",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"mocha": "^10.8.2",
|
|
56
56
|
"oclif": "^4",
|
|
57
57
|
"prettier": "^3.6.2",
|
|
58
|
+
"esmock": "^2.7.3",
|
|
58
59
|
"proxyquire": "^2.1.3",
|
|
59
60
|
"sinon": "^17.0.1",
|
|
60
61
|
"ts-node": "^10.9.2",
|