@hyperdrive.bot/bmad-workflow 1.0.26 → 1.0.27

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 (72) hide show
  1. package/dist/commands/epics/create.d.ts +1 -0
  2. package/dist/commands/lock/acquire.d.ts +54 -0
  3. package/dist/commands/lock/acquire.js +193 -0
  4. package/dist/commands/lock/cleanup.d.ts +38 -0
  5. package/dist/commands/lock/cleanup.js +148 -0
  6. package/dist/commands/lock/list.d.ts +31 -0
  7. package/dist/commands/lock/list.js +123 -0
  8. package/dist/commands/lock/release.d.ts +42 -0
  9. package/dist/commands/lock/release.js +134 -0
  10. package/dist/commands/lock/status.d.ts +34 -0
  11. package/dist/commands/lock/status.js +109 -0
  12. package/dist/commands/stories/create.d.ts +1 -0
  13. package/dist/commands/stories/develop.d.ts +4 -0
  14. package/dist/commands/stories/develop.js +55 -5
  15. package/dist/commands/stories/qa.d.ts +1 -0
  16. package/dist/commands/stories/qa.js +31 -0
  17. package/dist/commands/stories/review.d.ts +1 -0
  18. package/dist/commands/workflow.d.ts +11 -0
  19. package/dist/commands/workflow.js +120 -4
  20. package/dist/models/agent-options.d.ts +33 -0
  21. package/dist/models/agent-result.d.ts +10 -1
  22. package/dist/models/dispatch.d.ts +16 -0
  23. package/dist/models/dispatch.js +8 -0
  24. package/dist/models/index.d.ts +3 -0
  25. package/dist/models/index.js +2 -0
  26. package/dist/models/lock.d.ts +80 -0
  27. package/dist/models/lock.js +69 -0
  28. package/dist/models/phase-result.d.ts +8 -0
  29. package/dist/models/provider.js +1 -1
  30. package/dist/models/workflow-callbacks.d.ts +37 -0
  31. package/dist/models/workflow-config.d.ts +50 -0
  32. package/dist/services/agents/agent-runner-factory.d.ts +24 -15
  33. package/dist/services/agents/agent-runner-factory.js +95 -15
  34. package/dist/services/agents/channel-agent-runner.d.ts +76 -0
  35. package/dist/services/agents/channel-agent-runner.js +256 -0
  36. package/dist/services/agents/channel-session-manager.d.ts +126 -0
  37. package/dist/services/agents/channel-session-manager.js +260 -0
  38. package/dist/services/agents/claude-agent-runner.d.ts +9 -50
  39. package/dist/services/agents/claude-agent-runner.js +221 -199
  40. package/dist/services/agents/gemini-agent-runner.js +3 -0
  41. package/dist/services/agents/index.d.ts +1 -0
  42. package/dist/services/agents/index.js +1 -0
  43. package/dist/services/agents/opencode-agent-runner.js +3 -0
  44. package/dist/services/file-system/file-manager.d.ts +11 -0
  45. package/dist/services/file-system/file-manager.js +26 -0
  46. package/dist/services/git/git-ops.d.ts +58 -0
  47. package/dist/services/git/git-ops.js +73 -0
  48. package/dist/services/git/index.d.ts +3 -0
  49. package/dist/services/git/index.js +2 -0
  50. package/dist/services/git/push-conflict-handler.d.ts +32 -0
  51. package/dist/services/git/push-conflict-handler.js +84 -0
  52. package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
  53. package/dist/services/lock/git-backed-lock-service.js +173 -0
  54. package/dist/services/lock/lock-cleanup.d.ts +49 -0
  55. package/dist/services/lock/lock-cleanup.js +85 -0
  56. package/dist/services/lock/lock-service.d.ts +143 -0
  57. package/dist/services/lock/lock-service.js +290 -0
  58. package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
  59. package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
  60. package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
  61. package/dist/services/orchestration/workflow-orchestrator.js +181 -31
  62. package/dist/services/review/ai-review-scanner.js +1 -0
  63. package/dist/services/review/review-phase-executor.js +3 -0
  64. package/dist/services/review/self-heal-loop.js +1 -0
  65. package/dist/services/review/types.d.ts +2 -0
  66. package/dist/utils/errors.d.ts +17 -1
  67. package/dist/utils/errors.js +18 -0
  68. package/dist/utils/session-naming.d.ts +23 -0
  69. package/dist/utils/session-naming.js +30 -0
  70. package/dist/utils/shared-flags.d.ts +1 -0
  71. package/dist/utils/shared-flags.js +5 -0
  72. package/package.json +3 -2
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Encapsulates Claude CLI process spawning with comprehensive error handling,
5
5
  * logging, and timeout management for reliable AI agent execution.
6
+ *
7
+ * Uses spawn() with stream-json output format for real-time progress reporting.
6
8
  */
7
- import { exec } from 'node:child_process';
9
+ import { spawn } from 'node:child_process';
8
10
  import { mkdtemp, rm, writeFile } from 'node:fs/promises';
9
11
  import { tmpdir } from 'node:os';
10
12
  import { join } from 'node:path';
11
- import { promisify } from 'node:util';
12
- const execAsync = promisify(exec);
13
+ import { createInterface } from 'node:readline';
13
14
  import { PROVIDER_CONFIGS } from '../../models/provider.js';
14
15
  /**
15
16
  * Track active child processes for cleanup on SIGINT
@@ -47,70 +48,91 @@ const registerSigintHandler = (logger) => {
47
48
  });
48
49
  sigintHandlerRegistered = true;
49
50
  };
51
+ /**
52
+ * Parse a stream-json line and extract a human-readable summary.
53
+ * Returns null for events that aren't meaningful to display.
54
+ */
55
+ function parseStreamLine(line) {
56
+ try {
57
+ const event = JSON.parse(line);
58
+ const type = event.type;
59
+ if (type === 'assistant') {
60
+ const message = event.message;
61
+ if (!message)
62
+ return { summary: null, type, verboseText: null };
63
+ const content = message.content;
64
+ if (!content || content.length === 0)
65
+ return { summary: null, type, verboseText: null };
66
+ // Collect all text for verbose output
67
+ const verboseParts = [];
68
+ let summary = null;
69
+ for (const block of content) {
70
+ if (block.type === 'tool_use') {
71
+ const toolName = block.name;
72
+ const input = block.input;
73
+ const filePath = input?.file_path || input?.path || input?.pattern || input?.command;
74
+ if (filePath) {
75
+ const shortPath = String(filePath).length > 60
76
+ ? '...' + String(filePath).slice(-57)
77
+ : String(filePath);
78
+ summary = summary ?? `🔧 ${toolName}: ${shortPath}`;
79
+ }
80
+ else {
81
+ summary = summary ?? `🔧 ${toolName}`;
82
+ }
83
+ // Verbose: show tool name + full input as JSON
84
+ verboseParts.push(`[tool_use] ${toolName}: ${JSON.stringify(input, null, 2)}`);
85
+ }
86
+ else if (block.type === 'tool_result') {
87
+ const resultContent = block.content;
88
+ if (resultContent) {
89
+ verboseParts.push(resultContent);
90
+ }
91
+ }
92
+ else if (block.type === 'text') {
93
+ const text = (block.text || '').trim();
94
+ if (text.length > 0) {
95
+ summary = summary ?? `💬 ${text.split('\n')[0].slice(0, 80)}`;
96
+ verboseParts.push(text);
97
+ }
98
+ }
99
+ }
100
+ return {
101
+ summary,
102
+ type,
103
+ verboseText: verboseParts.length > 0 ? verboseParts.join('\n') : null,
104
+ };
105
+ }
106
+ if (type === 'result') {
107
+ const resultText = event.result;
108
+ return { finalOutput: resultText ?? '', summary: null, type, verboseText: null };
109
+ }
110
+ // system, rate_limit_event, etc. — skip
111
+ return { summary: null, type, verboseText: null };
112
+ }
113
+ catch {
114
+ // Unparseable line — skip
115
+ return { summary: null, type: 'unknown', verboseText: null };
116
+ }
117
+ }
50
118
  /**
51
119
  * ClaudeAgentRunner service for executing Claude AI agents
52
120
  *
53
121
  * Spawns Claude CLI processes to execute AI agents with specified prompts.
54
122
  * 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 ClaudeAgentRunner(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
- * ```
123
+ * Uses stream-json output format for real-time progress reporting.
66
124
  */
67
125
  export class ClaudeAgentRunner {
68
126
  provider = 'claude';
69
127
  config = PROVIDER_CONFIGS.claude;
70
128
  logger;
71
- /**
72
- * Create a new ClaudeAgentRunner instance
73
- *
74
- * @param logger - Logger instance for structured logging
75
- */
76
129
  constructor(logger) {
77
130
  this.logger = logger;
78
131
  registerSigintHandler(logger);
79
132
  }
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
133
  getActiveProcessCount() {
87
134
  return activeProcesses.size;
88
135
  }
89
- /**
90
- * Execute a Claude AI agent with the specified prompt and options
91
- *
92
- * This method spawns a Claude 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
136
  async runAgent(prompt, options) {
115
137
  const startTime = Date.now();
116
138
  const timeout = options.timeout ?? 1_800_000; // Default 30 minutes
@@ -123,6 +145,7 @@ export class ClaudeAgentRunner {
123
145
  exitCode: -1,
124
146
  output: '',
125
147
  success: false,
148
+ transport: 'subprocess',
126
149
  }, options.onResponse);
127
150
  }
128
151
  // Log execution start with metadata
@@ -135,127 +158,79 @@ export class ClaudeAgentRunner {
135
158
  // Invoke onPrompt callback if provided
136
159
  if (options.onPrompt) {
137
160
  try {
138
- // Create options object without callbacks for the callback
139
161
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
140
- const { onPrompt, onResponse, ...callbackOptions } = options;
162
+ const { onPrompt, onResponse, onStream, ...callbackOptions } = options;
141
163
  await onPrompt(prompt, callbackOptions);
142
164
  }
143
165
  catch (error) {
144
166
  this.logger.warn({ error: error.message }, 'Error in onPrompt callback');
145
167
  }
146
168
  }
147
- // For large prompts or prompts with special characters, use a temp file
148
- // This avoids shell escaping issues
149
169
  let tempDir = null;
150
- let tempFile = null;
151
- let stdoutData = '';
152
- let stderrData = '';
153
170
  try {
154
171
  // Create temp directory and file
155
172
  tempDir = await mkdtemp(join(tmpdir(), 'claude-prompt-'));
156
- tempFile = join(tempDir, 'prompt.txt');
173
+ const tempFile = join(tempDir, 'prompt.txt');
157
174
  await writeFile(tempFile, prompt, 'utf8');
158
- // Write system prompt to temp file if provided (Claude-only feature)
159
- // Uses --system-prompt (full replacement) instead of --append-system-prompt
160
- // to prevent the default Claude Code system prompt from competing with
161
- // our output format instructions (e.g., YAML-only output).
162
- let systemPromptArg = '';
175
+ // Write system prompt to temp file if provided
176
+ let systemPromptArg = [];
163
177
  if (options.systemPrompt) {
164
178
  const systemPromptFile = join(tempDir, 'system-prompt.txt');
165
179
  await writeFile(systemPromptFile, options.systemPrompt, 'utf8');
166
- systemPromptArg = `--system-prompt "$(cat '${systemPromptFile}')"`;
180
+ systemPromptArg = ['--system-prompt', options.systemPrompt];
181
+ }
182
+ // Build command args
183
+ const args = [...this.config.flags];
184
+ if (options.model) {
185
+ args.push(this.config.modelFlag, options.model);
186
+ }
187
+ if (options.flags) {
188
+ args.push(...options.flags);
167
189
  }
168
- // Build command with temp file
169
- const flags = this.config.flags.join(' ');
170
- const modelArg = options.model ? `${this.config.modelFlag} ${options.model}` : '';
171
- const extraFlags = options.flags ? options.flags.join(' ') : '';
172
- const command = `${this.config.command} ${flags} ${modelArg} ${extraFlags} ${systemPromptArg} < "${tempFile}"`.replace(/\s+/g, ' ').trim();
190
+ if (options.sessionName) {
191
+ args.push('--name', options.sessionName);
192
+ }
193
+ args.push(...systemPromptArg);
173
194
  // Log the command being executed
174
195
  this.logger.info({
196
+ args: args.join(' '),
175
197
  promptLength: prompt.length,
176
198
  tempFile,
177
- }, 'Executing command with temp file');
178
- // Use exec instead of spawn for better shell compatibility
179
- // Note: setting CLAUDECODE to undefined in a spread does NOT remove it —
180
- // Node coerces undefined to the string "undefined", which is still truthy.
181
- // We must delete the key from the env object to fully unset it.
199
+ }, 'Spawning Claude process with stream-json output');
200
+ // Spawn the process
182
201
  const env = { ...process.env };
183
202
  delete env.CLAUDECODE;
184
- const execOptions = {
203
+ const result = await this.spawnAndStream({
204
+ args,
205
+ cwd: options.cwd,
185
206
  env,
186
- maxBuffer: 10 * 1024 * 1024, // 10MB buffer
187
- shell: process.env.SHELL || '/bin/bash',
207
+ onStream: options.onStream,
208
+ onStreamVerbose: options.onStreamVerbose,
209
+ startTime,
210
+ stdinFile: tempFile,
188
211
  timeout,
189
- };
190
- if (options.cwd) {
191
- execOptions.cwd = options.cwd;
192
- this.logger.info({ cwd: options.cwd }, 'Setting working directory for agent');
193
- }
194
- const { stderr, stdout } = await execAsync(command, execOptions);
195
- stdoutData = stdout;
196
- stderrData = stderr;
197
- const duration = Date.now() - startTime;
198
- this.logger.info({
199
- duration,
200
- stderrLength: stderrData.length,
201
- stdoutLength: stdoutData.length,
202
- }, 'Claude CLI process completed successfully');
203
- const result = {
212
+ });
213
+ return this.returnWithCallback({
204
214
  agentType: options.agentType,
205
- duration,
206
- errors: stderrData,
207
- exitCode: 0,
208
- output: stdoutData,
209
- success: true,
210
- };
211
- return this.returnWithCallback(result, options.onResponse);
215
+ ...result,
216
+ transport: 'subprocess',
217
+ }, options.onResponse);
212
218
  }
213
219
  catch (error) {
214
- // Handle exec errors (includes timeout, non-zero exit, etc.)
215
220
  const duration = Date.now() - startTime;
216
- // Extract stdout/stderr from exec error if available
217
- const execError = error;
218
- if (execError.stdout)
219
- stdoutData = execError.stdout;
220
- if (execError.stderr)
221
- stderrData = execError.stderr;
222
- const exitCode = execError.code ?? -1;
223
- const isTimeout = execError.killed === true && execError.signal === 'SIGTERM';
224
- const wasKilled = execError.killed === true;
225
- const signal = execError.signal ?? null;
226
- // Build detailed error context for debugging
227
- const errorContext = this.buildErrorContext({
228
- cmd: execError.cmd,
229
- duration,
230
- exitCode,
231
- isTimeout,
232
- signal,
233
- stderrData,
234
- stdoutData,
235
- wasKilled,
236
- });
237
- this.logger.error({
238
- cmd: execError.cmd,
239
- duration,
240
- error: execError.message,
241
- exitCode,
242
- isTimeout,
243
- signal,
244
- stderrLength: stderrData.length,
245
- stdoutLength: stdoutData.length,
246
- wasKilled,
247
- }, isTimeout ? 'Claude CLI process timeout' : 'Claude CLI process error');
221
+ const err = error;
222
+ this.logger.error({ duration, error: err.message }, 'Claude agent execution error');
248
223
  return this.returnWithCallback({
249
224
  agentType: options.agentType,
250
225
  duration,
251
- errors: errorContext,
252
- exitCode: isTimeout ? 124 : exitCode,
253
- output: stdoutData,
226
+ errors: err.message,
227
+ exitCode: -1,
228
+ output: '',
254
229
  success: false,
230
+ transport: 'subprocess',
255
231
  }, options.onResponse);
256
232
  }
257
233
  finally {
258
- // Clean up temp file and directory
259
234
  if (tempDir) {
260
235
  try {
261
236
  await rm(tempDir, { force: true, recursive: true });
@@ -268,73 +243,110 @@ export class ClaudeAgentRunner {
268
243
  }
269
244
  }
270
245
  /**
271
- * Build detailed error context for debugging process failures
272
- * @private
246
+ * Spawn Claude process and stream output in real-time
273
247
  */
274
- buildErrorContext(info) {
275
- const lines = [];
276
- // Determine the type of failure
277
- if (info.isTimeout) {
278
- lines.push(`Process timeout after ${info.duration}ms`);
279
- }
280
- else if (info.wasKilled) {
281
- lines.push('Process was killed externally');
282
- // Explain common exit codes
283
- switch (info.exitCode) {
284
- case 130: {
285
- lines.push('Exit code 130 = SIGINT (user interrupt, Ctrl+C)');
286
- break;
248
+ async spawnAndStream(opts) {
249
+ const { args, cwd, env, onStream, onStreamVerbose, startTime, stdinFile, timeout } = opts;
250
+ return new Promise((resolve) => {
251
+ // Use shell to handle stdin redirection from file
252
+ const shellCommand = `${this.config.command} ${args.map((a) => this.shellEscape(a)).join(' ')} < "${stdinFile}"`;
253
+ const child = spawn(shellCommand, [], {
254
+ cwd,
255
+ env,
256
+ shell: process.env.SHELL || '/bin/bash',
257
+ stdio: ['ignore', 'pipe', 'pipe'],
258
+ });
259
+ activeProcesses.add(child);
260
+ let stderrData = '';
261
+ let finalOutput = '';
262
+ let timedOut = false;
263
+ // Set up timeout
264
+ const timeoutHandle = setTimeout(() => {
265
+ timedOut = true;
266
+ child.kill('SIGTERM');
267
+ }, timeout);
268
+ // Read stderr
269
+ child.stderr?.on('data', (chunk) => {
270
+ stderrData += chunk.toString();
271
+ });
272
+ // Parse stdout line by line (stream-json = JSONL)
273
+ const rl = createInterface({ input: child.stdout });
274
+ rl.on('line', (line) => {
275
+ if (!line.trim())
276
+ return;
277
+ const parsed = parseStreamLine(line);
278
+ // If this is the final result, capture it
279
+ if (parsed.finalOutput !== undefined) {
280
+ finalOutput = parsed.finalOutput;
287
281
  }
288
- case 137: {
289
- lines.push('Exit code 137 = SIGKILL (process forcefully killed)', 'Possible causes:', ' - System OOM killer', ' - Exceeded memory limits', ' - Docker/container killed the process');
290
- break;
282
+ // Fire verbose callback with raw text (everything Claude says)
283
+ if (parsed.verboseText && onStreamVerbose) {
284
+ try {
285
+ onStreamVerbose(parsed.verboseText);
286
+ }
287
+ catch {
288
+ // Never let callback errors kill the stream
289
+ }
291
290
  }
292
- case 143: {
293
- 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');
294
- break;
291
+ // Fire summary callback for meaningful events (spinner updates)
292
+ if (parsed.summary && onStream) {
293
+ try {
294
+ onStream(parsed.summary);
295
+ }
296
+ catch {
297
+ // Never let callback errors kill the stream
298
+ }
295
299
  }
296
- // No default
297
- }
298
- if (info.signal) {
299
- lines.push(`Signal received: ${info.signal}`);
300
- }
301
- else {
302
- lines.push('No signal information available (signal: null)');
303
- }
304
- }
305
- else {
306
- lines.push(`Process exited with code ${info.exitCode}`);
307
- }
308
- // Add duration context
309
- lines.push(`Duration: ${(info.duration / 1000).toFixed(2)}s`);
310
- // Add output context
311
- if (info.stdoutData.length > 0) {
312
- lines.push(`Stdout (${info.stdoutData.length} chars):`);
313
- // Truncate if too long, show last 500 chars which are often most relevant
314
- const truncatedStdout = info.stdoutData.length > 500 ? `...[truncated]...\n${info.stdoutData.slice(-500)}` : info.stdoutData;
315
- lines.push(truncatedStdout);
316
- }
317
- else {
318
- lines.push('Stdout: (empty - no output captured)');
319
- }
320
- if (info.stderrData.length > 0) {
321
- lines.push(`Stderr (${info.stderrData.length} chars):`);
322
- const truncatedStderr = info.stderrData.length > 500 ? `...[truncated]...\n${info.stderrData.slice(-500)}` : info.stderrData;
323
- lines.push(truncatedStderr);
324
- }
325
- else {
326
- lines.push('Stderr: (empty - no error output)');
327
- }
328
- // Add command for reference (truncate if too long)
329
- if (info.cmd) {
330
- const truncatedCmd = info.cmd.length > 200 ? `${info.cmd.slice(0, 200)}...[truncated]` : info.cmd;
331
- lines.push(`Command: ${truncatedCmd}`);
332
- }
333
- // Add troubleshooting suggestions for empty output
334
- if (info.stdoutData.length === 0 && info.stderrData.length === 0) {
335
- lines.push('', 'Troubleshooting (no output captured):', ' 1. Check system resources (memory, CPU)', ' 2. Verify Claude CLI is properly installed: `claude --version`', ' 3. Check for Claude API issues', ' 4. Review system logs: `dmesg | tail -50` (for OOM killer)', ' 5. Try running the command manually to see output');
336
- }
337
- return lines.join('\n');
300
+ });
301
+ child.on('close', (code, signal) => {
302
+ clearTimeout(timeoutHandle);
303
+ activeProcesses.delete(child);
304
+ const duration = Date.now() - startTime;
305
+ const exitCode = code ?? -1;
306
+ const wasKilled = signal !== null;
307
+ const isTimeout = timedOut;
308
+ if (isTimeout) {
309
+ this.logger.error({ duration, timeout }, 'Claude CLI process timeout');
310
+ }
311
+ else if (exitCode !== 0) {
312
+ this.logger.error({ duration, exitCode, signal, wasKilled }, 'Claude CLI process error');
313
+ }
314
+ else {
315
+ this.logger.info({ duration, outputLength: finalOutput.length, stderrLength: stderrData.length }, 'Claude CLI process completed successfully');
316
+ }
317
+ // Build error context for failures
318
+ let errors = stderrData;
319
+ if (isTimeout) {
320
+ errors = `Process timeout after ${duration}ms\n${stderrData}`;
321
+ }
322
+ else if (wasKilled) {
323
+ errors = `Process killed by signal ${signal} (exit code ${exitCode})\n${stderrData}`;
324
+ }
325
+ else if (exitCode !== 0) {
326
+ errors = `Process exited with code ${exitCode}\n${stderrData}`;
327
+ }
328
+ resolve({
329
+ duration,
330
+ errors,
331
+ exitCode: isTimeout ? 124 : exitCode,
332
+ output: finalOutput,
333
+ success: exitCode === 0 && !isTimeout,
334
+ });
335
+ });
336
+ child.on('error', (err) => {
337
+ clearTimeout(timeoutHandle);
338
+ activeProcesses.delete(child);
339
+ const duration = Date.now() - startTime;
340
+ this.logger.error({ duration, error: err.message }, 'Failed to spawn Claude process');
341
+ resolve({
342
+ duration,
343
+ errors: `Spawn error: ${err.message}`,
344
+ exitCode: -1,
345
+ output: finalOutput,
346
+ success: false,
347
+ });
348
+ });
349
+ });
338
350
  }
339
351
  /**
340
352
  * Invoke onResponse callback and return result
@@ -351,4 +363,14 @@ export class ClaudeAgentRunner {
351
363
  }
352
364
  return result;
353
365
  }
366
+ /**
367
+ * Shell-escape a single argument
368
+ */
369
+ shellEscape(arg) {
370
+ // If arg contains no special characters, return as-is
371
+ if (/^[\w./:=@-]+$/.test(arg))
372
+ return arg;
373
+ // Otherwise wrap in single quotes, escaping existing single quotes
374
+ return `'${arg.replace(/'/g, "'\\''")}'`;
375
+ }
354
376
  }
@@ -117,6 +117,7 @@ export class GeminiAgentRunner {
117
117
  exitCode: -1,
118
118
  output: '',
119
119
  success: false,
120
+ transport: 'subprocess',
120
121
  }, options.onResponse);
121
122
  }
122
123
  // Preprocess prompt to inline @file references
@@ -187,6 +188,7 @@ export class GeminiAgentRunner {
187
188
  exitCode: 0,
188
189
  output: stdoutData,
189
190
  success: true,
191
+ transport: 'subprocess',
190
192
  };
191
193
  return this.returnWithCallback(result, options.onResponse);
192
194
  }
@@ -232,6 +234,7 @@ export class GeminiAgentRunner {
232
234
  exitCode: isTimeout ? 124 : exitCode,
233
235
  output: stdoutData,
234
236
  success: false,
237
+ transport: 'subprocess',
235
238
  }, options.onResponse);
236
239
  }
237
240
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export * from './agent-runner-factory.js';
5
5
  export * from './agent-runner.js';
6
+ export * from './channel-agent-runner.js';
6
7
  export * from './claude-agent-runner.js';
7
8
  export * from './gemini-agent-runner.js';
8
9
  export * from './opencode-agent-runner.js';
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export * from './agent-runner-factory.js';
5
5
  export * from './agent-runner.js';
6
+ export * from './channel-agent-runner.js';
6
7
  export * from './claude-agent-runner.js';
7
8
  export * from './gemini-agent-runner.js';
8
9
  export * from './opencode-agent-runner.js';
@@ -123,6 +123,7 @@ export class OpenCodeAgentRunner {
123
123
  exitCode: -1,
124
124
  output: '',
125
125
  success: false,
126
+ transport: 'subprocess',
126
127
  }, options.onResponse);
127
128
  }
128
129
  // Log execution start with metadata
@@ -195,6 +196,7 @@ export class OpenCodeAgentRunner {
195
196
  exitCode: 0,
196
197
  output: parsedOutput,
197
198
  success: true,
199
+ transport: 'subprocess',
198
200
  };
199
201
  return this.returnWithCallback(result, options.onResponse);
200
202
  }
@@ -240,6 +242,7 @@ export class OpenCodeAgentRunner {
240
242
  exitCode: isTimeout ? 124 : exitCode,
241
243
  output: stdoutData,
242
244
  success: false,
245
+ transport: 'subprocess',
243
246
  }, options.onResponse);
244
247
  }
245
248
  finally {
@@ -109,6 +109,17 @@ export declare class FileManager {
109
109
  * await fileManager.writeFile('docs/epics/epic-1.md', epicContent)
110
110
  */
111
111
  writeFile(path: string, content: string): Promise<void>;
112
+ /**
113
+ * Delete a file from disk
114
+ *
115
+ * Uses fs.remove which is idempotent — safe to call if file doesn't exist.
116
+ *
117
+ * @param path - Path to the file to delete
118
+ * @throws {FileSystemError} If file cannot be deleted (permission denied, etc.)
119
+ * @example
120
+ * await fileManager.deleteFile('docs/stories/story.md.lock')
121
+ */
122
+ deleteFile(path: string): Promise<void>;
112
123
  /**
113
124
  * Match a file name against a simple glob pattern
114
125
  *
@@ -233,6 +233,32 @@ export class FileManager {
233
233
  });
234
234
  }
235
235
  }
236
+ /**
237
+ * Delete a file from disk
238
+ *
239
+ * Uses fs.remove which is idempotent — safe to call if file doesn't exist.
240
+ *
241
+ * @param path - Path to the file to delete
242
+ * @throws {FileSystemError} If file cannot be deleted (permission denied, etc.)
243
+ * @example
244
+ * await fileManager.deleteFile('docs/stories/story.md.lock')
245
+ */
246
+ async deleteFile(path) {
247
+ this.logger.debug('Deleting file: %s', path);
248
+ try {
249
+ await fs.remove(path);
250
+ this.logger.debug('File deleted successfully: %s', path);
251
+ }
252
+ catch (error) {
253
+ const err = error;
254
+ this.logger.error('Error deleting file %s: %O', path, err);
255
+ throw new FileSystemError(`Failed to delete file: ${path}: ${err.message}`, {
256
+ operation: 'deleteFile',
257
+ originalError: err.message,
258
+ path,
259
+ });
260
+ }
261
+ }
236
262
  /**
237
263
  * Match a file name against a simple glob pattern
238
264
  *