@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.
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/lock/acquire.d.ts +54 -0
- package/dist/commands/lock/acquire.js +193 -0
- package/dist/commands/lock/cleanup.d.ts +38 -0
- package/dist/commands/lock/cleanup.js +148 -0
- package/dist/commands/lock/list.d.ts +31 -0
- package/dist/commands/lock/list.js +123 -0
- package/dist/commands/lock/release.d.ts +42 -0
- package/dist/commands/lock/release.js +134 -0
- package/dist/commands/lock/status.d.ts +34 -0
- package/dist/commands/lock/status.js +109 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +4 -0
- package/dist/commands/stories/develop.js +55 -5
- package/dist/commands/stories/qa.d.ts +1 -0
- package/dist/commands/stories/qa.js +31 -0
- package/dist/commands/stories/review.d.ts +1 -0
- package/dist/commands/workflow.d.ts +11 -0
- package/dist/commands/workflow.js +120 -4
- package/dist/models/agent-options.d.ts +33 -0
- package/dist/models/agent-result.d.ts +10 -1
- package/dist/models/dispatch.d.ts +16 -0
- package/dist/models/dispatch.js +8 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +2 -0
- package/dist/models/lock.d.ts +80 -0
- package/dist/models/lock.js +69 -0
- package/dist/models/phase-result.d.ts +8 -0
- package/dist/models/provider.js +1 -1
- package/dist/models/workflow-callbacks.d.ts +37 -0
- package/dist/models/workflow-config.d.ts +50 -0
- package/dist/services/agents/agent-runner-factory.d.ts +24 -15
- package/dist/services/agents/agent-runner-factory.js +95 -15
- package/dist/services/agents/channel-agent-runner.d.ts +76 -0
- package/dist/services/agents/channel-agent-runner.js +256 -0
- package/dist/services/agents/channel-session-manager.d.ts +126 -0
- package/dist/services/agents/channel-session-manager.js +260 -0
- package/dist/services/agents/claude-agent-runner.d.ts +9 -50
- package/dist/services/agents/claude-agent-runner.js +221 -199
- package/dist/services/agents/gemini-agent-runner.js +3 -0
- package/dist/services/agents/index.d.ts +1 -0
- package/dist/services/agents/index.js +1 -0
- package/dist/services/agents/opencode-agent-runner.js +3 -0
- package/dist/services/file-system/file-manager.d.ts +11 -0
- package/dist/services/file-system/file-manager.js +26 -0
- package/dist/services/git/git-ops.d.ts +58 -0
- package/dist/services/git/git-ops.js +73 -0
- package/dist/services/git/index.d.ts +3 -0
- package/dist/services/git/index.js +2 -0
- package/dist/services/git/push-conflict-handler.d.ts +32 -0
- package/dist/services/git/push-conflict-handler.js +84 -0
- package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
- package/dist/services/lock/git-backed-lock-service.js +173 -0
- package/dist/services/lock/lock-cleanup.d.ts +49 -0
- package/dist/services/lock/lock-cleanup.js +85 -0
- package/dist/services/lock/lock-service.d.ts +143 -0
- package/dist/services/lock/lock-service.js +290 -0
- package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
- package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
- package/dist/services/orchestration/workflow-orchestrator.js +181 -31
- package/dist/services/review/ai-review-scanner.js +1 -0
- package/dist/services/review/review-phase-executor.js +3 -0
- package/dist/services/review/self-heal-loop.js +1 -0
- package/dist/services/review/types.d.ts +2 -0
- package/dist/utils/errors.d.ts +17 -1
- package/dist/utils/errors.js +18 -0
- package/dist/utils/session-naming.d.ts +23 -0
- package/dist/utils/session-naming.js +30 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +5 -0
- 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 {
|
|
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 {
|
|
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
|
|
159
|
-
|
|
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 =
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
}, '
|
|
178
|
-
//
|
|
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
|
|
203
|
+
const result = await this.spawnAndStream({
|
|
204
|
+
args,
|
|
205
|
+
cwd: options.cwd,
|
|
185
206
|
env,
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
onStream: options.onStream,
|
|
208
|
+
onStreamVerbose: options.onStreamVerbose,
|
|
209
|
+
startTime,
|
|
210
|
+
stdinFile: tempFile,
|
|
188
211
|
timeout,
|
|
189
|
-
};
|
|
190
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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:
|
|
252
|
-
exitCode:
|
|
253
|
-
output:
|
|
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
|
-
*
|
|
272
|
-
* @private
|
|
246
|
+
* Spawn Claude process and stream output in real-time
|
|
273
247
|
*/
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
}
|
|
@@ -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
|
*
|