aiag-cli 1.5.1 → 1.6.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.
@@ -5,6 +5,65 @@ import { readFeatureList, writeFeatureList, getProgress } from '../utils/feature
5
5
  import { appendToProgress, recordWorkCompleted, updateProgressSummary, updateNextSteps, } from '../utils/progress.js';
6
6
  import { printHeader, printError, printSuccess, printWarning, printSection, colors, icons, } from '../utils/output.js';
7
7
  import { PRIORITY_ORDER } from '../types.js';
8
+ /**
9
+ * 알려진 실패 패턴 (Common failure patterns)
10
+ */
11
+ const KNOWN_PATTERNS = [
12
+ {
13
+ pattern: /Cannot find module ['"](.+?)['"]/i,
14
+ category: 'import-error',
15
+ suggestedFix: 'Missing import or dependency. Check package.json and import statements.',
16
+ shouldRetry: true,
17
+ },
18
+ {
19
+ pattern: /Module ['"](.+?)['"] has no exported member ['"](.+?)['"]/i,
20
+ category: 'export-error',
21
+ suggestedFix: 'Incorrect import. Verify the export exists in the target module.',
22
+ shouldRetry: true,
23
+ },
24
+ {
25
+ pattern: /TypeError: Cannot read propert(?:y|ies) ['"](.+?)['"] of (undefined|null)/i,
26
+ category: 'null-reference',
27
+ suggestedFix: 'Add null/undefined check before accessing property. Use optional chaining (?.).',
28
+ shouldRetry: true,
29
+ },
30
+ {
31
+ pattern: /SyntaxError|Unexpected token/i,
32
+ category: 'syntax-error',
33
+ suggestedFix: 'Fix syntax error. Check for missing brackets, quotes, or semicolons.',
34
+ shouldRetry: true,
35
+ },
36
+ {
37
+ pattern: /Test.*timeout|ETIMEDOUT/i,
38
+ category: 'test-timeout',
39
+ suggestedFix: 'Async operation not awaited or infinite loop. Add await or increase timeout.',
40
+ shouldRetry: false, // Timeout은 재시도해도 같은 결과
41
+ },
42
+ {
43
+ pattern: /Self-verification failed|Not all acceptance criteria/i,
44
+ category: 'incomplete-implementation',
45
+ suggestedFix: 'Implementation incomplete. Review acceptance criteria and implement missing features.',
46
+ shouldRetry: true,
47
+ },
48
+ {
49
+ pattern: /Type ['"](.+?)['"] is not assignable to type ['"](.+?)['"]/i,
50
+ category: 'type-error',
51
+ suggestedFix: 'Type mismatch. Check TypeScript types and fix the assignment.',
52
+ shouldRetry: true,
53
+ },
54
+ {
55
+ pattern: /ENOENT.*no such file or directory/i,
56
+ category: 'file-not-found',
57
+ suggestedFix: 'File or directory missing. Create the required file or fix the path.',
58
+ shouldRetry: true,
59
+ },
60
+ {
61
+ pattern: /expected.*to.*but got/i,
62
+ category: 'assertion-failed',
63
+ suggestedFix: 'Test assertion failed. Review test expectations and implementation logic.',
64
+ shouldRetry: true,
65
+ },
66
+ ];
8
67
  /**
9
68
  * aiag auto [count] - 자동 연속 구현 모드
10
69
  * --loop: 무한 루프 모드 (Ctrl+C로 중단)
@@ -17,6 +76,7 @@ export async function auto(count = '5', options = {}) {
17
76
  const maxFeatures = isLoopMode ? Infinity : (parseInt(count, 10) || 5);
18
77
  const cooldownMs = (options.cooldown || 5) * 1000;
19
78
  const claudeTimeout = (options.timeout || 10) * 60 * 1000; // 기본 10분
79
+ const maxAttempts = parseInt(String(options.maxAttempts || 3), 10); // 기본 최대 3회 시도
20
80
  // verbose 로그 헬퍼
21
81
  const verboseLog = (message) => {
22
82
  if (isVerbose) {
@@ -30,10 +90,18 @@ export async function auto(count = '5', options = {}) {
30
90
  }
31
91
  // --resume 옵션: 진행 중인 작업 확인
32
92
  let resumeFeature;
93
+ let resumeContext;
33
94
  if (options.resume) {
34
95
  resumeFeature = getInProgressFeature(baseDir, featureList);
35
96
  if (resumeFeature) {
97
+ resumeContext = getFeatureContext(baseDir, resumeFeature.id);
36
98
  console.log(colors.info(`Resuming in-progress feature: ${resumeFeature.id}`));
99
+ if (resumeContext.attemptNumber > 1) {
100
+ console.log(colors.warning(` Attempt #${resumeContext.attemptNumber}`));
101
+ if (resumeContext.previousError) {
102
+ console.log(colors.dim(` Previous error: ${resumeContext.previousError.substring(0, 100)}...`));
103
+ }
104
+ }
37
105
  console.log('');
38
106
  }
39
107
  }
@@ -48,11 +116,14 @@ export async function auto(count = '5', options = {}) {
48
116
  console.log(colors.dim(` Working directory: ${baseDir}`));
49
117
  console.log(colors.dim(` Claude timeout: ${claudeTimeout / 1000}s`));
50
118
  console.log(colors.dim(` Cooldown: ${cooldownMs / 1000}s`));
119
+ console.log(colors.dim(` Max retry attempts: ${maxAttempts}`));
51
120
  console.log('');
52
121
  }
53
122
  const initialProgress = getProgress(baseDir);
54
123
  const results = [];
55
124
  const startTime = Date.now();
125
+ const sessionId = generateSessionId();
126
+ const sessionStartTime = new Date().toISOString();
56
127
  // Ctrl+C 핸들링 (loop 모드용)
57
128
  let shouldStop = false;
58
129
  const handleSignal = () => {
@@ -64,18 +135,22 @@ export async function auto(count = '5', options = {}) {
64
135
  process.on('SIGINT', handleSignal);
65
136
  process.on('SIGTERM', handleSignal);
66
137
  }
67
- // 자동화 시작 로그
138
+ // 자동화 시작 로그 (세션 메타데이터 강화)
68
139
  const autoStartLog = `
69
140
  ---
70
141
 
71
- ## Auto Session: ${new Date().toISOString()}
142
+ ## Auto Session: ${sessionId}
72
143
 
73
- ### Configuration
74
- - Mode: ${isLoopMode ? 'loop (infinite)' : `batch (${maxFeatures})`}
75
- - Resume: ${options.resume || false}
76
- - Category filter: ${options.category || 'all'}
77
- - Until feature: ${options.until || 'none'}
78
- - Dry run: ${options.dryRun || false}
144
+ ### Session Metadata
145
+ - **Session ID**: ${sessionId}
146
+ - **Started at**: ${sessionStartTime}
147
+ - **Mode**: ${isLoopMode ? 'loop (infinite)' : `batch (${maxFeatures})`}
148
+ - **Resume**: ${options.resume || false}
149
+ - **Category filter**: ${options.category || 'all'}
150
+ - **Until feature**: ${options.until || 'none'}
151
+ - **Max attempts**: ${maxAttempts}
152
+ - **Dry run**: ${options.dryRun || false}
153
+ - **Initial progress**: ${initialProgress.completed}/${initialProgress.total} features (${initialProgress.percentage}%)
79
154
 
80
155
  ### Processing Log
81
156
  `;
@@ -89,14 +164,45 @@ export async function auto(count = '5', options = {}) {
89
164
  const currentFeatureList = readFeatureList(baseDir);
90
165
  if (!currentFeatureList)
91
166
  break;
92
- // 다음 기능 선택 ( 번째 iteration에서 resume 체크)
167
+ // 다음 기능 선택 (retry 또는 신규)
93
168
  let feature;
169
+ let featureContext;
170
+ // 1. 첫 iteration에서 --resume으로 지정된 feature 처리
94
171
  if (iterationCount === 0 && resumeFeature) {
95
172
  feature = resumeFeature;
96
- resumeFeature = undefined; // 한 번만 사용
173
+ featureContext = resumeContext;
174
+ resumeFeature = undefined;
175
+ resumeContext = undefined;
176
+ verboseLog(`Using --resume feature: ${feature.id} (attempt ${featureContext?.attemptNumber})`);
97
177
  }
178
+ // 2. session_context.md에 실패한 feature가 있고 재시도 가능한지 확인
98
179
  else {
99
- feature = selectNextFeature(currentFeatureList, options);
180
+ const failedFeature = getInProgressFeature(baseDir, currentFeatureList);
181
+ if (failedFeature) {
182
+ const context = getFeatureContext(baseDir, failedFeature.id);
183
+ // 최대 시도 횟수 미만이면 자동 재시도
184
+ if (context.attemptNumber <= maxAttempts) {
185
+ feature = failedFeature;
186
+ featureContext = context;
187
+ verboseLog(`Auto-retrying failed feature: ${feature.id} (attempt ${context.attemptNumber}/${maxAttempts})`);
188
+ }
189
+ else {
190
+ // 최대 시도 횟수 초과 - 영구 실패 처리
191
+ verboseLog(`Feature ${failedFeature.id} exceeded max attempts (${maxAttempts}), moving to next`);
192
+ appendToProgress(baseDir, `- [!] ${failedFeature.id}: Permanently failed after ${maxAttempts} attempts\n`);
193
+ clearSessionContext(baseDir); // 컨텍스트 정리하고 다음 feature로
194
+ feature = selectNextFeature(currentFeatureList, options);
195
+ featureContext = { attemptNumber: 1 };
196
+ }
197
+ }
198
+ else {
199
+ // 실패한 feature 없음 - 다음 feature 선택
200
+ feature = selectNextFeature(currentFeatureList, options);
201
+ featureContext = { attemptNumber: 1 };
202
+ if (feature) {
203
+ verboseLog(`Selected next feature: ${feature.id}`);
204
+ }
205
+ }
100
206
  }
101
207
  if (!feature) {
102
208
  console.log('');
@@ -114,7 +220,10 @@ export async function auto(count = '5', options = {}) {
114
220
  const featureStartTime = Date.now();
115
221
  console.log('');
116
222
  const progressDisplay = isLoopMode ? `#${iterationCount}` : `${iterationCount}/${maxFeatures}`;
117
- console.log(colors.bold(`[${progressDisplay}] ${feature.id}: ${feature.description}`));
223
+ const attemptDisplay = (featureContext?.attemptNumber || 1) > 1
224
+ ? ` ${colors.warning(`[Retry ${featureContext?.attemptNumber}/${maxAttempts}]`)}`
225
+ : '';
226
+ console.log(colors.bold(`[${progressDisplay}] ${feature.id}: ${feature.description}${attemptDisplay}`));
118
227
  if (options.dryRun) {
119
228
  console.log(` ${icons.arrow} Would implement...`);
120
229
  console.log(` ${icons.arrow} Would test...`);
@@ -127,27 +236,49 @@ export async function auto(count = '5', options = {}) {
127
236
  continue;
128
237
  }
129
238
  try {
130
- // 1. Claude Code로 구현 시도
239
+ // 1. Update session context (in_progress)
240
+ updateSessionContext(baseDir, feature, {
241
+ status: 'in_progress',
242
+ attempts: featureContext?.attemptNumber || 1,
243
+ });
244
+ // 2. Build enhanced prompt context
245
+ verboseLog(`Building prompt context for ${feature.id}`);
246
+ const promptContext = await buildPromptContext(baseDir, feature, currentFeatureList, featureContext?.attemptNumber || 1, featureContext?.previousError);
247
+ // 3. Claude Code로 구현 시도
131
248
  console.log(` ${icons.arrow} Implementing...`);
132
249
  verboseLog(`Starting Claude Code for ${feature.id}`);
133
- const implementResult = await runClaudeCode(baseDir, feature, claudeTimeout, isVerbose);
250
+ const implementResult = await runClaudeCode(baseDir, feature, promptContext, claudeTimeout, isVerbose);
134
251
  if (!implementResult.success) {
135
252
  verboseLog(`Implementation failed: ${implementResult.error}`);
136
253
  throw new Error(implementResult.error || 'Implementation failed');
137
254
  }
138
255
  verboseLog('Implementation completed successfully');
139
- // 2. 테스트 실행
256
+ // 2. Self-Verification
257
+ console.log(` ${icons.arrow} Self-verifying...`);
258
+ verboseLog('Running self-verification of acceptance criteria');
259
+ const verificationResult = await runSelfVerification(baseDir, feature, claudeTimeout, isVerbose);
260
+ if (!verificationResult.allPassed) {
261
+ const failedCriteria = verificationResult.results
262
+ .filter((r) => !r.passed)
263
+ .map((r) => `- ${r.criterion}: ${r.evidence}`)
264
+ .join('\n');
265
+ verboseLog(`Self-verification failed:\n${failedCriteria}`);
266
+ throw new Error(`Self-verification failed. Not all acceptance criteria are met:\n${failedCriteria}`);
267
+ }
268
+ console.log(` ${icons.success} Verified`);
269
+ verboseLog(`All ${feature.acceptanceCriteria.length} criteria passed verification`);
270
+ // 3. 테스트 실행
140
271
  console.log(` ${icons.arrow} Testing...`);
141
272
  const testResult = await runTest(baseDir, feature);
142
273
  if (!testResult.success) {
143
274
  throw new Error('Test failed');
144
275
  }
145
276
  console.log(` ${icons.success} Passed`);
146
- // 3. 완료 처리
277
+ // 4. 완료 처리
147
278
  console.log(` ${icons.arrow} Completing...`);
148
279
  await markComplete(baseDir, feature.id);
149
280
  console.log(` ${icons.success} Done`);
150
- // 4. 커밋
281
+ // 5. 커밋
151
282
  console.log(` ${icons.arrow} Committing...`);
152
283
  const commitResult = await runCommit(baseDir, feature);
153
284
  if (commitResult.success) {
@@ -159,24 +290,48 @@ export async function auto(count = '5', options = {}) {
159
290
  status: 'success',
160
291
  duration: Date.now() - featureStartTime,
161
292
  });
293
+ // Clear session context (success)
294
+ clearSessionContext(baseDir);
162
295
  // progress.md에 기록
163
- appendToProgress(baseDir, `- [x] ${feature.id}: Completed automatically\n`);
296
+ const attemptSuffix = (featureContext?.attemptNumber || 1) > 1
297
+ ? ` (succeeded on attempt ${featureContext?.attemptNumber})`
298
+ : '';
299
+ appendToProgress(baseDir, `- [x] ${feature.id}: Completed automatically${attemptSuffix}\n`);
164
300
  }
165
301
  catch (error) {
166
302
  failedCount++;
167
303
  const errorMessage = error instanceof Error ? error.message : String(error);
304
+ // Analyze failure pattern
305
+ const failureAnalysis = analyzeFailure(errorMessage);
306
+ // Display failure info
168
307
  console.log(` ${icons.error} Failed: ${errorMessage}`);
308
+ // Verbose: show failure analysis
309
+ if (isVerbose && failureAnalysis.category !== 'unknown') {
310
+ verboseLog(`Failure category: ${failureAnalysis.category}`);
311
+ verboseLog(`Suggested fix: ${failureAnalysis.suggestion}`);
312
+ verboseLog(`Should retry: ${failureAnalysis.shouldRetry ? 'Yes' : 'No (consider manual fix)'}`);
313
+ }
169
314
  results.push({
170
315
  featureId: feature.id,
171
316
  status: 'failed',
172
317
  error: errorMessage,
173
318
  duration: Date.now() - featureStartTime,
174
319
  });
320
+ // Update session context (failed) with failure analysis
321
+ updateSessionContext(baseDir, feature, {
322
+ status: 'failed',
323
+ attempts: featureContext?.attemptNumber || 1,
324
+ error: errorMessage,
325
+ failureAnalysis,
326
+ });
175
327
  // 롤백
176
328
  console.log(` ${icons.arrow} Rolling back...`);
177
329
  await rollback(baseDir);
178
330
  // progress.md에 기록
179
- appendToProgress(baseDir, `- [ ] ${feature.id}: Failed - ${errorMessage}\n`);
331
+ const attemptInfo = `(attempt ${featureContext?.attemptNumber}/${maxAttempts})`;
332
+ const willRetry = (featureContext?.attemptNumber || 1) < maxAttempts ? ' - will retry' : ' - max attempts reached';
333
+ const categoryInfo = failureAnalysis.category !== 'unknown' ? ` [${failureAnalysis.category}]` : '';
334
+ appendToProgress(baseDir, `- [ ] ${feature.id}: Failed ${attemptInfo}${willRetry}${categoryInfo} - ${errorMessage}\n`);
180
335
  }
181
336
  // 쿨다운 (다음 iteration이 있고 중단 요청이 없을 때만)
182
337
  if (!shouldStop) {
@@ -190,25 +345,46 @@ export async function auto(count = '5', options = {}) {
190
345
  }
191
346
  // 최종 결과 출력
192
347
  const totalDuration = Date.now() - startTime;
348
+ const sessionEndTime = new Date().toISOString();
193
349
  const finalProgress = getProgress(baseDir);
350
+ // 성공/실패한 features 분류
351
+ const completedFeatures = results.filter((r) => r.status === 'success');
352
+ const failedFeatures = results.filter((r) => r.status === 'failed');
353
+ const skippedFeatures = results.filter((r) => r.status === 'skipped');
194
354
  printSection('Summary');
195
355
  console.log('');
196
356
  console.log(` ${colors.success('Completed:')} ${completedCount}`);
197
357
  console.log(` ${colors.error('Failed:')} ${failedCount}`);
198
- console.log(` ${colors.dim('Skipped:')} ${results.filter((r) => r.status === 'skipped').length}`);
358
+ console.log(` ${colors.dim('Skipped:')} ${skippedFeatures.length}`);
199
359
  console.log('');
200
360
  console.log(` ${colors.dim('Progress:')} ${initialProgress.percentage}% → ${finalProgress.percentage}%`);
201
361
  console.log(` ${colors.dim('Duration:')} ${formatDuration(totalDuration)}`);
202
362
  console.log('');
203
- // 최종 로그
363
+ // 최종 로그 (상세 세션 요약)
204
364
  const autoEndLog = `
205
- ### Summary
206
- - Processed: ${processedCount}
207
- - Completed: ${completedCount}
208
- - Failed: ${failedCount}
209
- - Progress: ${initialProgress.percentage}% ${finalProgress.percentage}%
210
- - Duration: ${formatDuration(totalDuration)}
365
+ ### Session Summary
366
+
367
+ #### Metrics
368
+ - **Session ID**: ${sessionId}
369
+ - **Started at**: ${sessionStartTime}
370
+ - **Ended at**: ${sessionEndTime}
371
+ - **Duration**: ${formatDuration(totalDuration)}
372
+ - **Processed**: ${processedCount} features
373
+ - **Completed**: ${completedCount} features
374
+ - **Failed**: ${failedCount} features
375
+ - **Skipped**: ${skippedFeatures.length} features
376
+ - **Progress**: ${initialProgress.percentage}% → ${finalProgress.percentage}%
377
+ - **Success Rate**: ${processedCount > 0 ? Math.round((completedCount / processedCount) * 100) : 0}%
211
378
 
379
+ ${completedFeatures.length > 0 ? `#### ✅ Completed Features
380
+ ${completedFeatures.map((r) => `- **${r.featureId}** (${formatDuration(r.duration)})`).join('\n')}
381
+ ` : ''}
382
+ ${failedFeatures.length > 0 ? `#### ❌ Failed Features
383
+ ${failedFeatures.map((r) => `- **${r.featureId}**: ${r.error} (${formatDuration(r.duration)})`).join('\n')}
384
+ ` : ''}
385
+ ${skippedFeatures.length > 0 ? `#### ⏭️ Skipped Features
386
+ ${skippedFeatures.map((r) => `- **${r.featureId}**`).join('\n')}
387
+ ` : ''}
212
388
  ---
213
389
  `;
214
390
  appendToProgress(baseDir, autoEndLog);
@@ -220,7 +396,7 @@ export async function auto(count = '5', options = {}) {
220
396
  }
221
397
  }
222
398
  /**
223
- * session_context.md에서 진행 중인 작업을 찾아 반환
399
+ * session_context.md에서 진행 중인 작업을 찾아 반환 (with context)
224
400
  */
225
401
  function getInProgressFeature(baseDir, featureList) {
226
402
  const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
@@ -245,6 +421,100 @@ function getInProgressFeature(baseDir, featureList) {
245
421
  return undefined;
246
422
  }
247
423
  }
424
+ /**
425
+ * session_context.md에서 feature의 컨텍스트 정보 추출
426
+ */
427
+ function getFeatureContext(baseDir, featureId) {
428
+ const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
429
+ if (!fs.existsSync(sessionContextPath)) {
430
+ return { attemptNumber: 1 };
431
+ }
432
+ try {
433
+ const content = fs.readFileSync(sessionContextPath, 'utf-8');
434
+ // Attempts 추출
435
+ const attemptsMatch = content.match(/- Attempts:\s*(\d+)/);
436
+ const attemptNumber = attemptsMatch ? parseInt(attemptsMatch[1], 10) + 1 : 1;
437
+ // Previous error 추출
438
+ const errorMatch = content.match(/## Previous Error\s*```\s*(.+?)\s*```/s);
439
+ const previousError = errorMatch ? errorMatch[1].trim() : undefined;
440
+ return {
441
+ attemptNumber,
442
+ previousError,
443
+ };
444
+ }
445
+ catch {
446
+ return { attemptNumber: 1 };
447
+ }
448
+ }
449
+ /**
450
+ * Update session_context.md with feature info and failure context
451
+ */
452
+ function updateSessionContext(baseDir, feature, context) {
453
+ const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
454
+ const timestamp = new Date().toISOString();
455
+ let content = `# Current Session Context
456
+
457
+ ## Active Feature
458
+
459
+ - ID: ${feature.id}
460
+ - Description: ${feature.description}
461
+ - Status: ${context.status}
462
+ - Started: ${timestamp}
463
+ - Attempts: ${context.attempts}
464
+
465
+ `;
466
+ if (context.error) {
467
+ content += `## Previous Error
468
+
469
+ \`\`\`
470
+ ${context.error}
471
+ \`\`\`
472
+
473
+ `;
474
+ // Add failure analysis if available
475
+ if (context.failureAnalysis && context.failureAnalysis.category !== 'unknown') {
476
+ content += `## Failure Analysis
477
+
478
+ - **Category**: ${context.failureAnalysis.category}
479
+ - **Suggested Fix**: ${context.failureAnalysis.suggestion}
480
+ - **Should Retry**: ${context.failureAnalysis.shouldRetry ? 'Yes' : 'No (manual fix recommended)'}
481
+
482
+ `;
483
+ }
484
+ }
485
+ content += `## Acceptance Criteria
486
+
487
+ ${feature.acceptanceCriteria.map((c, i) => `${i + 1}. [ ] ${c}`).join('\n')}
488
+
489
+ ## Next Action
490
+
491
+ ${context.status === 'failed' ? '- Retry with different approach based on error analysis' : '- Continue implementation or verify completion'}
492
+ `;
493
+ try {
494
+ fs.writeFileSync(sessionContextPath, content, 'utf-8');
495
+ }
496
+ catch (error) {
497
+ console.error('Failed to update session context:', error);
498
+ }
499
+ }
500
+ /**
501
+ * Clear session context (on successful completion)
502
+ */
503
+ function clearSessionContext(baseDir) {
504
+ const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
505
+ const content = `# Current Session Context
506
+
507
+ ## No Active Feature
508
+
509
+ Last updated: ${new Date().toISOString()}
510
+ `;
511
+ try {
512
+ fs.writeFileSync(sessionContextPath, content, 'utf-8');
513
+ }
514
+ catch (error) {
515
+ console.error('Failed to clear session context:', error);
516
+ }
517
+ }
248
518
  function selectNextFeature(featureList, options) {
249
519
  let candidates = featureList.features.filter((f) => !f.passes);
250
520
  // 카테고리 필터
@@ -264,8 +534,8 @@ function selectNextFeature(featureList, options) {
264
534
  candidates.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
265
535
  return candidates[0];
266
536
  }
267
- async function runClaudeCode(baseDir, feature, timeout, verbose) {
268
- const prompt = generatePrompt(feature);
537
+ async function runClaudeCode(baseDir, feature, context, timeout, verbose) {
538
+ const prompt = generateEnhancedPrompt(feature, context);
269
539
  // verbose 로그 헬퍼
270
540
  const verboseLog = (message) => {
271
541
  if (verbose) {
@@ -434,20 +704,392 @@ async function rollback(baseDir) {
434
704
  });
435
705
  });
436
706
  }
437
- function generatePrompt(feature) {
438
- const lines = [
439
- `${feature.id} feature를 구현해주세요.`,
440
- '',
441
- 'Acceptance Criteria:',
442
- ...feature.acceptanceCriteria.map((c) => `- ${c}`),
443
- '',
444
- ];
707
+ /**
708
+ * Run self-verification to check if all acceptance criteria are met
709
+ */
710
+ async function runSelfVerification(baseDir, feature, timeout, verbose) {
711
+ const verboseLog = (message) => {
712
+ if (verbose) {
713
+ console.log(colors.dim(` [verbose] ${message}`));
714
+ }
715
+ };
716
+ verboseLog(`Starting self-verification for ${feature.id}`);
717
+ const verificationPrompt = `
718
+ # Self-Verification for ${feature.id}
719
+
720
+ Your implementation of ${feature.id} is complete. Now you MUST verify that ALL acceptance criteria are satisfied.
721
+
722
+ ## Acceptance Criteria to Verify:
723
+
724
+ ${feature.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}
725
+
726
+ ## Verification Instructions:
727
+
728
+ For each criterion above, check if it is satisfied by:
729
+ 1. Inspecting the code you wrote
730
+ 2. Checking if files exist
731
+ 3. Verifying function signatures and implementations
732
+ 4. Confirming expected behavior
733
+
734
+ ## Response Format:
735
+
736
+ You MUST respond with EXACTLY this format for each criterion:
737
+
738
+ \`\`\`
739
+ CRITERION 1: [PASS/FAIL]
740
+ Evidence: [Brief explanation of how this criterion is met, or why it failed]
741
+
742
+ CRITERION 2: [PASS/FAIL]
743
+ Evidence: [Brief explanation]
744
+
745
+ ...
746
+ \`\`\`
747
+
748
+ Then, at the end, provide a final verdict:
749
+
750
+ \`\`\`
751
+ FINAL VERDICT: [ALL PASS / FAILED]
752
+ \`\`\`
753
+
754
+ **IMPORTANT:**
755
+ - Be HONEST and STRICT in your verification
756
+ - If ANY criterion is not met, mark it as FAIL
757
+ - Only respond ALL PASS if EVERY criterion is satisfied
758
+ - If you're unsure about a criterion, mark it as FAIL and explain why
759
+
760
+ Start verification now.
761
+ `;
762
+ const result = await runClaudeCodeForVerification(baseDir, verificationPrompt, timeout, verbose);
763
+ if (!result.success) {
764
+ verboseLog(`Verification failed to run: ${result.error}`);
765
+ return {
766
+ allPassed: false,
767
+ results: feature.acceptanceCriteria.map((c) => ({
768
+ criterion: c,
769
+ passed: false,
770
+ evidence: 'Verification did not complete',
771
+ })),
772
+ report: result.error || 'Verification failed',
773
+ };
774
+ }
775
+ // Parse verification output
776
+ return parseVerificationOutput(result.output || '', feature.acceptanceCriteria);
777
+ }
778
+ /**
779
+ * Run Claude Code specifically for verification (separate from implementation)
780
+ */
781
+ async function runClaudeCodeForVerification(baseDir, prompt, timeout, verbose) {
782
+ const verboseLog = (message) => {
783
+ if (verbose) {
784
+ console.log(colors.dim(` [verbose] ${message}`));
785
+ }
786
+ };
787
+ return new Promise((resolve) => {
788
+ const args = [
789
+ '--print',
790
+ '--dangerously-skip-permissions',
791
+ '--strict-mcp-config',
792
+ prompt,
793
+ ];
794
+ verboseLog(`Spawning claude for verification`);
795
+ const claude = spawn('claude', args, {
796
+ cwd: baseDir,
797
+ shell: false,
798
+ stdio: ['ignore', 'pipe', 'pipe'],
799
+ env: {
800
+ ...process.env,
801
+ MCP_TIMEOUT: '10000',
802
+ },
803
+ });
804
+ let stdout = '';
805
+ let stderr = '';
806
+ if (claude.stdout) {
807
+ claude.stdout.on('data', (data) => {
808
+ const chunk = data.toString();
809
+ stdout += chunk;
810
+ if (verbose) {
811
+ process.stdout.write(chunk);
812
+ }
813
+ });
814
+ }
815
+ if (claude.stderr) {
816
+ claude.stderr.on('data', (data) => {
817
+ const chunk = data.toString();
818
+ stderr += chunk;
819
+ if (verbose) {
820
+ process.stderr.write(chunk);
821
+ }
822
+ });
823
+ }
824
+ const timeoutId = setTimeout(() => {
825
+ verboseLog('Verification timeout, killing process...');
826
+ claude.kill('SIGTERM');
827
+ setTimeout(() => {
828
+ if (!claude.killed) {
829
+ claude.kill('SIGKILL');
830
+ }
831
+ }, 5000);
832
+ resolve({ success: false, error: `Verification timeout after ${timeout / 1000}s` });
833
+ }, timeout);
834
+ claude.on('close', (code) => {
835
+ clearTimeout(timeoutId);
836
+ verboseLog(`Verification process exited with code=${code}`);
837
+ if (code === 0) {
838
+ resolve({ success: true, output: stdout });
839
+ }
840
+ else {
841
+ resolve({ success: false, error: `Exit code ${code}`, output: stdout + stderr });
842
+ }
843
+ });
844
+ claude.on('error', (err) => {
845
+ clearTimeout(timeoutId);
846
+ verboseLog(`Verification spawn error: ${err.message}`);
847
+ resolve({ success: false, error: err.message });
848
+ });
849
+ });
850
+ }
851
+ /**
852
+ * Parse verification output from Claude
853
+ */
854
+ function parseVerificationOutput(output, acceptanceCriteria) {
855
+ const results = [];
856
+ let allPassed = false;
857
+ // Extract CRITERION N: PASS/FAIL lines
858
+ const criterionPattern = /CRITERION\s+(\d+):\s*(PASS|FAIL)\s*\n\s*Evidence:\s*(.+?)(?=\n\s*CRITERION|\n\s*FINAL|$)/gis;
859
+ let match;
860
+ const foundCriteria = new Map();
861
+ while ((match = criterionPattern.exec(output)) !== null) {
862
+ const index = parseInt(match[1], 10) - 1; // Convert to 0-based index
863
+ const passed = match[2].toUpperCase() === 'PASS';
864
+ const evidence = match[3].trim();
865
+ foundCriteria.set(index, { passed, evidence });
866
+ }
867
+ // Build results array
868
+ acceptanceCriteria.forEach((criterion, index) => {
869
+ const found = foundCriteria.get(index);
870
+ if (found) {
871
+ results.push({
872
+ criterion,
873
+ passed: found.passed,
874
+ evidence: found.evidence,
875
+ });
876
+ }
877
+ else {
878
+ // Criterion not found in output
879
+ results.push({
880
+ criterion,
881
+ passed: false,
882
+ evidence: 'Not verified (missing in output)',
883
+ });
884
+ }
885
+ });
886
+ // Check final verdict
887
+ const finalVerdictMatch = output.match(/FINAL\s+VERDICT:\s*(ALL\s+PASS|FAILED)/i);
888
+ if (finalVerdictMatch) {
889
+ allPassed = finalVerdictMatch[1].toUpperCase().includes('ALL PASS');
890
+ }
891
+ else {
892
+ // No final verdict found, check if all criteria passed
893
+ allPassed = results.every((r) => r.passed);
894
+ }
895
+ return {
896
+ allPassed,
897
+ results,
898
+ report: output,
899
+ };
900
+ }
901
+ /**
902
+ * Build context for enhanced prompt generation
903
+ */
904
+ async function buildPromptContext(baseDir, feature, featureList, attemptNumber = 1, previousError) {
905
+ // Get recent commits
906
+ const recentCommits = await getRecentCommits(baseDir, 5);
907
+ // Get recent progress
908
+ const recentProgress = getRecentProgress(baseDir);
909
+ // Get dependencies
910
+ const dependencies = (feature.dependsOn || [])
911
+ .map((depId) => featureList.features.find((f) => f.id === depId))
912
+ .filter((f) => f !== undefined && f.passes === true);
913
+ return {
914
+ recentCommits,
915
+ recentProgress,
916
+ dependencies,
917
+ workingDirectory: baseDir,
918
+ attemptNumber,
919
+ previousError,
920
+ };
921
+ }
922
+ /**
923
+ * Get recent git commits
924
+ */
925
+ async function getRecentCommits(baseDir, count) {
926
+ return new Promise((resolve) => {
927
+ const git = spawn('git', ['log', '--oneline', `-${count}`], {
928
+ cwd: baseDir,
929
+ shell: false,
930
+ stdio: ['ignore', 'pipe', 'pipe'],
931
+ });
932
+ let stdout = '';
933
+ if (git.stdout) {
934
+ git.stdout.on('data', (data) => {
935
+ stdout += data.toString();
936
+ });
937
+ }
938
+ git.on('close', () => {
939
+ const commits = stdout
940
+ .trim()
941
+ .split('\n')
942
+ .filter((line) => line.length > 0);
943
+ resolve(commits);
944
+ });
945
+ git.on('error', () => {
946
+ resolve([]);
947
+ });
948
+ });
949
+ }
950
+ /**
951
+ * Get recent progress from progress.md
952
+ */
953
+ function getRecentProgress(baseDir) {
954
+ try {
955
+ const progressPath = path.join(baseDir, '.aiag', 'progress.md');
956
+ if (!fs.existsSync(progressPath)) {
957
+ return 'No progress history available';
958
+ }
959
+ const content = fs.readFileSync(progressPath, 'utf-8');
960
+ const lines = content.split('\n');
961
+ // Get last session or last 10 lines
962
+ const recentLines = lines.slice(-20).join('\n');
963
+ return recentLines.trim() || 'No recent progress';
964
+ }
965
+ catch {
966
+ return 'Error reading progress';
967
+ }
968
+ }
969
+ /**
970
+ * Generate enhanced prompt with session checklist and context
971
+ */
972
+ function generateEnhancedPrompt(feature, context) {
973
+ const lines = [];
974
+ // Session Start Checklist
975
+ lines.push('# Session Start Checklist');
976
+ lines.push('');
977
+ lines.push(`□ 1. Working directory: ${context.workingDirectory}`);
978
+ lines.push('□ 2. Recent progress:');
979
+ lines.push(context.recentProgress.split('\n').map((l) => ` ${l}`).join('\n'));
980
+ lines.push('');
981
+ lines.push('□ 3. Recent commits:');
982
+ if (context.recentCommits.length > 0) {
983
+ context.recentCommits.forEach((commit) => {
984
+ lines.push(` - ${commit}`);
985
+ });
986
+ }
987
+ else {
988
+ lines.push(' - (no recent commits)');
989
+ }
990
+ lines.push('');
991
+ // Dependencies
992
+ if (context.dependencies.length > 0) {
993
+ lines.push('□ 4. Dependencies (already completed):');
994
+ context.dependencies.forEach((dep) => {
995
+ lines.push(` ✓ ${dep.id}: ${dep.description}`);
996
+ });
997
+ lines.push('');
998
+ }
999
+ lines.push('---');
1000
+ lines.push('');
1001
+ // Feature Implementation Section
1002
+ lines.push(`# Feature Implementation: ${feature.id}`);
1003
+ lines.push('');
1004
+ lines.push('## Description');
1005
+ lines.push(feature.description);
1006
+ lines.push('');
1007
+ lines.push('## Priority');
1008
+ lines.push(`${feature.priority} - Must complete before dependent features can proceed`);
1009
+ lines.push('');
1010
+ // Retry context if applicable
1011
+ if (context.attemptNumber && context.attemptNumber > 1) {
1012
+ lines.push('## ⚠️ Retry Attempt');
1013
+ lines.push(`This is attempt #${context.attemptNumber} for this feature.`);
1014
+ lines.push('');
1015
+ if (context.previousError) {
1016
+ lines.push('### Previous Failure:');
1017
+ lines.push('```');
1018
+ lines.push(context.previousError);
1019
+ lines.push('```');
1020
+ lines.push('');
1021
+ lines.push('**Please analyze the error and try a different approach.**');
1022
+ lines.push('');
1023
+ }
1024
+ }
1025
+ // Acceptance Criteria
1026
+ lines.push('## Acceptance Criteria');
1027
+ lines.push('');
1028
+ feature.acceptanceCriteria.forEach((criterion, index) => {
1029
+ lines.push(`${index + 1}. [ ] ${criterion}`);
1030
+ });
1031
+ lines.push('');
1032
+ // Test Command
445
1033
  if (feature.testCommand) {
446
- lines.push(`테스트 명령어: ${feature.testCommand}`);
1034
+ lines.push('## Test Command');
1035
+ lines.push('```bash');
1036
+ lines.push(feature.testCommand);
1037
+ lines.push('```');
1038
+ lines.push('');
447
1039
  }
1040
+ // Implementation Guidelines
1041
+ lines.push('## Implementation Guidelines');
1042
+ lines.push('');
1043
+ lines.push('### 1. ONE Feature Rule');
1044
+ lines.push(`- Implement ONLY ${feature.id}`);
1045
+ lines.push('- Do NOT modify unrelated code');
1046
+ lines.push('- If you discover issues in other features, note them in comments but DO NOT fix');
1047
+ lines.push('');
1048
+ lines.push('### 2. Code Quality');
1049
+ lines.push('- Follow existing codebase patterns');
1050
+ lines.push('- Add comments for complex logic (in Korean)');
1051
+ lines.push('- Use meaningful variable names');
1052
+ lines.push('- Maintain clean, readable code');
1053
+ lines.push('');
1054
+ lines.push('### 3. Testing Strategy');
1055
+ lines.push('- Run tests frequently during development');
1056
+ lines.push('- Tests must pass before marking complete');
1057
+ lines.push('- Fix any test failures immediately');
1058
+ lines.push('');
1059
+ lines.push('### 4. Incremental Progress');
1060
+ lines.push('- Make small, working commits if needed');
1061
+ lines.push('- Each intermediate state should compile and run');
1062
+ lines.push(`- Use commit message format: "feat(${feature.id}): [description]"`);
1063
+ lines.push('');
1064
+ lines.push('---');
1065
+ lines.push('');
1066
+ // Completion Criteria
1067
+ lines.push('# Completion Criteria');
1068
+ lines.push('');
1069
+ lines.push('Before marking this feature as complete, you MUST verify:');
1070
+ lines.push('');
1071
+ lines.push('1. ✓ All acceptance criteria are satisfied');
1072
+ lines.push(`2. ✓ Test command passes: \`${feature.testCommand || 'bun test'}\``);
1073
+ lines.push('3. ✓ Code follows project conventions');
1074
+ lines.push('4. ✓ No unintended side effects in other parts of codebase');
1075
+ lines.push('5. ✓ Clean state (code compiles and runs)');
448
1076
  lines.push('');
449
- lines.push('구현이 완료되면 테스트를 실행할 있도록 코드를 작성하세요.');
450
- lines.push('테스트 실패 시 변경사항은 롤백됩니다.');
1077
+ lines.push('**If ANY criterion fails, DO NOT mark as complete.**');
1078
+ lines.push('');
1079
+ lines.push('---');
1080
+ lines.push('');
1081
+ // Failure Handling
1082
+ lines.push('# On Failure');
1083
+ lines.push('');
1084
+ lines.push('If you cannot complete this feature:');
1085
+ lines.push('1. Document the specific blocker or error');
1086
+ lines.push('2. Rollback any breaking changes');
1087
+ lines.push('3. Leave codebase in clean, working state');
1088
+ lines.push('4. Report the failure reason clearly');
1089
+ lines.push('');
1090
+ lines.push('---');
1091
+ lines.push('');
1092
+ lines.push('**Start implementation now.**');
451
1093
  return lines.join('\n');
452
1094
  }
453
1095
  function sleep(ms) {
@@ -467,4 +1109,39 @@ function formatDuration(ms) {
467
1109
  return `${seconds}s`;
468
1110
  }
469
1111
  }
1112
+ /**
1113
+ * Analyze failure error message and match against known patterns
1114
+ */
1115
+ function analyzeFailure(errorMessage) {
1116
+ // Try to match against known patterns
1117
+ for (const pattern of KNOWN_PATTERNS) {
1118
+ if (pattern.pattern.test(errorMessage)) {
1119
+ return {
1120
+ category: pattern.category,
1121
+ shouldRetry: pattern.shouldRetry,
1122
+ suggestion: pattern.suggestedFix,
1123
+ matchedPattern: pattern,
1124
+ };
1125
+ }
1126
+ }
1127
+ // Unknown error - default to retry
1128
+ return {
1129
+ category: 'unknown',
1130
+ shouldRetry: true,
1131
+ suggestion: 'Unknown error. Review the error message carefully and debug step by step.',
1132
+ };
1133
+ }
1134
+ /**
1135
+ * Generate unique session ID based on timestamp
1136
+ */
1137
+ function generateSessionId() {
1138
+ const now = new Date();
1139
+ const year = now.getFullYear();
1140
+ const month = String(now.getMonth() + 1).padStart(2, '0');
1141
+ const day = String(now.getDate()).padStart(2, '0');
1142
+ const hours = String(now.getHours()).padStart(2, '0');
1143
+ const minutes = String(now.getMinutes()).padStart(2, '0');
1144
+ const seconds = String(now.getSeconds()).padStart(2, '0');
1145
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
1146
+ }
470
1147
  //# sourceMappingURL=auto.js.map