kiro-spec-engine 1.44.0 → 1.45.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.
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Agent Spawner — Process Manager
3
+ *
4
+ * Manages Codex CLI sub-processes via Node.js child_process.spawn.
5
+ * Each spawned agent executes a single Spec in full-auto mode and
6
+ * streams JSON Lines events back to the orchestrator.
7
+ *
8
+ * Requirements: 1.1 (spawn via child_process), 1.2 (CODEX_API_KEY env),
9
+ * 1.3 (--json flag), 1.4 (exit 0 → completed),
10
+ * 1.5 (exit non-0 → failed), 1.6 (timeout → terminate),
11
+ * 1.7 (register in AgentRegistry)
12
+ */
13
+
14
+ const { EventEmitter } = require('events');
15
+ const { spawn } = require('child_process');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ class AgentSpawner extends EventEmitter {
21
+ /**
22
+ * @param {string} workspaceRoot - Absolute path to the project root
23
+ * @param {import('./orchestrator-config').OrchestratorConfig} orchestratorConfig
24
+ * @param {import('../collab/agent-registry').AgentRegistry} agentRegistry
25
+ * @param {import('./bootstrap-prompt-builder').BootstrapPromptBuilder} bootstrapPromptBuilder
26
+ */
27
+ constructor(workspaceRoot, orchestratorConfig, agentRegistry, bootstrapPromptBuilder) {
28
+ super();
29
+ this._workspaceRoot = workspaceRoot;
30
+ this._orchestratorConfig = orchestratorConfig;
31
+ this._agentRegistry = agentRegistry;
32
+ this._bootstrapPromptBuilder = bootstrapPromptBuilder;
33
+ /** @type {Map<string, import('./agent-spawner').SpawnedAgent>} */
34
+ this._agents = new Map();
35
+ }
36
+
37
+ /**
38
+ * Spawn a Codex CLI sub-process to execute the given Spec.
39
+ *
40
+ * 1. Builds the bootstrap prompt via BootstrapPromptBuilder
41
+ * 2. Registers the agent in AgentRegistry
42
+ * 3. Spawns `codex exec --full-auto --json --sandbox danger-full-access "<prompt>"`
43
+ * 4. Sets up stdout/stderr/close handlers and timeout timer
44
+ *
45
+ * @param {string} specName - Spec to execute (e.g. "96-00-agent-orchestrator")
46
+ * @returns {Promise<object>} The SpawnedAgent record
47
+ */
48
+ async spawn(specName) {
49
+ const config = await this._orchestratorConfig.getConfig();
50
+
51
+ // Resolve API key: env var → ~/.codex/auth.json fallback
52
+ const apiKeyEnvVar = config.apiKeyEnvVar || 'CODEX_API_KEY';
53
+ let apiKey = process.env[apiKeyEnvVar];
54
+ if (!apiKey) {
55
+ apiKey = this._readCodexAuthFile();
56
+ }
57
+ if (!apiKey) {
58
+ throw new Error(
59
+ `Cannot find API key. Set environment variable ${apiKeyEnvVar}, ` +
60
+ 'or configure Codex CLI auth via `codex auth` (~/.codex/auth.json).'
61
+ );
62
+ }
63
+
64
+ // Build the bootstrap prompt (Req 2.1-2.3)
65
+ const prompt = await this._bootstrapPromptBuilder.buildPrompt(specName);
66
+
67
+ // Register in AgentRegistry (Req 1.7)
68
+ const { agentId } = await this._agentRegistry.register({
69
+ currentTask: { specName },
70
+ });
71
+
72
+ // Assemble command arguments (Req 1.1, 1.3)
73
+ const args = [
74
+ 'exec',
75
+ '--full-auto',
76
+ '--json',
77
+ '--sandbox', 'danger-full-access',
78
+ ...(config.codexArgs || []),
79
+ prompt,
80
+ ];
81
+
82
+ // Resolve codex command: config → auto-detect
83
+ const { command, prependArgs } = this._resolveCodexCommand(config);
84
+
85
+ // Spawn the child process (Req 1.1, 1.2)
86
+ const env = { ...process.env, [apiKeyEnvVar]: apiKey };
87
+ const child = spawn(command, [...prependArgs, ...args], {
88
+ cwd: this._workspaceRoot,
89
+ env,
90
+ stdio: ['ignore', 'pipe', 'pipe'],
91
+ windowsHide: true,
92
+ shell: command === 'npx',
93
+ });
94
+
95
+ const now = new Date().toISOString();
96
+
97
+ /** @type {object} */
98
+ const agent = {
99
+ agentId,
100
+ specName,
101
+ process: child,
102
+ status: 'running',
103
+ startedAt: now,
104
+ completedAt: null,
105
+ exitCode: null,
106
+ retryCount: 0,
107
+ stderr: '',
108
+ events: [],
109
+ };
110
+
111
+ this._agents.set(agentId, agent);
112
+
113
+ // --- stdout: parse JSON Lines events ---
114
+ this._setupStdoutHandler(agent);
115
+
116
+ // --- stderr: buffer for error reporting ---
117
+ this._setupStderrHandler(agent);
118
+
119
+ // --- process close: determine final status ---
120
+ this._setupCloseHandler(agent);
121
+
122
+ // --- timeout detection (Req 1.6) ---
123
+ this._setupTimeout(agent, config.timeoutSeconds);
124
+
125
+ return agent;
126
+ }
127
+
128
+ /**
129
+ * Terminate a specific sub-process.
130
+ * Sends SIGTERM first, then SIGKILL after a 5-second grace period.
131
+ *
132
+ * @param {string} agentId
133
+ * @returns {Promise<void>}
134
+ */
135
+ async kill(agentId) {
136
+ const agent = this._agents.get(agentId);
137
+ if (!agent || agent.status !== 'running') {
138
+ return;
139
+ }
140
+ await this._terminateProcess(agent);
141
+ }
142
+
143
+ /**
144
+ * Terminate all running sub-processes.
145
+ * @returns {Promise<void>}
146
+ */
147
+ async killAll() {
148
+ const killPromises = [];
149
+ for (const agent of this._agents.values()) {
150
+ if (agent.status === 'running') {
151
+ killPromises.push(this._terminateProcess(agent));
152
+ }
153
+ }
154
+ await Promise.all(killPromises);
155
+ }
156
+
157
+ /**
158
+ * Get all agents (active and completed).
159
+ * @returns {Map<string, object>}
160
+ */
161
+ getActiveAgents() {
162
+ return new Map(this._agents);
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Private helpers
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Parse stdout line-by-line for JSON Lines events.
171
+ * @param {object} agent
172
+ * @private
173
+ */
174
+ _setupStdoutHandler(agent) {
175
+ let buffer = '';
176
+
177
+ agent.process.stdout.on('data', (chunk) => {
178
+ buffer += chunk.toString();
179
+ const lines = buffer.split('\n');
180
+ // Keep the last (possibly incomplete) line in the buffer
181
+ buffer = lines.pop() || '';
182
+
183
+ for (const line of lines) {
184
+ const trimmed = line.trim();
185
+ if (!trimmed) continue;
186
+
187
+ try {
188
+ const event = JSON.parse(trimmed);
189
+ agent.events.push(event);
190
+ this.emit('agent:output', {
191
+ agentId: agent.agentId,
192
+ specName: agent.specName,
193
+ event,
194
+ });
195
+ } catch (_err) {
196
+ // Non-JSON line — ignore silently
197
+ }
198
+ }
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Buffer stderr output for error reporting.
204
+ * @param {object} agent
205
+ * @private
206
+ */
207
+ _setupStderrHandler(agent) {
208
+ agent.process.stderr.on('data', (chunk) => {
209
+ agent.stderr += chunk.toString();
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Handle process close event to determine final status.
215
+ * @param {object} agent
216
+ * @private
217
+ */
218
+ _setupCloseHandler(agent) {
219
+ agent.process.on('close', async (code) => {
220
+ // Clear timeout timer if still pending
221
+ if (agent._timeoutTimer) {
222
+ clearTimeout(agent._timeoutTimer);
223
+ agent._timeoutTimer = null;
224
+ }
225
+ if (agent._killTimer) {
226
+ clearTimeout(agent._killTimer);
227
+ agent._killTimer = null;
228
+ }
229
+
230
+ // Already finalized (e.g. by timeout handler) — skip
231
+ if (agent.status !== 'running') {
232
+ return;
233
+ }
234
+
235
+ agent.exitCode = code;
236
+ agent.completedAt = new Date().toISOString();
237
+
238
+ if (code === 0) {
239
+ // Req 1.4: exit 0 → completed
240
+ agent.status = 'completed';
241
+ this.emit('agent:completed', {
242
+ agentId: agent.agentId,
243
+ specName: agent.specName,
244
+ exitCode: code,
245
+ });
246
+ } else {
247
+ // Req 1.5: exit non-0 → failed
248
+ agent.status = 'failed';
249
+ this.emit('agent:failed', {
250
+ agentId: agent.agentId,
251
+ specName: agent.specName,
252
+ exitCode: code,
253
+ stderr: agent.stderr,
254
+ });
255
+ }
256
+
257
+ // Deregister from AgentRegistry
258
+ await this._deregisterAgent(agent.agentId);
259
+ });
260
+
261
+ // Handle spawn errors (e.g. command not found)
262
+ agent.process.on('error', async (err) => {
263
+ if (agent._timeoutTimer) {
264
+ clearTimeout(agent._timeoutTimer);
265
+ agent._timeoutTimer = null;
266
+ }
267
+
268
+ if (agent.status !== 'running') return;
269
+
270
+ agent.status = 'failed';
271
+ agent.completedAt = new Date().toISOString();
272
+ agent.stderr += `\nSpawn error: ${err.message}`;
273
+
274
+ this.emit('agent:failed', {
275
+ agentId: agent.agentId,
276
+ specName: agent.specName,
277
+ exitCode: null,
278
+ stderr: agent.stderr,
279
+ error: err.message,
280
+ });
281
+
282
+ await this._deregisterAgent(agent.agentId);
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Set up a timeout timer that terminates the process if it runs too long.
288
+ * Sends SIGTERM first, then SIGKILL after 5 seconds (Req 1.6).
289
+ *
290
+ * @param {object} agent
291
+ * @param {number} timeoutSeconds
292
+ * @private
293
+ */
294
+ _setupTimeout(agent, timeoutSeconds) {
295
+ if (!timeoutSeconds || timeoutSeconds <= 0) return;
296
+
297
+ agent._timeoutTimer = setTimeout(async () => {
298
+ if (agent.status !== 'running') return;
299
+
300
+ agent.status = 'timeout';
301
+ agent.completedAt = new Date().toISOString();
302
+
303
+ this.emit('agent:timeout', {
304
+ agentId: agent.agentId,
305
+ specName: agent.specName,
306
+ timeoutSeconds,
307
+ });
308
+
309
+ // SIGTERM → 5s grace → SIGKILL
310
+ try {
311
+ agent.process.kill('SIGTERM');
312
+ } catch (_err) {
313
+ // Process may have already exited
314
+ }
315
+
316
+ agent._killTimer = setTimeout(() => {
317
+ try {
318
+ agent.process.kill('SIGKILL');
319
+ } catch (_err) {
320
+ // Process may have already exited
321
+ }
322
+ }, 5000);
323
+
324
+ await this._deregisterAgent(agent.agentId);
325
+ }, timeoutSeconds * 1000);
326
+ }
327
+
328
+ /**
329
+ * Terminate a process: SIGTERM first, SIGKILL after 5s grace period.
330
+ * @param {object} agent
331
+ * @returns {Promise<void>}
332
+ * @private
333
+ */
334
+ _terminateProcess(agent) {
335
+ return new Promise((resolve) => {
336
+ // Clear any existing timeout timer
337
+ if (agent._timeoutTimer) {
338
+ clearTimeout(agent._timeoutTimer);
339
+ agent._timeoutTimer = null;
340
+ }
341
+
342
+ try {
343
+ agent.process.kill('SIGTERM');
344
+ } catch (_err) {
345
+ resolve();
346
+ return;
347
+ }
348
+
349
+ const killTimer = setTimeout(() => {
350
+ try {
351
+ agent.process.kill('SIGKILL');
352
+ } catch (_err) {
353
+ // Already exited
354
+ }
355
+ }, 5000);
356
+
357
+ agent.process.once('close', () => {
358
+ clearTimeout(killTimer);
359
+ resolve();
360
+ });
361
+
362
+ // Safety net: resolve after 10s regardless
363
+ setTimeout(() => {
364
+ clearTimeout(killTimer);
365
+ resolve();
366
+ }, 10000);
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Deregister an agent from the AgentRegistry.
372
+ * Failures are logged but do not propagate (non-fatal).
373
+ *
374
+ * @param {string} agentId
375
+ * @returns {Promise<void>}
376
+ * @private
377
+ */
378
+ async _deregisterAgent(agentId) {
379
+ try {
380
+ await this._agentRegistry.deregister(agentId);
381
+ } catch (err) {
382
+ console.warn(
383
+ `[AgentSpawner] Failed to deregister agent ${agentId}: ${err.message}`
384
+ );
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Read API key from Codex CLI's native auth file (~/.codex/auth.json).
390
+ * Returns the key string or null if not found.
391
+ *
392
+ * @returns {string|null}
393
+ * @private
394
+ */
395
+ _readCodexAuthFile() {
396
+ try {
397
+ const authPath = path.join(os.homedir(), '.codex', 'auth.json');
398
+ if (!fs.existsSync(authPath)) {
399
+ return null;
400
+ }
401
+ const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
402
+ return auth.OPENAI_API_KEY || auth.CODEX_API_KEY || null;
403
+ } catch (_err) {
404
+ return null;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Resolve the codex command and any prepended arguments.
410
+ *
411
+ * Priority:
412
+ * 1. config.codexCommand (user-specified, e.g. "npx @openai/codex" or "codex")
413
+ * 2. "codex" (default — assumes global install)
414
+ *
415
+ * When codexCommand contains spaces (e.g. "npx @openai/codex"),
416
+ * the first token becomes the command and the rest become prependArgs.
417
+ *
418
+ * @param {object} config
419
+ * @returns {{ command: string, prependArgs: string[] }}
420
+ * @private
421
+ */
422
+ _resolveCodexCommand(config) {
423
+ const raw = config.codexCommand || 'codex';
424
+ const parts = raw.trim().split(/\s+/);
425
+ return {
426
+ command: parts[0],
427
+ prependArgs: parts.slice(1),
428
+ };
429
+ }
430
+ }
431
+
432
+ module.exports = { AgentSpawner };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Bootstrap Prompt Builder
3
+ *
4
+ * Builds the initial prompt injected into each Codex CLI sub-agent so that it
5
+ * has full context about the target Spec, kse conventions, steering rules, and
6
+ * clear task-execution instructions.
7
+ *
8
+ * Requirements: 2.1 (Spec path), 2.2 (kse / steering context),
9
+ * 2.3 (task execution instructions), 2.4 (configurable template)
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs-extra');
14
+
15
+ // Steering files loaded into every prompt (order matters for readability)
16
+ const STEERING_FILES = [
17
+ 'CORE_PRINCIPLES.md',
18
+ 'ENVIRONMENT.md',
19
+ 'CURRENT_CONTEXT.md',
20
+ 'RULES_GUIDE.md',
21
+ ];
22
+
23
+ class BootstrapPromptBuilder {
24
+ /**
25
+ * @param {string} workspaceRoot - Absolute path to the project root
26
+ * @param {import('./orchestrator-config').OrchestratorConfig} orchestratorConfig
27
+ */
28
+ constructor(workspaceRoot, orchestratorConfig) {
29
+ this._workspaceRoot = workspaceRoot;
30
+ this._orchestratorConfig = orchestratorConfig;
31
+ }
32
+
33
+ /**
34
+ * Build the bootstrap prompt for a given Spec.
35
+ *
36
+ * When a custom template is configured (via orchestrator.json bootstrapTemplate),
37
+ * the template is loaded and placeholders are replaced:
38
+ * {{specName}} – the Spec name
39
+ * {{specPath}} – relative path to the Spec directory
40
+ * {{steeringContext}} – concatenated steering file contents
41
+ * {{taskInstructions}} – task execution instructions block
42
+ *
43
+ * Otherwise a sensible built-in default template is used.
44
+ *
45
+ * @param {string} specName - Name of the Spec (e.g. "96-00-agent-orchestrator")
46
+ * @returns {Promise<string>} The assembled prompt
47
+ */
48
+ async buildPrompt(specName) {
49
+ const specPath = `.kiro/specs/${specName}/`;
50
+ const steeringContext = await this._loadSteeringContext();
51
+ const taskInstructions = this._buildTaskInstructions(specName, specPath);
52
+ const readmeSummary = await this._loadReadmeSummary();
53
+ const specContext = await this._loadSpecContext(specPath);
54
+
55
+ const customTemplate = await this._orchestratorConfig.getBootstrapTemplate();
56
+
57
+ if (customTemplate) {
58
+ return this._renderTemplate(customTemplate, {
59
+ specName,
60
+ specPath,
61
+ steeringContext,
62
+ taskInstructions,
63
+ });
64
+ }
65
+
66
+ return this._buildDefaultPrompt({
67
+ specName,
68
+ specPath,
69
+ readmeSummary,
70
+ specContext,
71
+ steeringContext,
72
+ taskInstructions,
73
+ });
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Private helpers
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Load and concatenate all steering files from `.kiro/steering/`.
82
+ * Missing files are silently skipped.
83
+ * @returns {Promise<string>}
84
+ * @private
85
+ */
86
+ async _loadSteeringContext() {
87
+ const steeringDir = path.join(this._workspaceRoot, '.kiro', 'steering');
88
+ const sections = [];
89
+
90
+ for (const filename of STEERING_FILES) {
91
+ const filePath = path.join(steeringDir, filename);
92
+ try {
93
+ const content = await fs.readFile(filePath, 'utf8');
94
+ sections.push(`### ${filename}\n\n${content.trim()}`);
95
+ } catch (_err) {
96
+ // Steering file missing — skip silently
97
+ }
98
+ }
99
+
100
+ return sections.join('\n\n---\n\n');
101
+ }
102
+
103
+ /**
104
+ * Load a short summary from `.kiro/README.md`.
105
+ * Returns the first ~40 lines (up to the first `---` separator after the
106
+ * opening block) to keep the prompt concise.
107
+ * @returns {Promise<string>}
108
+ * @private
109
+ */
110
+ async _loadReadmeSummary() {
111
+ const readmePath = path.join(this._workspaceRoot, '.kiro', 'README.md');
112
+ try {
113
+ const content = await fs.readFile(readmePath, 'utf8');
114
+ // Take the first meaningful section (up to the capabilities list)
115
+ const lines = content.split('\n');
116
+ const summaryLines = [];
117
+ let separatorCount = 0;
118
+ for (const line of lines) {
119
+ if (line.trim() === '---') {
120
+ separatorCount++;
121
+ if (separatorCount >= 2) break; // stop after second separator
122
+ }
123
+ summaryLines.push(line);
124
+ }
125
+ return summaryLines.join('\n').trim();
126
+ } catch (_err) {
127
+ return 'kse (Kiro Spec Engine) — Spec-driven development project.';
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Load the Spec's own documents (requirements.md, design.md, tasks.md).
133
+ * Missing files are noted but do not cause failure.
134
+ * @param {string} specPath - Relative Spec directory path
135
+ * @returns {Promise<string>}
136
+ * @private
137
+ */
138
+ async _loadSpecContext(specPath) {
139
+ const specDir = path.join(this._workspaceRoot, specPath);
140
+ const docs = ['requirements.md', 'design.md', 'tasks.md'];
141
+ const sections = [];
142
+
143
+ for (const doc of docs) {
144
+ const filePath = path.join(specDir, doc);
145
+ try {
146
+ const content = await fs.readFile(filePath, 'utf8');
147
+ sections.push(`#### ${doc}\n\n${content.trim()}`);
148
+ } catch (_err) {
149
+ sections.push(`#### ${doc}\n\n(not found)`);
150
+ }
151
+ }
152
+
153
+ return sections.join('\n\n');
154
+ }
155
+
156
+ /**
157
+ * Build the task-execution instruction block.
158
+ * @param {string} specName
159
+ * @param {string} specPath
160
+ * @returns {string}
161
+ * @private
162
+ */
163
+ _buildTaskInstructions(specName, specPath) {
164
+ return [
165
+ `You are a sub-agent responsible for executing the Spec "${specName}".`,
166
+ '',
167
+ 'Instructions:',
168
+ `1. Read the task list at \`${specPath}tasks.md\`.`,
169
+ '2. Execute each task in order, starting from the first uncompleted task.',
170
+ '3. For each task, read the corresponding requirements and design sections.',
171
+ '4. Write all required code, tests, and documentation.',
172
+ '5. Mark each task as completed (change `[ ]` or `[-]` to `[x]`) after finishing.',
173
+ '6. Run relevant tests to verify your implementation before moving on.',
174
+ '7. If a task fails after multiple attempts, document the issue and continue.',
175
+ '',
176
+ 'Quality requirements:',
177
+ '- All code must compile and pass linting.',
178
+ '- All new functionality must have tests.',
179
+ '- Follow existing code patterns and conventions.',
180
+ '- Do not break existing tests.',
181
+ ].join('\n');
182
+ }
183
+
184
+ /**
185
+ * Assemble the default (built-in) prompt from its constituent parts.
186
+ * @param {object} parts
187
+ * @returns {string}
188
+ * @private
189
+ */
190
+ _buildDefaultPrompt({ specName, specPath, readmeSummary, specContext, steeringContext, taskInstructions }) {
191
+ const sections = [
192
+ '# Bootstrap Prompt',
193
+ '',
194
+ '## Project Overview',
195
+ '',
196
+ readmeSummary,
197
+ '',
198
+ '## Target Spec',
199
+ '',
200
+ `**Spec**: ${specName}`,
201
+ `**Path**: \`${specPath}\``,
202
+ '',
203
+ '## Spec Documents',
204
+ '',
205
+ specContext,
206
+ '',
207
+ '## Steering Context (Project Rules)',
208
+ '',
209
+ steeringContext,
210
+ '',
211
+ '## Task Execution Instructions',
212
+ '',
213
+ taskInstructions,
214
+ ];
215
+
216
+ return sections.join('\n');
217
+ }
218
+
219
+ /**
220
+ * Render a custom template by replacing `{{placeholder}}` tokens.
221
+ * @param {string} template
222
+ * @param {object} vars - { specName, specPath, steeringContext, taskInstructions }
223
+ * @returns {string}
224
+ * @private
225
+ */
226
+ _renderTemplate(template, vars) {
227
+ let result = template;
228
+ for (const [key, value] of Object.entries(vars)) {
229
+ // Replace all occurrences of {{key}}
230
+ result = result.split(`{{${key}}}`).join(value);
231
+ }
232
+ return result;
233
+ }
234
+ }
235
+
236
+ module.exports = { BootstrapPromptBuilder };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Orchestrator Module — Barrel Export
3
+ *
4
+ * Re-exports all orchestrator components for convenient consumption.
5
+ */
6
+
7
+ const { OrchestratorConfig } = require('./orchestrator-config');
8
+ const { BootstrapPromptBuilder } = require('./bootstrap-prompt-builder');
9
+ const { AgentSpawner } = require('./agent-spawner');
10
+ const { StatusMonitor } = require('./status-monitor');
11
+ const { OrchestrationEngine } = require('./orchestration-engine');
12
+
13
+ module.exports = {
14
+ OrchestratorConfig,
15
+ BootstrapPromptBuilder,
16
+ AgentSpawner,
17
+ StatusMonitor,
18
+ OrchestrationEngine,
19
+ };