aiag-cli 1.5.1 → 1.6.3

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,31 +236,65 @@ export async function auto(count = '5', options = {}) {
127
236
  continue;
128
237
  }
129
238
  try {
130
- // 1. Claude Code로 구현 시도
131
- console.log(` ${icons.arrow} Implementing...`);
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로 구현 시도
248
+ console.log(` ${icons.arrow} ${colors.cyan('work')} ${feature.id}`);
132
249
  verboseLog(`Starting Claude Code for ${feature.id}`);
133
- const implementResult = await runClaudeCode(baseDir, feature, claudeTimeout, isVerbose);
250
+ const implementStart = Date.now();
251
+ const implementResult = await runClaudeCode(baseDir, feature, promptContext, claudeTimeout, isVerbose);
134
252
  if (!implementResult.success) {
135
253
  verboseLog(`Implementation failed: ${implementResult.error}`);
136
254
  throw new Error(implementResult.error || 'Implementation failed');
137
255
  }
256
+ const implementDuration = Date.now() - implementStart;
257
+ console.log(` ${icons.success} work completed (${formatDuration(implementDuration)})`);
138
258
  verboseLog('Implementation completed successfully');
139
- // 2. 테스트 실행
140
- console.log(` ${icons.arrow} Testing...`);
259
+ // 2. Self-Verification
260
+ console.log(` ${icons.arrow} ${colors.cyan('verify')} ${feature.id}`);
261
+ verboseLog('Running self-verification of acceptance criteria');
262
+ const verifyStart = Date.now();
263
+ const verificationResult = await runSelfVerification(baseDir, feature, claudeTimeout, isVerbose);
264
+ if (!verificationResult.allPassed) {
265
+ const failedCriteria = verificationResult.results
266
+ .filter((r) => !r.passed)
267
+ .map((r) => `- ${r.criterion}: ${r.evidence}`)
268
+ .join('\n');
269
+ verboseLog(`Self-verification failed:\n${failedCriteria}`);
270
+ throw new Error(`Self-verification failed. Not all acceptance criteria are met:\n${failedCriteria}`);
271
+ }
272
+ const verifyDuration = Date.now() - verifyStart;
273
+ console.log(` ${icons.success} verify passed (${formatDuration(verifyDuration)})`);
274
+ verboseLog(`All ${feature.acceptanceCriteria.length} criteria passed verification`);
275
+ // 3. 테스트 실행
276
+ console.log(` ${icons.arrow} ${colors.cyan('test')} ${feature.id}`);
277
+ const testStart = Date.now();
141
278
  const testResult = await runTest(baseDir, feature);
142
279
  if (!testResult.success) {
143
280
  throw new Error('Test failed');
144
281
  }
145
- console.log(` ${icons.success} Passed`);
146
- // 3. 완료 처리
147
- console.log(` ${icons.arrow} Completing...`);
282
+ const testDuration = Date.now() - testStart;
283
+ console.log(` ${icons.success} test passed (${formatDuration(testDuration)})`);
284
+ // 4. 완료 처리
285
+ console.log(` ${icons.arrow} ${colors.cyan('complete')} ${feature.id}`);
148
286
  await markComplete(baseDir, feature.id);
149
- console.log(` ${icons.success} Done`);
150
- // 4. 커밋
151
- console.log(` ${icons.arrow} Committing...`);
287
+ console.log(` ${icons.success} marked as complete`);
288
+ // 5. 커밋
289
+ console.log(` ${icons.arrow} ${colors.cyan('commit')} ${feature.id}`);
290
+ const commitStart = Date.now();
152
291
  const commitResult = await runCommit(baseDir, feature);
153
292
  if (commitResult.success) {
154
- console.log(` ${icons.success} ${commitResult.hash}`);
293
+ const commitDuration = Date.now() - commitStart;
294
+ console.log(` ${icons.success} committed ${commitResult.hash} (${formatDuration(commitDuration)})`);
295
+ }
296
+ else {
297
+ console.log(` ${colors.warning('⚠')} commit failed (continuing anyway)`);
155
298
  }
156
299
  completedCount++;
157
300
  results.push({
@@ -159,24 +302,48 @@ export async function auto(count = '5', options = {}) {
159
302
  status: 'success',
160
303
  duration: Date.now() - featureStartTime,
161
304
  });
305
+ // Clear session context (success)
306
+ clearSessionContext(baseDir);
162
307
  // progress.md에 기록
163
- appendToProgress(baseDir, `- [x] ${feature.id}: Completed automatically\n`);
308
+ const attemptSuffix = (featureContext?.attemptNumber || 1) > 1
309
+ ? ` (succeeded on attempt ${featureContext?.attemptNumber})`
310
+ : '';
311
+ appendToProgress(baseDir, `- [x] ${feature.id}: Completed automatically${attemptSuffix}\n`);
164
312
  }
165
313
  catch (error) {
166
314
  failedCount++;
167
315
  const errorMessage = error instanceof Error ? error.message : String(error);
316
+ // Analyze failure pattern
317
+ const failureAnalysis = analyzeFailure(errorMessage);
318
+ // Display failure info
168
319
  console.log(` ${icons.error} Failed: ${errorMessage}`);
320
+ // Verbose: show failure analysis
321
+ if (isVerbose && failureAnalysis.category !== 'unknown') {
322
+ verboseLog(`Failure category: ${failureAnalysis.category}`);
323
+ verboseLog(`Suggested fix: ${failureAnalysis.suggestion}`);
324
+ verboseLog(`Should retry: ${failureAnalysis.shouldRetry ? 'Yes' : 'No (consider manual fix)'}`);
325
+ }
169
326
  results.push({
170
327
  featureId: feature.id,
171
328
  status: 'failed',
172
329
  error: errorMessage,
173
330
  duration: Date.now() - featureStartTime,
174
331
  });
332
+ // Update session context (failed) with failure analysis
333
+ updateSessionContext(baseDir, feature, {
334
+ status: 'failed',
335
+ attempts: featureContext?.attemptNumber || 1,
336
+ error: errorMessage,
337
+ failureAnalysis,
338
+ });
175
339
  // 롤백
176
340
  console.log(` ${icons.arrow} Rolling back...`);
177
341
  await rollback(baseDir);
178
342
  // progress.md에 기록
179
- appendToProgress(baseDir, `- [ ] ${feature.id}: Failed - ${errorMessage}\n`);
343
+ const attemptInfo = `(attempt ${featureContext?.attemptNumber}/${maxAttempts})`;
344
+ const willRetry = (featureContext?.attemptNumber || 1) < maxAttempts ? ' - will retry' : ' - max attempts reached';
345
+ const categoryInfo = failureAnalysis.category !== 'unknown' ? ` [${failureAnalysis.category}]` : '';
346
+ appendToProgress(baseDir, `- [ ] ${feature.id}: Failed ${attemptInfo}${willRetry}${categoryInfo} - ${errorMessage}\n`);
180
347
  }
181
348
  // 쿨다운 (다음 iteration이 있고 중단 요청이 없을 때만)
182
349
  if (!shouldStop) {
@@ -190,25 +357,46 @@ export async function auto(count = '5', options = {}) {
190
357
  }
191
358
  // 최종 결과 출력
192
359
  const totalDuration = Date.now() - startTime;
360
+ const sessionEndTime = new Date().toISOString();
193
361
  const finalProgress = getProgress(baseDir);
362
+ // 성공/실패한 features 분류
363
+ const completedFeatures = results.filter((r) => r.status === 'success');
364
+ const failedFeatures = results.filter((r) => r.status === 'failed');
365
+ const skippedFeatures = results.filter((r) => r.status === 'skipped');
194
366
  printSection('Summary');
195
367
  console.log('');
196
368
  console.log(` ${colors.success('Completed:')} ${completedCount}`);
197
369
  console.log(` ${colors.error('Failed:')} ${failedCount}`);
198
- console.log(` ${colors.dim('Skipped:')} ${results.filter((r) => r.status === 'skipped').length}`);
370
+ console.log(` ${colors.dim('Skipped:')} ${skippedFeatures.length}`);
199
371
  console.log('');
200
372
  console.log(` ${colors.dim('Progress:')} ${initialProgress.percentage}% → ${finalProgress.percentage}%`);
201
373
  console.log(` ${colors.dim('Duration:')} ${formatDuration(totalDuration)}`);
202
374
  console.log('');
203
- // 최종 로그
375
+ // 최종 로그 (상세 세션 요약)
204
376
  const autoEndLog = `
205
- ### Summary
206
- - Processed: ${processedCount}
207
- - Completed: ${completedCount}
208
- - Failed: ${failedCount}
209
- - Progress: ${initialProgress.percentage}% → ${finalProgress.percentage}%
210
- - Duration: ${formatDuration(totalDuration)}
377
+ ### Session Summary
211
378
 
379
+ #### Metrics
380
+ - **Session ID**: ${sessionId}
381
+ - **Started at**: ${sessionStartTime}
382
+ - **Ended at**: ${sessionEndTime}
383
+ - **Duration**: ${formatDuration(totalDuration)}
384
+ - **Processed**: ${processedCount} features
385
+ - **Completed**: ${completedCount} features
386
+ - **Failed**: ${failedCount} features
387
+ - **Skipped**: ${skippedFeatures.length} features
388
+ - **Progress**: ${initialProgress.percentage}% → ${finalProgress.percentage}%
389
+ - **Success Rate**: ${processedCount > 0 ? Math.round((completedCount / processedCount) * 100) : 0}%
390
+
391
+ ${completedFeatures.length > 0 ? `#### ✅ Completed Features
392
+ ${completedFeatures.map((r) => `- **${r.featureId}** (${formatDuration(r.duration)})`).join('\n')}
393
+ ` : ''}
394
+ ${failedFeatures.length > 0 ? `#### ❌ Failed Features
395
+ ${failedFeatures.map((r) => `- **${r.featureId}**: ${r.error} (${formatDuration(r.duration)})`).join('\n')}
396
+ ` : ''}
397
+ ${skippedFeatures.length > 0 ? `#### ⏭️ Skipped Features
398
+ ${skippedFeatures.map((r) => `- **${r.featureId}**`).join('\n')}
399
+ ` : ''}
212
400
  ---
213
401
  `;
214
402
  appendToProgress(baseDir, autoEndLog);
@@ -220,7 +408,7 @@ export async function auto(count = '5', options = {}) {
220
408
  }
221
409
  }
222
410
  /**
223
- * session_context.md에서 진행 중인 작업을 찾아 반환
411
+ * session_context.md에서 진행 중인 작업을 찾아 반환 (with context)
224
412
  */
225
413
  function getInProgressFeature(baseDir, featureList) {
226
414
  const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
@@ -245,6 +433,100 @@ function getInProgressFeature(baseDir, featureList) {
245
433
  return undefined;
246
434
  }
247
435
  }
436
+ /**
437
+ * session_context.md에서 feature의 컨텍스트 정보 추출
438
+ */
439
+ function getFeatureContext(baseDir, featureId) {
440
+ const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
441
+ if (!fs.existsSync(sessionContextPath)) {
442
+ return { attemptNumber: 1 };
443
+ }
444
+ try {
445
+ const content = fs.readFileSync(sessionContextPath, 'utf-8');
446
+ // Attempts 추출
447
+ const attemptsMatch = content.match(/- Attempts:\s*(\d+)/);
448
+ const attemptNumber = attemptsMatch ? parseInt(attemptsMatch[1], 10) + 1 : 1;
449
+ // Previous error 추출
450
+ const errorMatch = content.match(/## Previous Error\s*```\s*(.+?)\s*```/s);
451
+ const previousError = errorMatch ? errorMatch[1].trim() : undefined;
452
+ return {
453
+ attemptNumber,
454
+ previousError,
455
+ };
456
+ }
457
+ catch {
458
+ return { attemptNumber: 1 };
459
+ }
460
+ }
461
+ /**
462
+ * Update session_context.md with feature info and failure context
463
+ */
464
+ function updateSessionContext(baseDir, feature, context) {
465
+ const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
466
+ const timestamp = new Date().toISOString();
467
+ let content = `# Current Session Context
468
+
469
+ ## Active Feature
470
+
471
+ - ID: ${feature.id}
472
+ - Description: ${feature.description}
473
+ - Status: ${context.status}
474
+ - Started: ${timestamp}
475
+ - Attempts: ${context.attempts}
476
+
477
+ `;
478
+ if (context.error) {
479
+ content += `## Previous Error
480
+
481
+ \`\`\`
482
+ ${context.error}
483
+ \`\`\`
484
+
485
+ `;
486
+ // Add failure analysis if available
487
+ if (context.failureAnalysis && context.failureAnalysis.category !== 'unknown') {
488
+ content += `## Failure Analysis
489
+
490
+ - **Category**: ${context.failureAnalysis.category}
491
+ - **Suggested Fix**: ${context.failureAnalysis.suggestion}
492
+ - **Should Retry**: ${context.failureAnalysis.shouldRetry ? 'Yes' : 'No (manual fix recommended)'}
493
+
494
+ `;
495
+ }
496
+ }
497
+ content += `## Acceptance Criteria
498
+
499
+ ${feature.acceptanceCriteria.map((c, i) => `${i + 1}. [ ] ${c}`).join('\n')}
500
+
501
+ ## Next Action
502
+
503
+ ${context.status === 'failed' ? '- Retry with different approach based on error analysis' : '- Continue implementation or verify completion'}
504
+ `;
505
+ try {
506
+ fs.writeFileSync(sessionContextPath, content, 'utf-8');
507
+ }
508
+ catch (error) {
509
+ console.error('Failed to update session context:', error);
510
+ }
511
+ }
512
+ /**
513
+ * Clear session context (on successful completion)
514
+ */
515
+ function clearSessionContext(baseDir) {
516
+ const sessionContextPath = path.join(baseDir, '.aiag', 'session_context.md');
517
+ const content = `# Current Session Context
518
+
519
+ ## No Active Feature
520
+
521
+ Last updated: ${new Date().toISOString()}
522
+ `;
523
+ try {
524
+ fs.writeFileSync(sessionContextPath, content, 'utf-8');
525
+ }
526
+ catch (error) {
527
+ console.error('Failed to clear session context:', error);
528
+ }
529
+ }
248
530
  function selectNextFeature(featureList, options) {
249
531
  let candidates = featureList.features.filter((f) => !f.passes);
250
532
  // 카테고리 필터
@@ -264,8 +546,8 @@ function selectNextFeature(featureList, options) {
264
546
  candidates.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
265
547
  return candidates[0];
266
548
  }
267
- async function runClaudeCode(baseDir, feature, timeout, verbose) {
268
- const prompt = generatePrompt(feature);
549
+ async function runClaudeCode(baseDir, feature, context, timeout, verbose) {
550
+ const prompt = generateEnhancedPrompt(feature, context);
269
551
  // verbose 로그 헬퍼
270
552
  const verboseLog = (message) => {
271
553
  if (verbose) {
@@ -369,8 +651,20 @@ async function runTest(baseDir, feature) {
369
651
  shell: true,
370
652
  stdio: 'inherit',
371
653
  });
372
- test.on('close', (code) => {
373
- resolve({ success: code === 0 });
654
+ test.on('close', async (code) => {
655
+ if (code !== 0) {
656
+ resolve({ success: false });
657
+ return;
658
+ }
659
+ // UI 카테고리인 경우 브라우저 테스트 추가 실행
660
+ if (feature.category === 'ui') {
661
+ console.log(` ${icons.arrow} ${colors.cyan('browser-test')} ${feature.id}`);
662
+ const browserTestResult = await runBrowserTest(baseDir, feature);
663
+ resolve({ success: browserTestResult.success });
664
+ }
665
+ else {
666
+ resolve({ success: true });
667
+ }
374
668
  });
375
669
  test.on('error', () => {
376
670
  resolve({ success: false });
@@ -382,6 +676,111 @@ async function runTest(baseDir, feature) {
382
676
  }, 2 * 60 * 1000);
383
677
  });
384
678
  }
679
+ /**
680
+ * Run browser test using Puppeteer MCP server
681
+ * UI 카테고리 feature에 대해 자동으로 실행
682
+ */
683
+ async function runBrowserTest(baseDir, feature) {
684
+ try {
685
+ // Find HTML file in the project directory
686
+ const htmlFiles = fs.readdirSync(baseDir).filter((f) => f.endsWith('.html'));
687
+ if (htmlFiles.length === 0) {
688
+ console.log(` ${colors.dim(' No HTML file found, skipping browser test')}`);
689
+ return { success: true }; // UI feature without HTML is OK
690
+ }
691
+ const htmlFile = htmlFiles[0]; // Use first HTML file found
692
+ const htmlPath = path.join(baseDir, htmlFile);
693
+ const fileUrl = `file://${htmlPath}`;
694
+ console.log(` ${colors.dim(` Opening ${htmlFile} in browser...`)}`);
695
+ // Use Claude Code to run Puppeteer test via MCP
696
+ const browserTestPrompt = `
697
+ # Browser Test for ${feature.id}
698
+
699
+ Please use the Puppeteer MCP server to test the HTML page at:
700
+ ${fileUrl}
701
+
702
+ ## Test Instructions:
703
+
704
+ 1. Navigate to the URL above using mcp__puppeteer__puppeteer_navigate
705
+ 2. Take a screenshot using mcp__puppeteer__puppeteer_screenshot with name "${feature.id}-screenshot"
706
+ 3. Verify that:
707
+ - The page loads successfully (no errors in console)
708
+ - The page contains expected elements based on acceptance criteria:
709
+ ${feature.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join('\n')}
710
+
711
+ 4. Report if the page rendering is successful
712
+
713
+ If the page fails to load or doesn't meet the criteria, report the issue.
714
+ `;
715
+ const result = await runClaudeCodeForBrowserTest(baseDir, browserTestPrompt);
716
+ if (!result.success) {
717
+ console.log(` ${colors.error('✗')} browser test failed: ${result.error}`);
718
+ return { success: false, error: result.error };
719
+ }
720
+ console.log(` ${icons.success} browser test passed`);
721
+ return { success: true };
722
+ }
723
+ catch (error) {
724
+ const errorMsg = error instanceof Error ? error.message : String(error);
725
+ console.log(` ${colors.warning('⚠')} browser test error: ${errorMsg} (continuing)`);
726
+ return { success: true }; // Don't fail the whole feature for browser test errors
727
+ }
728
+ }
729
+ /**
730
+ * Run Claude Code specifically for browser testing
731
+ */
732
+ async function runClaudeCodeForBrowserTest(baseDir, prompt) {
733
+ return new Promise((resolve) => {
734
+ const args = [
735
+ '--print',
736
+ '--dangerously-skip-permissions',
737
+ prompt,
738
+ ];
739
+ const claude = spawn('claude', args, {
740
+ cwd: baseDir,
741
+ shell: false,
742
+ stdio: ['ignore', 'pipe', 'pipe'],
743
+ env: {
744
+ ...process.env,
745
+ MCP_TIMEOUT: '30000', // 30 seconds for browser operations
746
+ },
747
+ });
748
+ let stdout = '';
749
+ let stderr = '';
750
+ if (claude.stdout) {
751
+ claude.stdout.on('data', (data) => {
752
+ stdout += data.toString();
753
+ });
754
+ }
755
+ if (claude.stderr) {
756
+ claude.stderr.on('data', (data) => {
757
+ stderr += data.toString();
758
+ });
759
+ }
760
+ const timeoutId = setTimeout(() => {
761
+ claude.kill('SIGTERM');
762
+ setTimeout(() => {
763
+ if (!claude.killed) {
764
+ claude.kill('SIGKILL');
765
+ }
766
+ }, 5000);
767
+ resolve({ success: false, error: 'Browser test timeout' });
768
+ }, 60000); // 60 second timeout for browser tests
769
+ claude.on('close', (code) => {
770
+ clearTimeout(timeoutId);
771
+ if (code === 0) {
772
+ resolve({ success: true, output: stdout });
773
+ }
774
+ else {
775
+ resolve({ success: false, error: `Exit code ${code}`, output: stdout + stderr });
776
+ }
777
+ });
778
+ claude.on('error', (err) => {
779
+ clearTimeout(timeoutId);
780
+ resolve({ success: false, error: err.message });
781
+ });
782
+ });
783
+ }
385
784
  async function markComplete(baseDir, featureId) {
386
785
  const featureList = readFeatureList(baseDir);
387
786
  if (!featureList)
@@ -401,20 +800,51 @@ async function markComplete(baseDir, featureId) {
401
800
  async function runCommit(baseDir, feature) {
402
801
  return new Promise((resolve) => {
403
802
  const message = `feat: ${feature.id} - ${feature.description} (auto)`;
404
- const commit = spawn('git', ['add', '-A', '&&', 'git', 'commit', '-m', message], {
803
+ // Step 1: git add -A
804
+ const add = spawn('git', ['add', '-A'], {
405
805
  cwd: baseDir,
406
- shell: true,
407
- stdio: 'inherit',
806
+ shell: false,
807
+ stdio: 'pipe',
408
808
  });
409
- commit.on('close', (code) => {
410
- if (code === 0) {
411
- resolve({ success: true, hash: 'committed' });
412
- }
413
- else {
809
+ add.on('close', (addCode) => {
810
+ if (addCode !== 0) {
414
811
  resolve({ success: false });
812
+ return;
813
+ }
814
+ // Step 2: git commit with -F option (읽기 from stdin)
815
+ // 한글 및 특수문자가 포함된 메시지를 안전하게 처리
816
+ const commit = spawn('git', ['commit', '-F', '-'], {
817
+ cwd: baseDir,
818
+ shell: false,
819
+ stdio: ['pipe', 'pipe', 'pipe'],
820
+ });
821
+ // Write commit message to stdin
822
+ if (commit.stdin) {
823
+ commit.stdin.write(message);
824
+ commit.stdin.end();
415
825
  }
826
+ let stdout = '';
827
+ if (commit.stdout) {
828
+ commit.stdout.on('data', (data) => {
829
+ stdout += data.toString();
830
+ });
831
+ }
832
+ commit.on('close', (code) => {
833
+ if (code === 0) {
834
+ // Extract commit hash from git output
835
+ const hashMatch = stdout.match(/\[.+ ([a-f0-9]+)\]/);
836
+ const hash = hashMatch ? hashMatch[1] : 'committed';
837
+ resolve({ success: true, hash });
838
+ }
839
+ else {
840
+ resolve({ success: false });
841
+ }
842
+ });
843
+ commit.on('error', () => {
844
+ resolve({ success: false });
845
+ });
416
846
  });
417
- commit.on('error', () => {
847
+ add.on('error', () => {
418
848
  resolve({ success: false });
419
849
  });
420
850
  });
@@ -434,20 +864,392 @@ async function rollback(baseDir) {
434
864
  });
435
865
  });
436
866
  }
437
- function generatePrompt(feature) {
438
- const lines = [
439
- `${feature.id} feature를 구현해주세요.`,
440
- '',
441
- 'Acceptance Criteria:',
442
- ...feature.acceptanceCriteria.map((c) => `- ${c}`),
443
- '',
444
- ];
867
+ /**
868
+ * Run self-verification to check if all acceptance criteria are met
869
+ */
870
+ async function runSelfVerification(baseDir, feature, timeout, verbose) {
871
+ const verboseLog = (message) => {
872
+ if (verbose) {
873
+ console.log(colors.dim(` [verbose] ${message}`));
874
+ }
875
+ };
876
+ verboseLog(`Starting self-verification for ${feature.id}`);
877
+ const verificationPrompt = `
878
+ # Self-Verification for ${feature.id}
879
+
880
+ Your implementation of ${feature.id} is complete. Now you MUST verify that ALL acceptance criteria are satisfied.
881
+
882
+ ## Acceptance Criteria to Verify:
883
+
884
+ ${feature.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}
885
+
886
+ ## Verification Instructions:
887
+
888
+ For each criterion above, check if it is satisfied by:
889
+ 1. Inspecting the code you wrote
890
+ 2. Checking if files exist
891
+ 3. Verifying function signatures and implementations
892
+ 4. Confirming expected behavior
893
+
894
+ ## Response Format:
895
+
896
+ You MUST respond with EXACTLY this format for each criterion:
897
+
898
+ \`\`\`
899
+ CRITERION 1: [PASS/FAIL]
900
+ Evidence: [Brief explanation of how this criterion is met, or why it failed]
901
+
902
+ CRITERION 2: [PASS/FAIL]
903
+ Evidence: [Brief explanation]
904
+
905
+ ...
906
+ \`\`\`
907
+
908
+ Then, at the end, provide a final verdict:
909
+
910
+ \`\`\`
911
+ FINAL VERDICT: [ALL PASS / FAILED]
912
+ \`\`\`
913
+
914
+ **IMPORTANT:**
915
+ - Be HONEST and STRICT in your verification
916
+ - If ANY criterion is not met, mark it as FAIL
917
+ - Only respond ALL PASS if EVERY criterion is satisfied
918
+ - If you're unsure about a criterion, mark it as FAIL and explain why
919
+
920
+ Start verification now.
921
+ `;
922
+ const result = await runClaudeCodeForVerification(baseDir, verificationPrompt, timeout, verbose);
923
+ if (!result.success) {
924
+ verboseLog(`Verification failed to run: ${result.error}`);
925
+ return {
926
+ allPassed: false,
927
+ results: feature.acceptanceCriteria.map((c) => ({
928
+ criterion: c,
929
+ passed: false,
930
+ evidence: 'Verification did not complete',
931
+ })),
932
+ report: result.error || 'Verification failed',
933
+ };
934
+ }
935
+ // Parse verification output
936
+ return parseVerificationOutput(result.output || '', feature.acceptanceCriteria);
937
+ }
938
+ /**
939
+ * Run Claude Code specifically for verification (separate from implementation)
940
+ */
941
+ async function runClaudeCodeForVerification(baseDir, prompt, timeout, verbose) {
942
+ const verboseLog = (message) => {
943
+ if (verbose) {
944
+ console.log(colors.dim(` [verbose] ${message}`));
945
+ }
946
+ };
947
+ return new Promise((resolve) => {
948
+ const args = [
949
+ '--print',
950
+ '--dangerously-skip-permissions',
951
+ '--strict-mcp-config',
952
+ prompt,
953
+ ];
954
+ verboseLog(`Spawning claude for verification`);
955
+ const claude = spawn('claude', args, {
956
+ cwd: baseDir,
957
+ shell: false,
958
+ stdio: ['ignore', 'pipe', 'pipe'],
959
+ env: {
960
+ ...process.env,
961
+ MCP_TIMEOUT: '10000',
962
+ },
963
+ });
964
+ let stdout = '';
965
+ let stderr = '';
966
+ if (claude.stdout) {
967
+ claude.stdout.on('data', (data) => {
968
+ const chunk = data.toString();
969
+ stdout += chunk;
970
+ if (verbose) {
971
+ process.stdout.write(chunk);
972
+ }
973
+ });
974
+ }
975
+ if (claude.stderr) {
976
+ claude.stderr.on('data', (data) => {
977
+ const chunk = data.toString();
978
+ stderr += chunk;
979
+ if (verbose) {
980
+ process.stderr.write(chunk);
981
+ }
982
+ });
983
+ }
984
+ const timeoutId = setTimeout(() => {
985
+ verboseLog('Verification timeout, killing process...');
986
+ claude.kill('SIGTERM');
987
+ setTimeout(() => {
988
+ if (!claude.killed) {
989
+ claude.kill('SIGKILL');
990
+ }
991
+ }, 5000);
992
+ resolve({ success: false, error: `Verification timeout after ${timeout / 1000}s` });
993
+ }, timeout);
994
+ claude.on('close', (code) => {
995
+ clearTimeout(timeoutId);
996
+ verboseLog(`Verification process exited with code=${code}`);
997
+ if (code === 0) {
998
+ resolve({ success: true, output: stdout });
999
+ }
1000
+ else {
1001
+ resolve({ success: false, error: `Exit code ${code}`, output: stdout + stderr });
1002
+ }
1003
+ });
1004
+ claude.on('error', (err) => {
1005
+ clearTimeout(timeoutId);
1006
+ verboseLog(`Verification spawn error: ${err.message}`);
1007
+ resolve({ success: false, error: err.message });
1008
+ });
1009
+ });
1010
+ }
1011
+ /**
1012
+ * Parse verification output from Claude
1013
+ */
1014
+ function parseVerificationOutput(output, acceptanceCriteria) {
1015
+ const results = [];
1016
+ let allPassed = false;
1017
+ // Extract CRITERION N: PASS/FAIL lines
1018
+ const criterionPattern = /CRITERION\s+(\d+):\s*(PASS|FAIL)\s*\n\s*Evidence:\s*(.+?)(?=\n\s*CRITERION|\n\s*FINAL|$)/gis;
1019
+ let match;
1020
+ const foundCriteria = new Map();
1021
+ while ((match = criterionPattern.exec(output)) !== null) {
1022
+ const index = parseInt(match[1], 10) - 1; // Convert to 0-based index
1023
+ const passed = match[2].toUpperCase() === 'PASS';
1024
+ const evidence = match[3].trim();
1025
+ foundCriteria.set(index, { passed, evidence });
1026
+ }
1027
+ // Build results array
1028
+ acceptanceCriteria.forEach((criterion, index) => {
1029
+ const found = foundCriteria.get(index);
1030
+ if (found) {
1031
+ results.push({
1032
+ criterion,
1033
+ passed: found.passed,
1034
+ evidence: found.evidence,
1035
+ });
1036
+ }
1037
+ else {
1038
+ // Criterion not found in output
1039
+ results.push({
1040
+ criterion,
1041
+ passed: false,
1042
+ evidence: 'Not verified (missing in output)',
1043
+ });
1044
+ }
1045
+ });
1046
+ // Check final verdict
1047
+ const finalVerdictMatch = output.match(/FINAL\s+VERDICT:\s*(ALL\s+PASS|FAILED)/i);
1048
+ if (finalVerdictMatch) {
1049
+ allPassed = finalVerdictMatch[1].toUpperCase().includes('ALL PASS');
1050
+ }
1051
+ else {
1052
+ // No final verdict found, check if all criteria passed
1053
+ allPassed = results.every((r) => r.passed);
1054
+ }
1055
+ return {
1056
+ allPassed,
1057
+ results,
1058
+ report: output,
1059
+ };
1060
+ }
1061
+ /**
1062
+ * Build context for enhanced prompt generation
1063
+ */
1064
+ async function buildPromptContext(baseDir, feature, featureList, attemptNumber = 1, previousError) {
1065
+ // Get recent commits
1066
+ const recentCommits = await getRecentCommits(baseDir, 5);
1067
+ // Get recent progress
1068
+ const recentProgress = getRecentProgress(baseDir);
1069
+ // Get dependencies
1070
+ const dependencies = (feature.dependsOn || [])
1071
+ .map((depId) => featureList.features.find((f) => f.id === depId))
1072
+ .filter((f) => f !== undefined && f.passes === true);
1073
+ return {
1074
+ recentCommits,
1075
+ recentProgress,
1076
+ dependencies,
1077
+ workingDirectory: baseDir,
1078
+ attemptNumber,
1079
+ previousError,
1080
+ };
1081
+ }
1082
+ /**
1083
+ * Get recent git commits
1084
+ */
1085
+ async function getRecentCommits(baseDir, count) {
1086
+ return new Promise((resolve) => {
1087
+ const git = spawn('git', ['log', '--oneline', `-${count}`], {
1088
+ cwd: baseDir,
1089
+ shell: false,
1090
+ stdio: ['ignore', 'pipe', 'pipe'],
1091
+ });
1092
+ let stdout = '';
1093
+ if (git.stdout) {
1094
+ git.stdout.on('data', (data) => {
1095
+ stdout += data.toString();
1096
+ });
1097
+ }
1098
+ git.on('close', () => {
1099
+ const commits = stdout
1100
+ .trim()
1101
+ .split('\n')
1102
+ .filter((line) => line.length > 0);
1103
+ resolve(commits);
1104
+ });
1105
+ git.on('error', () => {
1106
+ resolve([]);
1107
+ });
1108
+ });
1109
+ }
1110
+ /**
1111
+ * Get recent progress from progress.md
1112
+ */
1113
+ function getRecentProgress(baseDir) {
1114
+ try {
1115
+ const progressPath = path.join(baseDir, '.aiag', 'progress.md');
1116
+ if (!fs.existsSync(progressPath)) {
1117
+ return 'No progress history available';
1118
+ }
1119
+ const content = fs.readFileSync(progressPath, 'utf-8');
1120
+ const lines = content.split('\n');
1121
+ // Get last session or last 10 lines
1122
+ const recentLines = lines.slice(-20).join('\n');
1123
+ return recentLines.trim() || 'No recent progress';
1124
+ }
1125
+ catch {
1126
+ return 'Error reading progress';
1127
+ }
1128
+ }
1129
+ /**
1130
+ * Generate enhanced prompt with session checklist and context
1131
+ */
1132
+ function generateEnhancedPrompt(feature, context) {
1133
+ const lines = [];
1134
+ // Session Start Checklist
1135
+ lines.push('# Session Start Checklist');
1136
+ lines.push('');
1137
+ lines.push(`□ 1. Working directory: ${context.workingDirectory}`);
1138
+ lines.push('□ 2. Recent progress:');
1139
+ lines.push(context.recentProgress.split('\n').map((l) => ` ${l}`).join('\n'));
1140
+ lines.push('');
1141
+ lines.push('□ 3. Recent commits:');
1142
+ if (context.recentCommits.length > 0) {
1143
+ context.recentCommits.forEach((commit) => {
1144
+ lines.push(` - ${commit}`);
1145
+ });
1146
+ }
1147
+ else {
1148
+ lines.push(' - (no recent commits)');
1149
+ }
1150
+ lines.push('');
1151
+ // Dependencies
1152
+ if (context.dependencies.length > 0) {
1153
+ lines.push('□ 4. Dependencies (already completed):');
1154
+ context.dependencies.forEach((dep) => {
1155
+ lines.push(` ✓ ${dep.id}: ${dep.description}`);
1156
+ });
1157
+ lines.push('');
1158
+ }
1159
+ lines.push('---');
1160
+ lines.push('');
1161
+ // Feature Implementation Section
1162
+ lines.push(`# Feature Implementation: ${feature.id}`);
1163
+ lines.push('');
1164
+ lines.push('## Description');
1165
+ lines.push(feature.description);
1166
+ lines.push('');
1167
+ lines.push('## Priority');
1168
+ lines.push(`${feature.priority} - Must complete before dependent features can proceed`);
1169
+ lines.push('');
1170
+ // Retry context if applicable
1171
+ if (context.attemptNumber && context.attemptNumber > 1) {
1172
+ lines.push('## ⚠️ Retry Attempt');
1173
+ lines.push(`This is attempt #${context.attemptNumber} for this feature.`);
1174
+ lines.push('');
1175
+ if (context.previousError) {
1176
+ lines.push('### Previous Failure:');
1177
+ lines.push('```');
1178
+ lines.push(context.previousError);
1179
+ lines.push('```');
1180
+ lines.push('');
1181
+ lines.push('**Please analyze the error and try a different approach.**');
1182
+ lines.push('');
1183
+ }
1184
+ }
1185
+ // Acceptance Criteria
1186
+ lines.push('## Acceptance Criteria');
1187
+ lines.push('');
1188
+ feature.acceptanceCriteria.forEach((criterion, index) => {
1189
+ lines.push(`${index + 1}. [ ] ${criterion}`);
1190
+ });
1191
+ lines.push('');
1192
+ // Test Command
445
1193
  if (feature.testCommand) {
446
- lines.push(`테스트 명령어: ${feature.testCommand}`);
1194
+ lines.push('## Test Command');
1195
+ lines.push('```bash');
1196
+ lines.push(feature.testCommand);
1197
+ lines.push('```');
1198
+ lines.push('');
447
1199
  }
1200
+ // Implementation Guidelines
1201
+ lines.push('## Implementation Guidelines');
1202
+ lines.push('');
1203
+ lines.push('### 1. ONE Feature Rule');
1204
+ lines.push(`- Implement ONLY ${feature.id}`);
1205
+ lines.push('- Do NOT modify unrelated code');
1206
+ lines.push('- If you discover issues in other features, note them in comments but DO NOT fix');
1207
+ lines.push('');
1208
+ lines.push('### 2. Code Quality');
1209
+ lines.push('- Follow existing codebase patterns');
1210
+ lines.push('- Add comments for complex logic (in Korean)');
1211
+ lines.push('- Use meaningful variable names');
1212
+ lines.push('- Maintain clean, readable code');
1213
+ lines.push('');
1214
+ lines.push('### 3. Testing Strategy');
1215
+ lines.push('- Run tests frequently during development');
1216
+ lines.push('- Tests must pass before marking complete');
1217
+ lines.push('- Fix any test failures immediately');
448
1218
  lines.push('');
449
- lines.push('구현이 완료되면 테스트를 실행할 수 있도록 코드를 작성하세요.');
450
- lines.push('테스트 실패 변경사항은 롤백됩니다.');
1219
+ lines.push('### 4. Incremental Progress');
1220
+ lines.push('- Make small, working commits if needed');
1221
+ lines.push('- Each intermediate state should compile and run');
1222
+ lines.push(`- Use commit message format: "feat(${feature.id}): [description]"`);
1223
+ lines.push('');
1224
+ lines.push('---');
1225
+ lines.push('');
1226
+ // Completion Criteria
1227
+ lines.push('# Completion Criteria');
1228
+ lines.push('');
1229
+ lines.push('Before marking this feature as complete, you MUST verify:');
1230
+ lines.push('');
1231
+ lines.push('1. ✓ All acceptance criteria are satisfied');
1232
+ lines.push(`2. ✓ Test command passes: \`${feature.testCommand || 'bun test'}\``);
1233
+ lines.push('3. ✓ Code follows project conventions');
1234
+ lines.push('4. ✓ No unintended side effects in other parts of codebase');
1235
+ lines.push('5. ✓ Clean state (code compiles and runs)');
1236
+ lines.push('');
1237
+ lines.push('**If ANY criterion fails, DO NOT mark as complete.**');
1238
+ lines.push('');
1239
+ lines.push('---');
1240
+ lines.push('');
1241
+ // Failure Handling
1242
+ lines.push('# On Failure');
1243
+ lines.push('');
1244
+ lines.push('If you cannot complete this feature:');
1245
+ lines.push('1. Document the specific blocker or error');
1246
+ lines.push('2. Rollback any breaking changes');
1247
+ lines.push('3. Leave codebase in clean, working state');
1248
+ lines.push('4. Report the failure reason clearly');
1249
+ lines.push('');
1250
+ lines.push('---');
1251
+ lines.push('');
1252
+ lines.push('**Start implementation now.**');
451
1253
  return lines.join('\n');
452
1254
  }
453
1255
  function sleep(ms) {
@@ -467,4 +1269,39 @@ function formatDuration(ms) {
467
1269
  return `${seconds}s`;
468
1270
  }
469
1271
  }
1272
+ /**
1273
+ * Analyze failure error message and match against known patterns
1274
+ */
1275
+ function analyzeFailure(errorMessage) {
1276
+ // Try to match against known patterns
1277
+ for (const pattern of KNOWN_PATTERNS) {
1278
+ if (pattern.pattern.test(errorMessage)) {
1279
+ return {
1280
+ category: pattern.category,
1281
+ shouldRetry: pattern.shouldRetry,
1282
+ suggestion: pattern.suggestedFix,
1283
+ matchedPattern: pattern,
1284
+ };
1285
+ }
1286
+ }
1287
+ // Unknown error - default to retry
1288
+ return {
1289
+ category: 'unknown',
1290
+ shouldRetry: true,
1291
+ suggestion: 'Unknown error. Review the error message carefully and debug step by step.',
1292
+ };
1293
+ }
1294
+ /**
1295
+ * Generate unique session ID based on timestamp
1296
+ */
1297
+ function generateSessionId() {
1298
+ const now = new Date();
1299
+ const year = now.getFullYear();
1300
+ const month = String(now.getMonth() + 1).padStart(2, '0');
1301
+ const day = String(now.getDate()).padStart(2, '0');
1302
+ const hours = String(now.getHours()).padStart(2, '0');
1303
+ const minutes = String(now.getMinutes()).padStart(2, '0');
1304
+ const seconds = String(now.getSeconds()).padStart(2, '0');
1305
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
1306
+ }
470
1307
  //# sourceMappingURL=auto.js.map