@hyperdrive.bot/bmad-workflow 1.0.4 → 1.0.6

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.
@@ -4,7 +4,7 @@
4
4
  /**
5
5
  * Supported AI providers
6
6
  */
7
- export type AIProvider = 'claude' | 'gemini';
7
+ export type AIProvider = 'claude' | 'gemini' | 'opencode';
8
8
  /**
9
9
  * Provider-specific configuration
10
10
  */
@@ -15,4 +15,9 @@ export const PROVIDER_CONFIGS = {
15
15
  flags: ['-p', '--yolo'],
16
16
  supportsFileReferences: false,
17
17
  },
18
+ opencode: {
19
+ command: 'opencode',
20
+ flags: ['run', '--format', 'json'],
21
+ supportsFileReferences: true,
22
+ },
18
23
  };
@@ -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
  }
@@ -5,3 +5,4 @@ export * from './agent-runner-factory.js';
5
5
  export * from './agent-runner.js';
6
6
  export * from './claude-agent-runner.js';
7
7
  export * from './gemini-agent-runner.js';
8
+ export * from './opencode-agent-runner.js';
@@ -5,3 +5,4 @@ export * from './agent-runner-factory.js';
5
5
  export * from './agent-runner.js';
6
6
  export * from './claude-agent-runner.js';
7
7
  export * from './gemini-agent-runner.js';
8
+ export * from './opencode-agent-runner.js';
@@ -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
- You MUST output tasks as BMAD-formatted STORIES with proper structure.
164
- Output ONLY valid YAML. DO NOT include markdown code fences or any other text.
165
-
166
- Each task will become a full story with:
167
- - Story ID: ${options.storyPrefix}-<number> (e.g., ${options.storyPrefix}-001, ${options.storyPrefix}-002)
168
- - User story format: "As a... I want... so that..."
169
- - Acceptance Criteria
170
- - Tasks/Subtasks breakdown
171
- - Dev Notes with testing info
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 that applies to all stories.
176
- Include best practices, coding standards, and common guidelines for implementing these stories.
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: [] # Array of story IDs that must complete first
182
+ dependencies: []
184
183
  parallelizable: true
185
184
  agentType: "dev"
186
- targetFiles: # Optional: specific files this story operates on
185
+ targetFiles:
187
186
  - "path/to/file.js"
188
187
  prompt: |
189
- Create a story following BMAD story format:
190
-
191
- # Story ${options.storyPrefix}-001: [Title]
192
-
193
- ## Status
194
- Draft
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
- [Story-formatted prompt as above]
204
+ Story-formatted prompt
237
205
  outputFile: "${sessionDir}/stories/${options.storyPrefix}-002.md"
238
206
 
239
- # ... more tasks (as stories)
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
- **CRITICAL FOR STORY MODE:**
243
- - Task IDs MUST be story IDs: ${options.storyPrefix}-001, ${options.storyPrefix}-002, etc.
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
- ## IMPORTANT RULES
249
- 1. Each story ID must be unique
250
- 2. Dependencies must reference valid story IDs
251
- 3. Circular dependencies are not allowed
252
- 4. Stories with no dependencies can run immediately
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
- ## EXAMPLE DEPENDENCY PATTERNS
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: [] # Array of task IDs that must complete first
280
- parallelizable: true # Can this run in parallel with other tasks?
281
- agentType: "dev" # Agent type: dev, architect, qa, etc.
282
- targetFiles: # Optional: specific files this task operates on
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"] # Must wait for 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
- # ... more tasks
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. Circular dependencies are not allowed
311
- 4. Tasks with no dependencies can run immediately
312
- 5. Tasks with the same dependencies can run in parallel (if parallelizable: true)
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
- Parallel then join:
322
- task-001 (deps: []) → [task-002, task-003] (deps: [task-001]) → task-004 (deps: [task-002, task-003])
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
- Now, decompose the goal into tasks. Output ONLY the YAML structure.
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
- // Find the end of YAML content (stop at empty lines or obvious non-YAML content)
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].trim();
425
- // Stop if we hit obvious non-YAML content
426
- if (line.startsWith('#') && !line.startsWith('# ') && i > startIndex + 5) {
427
- // Could be a comment, but if far from start, likely end of YAML
428
- endIndex = i;
429
- break;
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 = `The following YAML has syntax errors. Please fix ALL syntax errors and return ONLY the corrected YAML (no explanations, no markdown code fences, just the raw YAML):
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
- INVALID YAML:
497
+ YAML TO FIX:
543
498
  ${output}
544
499
 
545
- Please output the CORRECTED YAML only. Ensure:
546
- 1. Proper indentation (2 spaces per level)
547
- 2. All strings with special characters are quoted
548
- 3. All lists and mappings are properly formatted
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
- Output ONLY the corrected YAML:
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',
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",
4
+ "version": "1.0.6",
5
5
  "author": {
6
6
  "name": "DevSquad",
7
7
  "email": "marcelo@devsquad.email",