aiag-cli 1.7.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +125 -88
  2. package/dist/api/client.d.ts +170 -0
  3. package/dist/api/client.d.ts.map +1 -0
  4. package/dist/api/client.js +513 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/api/endpoints.d.ts +112 -0
  7. package/dist/api/endpoints.d.ts.map +1 -0
  8. package/dist/api/endpoints.js +150 -0
  9. package/dist/api/endpoints.js.map +1 -0
  10. package/dist/api/types.d.ts +395 -0
  11. package/dist/api/types.d.ts.map +1 -0
  12. package/dist/api/types.js +7 -0
  13. package/dist/api/types.js.map +1 -0
  14. package/dist/auth/credentials.d.ts +73 -0
  15. package/dist/auth/credentials.d.ts.map +1 -0
  16. package/dist/auth/credentials.js +150 -0
  17. package/dist/auth/credentials.js.map +1 -0
  18. package/dist/auth/device.d.ts +58 -0
  19. package/dist/auth/device.d.ts.map +1 -0
  20. package/dist/auth/device.js +235 -0
  21. package/dist/auth/device.js.map +1 -0
  22. package/dist/auth/token.d.ts +55 -0
  23. package/dist/auth/token.d.ts.map +1 -0
  24. package/dist/auth/token.js +153 -0
  25. package/dist/auth/token.js.map +1 -0
  26. package/dist/cli.js +51 -1
  27. package/dist/cli.js.map +1 -1
  28. package/dist/commands/auto.d.ts.map +1 -1
  29. package/dist/commands/auto.js +321 -236
  30. package/dist/commands/auto.js.map +1 -1
  31. package/dist/commands/complete.d.ts +1 -0
  32. package/dist/commands/complete.d.ts.map +1 -1
  33. package/dist/commands/complete.js +30 -0
  34. package/dist/commands/complete.js.map +1 -1
  35. package/dist/commands/connect.d.ts +25 -0
  36. package/dist/commands/connect.d.ts.map +1 -0
  37. package/dist/commands/connect.js +305 -0
  38. package/dist/commands/connect.js.map +1 -0
  39. package/dist/commands/init.d.ts.map +1 -1
  40. package/dist/commands/init.js +5 -1
  41. package/dist/commands/init.js.map +1 -1
  42. package/dist/commands/login.d.ts +19 -0
  43. package/dist/commands/login.d.ts.map +1 -0
  44. package/dist/commands/login.js +119 -0
  45. package/dist/commands/login.js.map +1 -0
  46. package/dist/commands/logout.d.ts +15 -0
  47. package/dist/commands/logout.d.ts.map +1 -0
  48. package/dist/commands/logout.js +50 -0
  49. package/dist/commands/logout.js.map +1 -0
  50. package/dist/commands/next.d.ts +1 -0
  51. package/dist/commands/next.d.ts.map +1 -1
  52. package/dist/commands/next.js +41 -2
  53. package/dist/commands/next.js.map +1 -1
  54. package/dist/commands/project.d.ts +13 -0
  55. package/dist/commands/project.d.ts.map +1 -0
  56. package/dist/commands/project.js +92 -0
  57. package/dist/commands/project.js.map +1 -0
  58. package/dist/commands/session.d.ts +10 -2
  59. package/dist/commands/session.d.ts.map +1 -1
  60. package/dist/commands/session.js +80 -3
  61. package/dist/commands/session.js.map +1 -1
  62. package/dist/commands/status.d.ts +1 -0
  63. package/dist/commands/status.d.ts.map +1 -1
  64. package/dist/commands/status.js +51 -0
  65. package/dist/commands/status.js.map +1 -1
  66. package/dist/commands/sync.d.ts +33 -0
  67. package/dist/commands/sync.d.ts.map +1 -0
  68. package/dist/commands/sync.js +555 -0
  69. package/dist/commands/sync.js.map +1 -0
  70. package/dist/commands/work.d.ts +1 -0
  71. package/dist/commands/work.d.ts.map +1 -1
  72. package/dist/commands/work.js +22 -1
  73. package/dist/commands/work.js.map +1 -1
  74. package/dist/config/global.d.ts +54 -0
  75. package/dist/config/global.d.ts.map +1 -0
  76. package/dist/config/global.js +110 -0
  77. package/dist/config/global.js.map +1 -0
  78. package/dist/prompts/coding.d.ts +31 -0
  79. package/dist/prompts/coding.d.ts.map +1 -0
  80. package/dist/prompts/coding.js +228 -0
  81. package/dist/prompts/coding.js.map +1 -0
  82. package/dist/prompts/index.d.ts +10 -0
  83. package/dist/prompts/index.d.ts.map +1 -0
  84. package/dist/prompts/index.js +10 -0
  85. package/dist/prompts/index.js.map +1 -0
  86. package/dist/prompts/initializer.d.ts +20 -0
  87. package/dist/prompts/initializer.d.ts.map +1 -0
  88. package/dist/prompts/initializer.js +147 -0
  89. package/dist/prompts/initializer.js.map +1 -0
  90. package/dist/sdk/client.d.ts +67 -0
  91. package/dist/sdk/client.d.ts.map +1 -0
  92. package/dist/sdk/client.js +196 -0
  93. package/dist/sdk/client.js.map +1 -0
  94. package/dist/sdk/index.d.ts +8 -0
  95. package/dist/sdk/index.d.ts.map +1 -0
  96. package/dist/sdk/index.js +8 -0
  97. package/dist/sdk/index.js.map +1 -0
  98. package/dist/sdk/security.d.ts +43 -0
  99. package/dist/sdk/security.d.ts.map +1 -0
  100. package/dist/sdk/security.js +214 -0
  101. package/dist/sdk/security.js.map +1 -0
  102. package/dist/types.d.ts +64 -0
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/utils/connection.d.ts +126 -0
  105. package/dist/utils/connection.d.ts.map +1 -0
  106. package/dist/utils/connection.js +226 -0
  107. package/dist/utils/connection.js.map +1 -0
  108. package/dist/utils/initializerAgent.d.ts +0 -5
  109. package/dist/utils/initializerAgent.d.ts.map +1 -1
  110. package/dist/utils/initializerAgent.js +112 -31
  111. package/dist/utils/initializerAgent.js.map +1 -1
  112. package/dist/utils/messages.d.ts +76 -0
  113. package/dist/utils/messages.d.ts.map +1 -1
  114. package/dist/utils/messages.js +87 -1
  115. package/dist/utils/messages.js.map +1 -1
  116. package/dist/utils/output.d.ts +10 -0
  117. package/dist/utils/output.d.ts.map +1 -1
  118. package/dist/utils/output.js +31 -0
  119. package/dist/utils/output.js.map +1 -1
  120. package/dist/utils/prompt.d.ts +10 -0
  121. package/dist/utils/prompt.d.ts.map +1 -0
  122. package/dist/utils/prompt.js +22 -0
  123. package/dist/utils/prompt.js.map +1 -0
  124. package/dist/utils/sseClient.d.ts +183 -0
  125. package/dist/utils/sseClient.d.ts.map +1 -0
  126. package/dist/utils/sseClient.js +391 -0
  127. package/dist/utils/sseClient.js.map +1 -0
  128. package/package.json +3 -2
@@ -3,9 +3,31 @@ import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { readFeatureList, writeFeatureList, getProgress } from '../utils/featureList.js';
5
5
  import { appendToProgress, recordWorkCompleted, updateProgressSummary, updateNextSteps, } from '../utils/progress.js';
6
- import { printHeader, printError, printSuccess, printWarning, printSection, colors, icons, } from '../utils/output.js';
6
+ import { printHeader, printError, printSuccess, printWarning, printSection, printInfo, colors, icons, } from '../utils/output.js';
7
7
  import { getInterruptedWork, updateSessionContext as updateSessionContextUtil, clearSessionContext as clearSessionContextUtil, } from '../utils/sessionContext.js';
8
+ import { loadConnection, saveCurrentSession, clearCurrentSession, } from '../utils/connection.js';
9
+ import { getApiClient } from '../api/client.js';
8
10
  import { PRIORITY_ORDER } from '../types.js';
11
+ import { getInitializerPrompt, getFeatureImplementationPrompt, } from '../prompts/index.js';
12
+ // Claude Agent SDK
13
+ import { runAgent, runFeatureImplementation, runInitializer, SECURITY_PRESET_DEVELOPMENT, } from '../sdk/index.js';
14
+ /**
15
+ * 서버에 Feature 로그 전송 (비동기, 실패해도 계속 진행)
16
+ */
17
+ async function sendFeatureLog(serverCtx, featureId, type, message, metadata) {
18
+ if (!serverCtx.connected || !serverCtx.client)
19
+ return;
20
+ try {
21
+ await serverCtx.client.sendLog(featureId, {
22
+ type,
23
+ message,
24
+ metadata,
25
+ });
26
+ }
27
+ catch {
28
+ // 로그 전송 실패해도 무시 (로컬 작업 계속 진행)
29
+ }
30
+ }
9
31
  /**
10
32
  * 알려진 실패 패턴 (Common failure patterns)
11
33
  */
@@ -76,7 +98,7 @@ export async function auto(count = '5', options = {}) {
76
98
  const isVerbose = options.verbose === true;
77
99
  const maxFeatures = isLoopMode ? Infinity : (parseInt(count, 10) || 5);
78
100
  const cooldownMs = (options.cooldown || 5) * 1000;
79
- const claudeTimeout = (options.timeout || 10) * 60 * 1000; // 기본 10
101
+ const claudeTimeout = (options.timeout || 30) * 60 * 1000; // 기본 30 (복잡한 기능 구현을 위해 증가)
80
102
  const maxAttempts = parseInt(String(options.maxAttempts || 3), 10); // 기본 최대 3회 시도
81
103
  // verbose 로그 헬퍼
82
104
  const verboseLog = (message) => {
@@ -108,6 +130,37 @@ export async function auto(count = '5', options = {}) {
108
130
  const startTime = Date.now();
109
131
  const sessionId = generateSessionId();
110
132
  const sessionStartTime = new Date().toISOString();
133
+ const processedInDryRun = new Set(); // dry-run 모드에서 처리된 feature 추적
134
+ // 서버 연결 초기화
135
+ const connection = loadConnection(baseDir);
136
+ const client = getApiClient();
137
+ const serverCtx = {
138
+ connected: false,
139
+ };
140
+ // 서버에 세션 시작 알림 (연결된 경우)
141
+ if (connection && client) {
142
+ try {
143
+ const sessionInfo = await client.startSession(connection.projectId, {
144
+ agentType: 'cli',
145
+ notes: `Auto mode started: ${isLoopMode ? 'loop' : `batch (${maxFeatures})`}`,
146
+ });
147
+ serverCtx.connected = true;
148
+ serverCtx.projectId = connection.projectId;
149
+ serverCtx.sessionId = sessionInfo.id;
150
+ serverCtx.client = client;
151
+ // 현재 세션 정보 저장
152
+ saveCurrentSession({
153
+ id: sessionInfo.id,
154
+ projectId: sessionInfo.projectId,
155
+ startedAt: sessionInfo.startedAt,
156
+ agentType: 'cli',
157
+ }, baseDir);
158
+ printInfo(colors.dim(`서버 세션 시작됨 (ID: ${sessionInfo.id.slice(0, 8)}...)`));
159
+ }
160
+ catch {
161
+ printWarning('서버 세션 등록 실패 (로컬 모드로 계속)');
162
+ }
163
+ }
111
164
  // Ctrl+C 핸들링 (loop 모드용)
112
165
  let shouldStop = false;
113
166
  const handleSignal = () => {
@@ -167,13 +220,13 @@ export async function auto(count = '5', options = {}) {
167
220
  verboseLog(`Feature ${interruptedWork.feature.id} exceeded max attempts (${maxAttempts}), moving to next`);
168
221
  appendToProgress(baseDir, `- [!] ${interruptedWork.feature.id}: Permanently failed after ${maxAttempts} attempts\n`);
169
222
  clearSessionContextUtil(baseDir); // 컨텍스트 정리하고 다음 feature로
170
- feature = selectNextFeature(currentFeatureList, options);
223
+ feature = selectNextFeature(currentFeatureList, options, processedInDryRun);
171
224
  featureContext = { attemptNumber: 1 };
172
225
  }
173
226
  }
174
227
  else {
175
228
  // 중단된 작업 없음 - 다음 feature 선택
176
- feature = selectNextFeature(currentFeatureList, options);
229
+ feature = selectNextFeature(currentFeatureList, options, processedInDryRun);
177
230
  featureContext = { attemptNumber: 1 };
178
231
  if (feature) {
179
232
  verboseLog(`Selected next feature: ${feature.id}`);
@@ -208,6 +261,7 @@ export async function auto(count = '5', options = {}) {
208
261
  status: 'skipped',
209
262
  duration: 0,
210
263
  });
264
+ processedInDryRun.add(feature.id); // dry-run에서 처리된 feature 추적
211
265
  continue;
212
266
  }
213
267
  try {
@@ -216,12 +270,18 @@ export async function auto(count = '5', options = {}) {
216
270
  status: 'in_progress',
217
271
  attempts: featureContext?.attemptNumber || 1,
218
272
  });
273
+ // 서버에 feature 시작 로그 전송
274
+ await sendFeatureLog(serverCtx, feature.id, 'started', `Started working on ${feature.id}: ${feature.description}`, {
275
+ attemptNumber: featureContext?.attemptNumber || 1,
276
+ priority: feature.priority,
277
+ category: feature.category,
278
+ });
219
279
  // 2. Build enhanced prompt context
220
280
  verboseLog(`Building prompt context for ${feature.id}`);
221
281
  const promptContext = await buildPromptContext(baseDir, feature, currentFeatureList, featureContext?.attemptNumber || 1, featureContext?.previousError);
222
- // 3. Claude Code로 구현 시도
282
+ // 3. Claude Code로 구현 시도 (SDK 사용)
223
283
  console.log(` ${icons.arrow} ${colors.cyan('work')} ${feature.id}`);
224
- verboseLog(`Starting Claude Code for ${feature.id}`);
284
+ verboseLog(`Starting Claude SDK for ${feature.id}`);
225
285
  const implementStart = Date.now();
226
286
  const implementResult = await runClaudeCode(baseDir, feature, promptContext, claudeTimeout, isVerbose);
227
287
  if (!implementResult.success) {
@@ -231,6 +291,8 @@ export async function auto(count = '5', options = {}) {
231
291
  const implementDuration = Date.now() - implementStart;
232
292
  console.log(` ${icons.success} work completed (${formatDuration(implementDuration)})`);
233
293
  verboseLog('Implementation completed successfully');
294
+ // 서버에 progress 로그 전송
295
+ await sendFeatureLog(serverCtx, feature.id, 'progress', `Implementation completed in ${formatDuration(implementDuration)}`, { phase: 'implementation', duration: implementDuration });
234
296
  // 2. Self-Verification
235
297
  console.log(` ${icons.arrow} ${colors.cyan('verify')} ${feature.id}`);
236
298
  verboseLog('Running self-verification of acceptance criteria');
@@ -247,6 +309,8 @@ export async function auto(count = '5', options = {}) {
247
309
  const verifyDuration = Date.now() - verifyStart;
248
310
  console.log(` ${icons.success} verify passed (${formatDuration(verifyDuration)})`);
249
311
  verboseLog(`All ${feature.acceptanceCriteria.length} criteria passed verification`);
312
+ // 서버에 verification 로그 전송
313
+ await sendFeatureLog(serverCtx, feature.id, 'progress', `Self-verification passed (${feature.acceptanceCriteria.length} criteria)`, { phase: 'verification', duration: verifyDuration });
250
314
  // 3. 테스트 실행
251
315
  console.log(` ${icons.arrow} ${colors.cyan('test')} ${feature.id}`);
252
316
  const testStart = Date.now();
@@ -256,6 +320,8 @@ export async function auto(count = '5', options = {}) {
256
320
  }
257
321
  const testDuration = Date.now() - testStart;
258
322
  console.log(` ${icons.success} test passed (${formatDuration(testDuration)})`);
323
+ // 서버에 test passed 로그 전송
324
+ await sendFeatureLog(serverCtx, feature.id, 'test_passed', `Tests passed in ${formatDuration(testDuration)}`, { phase: 'test', duration: testDuration, testCommand: feature.testCommand });
259
325
  // 4. 완료 처리
260
326
  console.log(` ${icons.arrow} ${colors.cyan('complete')} ${feature.id}`);
261
327
  await markComplete(baseDir, feature.id);
@@ -272,10 +338,17 @@ export async function auto(count = '5', options = {}) {
272
338
  console.log(` ${colors.warning('⚠')} commit failed (continuing anyway)`);
273
339
  }
274
340
  completedCount++;
341
+ const featureDuration = Date.now() - featureStartTime;
275
342
  results.push({
276
343
  featureId: feature.id,
277
344
  status: 'success',
278
- duration: Date.now() - featureStartTime,
345
+ duration: featureDuration,
346
+ });
347
+ // 서버에 feature 완료 로그 전송
348
+ await sendFeatureLog(serverCtx, feature.id, 'completed', `Feature ${feature.id} completed successfully in ${formatDuration(featureDuration)}`, {
349
+ duration: featureDuration,
350
+ commitHash: commitResult.hash,
351
+ attemptNumber: featureContext?.attemptNumber || 1,
279
352
  });
280
353
  // Clear session context (success)
281
354
  clearSessionContextUtil(baseDir);
@@ -290,6 +363,12 @@ export async function auto(count = '5', options = {}) {
290
363
  const errorMessage = error instanceof Error ? error.message : String(error);
291
364
  // Analyze failure pattern
292
365
  const failureAnalysis = analyzeFailure(errorMessage);
366
+ // 서버에 실패 로그 전송
367
+ await sendFeatureLog(serverCtx, feature.id, 'failed', `Feature ${feature.id} failed: ${errorMessage}`, {
368
+ attemptNumber: featureContext?.attemptNumber || 1,
369
+ failureCategory: failureAnalysis.category,
370
+ willRetry: (featureContext?.attemptNumber || 1) < maxAttempts && failureAnalysis.shouldRetry,
371
+ });
293
372
  // Display failure info
294
373
  console.log(` ${icons.error} Failed: ${errorMessage}`);
295
374
  // Verbose: show failure analysis
@@ -332,12 +411,35 @@ export async function auto(count = '5', options = {}) {
332
411
  }
333
412
  // 최종 결과 출력
334
413
  const totalDuration = Date.now() - startTime;
414
+ const totalDurationSeconds = Math.floor(totalDuration / 1000);
335
415
  const sessionEndTime = new Date().toISOString();
336
416
  const finalProgress = getProgress(baseDir);
337
417
  // 성공/실패한 features 분류
338
418
  const completedFeatures = results.filter((r) => r.status === 'success');
339
419
  const failedFeatures = results.filter((r) => r.status === 'failed');
340
420
  const skippedFeatures = results.filter((r) => r.status === 'skipped');
421
+ // 서버에 세션 종료 알림 (연결된 경우)
422
+ if (serverCtx.connected && serverCtx.client && serverCtx.sessionId) {
423
+ try {
424
+ await serverCtx.client.endSession(serverCtx.sessionId, {
425
+ workDone: completedFeatures.map((r) => `${r.featureId}: Completed automatically`),
426
+ commits: [], // 커밋 해시는 개별 feature에서 처리됨
427
+ nextSteps: failedFeatures.map((r) => `Retry ${r.featureId}: ${r.error?.slice(0, 50)}`),
428
+ notes: [
429
+ `Auto mode: ${isLoopMode ? 'loop' : `batch (${maxFeatures})`}`,
430
+ `Progress: ${initialProgress.percentage}% → ${finalProgress.percentage}%`,
431
+ `Success rate: ${processedCount > 0 ? Math.round((completedCount / processedCount) * 100) : 0}%`,
432
+ ],
433
+ duration: totalDurationSeconds,
434
+ });
435
+ // 현재 세션 정보 삭제
436
+ clearCurrentSession(baseDir);
437
+ printInfo(colors.dim(`서버 세션 종료됨 (${formatDurationKo(totalDurationSeconds)})`));
438
+ }
439
+ catch {
440
+ printWarning('서버 세션 종료 알림 실패');
441
+ }
442
+ }
341
443
  printSection('Summary');
342
444
  console.log('');
343
445
  console.log(` ${colors.success('Completed:')} ${completedCount}`);
@@ -382,19 +484,24 @@ ${skippedFeatures.map((r) => `- **${r.featureId}**`).join('\n')}
382
484
  printWarning('Auto mode finished with failures');
383
485
  }
384
486
  }
385
- function selectNextFeature(featureList, options) {
487
+ function selectNextFeature(featureList, options, excludeIds) {
386
488
  let candidates = featureList.features.filter((f) => !f.passes);
489
+ // dry-run 모드에서 이미 처리된 feature 제외
490
+ if (excludeIds && excludeIds.size > 0) {
491
+ candidates = candidates.filter((f) => !excludeIds.has(f.id));
492
+ }
387
493
  // 카테고리 필터
388
494
  if (options.category) {
389
495
  candidates = candidates.filter((f) => f.category === options.category);
390
496
  }
391
- // 의존성 확인
497
+ // 의존성 확인 (dry-run에서는 처리된 것도 완료로 간주)
392
498
  candidates = candidates.filter((f) => {
393
499
  if (!f.dependsOn || f.dependsOn.length === 0)
394
500
  return true;
395
501
  return f.dependsOn.every((depId) => {
396
502
  const dep = featureList.features.find((df) => df.id === depId);
397
- return dep?.passes === true;
503
+ // 실제로 passes이거나, dry-run에서 처리된 경우
504
+ return dep?.passes === true || (excludeIds && excludeIds.has(depId));
398
505
  });
399
506
  });
400
507
  // 우선순위로 정렬
@@ -402,100 +509,62 @@ function selectNextFeature(featureList, options) {
402
509
  return candidates[0];
403
510
  }
404
511
  async function runClaudeCode(baseDir, feature, context, timeout, verbose) {
405
- const prompt = generateEnhancedPrompt(feature, context);
512
+ // Two-Agent Pattern: 새로운 간결한 프롬프트 사용
513
+ const progress = getProgress(baseDir);
514
+ const codingContext = {
515
+ projectDir: context.workingDirectory,
516
+ recentCommits: context.recentCommits,
517
+ recentProgress: context.recentProgress,
518
+ completedFeatures: progress.completed,
519
+ totalFeatures: progress.total,
520
+ attemptNumber: context.attemptNumber,
521
+ previousError: context.previousError,
522
+ dependencies: context.dependencies,
523
+ };
524
+ const prompt = getFeatureImplementationPrompt(feature, codingContext);
406
525
  // verbose 로그 헬퍼
407
526
  const verboseLog = (message) => {
408
527
  if (verbose) {
409
528
  console.log(colors.dim(` [verbose] ${message}`));
410
529
  }
411
530
  };
412
- return new Promise((resolve) => {
413
- // Claude Code 인자 구성
414
- // --dangerously-skip-permissions: 권한 확인 건너뛰기 (비대화형 자동화에 필요)
415
- // --strict-mcp-config: MCP 서버 연결 건너뛰기 (자동화에서 MCP 서버 불필요)
416
- const args = [
417
- '--print',
418
- '--dangerously-skip-permissions',
419
- '--strict-mcp-config',
420
- prompt,
421
- ];
422
- verboseLog(`Spawning claude with args: ${args.slice(0, 3).join(' ')} [prompt...]`);
423
- verboseLog(`Timeout: ${timeout / 1000}s`);
424
- // MCP 서버 연결 타임아웃 설정 (기본 10초)
425
- // MCP 서버 초기화가 느릴 경우 Claude Code 시작이 지연되는 문제 방지
426
- const mcpTimeout = process.env.MCP_TIMEOUT || '10000';
427
- verboseLog(`MCP_TIMEOUT: ${mcpTimeout}ms`);
428
- // stdin은 ignore (Claude Code가 stdin 대기로 블로킹되는 것 방지)
429
- // stdout/stderr은 pipe로 출력 수집
430
- const claude = spawn('claude', args, {
431
- cwd: baseDir,
432
- shell: false,
433
- stdio: ['ignore', 'pipe', 'pipe'],
434
- env: {
435
- ...process.env,
436
- MCP_TIMEOUT: mcpTimeout,
531
+ // Claude Agent SDK 사용
532
+ verboseLog('Using Claude Agent SDK');
533
+ try {
534
+ const result = await runFeatureImplementation(feature, prompt, {
535
+ workingDirectory: baseDir,
536
+ timeout,
537
+ maxTurns: 200,
538
+ securityConfig: SECURITY_PRESET_DEVELOPMENT,
539
+ onLog: verboseLog,
540
+ onProgress: (message) => {
541
+ if (verbose && message.type === 'assistant') {
542
+ // 어시스턴트 메시지 실시간 출력
543
+ const content = message.message?.content;
544
+ if (Array.isArray(content)) {
545
+ for (const block of content) {
546
+ if (block && typeof block === 'object' && 'type' in block && block.type === 'text' && 'text' in block) {
547
+ process.stdout.write(block.text);
548
+ }
549
+ }
550
+ }
551
+ }
437
552
  },
438
553
  });
439
- let stdout = '';
440
- let stderr = '';
441
- // stdout 처리: 수집 + verbose일 때 실시간 출력
442
- if (claude.stdout) {
443
- claude.stdout.on('data', (data) => {
444
- const chunk = data.toString();
445
- stdout += chunk;
446
- if (verbose) {
447
- process.stdout.write(chunk);
448
- }
449
- });
554
+ if (result.success) {
555
+ verboseLog(`SDK execution completed: turns=${result.numTurns}, cost=$${result.costUsd?.toFixed(4)}`);
556
+ return { success: true, output: result.result };
450
557
  }
451
- // stderr 처리: 수집 + verbose일 때 실시간 출력
452
- if (claude.stderr) {
453
- claude.stderr.on('data', (data) => {
454
- const chunk = data.toString();
455
- stderr += chunk;
456
- if (verbose) {
457
- process.stderr.write(chunk);
458
- }
459
- });
558
+ else {
559
+ verboseLog(`SDK execution failed: ${result.error}`);
560
+ return { success: false, error: result.error, output: result.result };
460
561
  }
461
- const timeoutId = setTimeout(() => {
462
- verboseLog('Timeout reached, killing process...');
463
- claude.kill('SIGTERM');
464
- // SIGTERM 5초 대기, 그래도 안 죽으면 SIGKILL
465
- setTimeout(() => {
466
- if (!claude.killed) {
467
- verboseLog('Process still alive, sending SIGKILL...');
468
- claude.kill('SIGKILL');
469
- }
470
- }, 5000);
471
- const errorMsg = `Timeout after ${timeout / 1000}s`;
472
- if (stderr) {
473
- verboseLog(`Last stderr: ${stderr.slice(-500)}`);
474
- }
475
- resolve({ success: false, error: errorMsg, output: stdout + stderr });
476
- }, timeout);
477
- claude.on('close', (code, signal) => {
478
- clearTimeout(timeoutId);
479
- verboseLog(`Process exited with code=${code}, signal=${signal}`);
480
- if (code === 0) {
481
- resolve({ success: true, output: stdout });
482
- }
483
- else {
484
- const errorDetail = signal
485
- ? `Killed by signal ${signal}`
486
- : `Exit code ${code}`;
487
- if (stderr) {
488
- verboseLog(`stderr: ${stderr.slice(-500)}`);
489
- }
490
- resolve({ success: false, error: errorDetail, output: stdout + stderr });
491
- }
492
- });
493
- claude.on('error', (err) => {
494
- clearTimeout(timeoutId);
495
- verboseLog(`Spawn error: ${err.message}`);
496
- resolve({ success: false, error: err.message });
497
- });
498
- });
562
+ }
563
+ catch (error) {
564
+ const errorMessage = error instanceof Error ? error.message : String(error);
565
+ verboseLog(`SDK execution error: ${errorMessage}`);
566
+ return { success: false, error: errorMessage };
567
+ }
499
568
  }
500
569
  async function runTest(baseDir, feature) {
501
570
  const testCommand = feature.testCommand || 'bun test';
@@ -511,15 +580,15 @@ async function runTest(baseDir, feature) {
511
580
  resolve({ success: false });
512
581
  return;
513
582
  }
514
- // UI 카테고리인 경우 브라우저 테스트 추가 실행
515
- if (feature.category === 'ui') {
516
- console.log(` ${icons.arrow} ${colors.cyan('browser-test')} ${feature.id}`);
517
- const browserTestResult = await runBrowserTest(baseDir, feature);
518
- resolve({ success: browserTestResult.success });
519
- }
520
- else {
521
- resolve({ success: true });
522
- }
583
+ // UI 카테고리인 경우 브라우저 테스트 추가 실행 (auto 모드에서 비활성화)
584
+ // if (feature.category === 'ui') {
585
+ // console.log(` ${icons.arrow} ${colors.cyan('browser-test')} ${feature.id}`);
586
+ // const browserTestResult = await runBrowserTest(baseDir, feature);
587
+ // resolve({ success: browserTestResult.success });
588
+ // } else {
589
+ // resolve({ success: true });
590
+ // }
591
+ resolve({ success: true });
523
592
  });
524
593
  test.on('error', () => {
525
594
  resolve({ success: false });
@@ -532,7 +601,7 @@ async function runTest(baseDir, feature) {
532
601
  });
533
602
  }
534
603
  /**
535
- * Run browser test using Puppeteer MCP server
604
+ * Run browser test using Puppeteer MCP server (SDK 사용)
536
605
  * UI 카테고리 feature에 대해 자동으로 실행
537
606
  */
538
607
  async function runBrowserTest(baseDir, feature) {
@@ -547,7 +616,7 @@ async function runBrowserTest(baseDir, feature) {
547
616
  const htmlPath = path.join(baseDir, htmlFile);
548
617
  const fileUrl = `file://${htmlPath}`;
549
618
  console.log(` ${colors.dim(` Opening ${htmlFile} in browser...`)}`);
550
- // Use Claude Code to run Puppeteer test via MCP
619
+ // Use Claude Agent SDK to run Puppeteer test via MCP
551
620
  const browserTestPrompt = `
552
621
  # Browser Test for ${feature.id}
553
622
 
@@ -567,7 +636,13 @@ ${feature.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join('\n')}
567
636
 
568
637
  If the page fails to load or doesn't meet the criteria, report the issue.
569
638
  `;
570
- const result = await runClaudeCodeForBrowserTest(baseDir, browserTestPrompt);
639
+ const result = await runAgent({
640
+ workingDirectory: baseDir,
641
+ prompt: browserTestPrompt,
642
+ timeout: 60000, // 60 seconds for browser tests
643
+ maxTurns: 30,
644
+ securityConfig: SECURITY_PRESET_DEVELOPMENT,
645
+ });
571
646
  if (!result.success) {
572
647
  console.log(` ${colors.error('✗')} browser test failed: ${result.error}`);
573
648
  return { success: false, error: result.error };
@@ -581,61 +656,6 @@ If the page fails to load or doesn't meet the criteria, report the issue.
581
656
  return { success: true }; // Don't fail the whole feature for browser test errors
582
657
  }
583
658
  }
584
- /**
585
- * Run Claude Code specifically for browser testing
586
- */
587
- async function runClaudeCodeForBrowserTest(baseDir, prompt) {
588
- return new Promise((resolve) => {
589
- const args = [
590
- '--print',
591
- '--dangerously-skip-permissions',
592
- prompt,
593
- ];
594
- const claude = spawn('claude', args, {
595
- cwd: baseDir,
596
- shell: false,
597
- stdio: ['ignore', 'pipe', 'pipe'],
598
- env: {
599
- ...process.env,
600
- MCP_TIMEOUT: '30000', // 30 seconds for browser operations
601
- },
602
- });
603
- let stdout = '';
604
- let stderr = '';
605
- if (claude.stdout) {
606
- claude.stdout.on('data', (data) => {
607
- stdout += data.toString();
608
- });
609
- }
610
- if (claude.stderr) {
611
- claude.stderr.on('data', (data) => {
612
- stderr += data.toString();
613
- });
614
- }
615
- const timeoutId = setTimeout(() => {
616
- claude.kill('SIGTERM');
617
- setTimeout(() => {
618
- if (!claude.killed) {
619
- claude.kill('SIGKILL');
620
- }
621
- }, 5000);
622
- resolve({ success: false, error: 'Browser test timeout' });
623
- }, 60000); // 60 second timeout for browser tests
624
- claude.on('close', (code) => {
625
- clearTimeout(timeoutId);
626
- if (code === 0) {
627
- resolve({ success: true, output: stdout });
628
- }
629
- else {
630
- resolve({ success: false, error: `Exit code ${code}`, output: stdout + stderr });
631
- }
632
- });
633
- claude.on('error', (err) => {
634
- clearTimeout(timeoutId);
635
- resolve({ success: false, error: err.message });
636
- });
637
- });
638
- }
639
659
  async function markComplete(baseDir, featureId) {
640
660
  const featureList = readFeatureList(baseDir);
641
661
  if (!featureList)
@@ -774,7 +794,28 @@ FINAL VERDICT: [ALL PASS / FAILED]
774
794
 
775
795
  Start verification now.
776
796
  `;
777
- const result = await runClaudeCodeForVerification(baseDir, verificationPrompt, timeout, verbose);
797
+ // Claude Agent SDK를 사용한 검증
798
+ verboseLog('Running verification via SDK');
799
+ const result = await runAgent({
800
+ workingDirectory: baseDir,
801
+ prompt: verificationPrompt,
802
+ timeout,
803
+ maxTurns: 50,
804
+ securityConfig: SECURITY_PRESET_DEVELOPMENT,
805
+ onLog: verboseLog,
806
+ onProgress: (message) => {
807
+ if (verbose && message.type === 'assistant') {
808
+ const content = message.message?.content;
809
+ if (Array.isArray(content)) {
810
+ for (const block of content) {
811
+ if (block && typeof block === 'object' && 'type' in block && block.type === 'text' && 'text' in block) {
812
+ process.stdout.write(block.text);
813
+ }
814
+ }
815
+ }
816
+ }
817
+ },
818
+ });
778
819
  if (!result.success) {
779
820
  verboseLog(`Verification failed to run: ${result.error}`);
780
821
  return {
@@ -788,80 +829,7 @@ Start verification now.
788
829
  };
789
830
  }
790
831
  // Parse verification output
791
- return parseVerificationOutput(result.output || '', feature.acceptanceCriteria);
792
- }
793
- /**
794
- * Run Claude Code specifically for verification (separate from implementation)
795
- */
796
- async function runClaudeCodeForVerification(baseDir, prompt, timeout, verbose) {
797
- const verboseLog = (message) => {
798
- if (verbose) {
799
- console.log(colors.dim(` [verbose] ${message}`));
800
- }
801
- };
802
- return new Promise((resolve) => {
803
- const args = [
804
- '--print',
805
- '--dangerously-skip-permissions',
806
- '--strict-mcp-config',
807
- prompt,
808
- ];
809
- verboseLog(`Spawning claude for verification`);
810
- const claude = spawn('claude', args, {
811
- cwd: baseDir,
812
- shell: false,
813
- stdio: ['ignore', 'pipe', 'pipe'],
814
- env: {
815
- ...process.env,
816
- MCP_TIMEOUT: '10000',
817
- },
818
- });
819
- let stdout = '';
820
- let stderr = '';
821
- if (claude.stdout) {
822
- claude.stdout.on('data', (data) => {
823
- const chunk = data.toString();
824
- stdout += chunk;
825
- if (verbose) {
826
- process.stdout.write(chunk);
827
- }
828
- });
829
- }
830
- if (claude.stderr) {
831
- claude.stderr.on('data', (data) => {
832
- const chunk = data.toString();
833
- stderr += chunk;
834
- if (verbose) {
835
- process.stderr.write(chunk);
836
- }
837
- });
838
- }
839
- const timeoutId = setTimeout(() => {
840
- verboseLog('Verification timeout, killing process...');
841
- claude.kill('SIGTERM');
842
- setTimeout(() => {
843
- if (!claude.killed) {
844
- claude.kill('SIGKILL');
845
- }
846
- }, 5000);
847
- resolve({ success: false, error: `Verification timeout after ${timeout / 1000}s` });
848
- }, timeout);
849
- claude.on('close', (code) => {
850
- clearTimeout(timeoutId);
851
- verboseLog(`Verification process exited with code=${code}`);
852
- if (code === 0) {
853
- resolve({ success: true, output: stdout });
854
- }
855
- else {
856
- resolve({ success: false, error: `Exit code ${code}`, output: stdout + stderr });
857
- }
858
- });
859
- claude.on('error', (err) => {
860
- clearTimeout(timeoutId);
861
- verboseLog(`Verification spawn error: ${err.message}`);
862
- resolve({ success: false, error: err.message });
863
- });
864
- });
832
+ return parseVerificationOutput(result.result || '', feature.acceptanceCriteria);
865
833
  }
866
834
  /**
867
835
  * Parse verification output from Claude
@@ -1124,6 +1092,23 @@ function formatDuration(ms) {
1124
1092
  return `${seconds}s`;
1125
1093
  }
1126
1094
  }
1095
+ /**
1096
+ * 시간 포맷팅 (한글, 초 단위 입력)
1097
+ */
1098
+ function formatDurationKo(seconds) {
1099
+ const hours = Math.floor(seconds / 3600);
1100
+ const minutes = Math.floor((seconds % 3600) / 60);
1101
+ const secs = seconds % 60;
1102
+ if (hours > 0) {
1103
+ return `${hours}시간 ${minutes}분`;
1104
+ }
1105
+ else if (minutes > 0) {
1106
+ return `${minutes}분 ${secs}초`;
1107
+ }
1108
+ else {
1109
+ return `${secs}초`;
1110
+ }
1111
+ }
1127
1112
  /**
1128
1113
  * Analyze failure error message and match against known patterns
1129
1114
  */
@@ -1159,4 +1144,104 @@ function generateSessionId() {
1159
1144
  const seconds = String(now.getSeconds()).padStart(2, '0');
1160
1145
  return `${year}${month}${day}-${hours}${minutes}${seconds}`;
1161
1146
  }
1147
+ /**
1148
+ * 첫 실행 여부 감지
1149
+ * - feature_list.json이 없거나 비어있으면 첫 실행
1150
+ * - 모든 feature가 미완료 상태면 첫 실행으로 간주
1151
+ */
1152
+ function isFirstRun(baseDir) {
1153
+ const featureListPath = path.join(baseDir, '.aiag', 'feature_list.json');
1154
+ const progressPath = path.join(baseDir, '.aiag', 'progress.md');
1155
+ // feature_list.json이 없으면 첫 실행
1156
+ if (!fs.existsSync(featureListPath))
1157
+ return true;
1158
+ const featureList = readFeatureList(baseDir);
1159
+ // feature_list.json이 없거나 features가 비어있으면 첫 실행
1160
+ if (!featureList || featureList.features.length === 0)
1161
+ return true;
1162
+ // 모든 feature가 미완료 상태면 첫 실행으로 간주
1163
+ const hasAnyProgress = featureList.features.some((f) => f.passes);
1164
+ if (!hasAnyProgress) {
1165
+ // progress.md가 없으면 확실히 첫 실행
1166
+ if (!fs.existsSync(progressPath))
1167
+ return true;
1168
+ // progress.md가 있지만 내용이 초기 상태면 첫 실행
1169
+ try {
1170
+ const progressContent = fs.readFileSync(progressPath, 'utf-8');
1171
+ // 실질적인 세션 로그가 없으면 첫 실행
1172
+ if (!progressContent.includes('## Auto Session:'))
1173
+ return true;
1174
+ }
1175
+ catch {
1176
+ return true;
1177
+ }
1178
+ }
1179
+ return false;
1180
+ }
1181
+ /**
1182
+ * PRD 파일 읽기 (있는 경우)
1183
+ */
1184
+ function readPrdContent(baseDir) {
1185
+ const prdPaths = [
1186
+ path.join(baseDir, '.aiag', 'prd.md'),
1187
+ path.join(baseDir, 'PRD.md'),
1188
+ path.join(baseDir, 'prd.md'),
1189
+ path.join(baseDir, 'docs', 'prd.md'),
1190
+ path.join(baseDir, 'docs', 'PRD.md'),
1191
+ ];
1192
+ for (const prdPath of prdPaths) {
1193
+ if (fs.existsSync(prdPath)) {
1194
+ try {
1195
+ return fs.readFileSync(prdPath, 'utf-8');
1196
+ }
1197
+ catch {
1198
+ continue;
1199
+ }
1200
+ }
1201
+ }
1202
+ return undefined;
1203
+ }
1204
+ /**
1205
+ * Initializer 세션 실행 (SDK 사용)
1206
+ * 첫 실행 시 프로젝트 초기화를 위해 호출
1207
+ */
1208
+ async function runInitializerSession(baseDir, timeout, verbose) {
1209
+ const projectName = path.basename(baseDir);
1210
+ const featureList = readFeatureList(baseDir);
1211
+ const prdContent = readPrdContent(baseDir);
1212
+ const context = {
1213
+ projectDir: baseDir,
1214
+ projectName,
1215
+ hasExistingFeatures: featureList !== null && featureList.features.length > 0,
1216
+ featureCount: featureList?.features.length || 0,
1217
+ };
1218
+ const prompt = getInitializerPrompt(context, prdContent);
1219
+ const verboseLog = (message) => {
1220
+ if (verbose) {
1221
+ console.log(colors.dim(` [verbose] ${message}`));
1222
+ }
1223
+ };
1224
+ verboseLog('Running Initializer Agent via SDK');
1225
+ verboseLog(`PRD found: ${prdContent ? 'Yes' : 'No'}`);
1226
+ // SDK의 runInitializer 함수 사용
1227
+ const result = await runInitializer(prompt, {
1228
+ workingDirectory: baseDir,
1229
+ timeout,
1230
+ securityConfig: SECURITY_PRESET_DEVELOPMENT,
1231
+ onLog: verboseLog,
1232
+ onProgress: (message) => {
1233
+ if (verbose && message.type === 'assistant') {
1234
+ const content = message.message?.content;
1235
+ if (Array.isArray(content)) {
1236
+ for (const block of content) {
1237
+ if (block && typeof block === 'object' && 'type' in block && block.type === 'text' && 'text' in block) {
1238
+ process.stdout.write(block.text);
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ },
1244
+ });
1245
+ return { success: result.success, error: result.error };
1246
+ }
1162
1247
  //# sourceMappingURL=auto.js.map