@hyperdrive.bot/bmad-workflow 1.0.2
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/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- package/package.json +119 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Claude 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
|
+
* ClaudeAgentRunner service for executing Claude AI agents
|
|
13
|
+
*
|
|
14
|
+
* Spawns Claude 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 ClaudeAgentRunner(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 ClaudeAgentRunner implements AIProviderRunner {
|
|
29
|
+
readonly provider: AIProvider;
|
|
30
|
+
private readonly config;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
/**
|
|
33
|
+
* Create a new ClaudeAgentRunner 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 a Claude AI agent with the specified prompt and options
|
|
47
|
+
*
|
|
48
|
+
* This method spawns a Claude 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
|
+
* Build detailed error context for debugging process failures
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
private buildErrorContext;
|
|
76
|
+
/**
|
|
77
|
+
* Invoke onResponse callback and return result
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
private returnWithCallback;
|
|
81
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Claude 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
|
+
* ClaudeAgentRunner service for executing Claude AI agents
|
|
52
|
+
*
|
|
53
|
+
* Spawns Claude 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 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
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class ClaudeAgentRunner {
|
|
68
|
+
provider = 'claude';
|
|
69
|
+
config = PROVIDER_CONFIGS.claude;
|
|
70
|
+
logger;
|
|
71
|
+
/**
|
|
72
|
+
* Create a new ClaudeAgentRunner 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 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
|
+
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 Claude 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(), 'claude-prompt-'));
|
|
156
|
+
tempFile = join(tempDir, 'prompt.txt');
|
|
157
|
+
await writeFile(tempFile, prompt, 'utf8');
|
|
158
|
+
// Build command with temp file
|
|
159
|
+
const flags = this.config.flags.join(' ');
|
|
160
|
+
const command = `${this.config.command} ${flags} < "${tempFile}"`;
|
|
161
|
+
// Log the command being executed
|
|
162
|
+
this.logger.info({
|
|
163
|
+
promptLength: prompt.length,
|
|
164
|
+
tempFile,
|
|
165
|
+
}, 'Executing command with temp file');
|
|
166
|
+
// Use exec instead of spawn for better shell compatibility
|
|
167
|
+
const { stderr, stdout } = await execAsync(command, {
|
|
168
|
+
env: process.env,
|
|
169
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
170
|
+
shell: process.env.SHELL || '/bin/bash',
|
|
171
|
+
timeout,
|
|
172
|
+
});
|
|
173
|
+
stdoutData = stdout;
|
|
174
|
+
stderrData = stderr;
|
|
175
|
+
const duration = Date.now() - startTime;
|
|
176
|
+
this.logger.info({
|
|
177
|
+
duration,
|
|
178
|
+
stderrLength: stderrData.length,
|
|
179
|
+
stdoutLength: stdoutData.length,
|
|
180
|
+
}, 'Claude CLI process completed successfully');
|
|
181
|
+
const result = {
|
|
182
|
+
agentType: options.agentType,
|
|
183
|
+
duration,
|
|
184
|
+
errors: stderrData,
|
|
185
|
+
exitCode: 0,
|
|
186
|
+
output: stdoutData,
|
|
187
|
+
success: true,
|
|
188
|
+
};
|
|
189
|
+
return this.returnWithCallback(result, options.onResponse);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
// Handle exec errors (includes timeout, non-zero exit, etc.)
|
|
193
|
+
const duration = Date.now() - startTime;
|
|
194
|
+
// Extract stdout/stderr from exec error if available
|
|
195
|
+
const execError = error;
|
|
196
|
+
if (execError.stdout)
|
|
197
|
+
stdoutData = execError.stdout;
|
|
198
|
+
if (execError.stderr)
|
|
199
|
+
stderrData = execError.stderr;
|
|
200
|
+
const exitCode = execError.code ?? -1;
|
|
201
|
+
const isTimeout = execError.killed === true && execError.signal === 'SIGTERM';
|
|
202
|
+
const wasKilled = execError.killed === true;
|
|
203
|
+
const signal = execError.signal ?? null;
|
|
204
|
+
// Build detailed error context for debugging
|
|
205
|
+
const errorContext = this.buildErrorContext({
|
|
206
|
+
cmd: execError.cmd,
|
|
207
|
+
duration,
|
|
208
|
+
exitCode,
|
|
209
|
+
isTimeout,
|
|
210
|
+
signal,
|
|
211
|
+
stderrData,
|
|
212
|
+
stdoutData,
|
|
213
|
+
wasKilled,
|
|
214
|
+
});
|
|
215
|
+
this.logger.error({
|
|
216
|
+
cmd: execError.cmd,
|
|
217
|
+
duration,
|
|
218
|
+
error: execError.message,
|
|
219
|
+
exitCode,
|
|
220
|
+
isTimeout,
|
|
221
|
+
signal,
|
|
222
|
+
stderrLength: stderrData.length,
|
|
223
|
+
stdoutLength: stdoutData.length,
|
|
224
|
+
wasKilled,
|
|
225
|
+
}, isTimeout ? 'Claude CLI process timeout' : 'Claude CLI process error');
|
|
226
|
+
return this.returnWithCallback({
|
|
227
|
+
agentType: options.agentType,
|
|
228
|
+
duration,
|
|
229
|
+
errors: errorContext,
|
|
230
|
+
exitCode: isTimeout ? 124 : exitCode,
|
|
231
|
+
output: stdoutData,
|
|
232
|
+
success: false,
|
|
233
|
+
}, options.onResponse);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
// Clean up temp file and directory
|
|
237
|
+
if (tempDir) {
|
|
238
|
+
try {
|
|
239
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
240
|
+
this.logger.debug({ tempDir }, 'Cleaned up temp directory');
|
|
241
|
+
}
|
|
242
|
+
catch (cleanupError) {
|
|
243
|
+
this.logger.warn({ error: cleanupError.message, tempDir }, 'Failed to clean up temp directory');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Build detailed error context for debugging process failures
|
|
250
|
+
* @private
|
|
251
|
+
*/
|
|
252
|
+
buildErrorContext(info) {
|
|
253
|
+
const lines = [];
|
|
254
|
+
// Determine the type of failure
|
|
255
|
+
if (info.isTimeout) {
|
|
256
|
+
lines.push(`Process timeout after ${info.duration}ms`);
|
|
257
|
+
}
|
|
258
|
+
else if (info.wasKilled) {
|
|
259
|
+
lines.push('Process was killed externally');
|
|
260
|
+
// Explain common exit codes
|
|
261
|
+
switch (info.exitCode) {
|
|
262
|
+
case 130: {
|
|
263
|
+
lines.push('Exit code 130 = SIGINT (user interrupt, Ctrl+C)');
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 137: {
|
|
267
|
+
lines.push('Exit code 137 = SIGKILL (process forcefully killed)', 'Possible causes:', ' - System OOM killer', ' - Exceeded memory limits', ' - Docker/container killed the process');
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case 143: {
|
|
271
|
+
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');
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
// No default
|
|
275
|
+
}
|
|
276
|
+
if (info.signal) {
|
|
277
|
+
lines.push(`Signal received: ${info.signal}`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
lines.push('No signal information available (signal: null)');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
lines.push(`Process exited with code ${info.exitCode}`);
|
|
285
|
+
}
|
|
286
|
+
// Add duration context
|
|
287
|
+
lines.push(`Duration: ${(info.duration / 1000).toFixed(2)}s`);
|
|
288
|
+
// Add output context
|
|
289
|
+
if (info.stdoutData.length > 0) {
|
|
290
|
+
lines.push(`Stdout (${info.stdoutData.length} chars):`);
|
|
291
|
+
// Truncate if too long, show last 500 chars which are often most relevant
|
|
292
|
+
const truncatedStdout = info.stdoutData.length > 500 ? `...[truncated]...\n${info.stdoutData.slice(-500)}` : info.stdoutData;
|
|
293
|
+
lines.push(truncatedStdout);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
lines.push('Stdout: (empty - no output captured)');
|
|
297
|
+
}
|
|
298
|
+
if (info.stderrData.length > 0) {
|
|
299
|
+
lines.push(`Stderr (${info.stderrData.length} chars):`);
|
|
300
|
+
const truncatedStderr = info.stderrData.length > 500 ? `...[truncated]...\n${info.stderrData.slice(-500)}` : info.stderrData;
|
|
301
|
+
lines.push(truncatedStderr);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
lines.push('Stderr: (empty - no error output)');
|
|
305
|
+
}
|
|
306
|
+
// Add command for reference (truncate if too long)
|
|
307
|
+
if (info.cmd) {
|
|
308
|
+
const truncatedCmd = info.cmd.length > 200 ? `${info.cmd.slice(0, 200)}...[truncated]` : info.cmd;
|
|
309
|
+
lines.push(`Command: ${truncatedCmd}`);
|
|
310
|
+
}
|
|
311
|
+
// Add troubleshooting suggestions for empty output
|
|
312
|
+
if (info.stdoutData.length === 0 && info.stderrData.length === 0) {
|
|
313
|
+
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');
|
|
314
|
+
}
|
|
315
|
+
return lines.join('\n');
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Invoke onResponse callback and return result
|
|
319
|
+
* @private
|
|
320
|
+
*/
|
|
321
|
+
async returnWithCallback(result, onResponse) {
|
|
322
|
+
if (onResponse) {
|
|
323
|
+
try {
|
|
324
|
+
await onResponse(result);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
this.logger.warn({ error: error.message }, 'Error in onResponse callback');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeminiAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Gemini CLI process spawning with comprehensive error handling,
|
|
5
|
+
* logging, and timeout management for reliable AI agent execution.
|
|
6
|
+
*
|
|
7
|
+
* Key difference from Claude: Gemini CLI doesn't support @file references natively,
|
|
8
|
+
* so this runner preprocesses prompts to inline file content before execution.
|
|
9
|
+
*/
|
|
10
|
+
import type pino from 'pino';
|
|
11
|
+
import type { AgentOptions, AgentResult } from '../../models/index.js';
|
|
12
|
+
import type { AIProvider } from '../../models/provider.js';
|
|
13
|
+
import type { AIProviderRunner } from './agent-runner.js';
|
|
14
|
+
/**
|
|
15
|
+
* GeminiAgentRunner service for executing Gemini AI agents
|
|
16
|
+
*
|
|
17
|
+
* Spawns Gemini CLI processes to execute AI agents with specified prompts.
|
|
18
|
+
* Preprocesses @file references to inline file content since Gemini CLI
|
|
19
|
+
* doesn't support native file references like Claude.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const logger = createLogger({ namespace: 'agent-runner' })
|
|
24
|
+
* const runner = new GeminiAgentRunner(logger)
|
|
25
|
+
* const result = await runner.runAgent({
|
|
26
|
+
* prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
|
|
27
|
+
* agentType: 'architect',
|
|
28
|
+
* timeout: 300000
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class GeminiAgentRunner implements AIProviderRunner {
|
|
33
|
+
readonly provider: AIProvider;
|
|
34
|
+
private readonly config;
|
|
35
|
+
private readonly logger;
|
|
36
|
+
/**
|
|
37
|
+
* Create a new GeminiAgentRunner instance
|
|
38
|
+
*
|
|
39
|
+
* @param logger - Logger instance for structured logging
|
|
40
|
+
*/
|
|
41
|
+
constructor(logger: pino.Logger);
|
|
42
|
+
/**
|
|
43
|
+
* Get the count of active processes
|
|
44
|
+
* (Useful for testing and monitoring purposes)
|
|
45
|
+
*
|
|
46
|
+
* @returns Number of currently active child processes
|
|
47
|
+
*/
|
|
48
|
+
getActiveProcessCount(): number;
|
|
49
|
+
/**
|
|
50
|
+
* Execute a Gemini AI agent with the specified prompt and options
|
|
51
|
+
*
|
|
52
|
+
* This method spawns a Gemini CLI process, captures output, handles errors,
|
|
53
|
+
* and enforces timeouts. It preprocesses @file references to inline content
|
|
54
|
+
* since Gemini CLI doesn't support native file references.
|
|
55
|
+
*
|
|
56
|
+
* @param prompt - The prompt to execute with the agent
|
|
57
|
+
* @param options - Agent execution options (without prompt)
|
|
58
|
+
* @returns AgentResult with success status, output, errors, and metadata
|
|
59
|
+
*/
|
|
60
|
+
runAgent(prompt: string, options: Omit<AgentOptions, 'prompt'>): Promise<AgentResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Build detailed error context for debugging process failures
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
private buildErrorContext;
|
|
66
|
+
/**
|
|
67
|
+
* Preprocess prompt to inline @file references
|
|
68
|
+
*
|
|
69
|
+
* Since Gemini CLI doesn't support @file syntax natively, this method
|
|
70
|
+
* finds all @file references and replaces them with the actual file content.
|
|
71
|
+
*
|
|
72
|
+
* @param prompt - The original prompt with @file references
|
|
73
|
+
* @param basePath - Base path for resolving relative file paths
|
|
74
|
+
* @returns Preprocessed prompt with inlined file content
|
|
75
|
+
*/
|
|
76
|
+
private preprocessPrompt;
|
|
77
|
+
/**
|
|
78
|
+
* Invoke onResponse callback and return result
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
private returnWithCallback;
|
|
82
|
+
}
|