@itz4blitz/agentful 1.2.0 → 1.3.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 (59) hide show
  1. package/README.md +28 -1
  2. package/bin/cli.js +11 -1055
  3. package/bin/hooks/block-file-creation.js +271 -0
  4. package/bin/hooks/product-spec-watcher.js +151 -0
  5. package/lib/index.js +0 -11
  6. package/lib/init.js +2 -21
  7. package/lib/parallel-execution.js +235 -0
  8. package/lib/presets.js +26 -4
  9. package/package.json +4 -7
  10. package/template/.claude/agents/architect.md +2 -2
  11. package/template/.claude/agents/backend.md +17 -30
  12. package/template/.claude/agents/frontend.md +17 -39
  13. package/template/.claude/agents/orchestrator.md +63 -4
  14. package/template/.claude/agents/product-analyzer.md +1 -1
  15. package/template/.claude/agents/tester.md +16 -29
  16. package/template/.claude/commands/agentful-generate.md +221 -14
  17. package/template/.claude/commands/agentful-init.md +621 -0
  18. package/template/.claude/commands/agentful-product.md +1 -1
  19. package/template/.claude/commands/agentful-start.md +99 -1
  20. package/template/.claude/product/EXAMPLES.md +2 -2
  21. package/template/.claude/product/index.md +1 -1
  22. package/template/.claude/settings.json +22 -0
  23. package/template/.claude/skills/research/SKILL.md +432 -0
  24. package/template/CLAUDE.md +5 -6
  25. package/template/bin/hooks/architect-drift-detector.js +242 -0
  26. package/template/bin/hooks/product-spec-watcher.js +151 -0
  27. package/version.json +1 -1
  28. package/bin/hooks/post-agent.js +0 -101
  29. package/bin/hooks/post-feature.js +0 -227
  30. package/bin/hooks/pre-agent.js +0 -118
  31. package/bin/hooks/pre-feature.js +0 -138
  32. package/lib/VALIDATION_README.md +0 -455
  33. package/lib/ci/claude-action-integration.js +0 -641
  34. package/lib/ci/index.js +0 -10
  35. package/lib/core/analyzer.js +0 -497
  36. package/lib/core/cli.js +0 -141
  37. package/lib/core/detectors/conventions.js +0 -342
  38. package/lib/core/detectors/framework.js +0 -276
  39. package/lib/core/detectors/index.js +0 -15
  40. package/lib/core/detectors/language.js +0 -199
  41. package/lib/core/detectors/patterns.js +0 -356
  42. package/lib/core/generator.js +0 -626
  43. package/lib/core/index.js +0 -9
  44. package/lib/core/output-parser.js +0 -458
  45. package/lib/core/storage.js +0 -515
  46. package/lib/core/templates.js +0 -556
  47. package/lib/pipeline/cli.js +0 -423
  48. package/lib/pipeline/engine.js +0 -928
  49. package/lib/pipeline/executor.js +0 -440
  50. package/lib/pipeline/index.js +0 -33
  51. package/lib/pipeline/integrations.js +0 -559
  52. package/lib/pipeline/schemas.js +0 -288
  53. package/lib/remote/client.js +0 -361
  54. package/lib/server/auth.js +0 -270
  55. package/lib/server/client-example.js +0 -190
  56. package/lib/server/executor.js +0 -477
  57. package/lib/server/index.js +0 -494
  58. package/lib/update-helpers.js +0 -505
  59. package/lib/validation.js +0 -460
@@ -1,477 +0,0 @@
1
- /**
2
- * Agent Execution Logic for Agentful Server
3
- *
4
- * Spawns Claude Code CLI with agent prompts and manages execution state.
5
- *
6
- * @module server/executor
7
- */
8
-
9
- import { spawn } from 'child_process';
10
- import { randomUUID } from 'crypto';
11
- import { loadAgentDefinition } from '../ci/claude-action-integration.js';
12
-
13
- /**
14
- * Execution states
15
- */
16
- export const ExecutionState = {
17
- PENDING: 'pending',
18
- RUNNING: 'running',
19
- COMPLETED: 'completed',
20
- FAILED: 'failed',
21
- };
22
-
23
- /**
24
- * Maximum output size (1MB per execution)
25
- */
26
- const MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
27
-
28
- /**
29
- * Maximum task length (10KB)
30
- */
31
- const MAX_TASK_LENGTH = 10 * 1024;
32
-
33
- /**
34
- * Allowed environment variable whitelist
35
- */
36
- const ALLOWED_ENV_VARS = new Set(['NODE_ENV', 'DEBUG', 'LOG_LEVEL']);
37
-
38
- /**
39
- * Validate agent name to prevent path traversal
40
- * @param {string} agentName - Agent name to validate
41
- * @returns {boolean} True if valid
42
- */
43
- function isValidAgentName(agentName) {
44
- // Only allow alphanumeric, hyphens, and underscores
45
- return /^[a-zA-Z0-9_-]+$/.test(agentName);
46
- }
47
-
48
- /**
49
- * Sanitize task input to prevent command injection
50
- * @param {string} task - Task description
51
- * @returns {Object} Validation result { valid: boolean, error?: string }
52
- */
53
- function validateTask(task) {
54
- if (typeof task !== 'string') {
55
- return { valid: false, error: 'Task must be a string' };
56
- }
57
-
58
- if (task.length > MAX_TASK_LENGTH) {
59
- return {
60
- valid: false,
61
- error: `Task exceeds maximum length of ${MAX_TASK_LENGTH / 1024}KB`
62
- };
63
- }
64
-
65
- // Check for shell metacharacters that could be dangerous
66
- const dangerousPatterns = [
67
- /\$\(/, // Command substitution
68
- /`/, // Backtick command substitution
69
- /\|\|/, // Or operator
70
- /&&/, // And operator
71
- /;/, // Command separator
72
- />/, // Output redirection
73
- /</, // Input redirection
74
- ];
75
-
76
- for (const pattern of dangerousPatterns) {
77
- if (pattern.test(task)) {
78
- return {
79
- valid: false,
80
- error: 'Task contains potentially dangerous shell metacharacters'
81
- };
82
- }
83
- }
84
-
85
- return { valid: true };
86
- }
87
-
88
- /**
89
- * Filter environment variables to whitelist only
90
- * @param {Object} env - Environment variables
91
- * @returns {Object} Filtered environment variables
92
- */
93
- function filterEnvironmentVars(env) {
94
- const filtered = {};
95
- for (const [key, value] of Object.entries(env)) {
96
- if (ALLOWED_ENV_VARS.has(key)) {
97
- filtered[key] = value;
98
- }
99
- }
100
- return filtered;
101
- }
102
-
103
- /**
104
- * In-memory execution store
105
- * In production, use a database or distributed cache
106
- */
107
- const executions = new Map();
108
-
109
- /**
110
- * Build agent prompt from task
111
- * @param {Object} agent - Agent definition
112
- * @param {string} task - Task description
113
- * @returns {string} Formatted prompt
114
- */
115
- function buildAgentPrompt(agent, task) {
116
- return `# Task for ${agent.metadata.name} Agent
117
-
118
- ${task}
119
-
120
- ---
121
-
122
- # Agent Instructions
123
-
124
- ${agent.instructions}
125
- `;
126
- }
127
-
128
- /**
129
- * Execute agent with Claude Code CLI
130
- * @param {string} agentName - Name of the agent
131
- * @param {string} task - Task description
132
- * @param {Object} options - Execution options
133
- * @param {string} [options.projectRoot] - Project root directory
134
- * @param {number} [options.timeout] - Execution timeout in ms
135
- * @param {Object} [options.env] - Additional environment variables
136
- * @param {boolean} [options.async=false] - If true, return immediately with executionId
137
- * @returns {Promise<Object>} Execution result (or just executionId if async=true)
138
- */
139
- export async function executeAgent(agentName, task, options = {}) {
140
- const {
141
- projectRoot = process.cwd(),
142
- timeout = 10 * 60 * 1000, // 10 minutes default
143
- env = {},
144
- async = false, // New option for non-blocking execution
145
- } = options;
146
-
147
- // Validate agent name to prevent path traversal
148
- if (!isValidAgentName(agentName)) {
149
- throw new Error(
150
- `Invalid agent name: "${agentName}". ` +
151
- `Agent names must contain only alphanumeric characters, hyphens, and underscores.`
152
- );
153
- }
154
-
155
- // Validate and sanitize task input
156
- const taskValidation = validateTask(task);
157
- if (!taskValidation.valid) {
158
- throw new Error(`Invalid task: ${taskValidation.error}`);
159
- }
160
-
161
- // Filter environment variables to whitelist
162
- const filteredEnv = filterEnvironmentVars(env);
163
-
164
- const executionId = randomUUID();
165
-
166
- // Initialize execution record
167
- const execution = {
168
- id: executionId,
169
- agent: agentName,
170
- task,
171
- state: ExecutionState.PENDING,
172
- startTime: Date.now(),
173
- endTime: null,
174
- output: '',
175
- error: null,
176
- exitCode: null,
177
- };
178
-
179
- executions.set(executionId, execution);
180
-
181
- // If async mode, start execution in background and return immediately
182
- if (async) {
183
- // Start execution in background (don't await)
184
- runAgentExecution(executionId, agentName, task, {
185
- projectRoot,
186
- timeout,
187
- filteredEnv,
188
- }).catch((error) => {
189
- // Update execution with error if background execution fails
190
- const exec = executions.get(executionId);
191
- if (exec) {
192
- exec.state = ExecutionState.FAILED;
193
- exec.endTime = Date.now();
194
- exec.error = error.message;
195
- exec.exitCode = -1;
196
- }
197
- });
198
-
199
- return {
200
- executionId,
201
- state: ExecutionState.PENDING,
202
- message: 'Execution started in background',
203
- };
204
- }
205
-
206
- // Synchronous mode - wait for completion and return full result
207
- return runAgentExecution(executionId, agentName, task, {
208
- projectRoot,
209
- timeout,
210
- filteredEnv,
211
- });
212
- }
213
-
214
- /**
215
- * Internal function to run agent execution
216
- * @param {string} executionId - Execution ID
217
- * @param {string} agentName - Agent name
218
- * @param {string} task - Task description
219
- * @param {Object} options - Execution options
220
- * @returns {Promise<Object>} Execution result
221
- */
222
- async function runAgentExecution(executionId, agentName, task, options) {
223
- const { projectRoot, timeout, filteredEnv } = options;
224
- const execution = executions.get(executionId);
225
-
226
- if (!execution) {
227
- throw new Error(`Execution ${executionId} not found`);
228
- }
229
-
230
- try {
231
- // Load agent definition
232
- const agent = await loadAgentDefinition(agentName, projectRoot);
233
-
234
- // Build prompt
235
- const prompt = buildAgentPrompt(agent, task);
236
-
237
- // Update state to running
238
- execution.state = ExecutionState.RUNNING;
239
- execution.agentMetadata = agent.metadata;
240
-
241
- // Spawn Claude Code CLI
242
- const claude = spawn('claude', ['-p', prompt], {
243
- cwd: projectRoot,
244
- env: {
245
- ...process.env,
246
- ...filteredEnv,
247
- // Disable interactive prompts
248
- CLAUDE_NON_INTERACTIVE: '1',
249
- },
250
- timeout,
251
- });
252
-
253
- // Capture output with size limit
254
- const outputChunks = [];
255
- const errorChunks = [];
256
- let outputSize = 0;
257
- let outputTruncated = false;
258
-
259
- claude.stdout.on('data', (data) => {
260
- const chunk = data.toString();
261
- outputChunks.push(chunk);
262
-
263
- // Check output size limit
264
- if (outputSize < MAX_OUTPUT_SIZE) {
265
- const remainingSpace = MAX_OUTPUT_SIZE - outputSize;
266
- const chunkToAdd = chunk.length <= remainingSpace
267
- ? chunk
268
- : chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
269
-
270
- execution.output += chunkToAdd;
271
- outputSize += chunk.length;
272
-
273
- if (chunk.length > remainingSpace) {
274
- outputTruncated = true;
275
- }
276
- }
277
- });
278
-
279
- claude.stderr.on('data', (data) => {
280
- const chunk = data.toString();
281
- errorChunks.push(chunk);
282
-
283
- // Check output size limit
284
- if (outputSize < MAX_OUTPUT_SIZE) {
285
- const remainingSpace = MAX_OUTPUT_SIZE - outputSize;
286
- const chunkToAdd = chunk.length <= remainingSpace
287
- ? chunk
288
- : chunk.substring(0, remainingSpace) + '\n[Output truncated - limit reached]';
289
-
290
- execution.output += chunkToAdd;
291
- outputSize += chunk.length;
292
-
293
- if (chunk.length > remainingSpace) {
294
- outputTruncated = true;
295
- }
296
- }
297
- });
298
-
299
- // Wait for completion
300
- let timeoutHandle = null;
301
- const exitCode = await new Promise((resolve, reject) => {
302
- claude.on('close', (code) => {
303
- // Clear timeout on normal completion
304
- if (timeoutHandle) {
305
- clearTimeout(timeoutHandle);
306
- timeoutHandle = null;
307
- }
308
- resolve(code);
309
- });
310
-
311
- claude.on('error', (error) => {
312
- // Clear timeout on error
313
- if (timeoutHandle) {
314
- clearTimeout(timeoutHandle);
315
- timeoutHandle = null;
316
- }
317
- reject(error);
318
- });
319
-
320
- // Handle timeout
321
- if (timeout) {
322
- timeoutHandle = setTimeout(() => {
323
- timeoutHandle = null;
324
- claude.kill('SIGTERM');
325
- reject(new Error(`Execution timeout after ${timeout}ms`));
326
- }, timeout);
327
- }
328
- });
329
-
330
- // Add truncation notice if output was limited
331
- if (outputTruncated) {
332
- execution.output += '\n\n[Note: Output was truncated due to size limit]';
333
- }
334
-
335
- // Update execution record
336
- execution.endTime = Date.now();
337
- execution.exitCode = exitCode;
338
-
339
- if (exitCode === 0) {
340
- execution.state = ExecutionState.COMPLETED;
341
- } else {
342
- execution.state = ExecutionState.FAILED;
343
- execution.error = `Claude exited with code ${exitCode}`;
344
- }
345
-
346
- return {
347
- executionId,
348
- state: execution.state,
349
- exitCode,
350
- };
351
- } catch (error) {
352
- // Update execution with error
353
- execution.state = ExecutionState.FAILED;
354
- execution.endTime = Date.now();
355
- execution.error = error.message;
356
- execution.exitCode = -1;
357
-
358
- return {
359
- executionId,
360
- state: ExecutionState.FAILED,
361
- error: error.message,
362
- };
363
- }
364
- }
365
-
366
- /**
367
- * Get execution status
368
- * @param {string} executionId - Execution ID
369
- * @returns {Object|null} Execution details or null if not found
370
- */
371
- export function getExecutionStatus(executionId) {
372
- const execution = executions.get(executionId);
373
-
374
- if (!execution) {
375
- return null;
376
- }
377
-
378
- // Calculate duration
379
- const duration = execution.endTime
380
- ? execution.endTime - execution.startTime
381
- : Date.now() - execution.startTime;
382
-
383
- return {
384
- id: execution.id,
385
- agent: execution.agent,
386
- task: execution.task,
387
- state: execution.state,
388
- startTime: execution.startTime,
389
- endTime: execution.endTime,
390
- duration,
391
- output: execution.output,
392
- error: execution.error,
393
- exitCode: execution.exitCode,
394
- metadata: execution.agentMetadata,
395
- };
396
- }
397
-
398
- /**
399
- * List all executions (with optional filtering)
400
- * @param {Object} filters - Filter options
401
- * @param {string} [filters.agent] - Filter by agent name
402
- * @param {string} [filters.state] - Filter by state
403
- * @param {number} [filters.limit] - Maximum number of results
404
- * @returns {Object[]} Array of execution summaries
405
- */
406
- export function listExecutions(filters = {}) {
407
- const { agent, state, limit = 100 } = filters;
408
-
409
- let results = Array.from(executions.values());
410
-
411
- // Apply filters
412
- if (agent) {
413
- results = results.filter((e) => e.agent === agent);
414
- }
415
-
416
- if (state) {
417
- results = results.filter((e) => e.state === state);
418
- }
419
-
420
- // Sort by start time (newest first)
421
- results.sort((a, b) => b.startTime - a.startTime);
422
-
423
- // Limit results
424
- results = results.slice(0, limit);
425
-
426
- // Return summary (no output to keep response small)
427
- return results.map((e) => ({
428
- id: e.id,
429
- agent: e.agent,
430
- task: e.task,
431
- state: e.state,
432
- startTime: e.startTime,
433
- endTime: e.endTime,
434
- duration: e.endTime ? e.endTime - e.startTime : Date.now() - e.startTime,
435
- exitCode: e.exitCode,
436
- }));
437
- }
438
-
439
- /**
440
- * Clean up old executions (to prevent memory leak)
441
- * @param {number} maxAge - Maximum age in ms (default: 1 hour)
442
- * @returns {number} Number of executions cleaned up
443
- */
444
- export function cleanupExecutions(maxAge = 60 * 60 * 1000) {
445
- const cutoff = Date.now() - maxAge;
446
- let cleaned = 0;
447
-
448
- for (const [id, execution] of executions.entries()) {
449
- if (execution.endTime && execution.endTime < cutoff) {
450
- executions.delete(id);
451
- cleaned++;
452
- }
453
- }
454
-
455
- return cleaned;
456
- }
457
-
458
- /**
459
- * Start periodic cleanup (runs every hour)
460
- */
461
- export function startPeriodicCleanup() {
462
- setInterval(() => {
463
- const cleaned = cleanupExecutions();
464
- if (cleaned > 0) {
465
- console.log(`Cleaned up ${cleaned} old executions`);
466
- }
467
- }, 60 * 60 * 1000); // Run every hour
468
- }
469
-
470
- export default {
471
- executeAgent,
472
- getExecutionStatus,
473
- listExecutions,
474
- cleanupExecutions,
475
- startPeriodicCleanup,
476
- ExecutionState,
477
- };