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.
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/auto.d.ts +1 -0
- package/dist/commands/auto.d.ts.map +1 -1
- package/dist/commands/auto.js +718 -41
- package/dist/commands/auto.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +21 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/validate.d.ts +10 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +59 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/utils/validation.d.ts +27 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +303 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +1 -1
package/dist/commands/auto.js
CHANGED
|
@@ -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: ${
|
|
142
|
+
## Auto Session: ${sessionId}
|
|
72
143
|
|
|
73
|
-
###
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
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
|
-
// 다음 기능 선택 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
277
|
+
// 4. 완료 처리
|
|
147
278
|
console.log(` ${icons.arrow} Completing...`);
|
|
148
279
|
await markComplete(baseDir, feature.id);
|
|
149
280
|
console.log(` ${icons.success} Done`);
|
|
150
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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:')} ${
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
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 =
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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(
|
|
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
|