@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,440 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { EventEmitter } from 'events';
5
+
6
+ /**
7
+ * Agent Execution Runtime
8
+ *
9
+ * Handles the actual execution of AI agents:
10
+ * - Agent invocation via subprocess or API
11
+ * - Context passing between jobs
12
+ * - Streaming logs and progress
13
+ * - Cancellation and cleanup
14
+ * - Result collection
15
+ */
16
+
17
+ /**
18
+ * Agent Executor
19
+ *
20
+ * Executes agents and manages their lifecycle
21
+ */
22
+ export class AgentExecutor extends EventEmitter {
23
+ constructor(options = {}) {
24
+ super();
25
+
26
+ this.options = {
27
+ agentsDir: options.agentsDir || '.claude/agents',
28
+ tempDir: options.tempDir || '.agentful/pipelines/temp',
29
+ claudeCommand: options.claudeCommand || 'claude',
30
+ streamLogs: options.streamLogs !== false,
31
+ ...options
32
+ };
33
+
34
+ this.activeExecutions = new Map(); // executionId -> ExecutionContext
35
+ }
36
+
37
+ /**
38
+ * Execute an agent
39
+ *
40
+ * @param {Object} jobDef - Job definition from pipeline
41
+ * @param {Object} context - Execution context (inputs, variables)
42
+ * @param {Object} options - Execution options
43
+ * @returns {Promise<Object>} Execution result with output
44
+ */
45
+ async execute(jobDef, context, options = {}) {
46
+ const executionId = this._generateExecutionId(jobDef.id);
47
+
48
+ // Build execution context
49
+ const execContext = {
50
+ executionId,
51
+ jobId: jobDef.id,
52
+ agent: jobDef.agent,
53
+ startTime: Date.now(),
54
+ cancelled: false,
55
+ process: null,
56
+ output: '',
57
+ error: '',
58
+ exitCode: null
59
+ };
60
+
61
+ this.activeExecutions.set(executionId, execContext);
62
+
63
+ try {
64
+ // Prepare agent invocation
65
+ const agentFile = await this._resolveAgentFile(jobDef.agent);
66
+ const prompt = await this._buildAgentPrompt(jobDef, context);
67
+
68
+ // Determine execution method
69
+ const method = jobDef.execution?.method || 'subprocess';
70
+
71
+ let result;
72
+ if (method === 'subprocess') {
73
+ result = await this._executeViaSubprocess(execContext, agentFile, prompt, context, options);
74
+ } else if (method === 'api') {
75
+ result = await this._executeViaAPI(execContext, jobDef, prompt, context, options);
76
+ } else {
77
+ throw new Error(`Unknown execution method: ${method}`);
78
+ }
79
+
80
+ return result;
81
+
82
+ } finally {
83
+ this.activeExecutions.delete(executionId);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Cancel an active execution
89
+ *
90
+ * @param {string} executionId - Execution ID to cancel
91
+ * @returns {Promise<boolean>} True if cancelled, false if not found
92
+ */
93
+ async cancel(executionId) {
94
+ const execContext = this.activeExecutions.get(executionId);
95
+ if (!execContext) return false;
96
+
97
+ execContext.cancelled = true;
98
+
99
+ if (execContext.process) {
100
+ // Kill the subprocess
101
+ execContext.process.kill('SIGTERM');
102
+
103
+ // Force kill after timeout
104
+ setTimeout(() => {
105
+ if (execContext.process && !execContext.process.killed) {
106
+ execContext.process.kill('SIGKILL');
107
+ }
108
+ }, 5000);
109
+ }
110
+
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Get execution status
116
+ *
117
+ * @param {string} executionId - Execution ID
118
+ * @returns {Object|null} Execution context or null if not found
119
+ */
120
+ getExecutionStatus(executionId) {
121
+ return this.activeExecutions.get(executionId) || null;
122
+ }
123
+
124
+ /**
125
+ * Internal: Execute via subprocess
126
+ *
127
+ * @private
128
+ */
129
+ async _executeViaSubprocess(execContext, agentFile, prompt, context, options) {
130
+ // Write context to temp file
131
+ const contextFile = await this._writeContextFile(execContext.executionId, context);
132
+ const outputFile = path.join(this.options.tempDir, `${execContext.executionId}-output.json`);
133
+
134
+ // Prepare agent prompt with context reference
135
+ const fullPrompt = this._injectContextReference(prompt, contextFile, outputFile);
136
+
137
+ // Read agent definition file
138
+ const agentContent = await fs.readFile(agentFile, 'utf-8');
139
+
140
+ // Combine agent definition with prompt
141
+ const combinedPrompt = `${agentContent}\n\n---\n\n${fullPrompt}`;
142
+
143
+ return new Promise((resolve, reject) => {
144
+ // Spawn Claude Code with -p flag
145
+ const args = ['-p', combinedPrompt];
146
+
147
+ if (options.timeout) {
148
+ args.push('--timeout', options.timeout.toString());
149
+ }
150
+
151
+ const proc = spawn(this.options.claudeCommand, args, {
152
+ cwd: process.cwd(),
153
+ env: { ...process.env }
154
+ });
155
+
156
+ execContext.process = proc;
157
+
158
+ // Setup timeout
159
+ let timeoutHandle;
160
+ if (options.timeout) {
161
+ timeoutHandle = setTimeout(() => {
162
+ if (!execContext.cancelled) {
163
+ proc.kill('SIGTERM');
164
+ reject(new Error(`Agent execution timed out after ${options.timeout}ms`));
165
+ }
166
+ }, options.timeout);
167
+ }
168
+
169
+ // Capture stdout
170
+ proc.stdout.on('data', (data) => {
171
+ const text = data.toString();
172
+ execContext.output += text;
173
+
174
+ if (this.options.streamLogs && options.onLog) {
175
+ options.onLog(text);
176
+ }
177
+
178
+ // Try to extract progress updates
179
+ this._extractProgress(text, options.onProgress);
180
+ });
181
+
182
+ // Capture stderr
183
+ proc.stderr.on('data', (data) => {
184
+ const text = data.toString();
185
+ execContext.error += text;
186
+
187
+ if (this.options.streamLogs && options.onLog) {
188
+ options.onLog(`[ERROR] ${text}`);
189
+ }
190
+ });
191
+
192
+ // Handle process exit
193
+ proc.on('close', async (code) => {
194
+ if (timeoutHandle) clearTimeout(timeoutHandle);
195
+
196
+ execContext.exitCode = code;
197
+ execContext.process = null;
198
+
199
+ try {
200
+ // Read output file if exists
201
+ let output = null;
202
+ try {
203
+ const outputContent = await fs.readFile(outputFile, 'utf-8');
204
+ output = JSON.parse(outputContent);
205
+ } catch (error) {
206
+ // No output file or invalid JSON - use stdout
207
+ output = { stdout: execContext.output };
208
+ }
209
+
210
+ // Cleanup temp files
211
+ await this._cleanupTempFiles(execContext.executionId);
212
+
213
+ if (execContext.cancelled) {
214
+ reject(new Error('Execution was cancelled'));
215
+ } else if (code === 0) {
216
+ resolve({
217
+ success: true,
218
+ output,
219
+ duration: Date.now() - execContext.startTime
220
+ });
221
+ } else {
222
+ reject(new Error(`Agent failed with exit code ${code}: ${execContext.error}`));
223
+ }
224
+ } catch (error) {
225
+ reject(error);
226
+ }
227
+ });
228
+
229
+ proc.on('error', (error) => {
230
+ if (timeoutHandle) clearTimeout(timeoutHandle);
231
+ reject(new Error(`Failed to spawn agent: ${error.message}`));
232
+ });
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Internal: Execute via API
238
+ *
239
+ * @private
240
+ */
241
+ async _executeViaAPI(execContext, jobDef, prompt, context, options) {
242
+ // This would integrate with Claude API directly
243
+ // For now, throw error indicating this needs to be implemented
244
+
245
+ throw new Error('API execution not yet implemented. Use subprocess method.');
246
+
247
+ // Future implementation would look like:
248
+ // const response = await fetch('https://api.anthropic.com/v1/messages', {
249
+ // method: 'POST',
250
+ // headers: { ... },
251
+ // body: JSON.stringify({ ... })
252
+ // });
253
+ }
254
+
255
+ /**
256
+ * Internal: Resolve agent file path
257
+ *
258
+ * @private
259
+ */
260
+ async _resolveAgentFile(agentName) {
261
+ // Try with .md extension
262
+ let agentFile = path.join(this.options.agentsDir, `${agentName}.md`);
263
+
264
+ try {
265
+ await fs.access(agentFile);
266
+ return agentFile;
267
+ } catch (error) {
268
+ // Try without extension
269
+ agentFile = path.join(this.options.agentsDir, agentName);
270
+
271
+ try {
272
+ await fs.access(agentFile);
273
+ return agentFile;
274
+ } catch (error) {
275
+ throw new Error(`Agent not found: ${agentName}`);
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Internal: Build agent prompt from job definition
282
+ *
283
+ * @private
284
+ */
285
+ async _buildAgentPrompt(jobDef, context) {
286
+ let prompt = jobDef.prompt || '';
287
+
288
+ // Replace variables in prompt
289
+ prompt = this._interpolateVariables(prompt, context);
290
+
291
+ // Add task description
292
+ if (jobDef.task) {
293
+ prompt = `${jobDef.task}\n\n${prompt}`;
294
+ }
295
+
296
+ return prompt;
297
+ }
298
+
299
+ /**
300
+ * Internal: Interpolate variables in text
301
+ *
302
+ * @private
303
+ */
304
+ _interpolateVariables(text, context) {
305
+ return text.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
306
+ const value = this._getNestedValue(context, path);
307
+ return value !== undefined ? value : match;
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Internal: Get nested value from object
313
+ *
314
+ * @private
315
+ */
316
+ _getNestedValue(obj, path) {
317
+ return path.split('.').reduce((current, prop) => {
318
+ return current && current[prop] !== undefined ? current[prop] : undefined;
319
+ }, obj);
320
+ }
321
+
322
+ /**
323
+ * Internal: Inject context reference into prompt
324
+ *
325
+ * @private
326
+ */
327
+ _injectContextReference(prompt, contextFile, outputFile) {
328
+ return `
329
+ # Pipeline Job Context
330
+
331
+ Input context is available at: ${contextFile}
332
+ Write your output to: ${outputFile}
333
+
334
+ Output format (JSON):
335
+ \`\`\`json
336
+ {
337
+ "success": true,
338
+ "data": { ... },
339
+ "message": "Job completed successfully"
340
+ }
341
+ \`\`\`
342
+
343
+ ---
344
+
345
+ # Task
346
+
347
+ ${prompt}
348
+ `;
349
+ }
350
+
351
+ /**
352
+ * Internal: Write context to temp file
353
+ *
354
+ * @private
355
+ */
356
+ async _writeContextFile(executionId, context) {
357
+ const contextFile = path.join(this.options.tempDir, `${executionId}-context.json`);
358
+ await fs.mkdir(this.options.tempDir, { recursive: true });
359
+ await fs.writeFile(contextFile, JSON.stringify(context, null, 2));
360
+ return contextFile;
361
+ }
362
+
363
+ /**
364
+ * Internal: Write prompt to temp file
365
+ *
366
+ * @private
367
+ */
368
+ async _writePromptFile(executionId, prompt) {
369
+ const promptFile = path.join(this.options.tempDir, `${executionId}-prompt.txt`);
370
+ await fs.mkdir(this.options.tempDir, { recursive: true });
371
+ await fs.writeFile(promptFile, prompt);
372
+ return promptFile;
373
+ }
374
+
375
+
376
+ /**
377
+ * Internal: Cleanup temp files
378
+ *
379
+ * @private
380
+ */
381
+ async _cleanupTempFiles(executionId) {
382
+ const files = [
383
+ path.join(this.options.tempDir, `${executionId}-context.json`),
384
+ path.join(this.options.tempDir, `${executionId}-output.json`),
385
+ path.join(this.options.tempDir, `${executionId}-prompt.txt`)
386
+ ];
387
+
388
+ for (const file of files) {
389
+ try {
390
+ await fs.unlink(file);
391
+ } catch (error) {
392
+ // Ignore errors - file might not exist
393
+ }
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Internal: Extract progress from output
399
+ *
400
+ * @private
401
+ */
402
+ _extractProgress(text, onProgress) {
403
+ if (!onProgress) return;
404
+
405
+ // Look for progress markers: [PROGRESS: 45%] or similar
406
+ const progressMatch = text.match(/\[PROGRESS:\s*(\d+)%\]/i);
407
+ if (progressMatch) {
408
+ const progress = parseInt(progressMatch[1], 10);
409
+ onProgress(progress);
410
+ return;
411
+ }
412
+
413
+ // Look for task completion markers
414
+ if (text.includes('Task completed') || text.includes('✓')) {
415
+ onProgress(100);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Internal: Generate execution ID
421
+ *
422
+ * @private
423
+ */
424
+ _generateExecutionId(jobId) {
425
+ const timestamp = Date.now();
426
+ const random = Math.random().toString(36).substring(2, 9);
427
+ return `${jobId}-${timestamp}-${random}`;
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Agent Executor Factory
433
+ *
434
+ * Creates agent executor with custom configuration
435
+ */
436
+ export function createAgentExecutor(options = {}) {
437
+ return new AgentExecutor(options);
438
+ }
439
+
440
+ export default AgentExecutor;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pipeline Orchestration System
3
+ *
4
+ * Entry point for agentful pipeline orchestration
5
+ */
6
+
7
+ export { PipelineEngine, JobStatus, PipelineStatus } from './engine.js';
8
+ export { AgentExecutor, createAgentExecutor } from './executor.js';
9
+ export {
10
+ GitHubActionsAdapter,
11
+ GitLabCIAdapter,
12
+ JenkinsAdapter,
13
+ WebhookHandler
14
+ } from './integrations.js';
15
+ export { pipelineSchema, validatePipeline } from './schemas.js';
16
+
17
+ // Re-export for convenience
18
+ export * from './engine.js';
19
+ export * from './executor.js';
20
+ export * from './integrations.js';
21
+ export * from './schemas.js';
22
+
23
+ export default {
24
+ PipelineEngine,
25
+ AgentExecutor,
26
+ createAgentExecutor,
27
+ GitHubActionsAdapter,
28
+ GitLabCIAdapter,
29
+ JenkinsAdapter,
30
+ WebhookHandler,
31
+ pipelineSchema,
32
+ validatePipeline
33
+ };