@itz4blitz/agentful 0.4.0 → 1.0.0

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 (93) hide show
  1. package/README.md +131 -16
  2. package/bin/cli.js +1031 -47
  3. package/bin/hooks/README.md +338 -82
  4. package/bin/hooks/analyze-trigger.js +69 -0
  5. package/bin/hooks/block-random-docs.js +77 -0
  6. package/bin/hooks/health-check.js +153 -0
  7. package/bin/hooks/post-agent.js +101 -0
  8. package/bin/hooks/post-feature.js +227 -0
  9. package/bin/hooks/pre-agent.js +118 -0
  10. package/bin/hooks/pre-feature.js +138 -0
  11. package/lib/VALIDATION_README.md +455 -0
  12. package/lib/atomic.js +350 -0
  13. package/lib/ci/claude-action-integration.js +641 -0
  14. package/lib/ci/index.js +10 -0
  15. package/lib/core/CLAUDE_EXECUTOR.md +371 -0
  16. package/lib/core/README.md +321 -0
  17. package/lib/core/analyzer.js +497 -0
  18. package/lib/core/claude-executor.example.js +210 -0
  19. package/lib/core/claude-executor.js +1046 -0
  20. package/lib/core/cli.js +141 -0
  21. package/lib/core/detectors/conventions.js +342 -0
  22. package/lib/core/detectors/framework.js +276 -0
  23. package/lib/core/detectors/index.js +15 -0
  24. package/lib/core/detectors/language.js +199 -0
  25. package/lib/core/detectors/patterns.js +356 -0
  26. package/lib/core/generator.js +626 -0
  27. package/lib/core/index.js +9 -0
  28. package/lib/core/output-parser.example.js +250 -0
  29. package/lib/core/output-parser.js +458 -0
  30. package/lib/core/storage.js +515 -0
  31. package/lib/core/templates.js +556 -0
  32. package/lib/index.js +32 -0
  33. package/lib/init.js +252 -21
  34. package/lib/pipeline/cli.js +423 -0
  35. package/lib/pipeline/engine.js +928 -0
  36. package/lib/pipeline/executor.js +440 -0
  37. package/lib/pipeline/index.js +33 -0
  38. package/lib/pipeline/integrations.js +559 -0
  39. package/lib/pipeline/schemas.js +288 -0
  40. package/lib/presets.js +207 -0
  41. package/lib/remote/client.js +361 -0
  42. package/lib/server/auth.js +286 -0
  43. package/lib/server/client-example.js +190 -0
  44. package/lib/server/executor.js +426 -0
  45. package/lib/server/index.js +469 -0
  46. package/lib/update-helpers.js +505 -0
  47. package/lib/validation.js +460 -0
  48. package/package.json +19 -2
  49. package/template/.claude/agents/architect.md +260 -0
  50. package/template/.claude/agents/backend.md +203 -0
  51. package/template/.claude/agents/fixer.md +244 -0
  52. package/template/.claude/agents/frontend.md +232 -0
  53. package/template/.claude/agents/orchestrator.md +528 -0
  54. package/template/.claude/agents/product-analyzer.md +1130 -0
  55. package/template/.claude/agents/reviewer.md +229 -0
  56. package/template/.claude/agents/tester.md +242 -0
  57. package/{.claude → template/.claude}/commands/agentful-analyze.md +151 -43
  58. package/template/.claude/commands/agentful-decide.md +470 -0
  59. package/{.claude → template/.claude}/commands/agentful-product.md +89 -5
  60. package/template/.claude/commands/agentful-start.md +432 -0
  61. package/{.claude → template/.claude}/commands/agentful-status.md +88 -3
  62. package/template/.claude/commands/agentful-update.md +402 -0
  63. package/template/.claude/commands/agentful-validate.md +369 -0
  64. package/{.claude → template/.claude}/commands/agentful.md +110 -183
  65. package/template/.claude/product/EXAMPLES.md +167 -0
  66. package/{.claude → template/.claude}/settings.json +9 -13
  67. package/{.claude → template/.claude}/skills/conversation/SKILL.md +13 -7
  68. package/template/.claude/skills/deployment/SKILL.md +116 -0
  69. package/template/.claude/skills/product-planning/SKILL.md +463 -0
  70. package/template/.claude/skills/testing/SKILL.md +228 -0
  71. package/template/.claude/skills/validation/SKILL.md +650 -0
  72. package/template/CLAUDE.md +73 -5
  73. package/template/bin/hooks/block-random-docs.js +121 -0
  74. package/version.json +1 -1
  75. package/.claude/agents/architect.md +0 -524
  76. package/.claude/agents/backend.md +0 -315
  77. package/.claude/agents/fixer.md +0 -263
  78. package/.claude/agents/frontend.md +0 -274
  79. package/.claude/agents/orchestrator.md +0 -283
  80. package/.claude/agents/product-analyzer.md +0 -792
  81. package/.claude/agents/reviewer.md +0 -332
  82. package/.claude/agents/tester.md +0 -410
  83. package/.claude/commands/agentful-decide.md +0 -214
  84. package/.claude/commands/agentful-start.md +0 -182
  85. package/.claude/commands/agentful-validate.md +0 -127
  86. package/.claude/product/EXAMPLES.md +0 -610
  87. package/.claude/product/README.md +0 -326
  88. package/.claude/skills/validation/SKILL.md +0 -271
  89. package/bin/hooks/analyze-trigger.sh +0 -57
  90. package/bin/hooks/health-check.sh +0 -36
  91. /package/{.claude → template/.claude}/commands/agentful-generate.md +0 -0
  92. /package/{.claude → template/.claude}/product/index.md +0 -0
  93. /package/{.claude → template/.claude}/skills/product-tracking/SKILL.md +0 -0
@@ -0,0 +1,1046 @@
1
+ /**
2
+ * Unified Claude Code Executor Abstraction
3
+ *
4
+ * Consolidates three different Claude invocation methods:
5
+ * 1. Task API - Use Claude Code's Task() API (preferred, works in slash commands)
6
+ * 2. Subprocess - Spawn claude CLI with -p flag (for server/pipeline)
7
+ * 3. API - Direct Anthropic API calls (future, not yet implemented)
8
+ *
9
+ * Provides streaming execution with real-time progress updates,
10
+ * question detection, error handling, and retry logic.
11
+ *
12
+ * @module core/claude-executor
13
+ */
14
+
15
+ import { spawn } from 'child_process';
16
+ import { EventEmitter } from 'events';
17
+ import { randomUUID } from 'crypto';
18
+ import fs from 'fs/promises';
19
+ import path from 'path';
20
+ import { loadAgentDefinition } from '../ci/claude-action-integration.js';
21
+
22
+ /**
23
+ * Execution modes
24
+ */
25
+ export const ExecutionMode = {
26
+ TASK_API: 'task-api',
27
+ SUBPROCESS: 'subprocess',
28
+ API: 'api',
29
+ };
30
+
31
+ /**
32
+ * Execution states
33
+ */
34
+ export const ExecutionState = {
35
+ PENDING: 'pending',
36
+ RUNNING: 'running',
37
+ COMPLETED: 'completed',
38
+ FAILED: 'failed',
39
+ CANCELLED: 'cancelled',
40
+ };
41
+
42
+ /**
43
+ * Maximum output size (1MB per execution)
44
+ */
45
+ const MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
46
+
47
+ /**
48
+ * Maximum task length (10KB)
49
+ */
50
+ const MAX_TASK_LENGTH = 10 * 1024;
51
+
52
+ /**
53
+ * Default timeout (10 minutes)
54
+ */
55
+ const DEFAULT_TIMEOUT = 10 * 60 * 1000;
56
+
57
+ /**
58
+ * Output Parser
59
+ *
60
+ * Detects progress markers, questions, and errors in streaming output
61
+ */
62
+ class OutputParser {
63
+ constructor() {
64
+ this.buffer = '';
65
+ }
66
+
67
+ /**
68
+ * Parse a chunk of output
69
+ *
70
+ * @param {string} chunk - Raw output chunk
71
+ * @returns {Object} Parsed events { progress?, question?, error? }
72
+ */
73
+ parse(chunk) {
74
+ this.buffer += chunk;
75
+ const events = {};
76
+
77
+ // Detect progress markers: [PROGRESS: 45%] or Progress: 45%
78
+ const progressMatch = chunk.match(/(?:\[PROGRESS:\s*(\d+)%\]|Progress:\s*(\d+)%)/i);
79
+ if (progressMatch) {
80
+ const progress = parseInt(progressMatch[1] || progressMatch[2], 10);
81
+ events.progress = { percentage: progress, raw: progressMatch[0] };
82
+ }
83
+
84
+ // Detect task completion markers
85
+ if (chunk.includes('Task completed') || chunk.includes('✓ Complete')) {
86
+ events.progress = { percentage: 100, raw: 'Task completed' };
87
+ }
88
+
89
+ // Detect questions: Lines ending with "?" or "Please provide"
90
+ const questionMatch = chunk.match(/^(.+\?)\s*$/m) || chunk.match(/(Please provide .+)/);
91
+ if (questionMatch) {
92
+ events.question = {
93
+ text: questionMatch[1].trim(),
94
+ timestamp: Date.now()
95
+ };
96
+ }
97
+
98
+ // Detect errors: Lines starting with "Error:", "ERROR:", or containing "failed"
99
+ const errorMatch = chunk.match(/(?:Error:|ERROR:|❌)\s*(.+)/i);
100
+ if (errorMatch) {
101
+ events.error = {
102
+ message: errorMatch[1].trim(),
103
+ timestamp: Date.now()
104
+ };
105
+ }
106
+
107
+ return events;
108
+ }
109
+
110
+ /**
111
+ * Reset the buffer
112
+ */
113
+ reset() {
114
+ this.buffer = '';
115
+ }
116
+
117
+ /**
118
+ * Get full buffered output
119
+ *
120
+ * @returns {string} Complete output
121
+ */
122
+ getBuffer() {
123
+ return this.buffer;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Unified Claude Code Executor
129
+ *
130
+ * Provides consistent interface for executing agents across different modes:
131
+ * - Task API: Uses Claude Code's Task() API (for slash commands)
132
+ * - Subprocess: Spawns claude CLI with -p flag (for server/pipeline)
133
+ * - API: Direct Anthropic API calls (future)
134
+ *
135
+ * @extends EventEmitter
136
+ * @emits chunk - Streaming output chunk
137
+ * @emits progress - Progress update (0-100)
138
+ * @emits question - Question detected in output
139
+ * @emits error - Error during execution
140
+ * @emits complete - Execution completed
141
+ * @emits retry - Retry attempt starting
142
+ * @emits cancelled - Execution cancelled
143
+ */
144
+ export class ClaudeExecutor extends EventEmitter {
145
+ /**
146
+ * Create a new Claude executor
147
+ *
148
+ * @param {Object} options - Executor options
149
+ * @param {string} [options.mode='subprocess'] - Execution mode
150
+ * @param {string} [options.projectRoot=process.cwd()] - Project root directory
151
+ * @param {string} [options.agentsDir='.claude/agents'] - Agents directory
152
+ * @param {string} [options.claudeCommand='claude'] - Claude CLI command
153
+ * @param {string} [options.workingDir=process.cwd()] - Working directory (alias for projectRoot)
154
+ * @param {number} [options.timeout=600000] - Default timeout in ms
155
+ * @param {number} [options.maxOutputSize=1048576] - Maximum output size
156
+ * @param {boolean} [options.streamOutput=true] - Stream output as chunks
157
+ * @param {number} [options.maxRetries=2] - Maximum retry attempts
158
+ */
159
+ constructor(options = {}) {
160
+ super();
161
+
162
+ this.options = {
163
+ mode: options.mode || ExecutionMode.SUBPROCESS,
164
+ projectRoot: options.projectRoot || options.workingDir || process.cwd(),
165
+ agentsDir: options.agentsDir || '.claude/agents',
166
+ claudeCommand: options.claudeCommand || 'claude',
167
+ workingDir: options.workingDir || options.projectRoot || process.cwd(),
168
+ timeout: options.timeout || DEFAULT_TIMEOUT,
169
+ maxOutputSize: options.maxOutputSize || MAX_OUTPUT_SIZE,
170
+ streamOutput: options.streamOutput !== false,
171
+ maxRetries: options.maxRetries || 2,
172
+ ...options,
173
+ };
174
+
175
+ // Validate mode
176
+ if (!Object.values(ExecutionMode).includes(this.options.mode)) {
177
+ throw new Error(
178
+ `Invalid execution mode: ${this.options.mode}. ` +
179
+ `Must be one of: ${Object.values(ExecutionMode).join(', ')}`
180
+ );
181
+ }
182
+
183
+ this.activeExecutions = new Map();
184
+ }
185
+
186
+ /**
187
+ * Execute an agent with a task (main execution method)
188
+ *
189
+ * @param {string} agentName - Name of the agent
190
+ * @param {string} task - Task description
191
+ * @param {Object} [context={}] - Additional context
192
+ * @param {Object} [context.files] - Files to include in context
193
+ * @param {Object} [context.requirements] - Requirements to include
194
+ * @param {Object} [context.variables] - Variables to interpolate
195
+ * @param {Object} [options={}] - Execution options
196
+ * @param {number} [options.timeout] - Override default timeout
197
+ * @param {boolean} [options.streamOutput] - Override streaming setting
198
+ * @returns {Promise<Object>} Execution result
199
+ */
200
+ async execute(agentName, task, context = {}, options = {}) {
201
+ // Validate inputs
202
+ this._validateTask(task);
203
+
204
+ const executionId = randomUUID();
205
+ const execOptions = { ...this.options, ...options };
206
+
207
+ // Initialize execution context
208
+ const execContext = {
209
+ id: executionId,
210
+ agent: agentName,
211
+ task,
212
+ state: ExecutionState.PENDING,
213
+ startTime: Date.now(),
214
+ endTime: null,
215
+ retries: 0,
216
+ output: '',
217
+ error: null,
218
+ exitCode: null,
219
+ };
220
+
221
+ this.activeExecutions.set(executionId, execContext);
222
+
223
+ try {
224
+ // Load agent definition
225
+ const agentDef = await this.loadAgent(agentName);
226
+ execContext.agentMetadata = agentDef.metadata;
227
+
228
+ // Build full prompt
229
+ const prompt = await this.buildFullPrompt(agentDef, task, context);
230
+
231
+ // Update state
232
+ execContext.state = ExecutionState.RUNNING;
233
+
234
+ // Execute based on mode
235
+ let result;
236
+ switch (this.options.mode) {
237
+ case ExecutionMode.TASK_API:
238
+ result = await this.executeViaTaskAPI(execContext, agentDef, prompt, context, execOptions);
239
+ break;
240
+
241
+ case ExecutionMode.SUBPROCESS:
242
+ result = await this._executeViaSubprocessInternal(execContext, agentDef, prompt, context, execOptions);
243
+ break;
244
+
245
+ case ExecutionMode.API:
246
+ result = await this.executeViaAPI(execContext, agentDef, prompt, context, execOptions);
247
+ break;
248
+
249
+ default:
250
+ throw new Error(`Unsupported execution mode: ${this.options.mode}`);
251
+ }
252
+
253
+ // Update execution context
254
+ execContext.state = ExecutionState.COMPLETED;
255
+ execContext.endTime = Date.now();
256
+ execContext.output = result.output;
257
+
258
+ this.emit('complete', {
259
+ executionId,
260
+ duration: execContext.endTime - execContext.startTime,
261
+ result,
262
+ });
263
+
264
+ return result;
265
+
266
+ } catch (error) {
267
+ // Check if we should retry
268
+ if (execContext.retries < execOptions.maxRetries && this._isRetryableError(error)) {
269
+ execContext.retries++;
270
+ const delay = Math.pow(2, execContext.retries) * 1000; // Exponential backoff
271
+
272
+ this.emit('retry', {
273
+ executionId,
274
+ attempt: execContext.retries,
275
+ maxRetries: execOptions.maxRetries,
276
+ delay,
277
+ error: error.message,
278
+ });
279
+
280
+ await this._sleep(delay);
281
+
282
+ // Retry execution
283
+ return this.execute(agentName, task, context, options);
284
+ }
285
+
286
+ // Update execution context with error
287
+ execContext.state = ExecutionState.FAILED;
288
+ execContext.endTime = Date.now();
289
+ execContext.error = error.message;
290
+
291
+ this.emit('error', {
292
+ executionId,
293
+ error: error.message,
294
+ duration: execContext.endTime - execContext.startTime,
295
+ });
296
+
297
+ throw error;
298
+
299
+ } finally {
300
+ // Clean up after delay to allow status queries
301
+ setTimeout(() => {
302
+ this.activeExecutions.delete(executionId);
303
+ }, 60000); // Keep for 1 minute
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Load agent definition from .claude/agents/
309
+ *
310
+ * @param {string} agentName - Name of the agent
311
+ * @returns {Promise<Object>} Agent definition with metadata and instructions
312
+ */
313
+ async loadAgent(agentName) {
314
+ return loadAgentDefinition(agentName, this.options.projectRoot);
315
+ }
316
+
317
+ /**
318
+ * Build full prompt from agent definition, task, and context
319
+ *
320
+ * @param {Object} agentDef - Agent definition
321
+ * @param {string} task - Task description
322
+ * @param {Object} context - Additional context
323
+ * @returns {Promise<string>} Complete prompt
324
+ */
325
+ async buildFullPrompt(agentDef, task, context = {}) {
326
+ let prompt = `# Task for ${agentDef.metadata.name} Agent
327
+
328
+ ${task}
329
+
330
+ `;
331
+
332
+ // Add context sections
333
+ if (context.files && Array.isArray(context.files)) {
334
+ prompt += `## Relevant Files
335
+
336
+ ${context.files.map(f => `- ${f}`).join('\n')}
337
+
338
+ `;
339
+ }
340
+
341
+ if (context.requirements) {
342
+ prompt += `## Requirements
343
+
344
+ ${context.requirements}
345
+
346
+ `;
347
+ }
348
+
349
+ if (context.variables && Object.keys(context.variables).length > 0) {
350
+ prompt += `## Variables
351
+
352
+ ${Object.entries(context.variables).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
353
+
354
+ `;
355
+ }
356
+
357
+ // Add agent instructions
358
+ prompt += `---
359
+
360
+ # Agent Instructions
361
+
362
+ ${agentDef.instructions}
363
+ `;
364
+
365
+ return prompt;
366
+ }
367
+
368
+ /**
369
+ * Execute via Claude Code Task API
370
+ *
371
+ * @param {Object} execContext - Execution context
372
+ * @param {Object} agentDef - Agent definition
373
+ * @param {string} prompt - Full prompt
374
+ * @param {Object} context - Additional context
375
+ * @param {Object} options - Execution options
376
+ * @returns {Promise<Object>} Execution result
377
+ */
378
+ async executeViaTaskAPI(execContext, agentDef, prompt, context, options) {
379
+ // Task API requires the Task function to be available in the environment
380
+ // This is typically only available when running inside Claude Code slash commands
381
+
382
+ if (typeof Task === 'undefined') {
383
+ throw new Error(
384
+ 'Task API is not available. ' +
385
+ 'This execution mode only works inside Claude Code slash commands. ' +
386
+ 'Use subprocess or api mode instead.'
387
+ );
388
+ }
389
+
390
+ try {
391
+ const result = await Task(prompt, {
392
+ timeout: options.timeout,
393
+ });
394
+
395
+ return {
396
+ success: true,
397
+ output: result,
398
+ mode: ExecutionMode.TASK_API,
399
+ };
400
+
401
+ } catch (error) {
402
+ throw new Error(`Task API execution failed: ${error.message}`);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Execute via Anthropic API
408
+ *
409
+ * @param {Object} execContext - Execution context
410
+ * @param {Object} agentDef - Agent definition
411
+ * @param {string} prompt - Full prompt
412
+ * @param {Object} context - Additional context
413
+ * @param {Object} options - Execution options
414
+ * @returns {Promise<Object>} Execution result
415
+ */
416
+ async executeViaAPI(execContext, agentDef, prompt, context, options) {
417
+ // Direct API execution not yet implemented
418
+ // This would require ANTHROPIC_API_KEY and direct fetch calls
419
+
420
+ throw new Error(
421
+ 'Direct API execution not yet implemented. ' +
422
+ 'Use task-api or subprocess mode instead. ' +
423
+ 'To implement API mode, add Anthropic API key and implement API client.'
424
+ );
425
+
426
+ // Future implementation:
427
+ // const apiKey = process.env.ANTHROPIC_API_KEY;
428
+ // if (!apiKey) {
429
+ // throw new Error('ANTHROPIC_API_KEY environment variable not set');
430
+ // }
431
+ //
432
+ // const response = await fetch('https://api.anthropic.com/v1/messages', {
433
+ // method: 'POST',
434
+ // headers: {
435
+ // 'Content-Type': 'application/json',
436
+ // 'x-api-key': apiKey,
437
+ // 'anthropic-version': '2023-06-01',
438
+ // },
439
+ // body: JSON.stringify({
440
+ // model: 'claude-sonnet-4',
441
+ // max_tokens: 4096,
442
+ // messages: [{ role: 'user', content: prompt }],
443
+ // }),
444
+ // });
445
+ //
446
+ // if (!response.ok) {
447
+ // throw new Error(`API error: ${response.status} ${response.statusText}`);
448
+ // }
449
+ //
450
+ // const result = await response.json();
451
+ // return {
452
+ // success: true,
453
+ // output: result.content[0].text,
454
+ // mode: ExecutionMode.API,
455
+ // };
456
+ }
457
+
458
+ /**
459
+ * Internal method for subprocess execution (used by execute() and backward compat)
460
+ *
461
+ * @private
462
+ */
463
+ async _executeViaSubprocessInternal(execContext, agentDef, prompt, context, options) {
464
+ return new Promise((resolve, reject) => {
465
+ const args = ['-p', prompt];
466
+
467
+ if (options.timeout) {
468
+ args.push('--timeout', options.timeout.toString());
469
+ }
470
+
471
+ const proc = spawn(options.claudeCommand, args, {
472
+ cwd: options.workingDir,
473
+ env: {
474
+ ...process.env,
475
+ CLAUDE_NON_INTERACTIVE: '1', // Disable interactive prompts
476
+ },
477
+ });
478
+
479
+ execContext.process = proc;
480
+ execContext.parser = new OutputParser();
481
+
482
+ let outputSize = 0;
483
+ let outputTruncated = false;
484
+
485
+ // Setup timeout
486
+ const timeoutHandle = setTimeout(() => {
487
+ proc.kill('SIGTERM');
488
+ reject(new Error(`Execution timeout after ${options.timeout}ms`));
489
+ }, options.timeout);
490
+
491
+ // Capture stdout
492
+ proc.stdout.on('data', (data) => {
493
+ const chunk = data.toString();
494
+
495
+ // Check output size limit
496
+ if (outputSize < options.maxOutputSize) {
497
+ const remainingSpace = options.maxOutputSize - outputSize;
498
+ const chunkToAdd = chunk.length <= remainingSpace
499
+ ? chunk
500
+ : chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
501
+
502
+ execContext.output += chunkToAdd;
503
+ outputSize += chunk.length;
504
+
505
+ if (chunk.length > remainingSpace) {
506
+ outputTruncated = true;
507
+ }
508
+
509
+ // Emit chunk if streaming enabled
510
+ if (options.streamOutput) {
511
+ this.emit('chunk', { executionId: execContext.id, chunk: chunkToAdd, text: chunkToAdd });
512
+ }
513
+
514
+ // Parse for structured events
515
+ const events = execContext.parser.parse(chunk);
516
+
517
+ if (events.progress) {
518
+ this.emit('progress', {
519
+ executionId: execContext.id,
520
+ percentage: events.progress.percentage,
521
+ message: events.progress.raw,
522
+ timestamp: Date.now()
523
+ });
524
+ }
525
+
526
+ if (events.question) {
527
+ this.emit('question', {
528
+ executionId: execContext.id,
529
+ text: events.question.text,
530
+ timestamp: events.question.timestamp
531
+ });
532
+ }
533
+
534
+ if (events.error) {
535
+ this.emit('error', {
536
+ executionId: execContext.id,
537
+ message: events.error.message,
538
+ timestamp: events.error.timestamp
539
+ });
540
+ }
541
+ }
542
+ });
543
+
544
+ // Capture stderr
545
+ proc.stderr.on('data', (data) => {
546
+ const chunk = data.toString();
547
+ execContext.output += `[ERROR] ${chunk}`;
548
+
549
+ if (options.streamOutput) {
550
+ this.emit('chunk', { executionId: execContext.id, chunk: `[ERROR] ${chunk}`, text: `[ERROR] ${chunk}` });
551
+ }
552
+
553
+ this.emit('error', {
554
+ executionId: execContext.id,
555
+ message: chunk,
556
+ timestamp: Date.now(),
557
+ source: 'stderr'
558
+ });
559
+ });
560
+
561
+ // Handle process exit
562
+ proc.on('close', (code) => {
563
+ clearTimeout(timeoutHandle);
564
+ execContext.exitCode = code;
565
+ execContext.process = null;
566
+
567
+ if (outputTruncated) {
568
+ execContext.output += '\n\n[Note: Output was truncated due to size limit]';
569
+ }
570
+
571
+ if (code === 0) {
572
+ resolve({
573
+ success: true,
574
+ output: execContext.output,
575
+ exitCode: code,
576
+ mode: ExecutionMode.SUBPROCESS,
577
+ truncated: outputTruncated,
578
+ });
579
+ } else {
580
+ reject(new Error(`Claude exited with code ${code}`));
581
+ }
582
+ });
583
+
584
+ // Handle spawn errors
585
+ proc.on('error', (error) => {
586
+ clearTimeout(timeoutHandle);
587
+ reject(new Error(`Failed to spawn claude: ${error.message}`));
588
+ });
589
+ });
590
+ }
591
+
592
+ /**
593
+ * Execute agent via subprocess (backward compatibility method)
594
+ *
595
+ * @param {string} agentName - Name of the agent
596
+ * @param {string} task - Task description
597
+ * @param {Object} options - Execution options
598
+ * @param {string} [options.prompt] - Custom prompt (overrides task)
599
+ * @param {number} [options.timeout] - Execution timeout in ms
600
+ * @param {string} [options.workingDir] - Working directory
601
+ * @returns {Promise<Object>} Execution result
602
+ */
603
+ async executeViaSubprocess(agentName, task, options = {}) {
604
+ const executionId = randomUUID();
605
+ const prompt = options.prompt || task;
606
+ const timeout = options.timeout || this.options.timeout;
607
+ const workingDir = options.workingDir || this.options.workingDir;
608
+
609
+ const execution = {
610
+ id: executionId,
611
+ agent: agentName,
612
+ task,
613
+ state: ExecutionState.PENDING,
614
+ startTime: Date.now(),
615
+ endTime: null,
616
+ process: null,
617
+ parser: new OutputParser(),
618
+ output: '',
619
+ error: '',
620
+ exitCode: null,
621
+ };
622
+
623
+ this.activeExecutions.set(executionId, execution);
624
+
625
+ try {
626
+ execution.state = ExecutionState.RUNNING;
627
+
628
+ // Emit start event
629
+ this.emit('start', {
630
+ executionId,
631
+ agent: agentName,
632
+ task,
633
+ timestamp: execution.startTime
634
+ });
635
+
636
+ return await new Promise((resolve, reject) => {
637
+ // Spawn Claude Code CLI
638
+ const args = ['-p', prompt];
639
+ const proc = spawn(this.options.claudeCommand, args, {
640
+ cwd: workingDir,
641
+ env: {
642
+ ...process.env,
643
+ CLAUDE_NON_INTERACTIVE: '1',
644
+ },
645
+ });
646
+
647
+ execution.process = proc;
648
+
649
+ let timeoutHandle = null;
650
+ let outputSize = 0;
651
+ let outputTruncated = false;
652
+
653
+ // Setup timeout
654
+ if (timeout) {
655
+ timeoutHandle = setTimeout(() => {
656
+ proc.kill('SIGTERM');
657
+ reject(new Error(`Execution timeout after ${timeout}ms`));
658
+ }, timeout);
659
+ }
660
+
661
+ // Handle stdout - emit chunks and parse for events
662
+ proc.stdout.on('data', (data) => {
663
+ const chunk = data.toString();
664
+
665
+ // Check output size limit
666
+ if (outputSize < this.options.maxOutputSize) {
667
+ const remainingSpace = this.options.maxOutputSize - outputSize;
668
+ const chunkToStore = chunk.length <= remainingSpace
669
+ ? chunk
670
+ : chunk.substring(0, remainingSpace);
671
+
672
+ execution.output += chunkToStore;
673
+ outputSize += chunk.length;
674
+
675
+ if (chunk.length > remainingSpace) {
676
+ outputTruncated = true;
677
+ execution.output += '\n[Output truncated - limit reached]';
678
+ }
679
+ }
680
+
681
+ // Emit raw chunk event
682
+ this.emit('chunk', {
683
+ executionId,
684
+ text: chunk,
685
+ timestamp: Date.now()
686
+ });
687
+
688
+ // Parse for structured events
689
+ const events = execution.parser.parse(chunk);
690
+
691
+ if (events.progress) {
692
+ this.emit('progress', {
693
+ executionId,
694
+ percentage: events.progress.percentage,
695
+ message: events.progress.raw,
696
+ timestamp: Date.now()
697
+ });
698
+ }
699
+
700
+ if (events.question) {
701
+ this.emit('question', {
702
+ executionId,
703
+ text: events.question.text,
704
+ timestamp: events.question.timestamp
705
+ });
706
+ }
707
+
708
+ if (events.error) {
709
+ this.emit('error', {
710
+ executionId,
711
+ message: events.error.message,
712
+ timestamp: events.error.timestamp
713
+ });
714
+ }
715
+ });
716
+
717
+ // Handle stderr - emit as errors
718
+ proc.stderr.on('data', (data) => {
719
+ const chunk = data.toString();
720
+ execution.error += chunk;
721
+
722
+ this.emit('error', {
723
+ executionId,
724
+ message: chunk,
725
+ timestamp: Date.now(),
726
+ source: 'stderr'
727
+ });
728
+ });
729
+
730
+ // Handle process exit
731
+ proc.on('close', (code) => {
732
+ if (timeoutHandle) {
733
+ clearTimeout(timeoutHandle);
734
+ }
735
+
736
+ execution.exitCode = code;
737
+ execution.endTime = Date.now();
738
+ execution.process = null;
739
+
740
+ const duration = execution.endTime - execution.startTime;
741
+
742
+ if (code === 0) {
743
+ execution.state = ExecutionState.COMPLETED;
744
+
745
+ // Emit complete event
746
+ this.emit('complete', {
747
+ executionId,
748
+ output: execution.output,
749
+ duration,
750
+ timestamp: execution.endTime
751
+ });
752
+
753
+ resolve({
754
+ executionId,
755
+ state: ExecutionState.COMPLETED,
756
+ output: execution.output,
757
+ duration,
758
+ exitCode: code,
759
+ truncated: outputTruncated
760
+ });
761
+ } else {
762
+ execution.state = ExecutionState.FAILED;
763
+
764
+ reject(new Error(
765
+ `Agent execution failed with exit code ${code}: ${execution.error || 'No error details'}`
766
+ ));
767
+ }
768
+
769
+ // Cleanup execution from active map
770
+ this.activeExecutions.delete(executionId);
771
+ });
772
+
773
+ proc.on('error', (error) => {
774
+ if (timeoutHandle) {
775
+ clearTimeout(timeoutHandle);
776
+ }
777
+
778
+ execution.state = ExecutionState.FAILED;
779
+ execution.endTime = Date.now();
780
+
781
+ this.emit('error', {
782
+ executionId,
783
+ message: error.message,
784
+ timestamp: Date.now(),
785
+ source: 'spawn'
786
+ });
787
+
788
+ reject(new Error(`Failed to spawn Claude Code: ${error.message}`));
789
+
790
+ // Cleanup execution from active map
791
+ this.activeExecutions.delete(executionId);
792
+ });
793
+ });
794
+
795
+ } catch (error) {
796
+ execution.state = ExecutionState.FAILED;
797
+ execution.endTime = Date.now();
798
+ execution.error = error.message;
799
+
800
+ this.activeExecutions.delete(executionId);
801
+ throw error;
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Execute with streaming callbacks
807
+ *
808
+ * @param {string} agentName - Name of the agent
809
+ * @param {string} task - Task description
810
+ * @param {Object} callbacks - Callback functions
811
+ * @param {Function} [callbacks.onChunk] - Called for each output chunk
812
+ * @param {Function} [callbacks.onProgress] - Called when progress detected
813
+ * @param {Function} [callbacks.onQuestion] - Called when question detected
814
+ * @param {Function} [callbacks.onError] - Called when error detected
815
+ * @param {Object} options - Execution options
816
+ * @returns {Promise<Object>} Execution result
817
+ */
818
+ async executeWithStreaming(agentName, task, callbacks = {}, options = {}) {
819
+ const { onChunk, onProgress, onQuestion, onError } = callbacks;
820
+
821
+ // Register event listeners
822
+ const listeners = {};
823
+
824
+ if (onChunk) {
825
+ listeners.chunk = (event) => {
826
+ if (event.executionId) {
827
+ onChunk(event.text);
828
+ }
829
+ };
830
+ this.on('chunk', listeners.chunk);
831
+ }
832
+
833
+ if (onProgress) {
834
+ listeners.progress = (event) => {
835
+ if (event.executionId) {
836
+ onProgress(event.percentage, event.message);
837
+ }
838
+ };
839
+ this.on('progress', listeners.progress);
840
+ }
841
+
842
+ if (onQuestion) {
843
+ listeners.question = (event) => {
844
+ if (event.executionId) {
845
+ onQuestion(event.text);
846
+ }
847
+ };
848
+ this.on('question', listeners.question);
849
+ }
850
+
851
+ if (onError) {
852
+ listeners.error = (event) => {
853
+ if (event.executionId) {
854
+ onError(event.message, event.source);
855
+ }
856
+ };
857
+ this.on('error', listeners.error);
858
+ }
859
+
860
+ try {
861
+ // Execute with subprocess
862
+ const result = await this.executeViaSubprocess(agentName, task, options);
863
+ return result;
864
+ } finally {
865
+ // Cleanup listeners
866
+ for (const [event, listener] of Object.entries(listeners)) {
867
+ this.removeListener(event, listener);
868
+ }
869
+ }
870
+ }
871
+
872
+ /**
873
+ * Cancel an active execution
874
+ *
875
+ * @param {string} executionId - Execution ID to cancel
876
+ * @returns {boolean} True if cancelled, false if not found
877
+ */
878
+ cancel(executionId) {
879
+ const execution = this.activeExecutions.get(executionId);
880
+ if (!execution) {
881
+ return false;
882
+ }
883
+
884
+ if (execution.process) {
885
+ execution.state = ExecutionState.CANCELLED;
886
+ execution.process.kill('SIGTERM');
887
+
888
+ // Force kill after timeout
889
+ setTimeout(() => {
890
+ if (execution.process && !execution.process.killed) {
891
+ execution.process.kill('SIGKILL');
892
+ }
893
+ }, 5000);
894
+
895
+ return true;
896
+ }
897
+
898
+ return false;
899
+ }
900
+
901
+ /**
902
+ * Get execution status
903
+ *
904
+ * @param {string} executionId - Execution ID
905
+ * @returns {Object|null} Execution status or null if not found
906
+ */
907
+ getExecutionStatus(executionId) {
908
+ const execution = this.activeExecutions.get(executionId);
909
+ if (!execution) {
910
+ return null;
911
+ }
912
+
913
+ const duration = execution.endTime
914
+ ? execution.endTime - execution.startTime
915
+ : Date.now() - execution.startTime;
916
+
917
+ return {
918
+ id: execution.id,
919
+ agent: execution.agent,
920
+ task: execution.task,
921
+ state: execution.state,
922
+ startTime: execution.startTime,
923
+ endTime: execution.endTime,
924
+ duration,
925
+ exitCode: execution.exitCode,
926
+ outputLength: execution.output.length,
927
+ errorLength: execution.error.length
928
+ };
929
+ }
930
+
931
+ /**
932
+ * List all active executions
933
+ *
934
+ * @returns {Object[]} Array of execution statuses
935
+ */
936
+ listActiveExecutions() {
937
+ return Array.from(this.activeExecutions.keys()).map(id =>
938
+ this.getExecutionStatus(id)
939
+ );
940
+ }
941
+
942
+ /**
943
+ * Validate task input
944
+ *
945
+ * @private
946
+ * @param {string} task - Task description
947
+ * @throws {Error} If task is invalid
948
+ */
949
+ _validateTask(task) {
950
+ if (typeof task !== 'string') {
951
+ throw new Error('Task must be a string');
952
+ }
953
+
954
+ if (task.length === 0) {
955
+ throw new Error('Task cannot be empty');
956
+ }
957
+
958
+ if (task.length > MAX_TASK_LENGTH) {
959
+ throw new Error(
960
+ `Task exceeds maximum length of ${MAX_TASK_LENGTH / 1024}KB`
961
+ );
962
+ }
963
+
964
+ // Check for dangerous shell metacharacters
965
+ const dangerousPatterns = [
966
+ /\$\(/, // Command substitution
967
+ /`/, // Backtick command substitution
968
+ ];
969
+
970
+ for (const pattern of dangerousPatterns) {
971
+ if (pattern.test(task)) {
972
+ throw new Error(
973
+ 'Task contains potentially dangerous shell metacharacters'
974
+ );
975
+ }
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Check if error is retryable
981
+ *
982
+ * @private
983
+ * @param {Error} error - Error to check
984
+ * @returns {boolean} True if retryable
985
+ */
986
+ _isRetryableError(error) {
987
+ const retryablePatterns = [
988
+ /timeout/i,
989
+ /ECONNREFUSED/i,
990
+ /ETIMEDOUT/i,
991
+ /rate limit/i,
992
+ /429/,
993
+ ];
994
+
995
+ return retryablePatterns.some(pattern =>
996
+ pattern.test(error.message)
997
+ );
998
+ }
999
+
1000
+ /**
1001
+ * Sleep for specified duration
1002
+ *
1003
+ * @private
1004
+ * @param {number} ms - Duration in milliseconds
1005
+ * @returns {Promise<void>}
1006
+ */
1007
+ _sleep(ms) {
1008
+ return new Promise(resolve => setTimeout(resolve, ms));
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Create a Claude executor instance
1014
+ *
1015
+ * @param {Object} options - Executor options
1016
+ * @returns {ClaudeExecutor} Executor instance
1017
+ */
1018
+ export function createClaudeExecutor(options = {}) {
1019
+ return new ClaudeExecutor(options);
1020
+ }
1021
+
1022
+ /**
1023
+ * Create a Claude executor with specified mode (convenience method)
1024
+ *
1025
+ * @param {Object} options - Executor options
1026
+ * @returns {ClaudeExecutor} Executor instance
1027
+ */
1028
+ export function createExecutor(options = {}) {
1029
+ return new ClaudeExecutor(options);
1030
+ }
1031
+
1032
+ /**
1033
+ * Execute a one-off agent task (convenience method)
1034
+ *
1035
+ * @param {string} agentName - Agent name
1036
+ * @param {string} task - Task description
1037
+ * @param {Object} context - Execution context
1038
+ * @param {Object} options - Executor options
1039
+ * @returns {Promise<Object>} Execution result
1040
+ */
1041
+ export async function executeAgent(agentName, task, context = {}, options = {}) {
1042
+ const executor = new ClaudeExecutor(options);
1043
+ return executor.execute(agentName, task, context, options);
1044
+ }
1045
+
1046
+ export default ClaudeExecutor;