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.
- package/README.md +11 -11
- 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 +896 -59
- 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/output.d.ts +1 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +1 -0
- package/dist/utils/output.js.map +1 -1
- 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,31 +236,65 @@ export async function auto(count = '5', options = {}) {
|
|
|
127
236
|
continue;
|
|
128
237
|
}
|
|
129
238
|
try {
|
|
130
|
-
// 1.
|
|
131
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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}
|
|
150
|
-
//
|
|
151
|
-
console.log(` ${icons.arrow}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:')} ${
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
803
|
+
// Step 1: git add -A
|
|
804
|
+
const add = spawn('git', ['add', '-A'], {
|
|
405
805
|
cwd: baseDir,
|
|
406
|
-
shell:
|
|
407
|
-
stdio: '
|
|
806
|
+
shell: false,
|
|
807
|
+
stdio: 'pipe',
|
|
408
808
|
});
|
|
409
|
-
|
|
410
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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(
|
|
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
|