@silbaram/artifact-driven-agent 0.1.6 → 0.1.9

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 (80) hide show
  1. package/README.md +709 -516
  2. package/ai-dev-team/.ada-status.json +10 -0
  3. package/ai-dev-team/.ada-version +6 -0
  4. package/ai-dev-team/.current-template +1 -0
  5. package/ai-dev-team/.sessions/logs/20260124-014551-00f04724.log +5 -0
  6. package/ai-dev-team/.sessions/logs/20260124-014623-cb2b1d44.log +5 -0
  7. package/ai-dev-team/ada.config.json +15 -0
  8. package/ai-dev-team/artifacts/api.md +212 -0
  9. package/ai-dev-team/artifacts/decision.md +72 -0
  10. package/ai-dev-team/artifacts/improvement-reports/IMP-0000-template.md +57 -0
  11. package/ai-dev-team/artifacts/plan.md +187 -0
  12. package/ai-dev-team/artifacts/project.md +193 -0
  13. package/ai-dev-team/artifacts/sprints/_template/docs/release-notes.md +37 -0
  14. package/ai-dev-team/artifacts/sprints/_template/meta.md +54 -0
  15. package/ai-dev-team/artifacts/sprints/_template/retrospective.md +50 -0
  16. package/ai-dev-team/artifacts/sprints/_template/review-reports/review-template.md +49 -0
  17. package/ai-dev-team/artifacts/sprints/_template/tasks/task-template.md +43 -0
  18. package/ai-dev-team/artifacts/ui.md +104 -0
  19. package/ai-dev-team/roles/analyzer.md +265 -0
  20. package/ai-dev-team/roles/developer.md +222 -0
  21. package/ai-dev-team/roles/documenter.md +715 -0
  22. package/ai-dev-team/roles/improver.md +461 -0
  23. package/ai-dev-team/roles/manager.md +544 -0
  24. package/ai-dev-team/roles/planner.md +398 -0
  25. package/ai-dev-team/roles/reviewer.md +294 -0
  26. package/ai-dev-team/rules/api-change.md +198 -0
  27. package/ai-dev-team/rules/document-priority.md +199 -0
  28. package/ai-dev-team/rules/escalation.md +172 -0
  29. package/ai-dev-team/rules/iteration.md +236 -0
  30. package/ai-dev-team/rules/rfc.md +31 -0
  31. package/ai-dev-team/rules/rollback.md +218 -0
  32. package/bin/cli.js +49 -5
  33. package/core/artifacts/sprints/_template/meta.md +4 -4
  34. package/core/docs-templates/mkdocs/docs/architecture/overview.md +29 -0
  35. package/core/docs-templates/mkdocs/docs/changelog.md +36 -0
  36. package/core/docs-templates/mkdocs/docs/contributing/contributing.md +60 -0
  37. package/core/docs-templates/mkdocs/docs/getting-started/configuration.md +51 -0
  38. package/core/docs-templates/mkdocs/docs/getting-started/installation.md +41 -0
  39. package/core/docs-templates/mkdocs/docs/getting-started/quick-start.md +56 -0
  40. package/core/docs-templates/mkdocs/docs/guides/api-reference.md +83 -0
  41. package/core/docs-templates/mkdocs/docs/index.md +32 -0
  42. package/core/docs-templates/mkdocs/mkdocs.yml +86 -0
  43. package/core/roles/analyzer.md +32 -10
  44. package/core/roles/developer.md +222 -223
  45. package/core/roles/documenter.md +592 -170
  46. package/core/roles/improver.md +461 -0
  47. package/core/roles/manager.md +4 -1
  48. package/core/roles/planner.md +160 -10
  49. package/core/roles/reviewer.md +31 -3
  50. package/core/rules/document-priority.md +2 -1
  51. package/core/rules/rollback.md +3 -3
  52. package/package.json +1 -1
  53. package/src/commands/config.js +371 -0
  54. package/src/commands/docs.js +502 -0
  55. package/src/commands/interactive.js +324 -33
  56. package/src/commands/monitor.js +236 -0
  57. package/src/commands/run.js +360 -122
  58. package/src/commands/sessions.js +270 -70
  59. package/src/commands/setup.js +22 -1
  60. package/src/commands/sprint.js +295 -54
  61. package/src/commands/status.js +34 -1
  62. package/src/commands/upgrade.js +416 -0
  63. package/src/commands/validate.js +4 -3
  64. package/src/index.js +1 -0
  65. package/src/ui/dashboard.js +518 -0
  66. package/src/ui/keyHandler.js +147 -0
  67. package/src/ui/quickActions.js +111 -0
  68. package/src/utils/config.js +74 -0
  69. package/src/utils/files.js +70 -3
  70. package/src/utils/sessionState.js +472 -328
  71. package/src/utils/sessionState.process.test.js +101 -0
  72. package/src/utils/sessionState.test.js +183 -0
  73. package/src/utils/sprintUtils.js +134 -0
  74. package/src/utils/taskParser.js +134 -0
  75. package/src/utils/taskParser.test.js +76 -0
  76. package/ai-dev-team/artifacts/features/_template/qa.md +0 -16
  77. package/examples/todo-app/README.md +0 -23
  78. package/examples/todo-app/artifacts/backlog.md +0 -23
  79. package/examples/todo-app/artifacts/plan.md +0 -23
  80. package/examples/todo-app/artifacts/project.md +0 -23
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import chalk from 'chalk';
5
+ import inquirer from 'inquirer';
5
6
  import {
6
7
  getWorkspaceDir,
7
8
  getSessionsDir,
@@ -15,33 +16,31 @@ import {
15
16
  import {
16
17
  registerSession,
17
18
  unregisterSession,
18
- updateSessionStatus,
19
+ updateSessionDetails,
19
20
  getActiveSessions,
20
21
  getPendingQuestions
21
22
  } from '../utils/sessionState.js';
22
-
23
- export async function run(role, tool) {
24
- if (!isWorkspaceSetup()) {
25
- console.log(chalk.red('❌ 먼저 setup을 실행하세요.'));
26
- console.log(chalk.gray(' ada setup'));
27
- process.exit(1);
28
- }
29
-
23
+ import { getToolForRole } from '../utils/config.js';
24
+
25
+ /**
26
+ * [API] AI 에이전트 세션 실행 (핵심 로직)
27
+ * @param {string} role - 실행할 역할 (예: 'developer')
28
+ * @param {string} tool - 사용할 도구 (예: 'claude')
29
+ * @param {object} options - 추가 옵션
30
+ * @returns {Promise<object>} 세션 결과 정보
31
+ */
32
+ export async function executeAgentSession(role, tool, options = {}) {
30
33
  const roles = getAvailableRoles();
31
34
  const tools = ['claude', 'codex', 'gemini', 'copilot'];
32
35
 
33
36
  // 역할 검증
34
37
  if (!roles.includes(role)) {
35
- console.log(chalk.red(`❌ 수 없는 역할: ${role}`));
36
- console.log(chalk.gray(`사용 가능: ${roles.join(', ')}`));
37
- process.exit(1);
38
+ throw new Error(`알 수 없는 역할입니다: ${role} (사용 가능: ${roles.join(', ')})`);
38
39
  }
39
40
 
40
41
  // 도구 검증
41
42
  if (!tools.includes(tool)) {
42
- console.log(chalk.red(`❌ 수 없는 도구: ${tool}`));
43
- console.log(chalk.gray(`사용 가능: ${tools.join(', ')}`));
44
- process.exit(1);
43
+ throw new Error(`알 수 없는 도구입니다: ${tool} (사용 가능: ${tools.join(', ')})`);
45
44
  }
46
45
 
47
46
  const workspace = getWorkspaceDir();
@@ -55,7 +54,7 @@ export async function run(role, tool) {
55
54
  fs.ensureDirSync(sessionDir);
56
55
  fs.ensureDirSync(logsDir);
57
56
 
58
- // 세션 정보 저장
57
+ // 세션 정보 객체
59
58
  const sessionInfo = {
60
59
  session_id: sessionId,
61
60
  role: role,
@@ -64,38 +63,199 @@ export async function run(role, tool) {
64
63
  started_at: getTimestamp(),
65
64
  status: 'active'
66
65
  };
67
- fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify(sessionInfo, null, 2));
66
+ const sessionFile = path.join(sessionDir, 'session.json');
67
+ fs.writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
68
68
 
69
- // 로그 파일 초기화
69
+ // 로그 헬퍼
70
70
  const logFile = path.join(logsDir, `${sessionId}.log`);
71
71
  const logMessage = (level, msg) => {
72
72
  const line = `[${getTimestamp()}] [${level}] ${msg}\n`;
73
73
  fs.appendFileSync(logFile, line);
74
+ // 옵션에 따라 콘솔 출력 제어 가능 (현재는 항상 출력)
74
75
  };
75
76
 
76
- logMessage('INFO', `세션 시작: role=${role}, tool=${tool}, template=${template}`);
77
+ // 세션 정리 함수 (시그널 핸들러 정상 종료에서 공통 사용)
78
+ let isCleanedUp = false;
79
+ const cleanupSession = (status, reason = null) => {
80
+ if (isCleanedUp) return;
81
+ isCleanedUp = true;
82
+
83
+ sessionInfo.status = status;
84
+ sessionInfo.ended_at = getTimestamp();
85
+ if (reason) {
86
+ sessionInfo.termination_reason = reason;
87
+ }
88
+
89
+ try {
90
+ fs.writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
91
+ logMessage('INFO', `세션 종료 (${status}): ${reason || '정상 종료'}`);
92
+ unregisterSession(sessionId);
93
+ } catch (err) {
94
+ // 정리 중 에러는 무시 (이미 종료 중)
95
+ }
96
+ };
77
97
 
78
- // 멀티 세션: 상태 파일에 세션 등록
79
- registerSession(sessionId, role, tool);
80
- logMessage('INFO', `세션 등록: ${sessionId}`);
98
+ // 시그널 핸들러 (Ctrl+C 강제 종료 시 세션 정리)
99
+ const handleSignal = (signal) => {
100
+ logMessage('INFO', `시그널 수신: ${signal}`);
101
+ cleanupSession('completed', `사용자 종료 (${signal})`);
81
102
 
82
- // 역할 파일 경로
83
- const roleFile = path.join(workspace, 'roles', `${role}.md`);
84
- const roleContent = fs.readFileSync(roleFile, 'utf-8');
103
+ // 핸들러 제거
104
+ process.removeListener('SIGINT', handleSignal);
105
+ process.removeListener('SIGTERM', handleSignal);
85
106
 
86
- // 시스템 프롬프트 생성
87
- const systemPrompt = buildSystemPrompt(workspace, role, roleContent);
107
+ // exitOnSignal 옵션이 false가 아니면 프로세스 종료 (기본값: true)
108
+ if (options.exitOnSignal !== false) {
109
+ setTimeout(() => {
110
+ process.exit(0);
111
+ }, 100);
112
+ }
113
+ };
114
+
115
+ // 시그널 핸들러 등록
116
+ process.on('SIGINT', handleSignal);
117
+ process.on('SIGTERM', handleSignal);
118
+
119
+ try {
120
+ logMessage('INFO', `세션 시작: role=${role}, tool=${tool}, template=${template}`);
121
+
122
+ // 멀티 세션 등록
123
+ registerSession(sessionId, role, tool);
124
+ logMessage('INFO', `세션 등록: ${sessionId}`);
125
+
126
+ // 역할 파일 로드 (옵션으로 오버라이드 가능)
127
+ let systemPrompt;
128
+ if (options.systemPromptOverride) {
129
+ systemPrompt = options.systemPromptOverride;
130
+ logMessage('INFO', '시스템 프롬프트 오버라이드 사용됨');
131
+ } else {
132
+ const roleFile = path.join(workspace, 'roles', `${role}.md`);
133
+ if (!fs.existsSync(roleFile)) {
134
+ throw new Error(`역할 파일이 존재하지 않습니다: ${roleFile}`);
135
+ }
136
+ const roleContent = fs.readFileSync(roleFile, 'utf-8');
137
+ systemPrompt = buildSystemPrompt(workspace, role, roleContent);
138
+ }
139
+
140
+ // 프롬프트 파일 저장
141
+ const promptFile = path.join(sessionDir, 'system-prompt.md');
142
+ fs.writeFileSync(promptFile, systemPrompt, 'utf-8');
143
+ logMessage('INFO', `시스템 프롬프트 저장: ${promptFile}`);
144
+
145
+ // 터미널 UI 출력 (Headless 모드가 아닐 때만)
146
+ if (!options.headless) {
147
+ printSessionBanner(role, tool, sessionId, template);
148
+ }
149
+
150
+ // AI 도구 프로세스 실행
151
+ const handleSpawn = (child) => {
152
+ sessionInfo.pid = child.pid;
153
+ fs.writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
154
+ updateSessionDetails(sessionId, { pid: child.pid });
155
+ };
156
+
157
+ const output = await launchTool(tool, systemPrompt, promptFile, logMessage, {
158
+ ...options,
159
+ onSpawn: handleSpawn
160
+ });
88
161
 
89
- // 시스템 프롬프트를 파일로 저장 (AI 도구가 읽을 수 있도록)
90
- const promptFile = path.join(sessionDir, 'system-prompt.md');
91
- fs.writeFileSync(promptFile, systemPrompt, 'utf-8');
92
- logMessage('INFO', `시스템 프롬프트 저장: ${promptFile}`);
162
+ // 시그널 핸들러 제거
163
+ process.removeListener('SIGINT', handleSignal);
164
+ process.removeListener('SIGTERM', handleSignal);
93
165
 
94
- // 다른 활성 세션 확인
166
+ // 정상 종료 처리
167
+ if (output) {
168
+ sessionInfo.output = output;
169
+ }
170
+ cleanupSession('completed');
171
+
172
+ // 캡처된 출력 반환
173
+ return { ...sessionInfo, output };
174
+
175
+ } catch (error) {
176
+ // 시그널 핸들러 제거
177
+ process.removeListener('SIGINT', handleSignal);
178
+ process.removeListener('SIGTERM', handleSignal);
179
+
180
+ // 에러가 사용자 종료(exit code 비정상)인 경우 completed로 처리
181
+ const isUserTermination = error.message && (
182
+ error.message.includes('exited with code 130') || // SIGINT
183
+ error.message.includes('exited with code 143') || // SIGTERM
184
+ error.message.includes('exited with code 1') // 일반 종료
185
+ );
186
+
187
+ if (isUserTermination) {
188
+ cleanupSession('completed', '사용자 종료');
189
+ return { ...sessionInfo };
190
+ }
191
+
192
+ // 실제 에러 처리
193
+ sessionInfo.error = error.message;
194
+ cleanupSession('error', error.message);
195
+ logMessage('ERROR', error.message);
196
+
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * [CLI] 실행 명령어 핸들러
203
+ * 사용자 입력을 처리하고 executeAgentSession을 호출
204
+ */
205
+ export async function runCommand(role, tool) {
206
+ if (!isWorkspaceSetup()) {
207
+ console.log(chalk.red('❌ 먼저 setup을 실행하세요.'));
208
+ console.log(chalk.gray(' ada setup'));
209
+ process.exit(1);
210
+ }
211
+
212
+ try {
213
+ // 1. 역할 선택 (입력 없으면 질문)
214
+ if (!role) {
215
+ const roles = getAvailableRoles();
216
+ const answer = await inquirer.prompt([
217
+ {
218
+ type: 'list',
219
+ name: 'role',
220
+ message: '실행할 역할을 선택하세요:',
221
+ choices: roles
222
+ }
223
+ ]);
224
+ role = answer.role;
225
+ }
226
+
227
+ // 2. 도구 자동 선택 (입력 없으면 설정 파일 참조)
228
+ if (!tool) {
229
+ tool = getToolForRole(role);
230
+ console.log(chalk.gray(`ℹ️ 설정된 기본 도구를 사용합니다: ${tool}`));
231
+ }
232
+
233
+ // 3. 세션 실행
234
+ await executeAgentSession(role, tool);
235
+
236
+ } catch (error) {
237
+ console.error(chalk.red('\n❌ 실행 중 오류가 발생했습니다:'));
238
+ console.error(chalk.white(error.message));
239
+ process.exit(1);
240
+ }
241
+ }
242
+
243
+ // 기존 CLI 호환성을 위해 run이라는 이름으로 export
244
+ export { runCommand as run };
245
+ // 시스템 프롬프트 생성 로직도 외부에서 쓸 수 있게 export
246
+ export { buildSystemPrompt };
247
+
248
+
249
+ // ============================================================================
250
+ // 내부 헬퍼 함수들
251
+ // ============================================================================
252
+
253
+ function printSessionBanner(role, tool, sessionId, template) {
254
+ // 다른 활성 세션 정보
95
255
  const activeSessions = getActiveSessions().filter(s => s.sessionId !== sessionId);
96
256
  const pendingQuestions = getPendingQuestions();
97
257
 
98
- // 터미널 타이틀 설정
258
+ // 터미널 타이틀
99
259
  const terminalTitle = `ADA: ${role} (${tool})`;
100
260
  process.stdout.write(`\x1b]0;${terminalTitle}\x07`);
101
261
 
@@ -105,16 +265,15 @@ export async function run(role, tool) {
105
265
  console.log(chalk.cyan('━'.repeat(60)));
106
266
  console.log('');
107
267
 
108
- // 역할 강조 표시
109
268
  const roleEmojis = {
110
269
  'analyzer': '🔍',
111
270
  'planner': '📋',
271
+ 'improver': '🔧',
112
272
  'architect': '🏛️',
113
273
  'developer': '💻',
114
274
  'backend': '⚙️',
115
275
  'frontend': '🎨',
116
276
  'reviewer': '👀',
117
- 'qa': '🧪',
118
277
  'manager': '👔',
119
278
  'library-developer': '📚',
120
279
  'game-logic': '🎮',
@@ -130,10 +289,9 @@ export async function run(role, tool) {
130
289
  console.log(chalk.white(` 템플릿: ${chalk.green(template)}`));
131
290
  console.log(chalk.white(` 도구: ${chalk.green(tool)}`));
132
291
  console.log(chalk.white(` 작업공간: ${chalk.gray('ai-dev-team/')}`));
133
- console.log(chalk.white(` 로그: ${chalk.gray(`.sessions/logs/${sessionId}.log`)}`));
292
+ console.log(chalk.white(` 로그: ${chalk.gray('.sessions/logs/' + sessionId + '.log')}`));
134
293
  console.log('');
135
294
 
136
- // 멀티 세션 정보 표시
137
295
  if (activeSessions.length > 0) {
138
296
  console.log(chalk.white(` 🔗 활성 세션: ${chalk.yellow(activeSessions.length)}개`));
139
297
  activeSessions.forEach(s => {
@@ -149,32 +307,6 @@ export async function run(role, tool) {
149
307
 
150
308
  console.log(chalk.cyan('━'.repeat(60)));
151
309
  console.log('');
152
-
153
- // 도구별 실행
154
- try {
155
- await launchTool(tool, systemPrompt, promptFile, logMessage);
156
-
157
- // 세션 완료 처리
158
- sessionInfo.status = 'completed';
159
- sessionInfo.ended_at = getTimestamp();
160
- fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify(sessionInfo, null, 2));
161
- logMessage('INFO', '세션 종료');
162
-
163
- // 멀티 세션: 상태 파일에서 세션 제거
164
- unregisterSession(sessionId);
165
- logMessage('INFO', `세션 해제: ${sessionId}`);
166
- } catch (error) {
167
- sessionInfo.status = 'error';
168
- sessionInfo.error = error.message;
169
- fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify(sessionInfo, null, 2));
170
- logMessage('ERROR', error.message);
171
-
172
- // 멀티 세션: 에러 시에도 세션 제거
173
- unregisterSession(sessionId);
174
- logMessage('INFO', `세션 해제 (에러): ${sessionId}`);
175
-
176
- throw error;
177
- }
178
310
  }
179
311
 
180
312
  function buildSystemPrompt(workspace, role, roleContent) {
@@ -185,21 +317,40 @@ function buildSystemPrompt(workspace, role, roleContent) {
185
317
  prompt += roleContent;
186
318
  prompt += '\n\n---\n\n';
187
319
 
188
- // 1. 규칙 문서 전체 포함 (규칙은 반드시 알아야 함)
320
+ // 1. 규칙 문서 - 역할별 필수 규칙만 포함
321
+ const roleRules = {
322
+ planner: ['iteration.md', 'escalation.md', 'document-priority.md'],
323
+ improver: ['iteration.md', 'escalation.md', 'document-priority.md', 'rfc.md'],
324
+ developer: ['iteration.md', 'escalation.md', 'rollback.md', 'document-priority.md', 'rfc.md'],
325
+ reviewer: ['iteration.md', 'rollback.md', 'escalation.md', 'document-priority.md'],
326
+ documenter: ['escalation.md', 'document-priority.md'],
327
+ analyzer: ['escalation.md', 'document-priority.md'],
328
+ manager: ['escalation.md', 'document-priority.md', 'rfc.md'] // Manager는 모든 규칙 참고
329
+ };
330
+
331
+ const requiredRules = roleRules[role] || [];
332
+
189
333
  prompt += '# 규칙 (Rules)\n\n';
190
- if (fs.existsSync(rulesDir)) {
191
- const rules = fs.readdirSync(rulesDir).filter(f => f.endsWith('.md'));
192
- rules.forEach(ruleFile => {
334
+ prompt += `이 역할에 적용되는 필수 규칙: ${requiredRules.join(', ')}\n\n`;
335
+
336
+ if (fs.existsSync(rulesDir) && requiredRules.length > 0) {
337
+ requiredRules.forEach(ruleFile => {
193
338
  const rulePath = path.join(rulesDir, ruleFile);
194
- try {
195
- const content = fs.readFileSync(rulePath, 'utf-8');
196
- prompt += `## ${ruleFile}\n\n`;
197
- prompt += content;
198
- prompt += '\n\n---\n\n';
199
- } catch (err) {
200
- prompt += `## ${ruleFile} (읽기 실패)\n\n`;
339
+ if (fs.existsSync(rulePath)) {
340
+ try {
341
+ const content = fs.readFileSync(rulePath, 'utf-8');
342
+ prompt += `## ${ruleFile}\n\n`;
343
+ prompt += content;
344
+ prompt += '\n\n---\n\n';
345
+ } catch (err) {
346
+ prompt += `## ${ruleFile} (읽기 실패)\n\n`;
347
+ }
348
+ } else {
349
+ prompt += `## ${ruleFile} (파일 없음)\n\n`;
201
350
  }
202
351
  });
352
+ } else if (requiredRules.length === 0) {
353
+ prompt += '(이 역할에 필수 규칙이 지정되지 않았습니다)\n\n';
203
354
  }
204
355
 
205
356
  // 2. 핵심 산출물 전체 포함 (우선순위 높은 문서)
@@ -228,6 +379,8 @@ function buildSystemPrompt(workspace, role, roleContent) {
228
379
  });
229
380
 
230
381
  // 2.1 현재 활성 스프린트 포함
382
+ prompt += '# 현재 스프린트 정보\n\n';
383
+
231
384
  const sprintsDir = path.join(artifactsDir, 'sprints');
232
385
  if (fs.existsSync(sprintsDir)) {
233
386
  const sprints = fs.readdirSync(sprintsDir, { withFileTypes: true })
@@ -256,22 +409,65 @@ function buildSystemPrompt(workspace, role, roleContent) {
256
409
  }
257
410
  }
258
411
 
259
- // 스프린트의 Task 파일 목록
412
+ // 스프린트의 Task 파일 전체 포함
260
413
  const sprintTasksDir = path.join(sprintsDir, activeSprint, 'tasks');
261
414
  if (fs.existsSync(sprintTasksDir)) {
262
415
  const taskFiles = fs.readdirSync(sprintTasksDir)
263
416
  .filter(f => f.endsWith('.md') && !f.includes('template'));
264
417
 
265
418
  if (taskFiles.length > 0) {
266
- prompt += `## 현재 스프린트 Task 파일 목록\n\n`;
267
- prompt += `다음 Task 파일들을 필요 시 읽어서 확인하세요:\n`;
419
+ prompt += `## 현재 스프린트 Task 파일들\n\n`;
420
+
421
+ // 각 Task 파일 내용 포함
268
422
  taskFiles.forEach(f => {
269
- prompt += `- sprints/${activeSprint}/tasks/${f}\n`;
423
+ const taskPath = path.join(sprintTasksDir, f);
424
+ try {
425
+ const taskContent = fs.readFileSync(taskPath, 'utf-8');
426
+ prompt += `### ${f}\n\n`;
427
+ prompt += taskContent;
428
+ prompt += '\n\n---\n\n';
429
+ } catch (err) {
430
+ prompt += `### ${f} (읽기 실패)\n\n`;
431
+ }
270
432
  });
271
- prompt += '\n---\n\n';
433
+ } else {
434
+ // Task 파일이 없는 경우
435
+ prompt += `## ⚠️ 스프린트에 Task 없음\n\n`;
436
+ prompt += `현재 스프린트(${activeSprint})에 할당된 Task가 없습니다.\n\n`;
437
+ prompt += '**다음 단계:**\n';
438
+ prompt += '1. `ada sprint add task-001 task-002` 명령으로 Task 할당\n';
439
+ prompt += '2. Developer 세션 재시작\n\n';
440
+ prompt += '---\n\n';
272
441
  }
442
+ } else {
443
+ // tasks 디렉토리가 없는 경우
444
+ prompt += `## ⚠️ tasks 디렉토리 없음\n\n`;
445
+ prompt += `현재 스프린트(${activeSprint})에 tasks 디렉토리가 없습니다.\n\n`;
446
+ prompt += '스프린트 구조가 올바르지 않습니다. `ada sprint create` 명령으로 재생성하세요.\n\n';
447
+ prompt += '---\n\n';
273
448
  }
449
+ } else {
450
+ // 스프린트가 없는 경우
451
+ prompt += '## ⚠️ 현재 활성 스프린트 없음\n\n';
452
+ prompt += '스프린트가 아직 생성되지 않았습니다.\n\n';
453
+ prompt += '**다음 단계:**\n';
454
+ prompt += '1. Planner가 plan.md와 backlog/ Task를 작성했는지 확인\n';
455
+ prompt += '2. `ada sprint create` 명령으로 스프린트 생성\n';
456
+ prompt += '3. `ada sprint add task-001 task-002` 명령으로 Task 할당\n';
457
+ prompt += '4. Developer 세션 재시작\n\n';
458
+ prompt += '**참고:** Developer는 스프린트가 있어야 작업할 수 있습니다.\n';
459
+ prompt += '스프린트 없이는 어떤 Task를 해야 할지 알 수 없습니다.\n\n';
460
+ prompt += '---\n\n';
274
461
  }
462
+ } else {
463
+ // sprints 디렉토리 자체가 없는 경우
464
+ prompt += '## ⚠️ sprints 디렉토리 없음\n\n';
465
+ prompt += 'sprints 디렉토리가 존재하지 않습니다.\n\n';
466
+ prompt += '**다음 단계:**\n';
467
+ prompt += '1. `ada sprint create` 명령으로 첫 스프린트 생성\n';
468
+ prompt += '2. Task를 스프린트에 추가\n';
469
+ prompt += '3. Developer 세션 재시작\n\n';
470
+ prompt += '---\n\n';
275
471
  }
276
472
 
277
473
  // 2.2 Backlog Task 목록
@@ -386,7 +582,7 @@ function buildSystemPrompt(workspace, role, roleContent) {
386
582
  return prompt;
387
583
  }
388
584
 
389
- async function launchTool(tool, systemPrompt, promptFile, logMessage) {
585
+ async function launchTool(tool, systemPrompt, promptFile, logMessage, options = {}) {
390
586
  // 프롬프트 파일의 상대 경로 (작업 디렉토리 기준)
391
587
  const relativePromptPath = path.relative(process.cwd(), promptFile);
392
588
 
@@ -420,76 +616,118 @@ async function launchTool(tool, systemPrompt, promptFile, logMessage) {
420
616
  };
421
617
 
422
618
  const config = commands[tool];
423
- const { cmd, args } = config;
619
+ let { cmd, args } = config;
620
+ let usePromptStdin = false;
621
+ let promptInput = null;
622
+
623
+ if (options.captureOutput) {
624
+ // captureOutput 모드일 때 Claude는 --print 옵션 필요
625
+ if (tool === 'claude') {
626
+ args = ['--print', '-p', '위 시스템 프롬프트의 지시에 따라 JSON으로 응답하세요.', '--system-prompt-file', promptFile];
627
+ }
424
628
 
425
- // 도구 존재 확인
426
- const which = spawn('which', [cmd], { shell: true });
629
+ // codex exec는 프롬프트를 stdin으로 받는다
630
+ if (tool === 'codex') {
631
+ args = ['exec', '-'];
632
+ usePromptStdin = true;
633
+ promptInput = systemPrompt;
634
+ }
635
+
636
+ // gemini는 Stdin으로 프롬프트 전달 (에러 해결: No input provided via stdin)
637
+ if (tool === 'gemini') {
638
+ const userPrompt = '위 시스템 프롬프트의 지시에 따라 JSON으로 응답하세요.';
639
+ args = ['-o', 'text'];
640
+ usePromptStdin = true;
641
+ promptInput = userPrompt;
642
+ }
643
+ }
644
+
645
+ // 도구 존재 확인 (Windows: where, Unix: which)
646
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
647
+ const which = spawn(whichCmd, [cmd], { shell: true });
427
648
 
428
649
  return new Promise((resolve, reject) => {
429
650
  which.on('close', (code) => {
430
651
  if (code !== 0) {
431
652
  console.log(chalk.yellow(`⚠️ ${tool} CLI가 설치되어 있지 않습니다.`));
432
- console.log('');
433
- console.log(chalk.white('시스템 프롬프트가 다음 파일에 저장되었습니다:'));
434
- console.log(chalk.cyan(` ${relativePromptPath}`));
435
- console.log('');
436
- console.log(chalk.gray('─'.repeat(60)));
437
- console.log(systemPrompt);
438
- console.log(chalk.gray('─'.repeat(60)));
439
- console.log('');
440
- console.log(chalk.gray('위 내용을 복사하여 AI 도구에 붙여넣거나, 파일을 읽도록 하세요.'));
441
653
  logMessage('WARN', `${tool} CLI not found, prompt displayed`);
442
- resolve();
654
+ resolve(null);
443
655
  return;
444
656
  }
445
657
 
446
- // 도구별 안내 메시지
447
- console.log('');
448
- if (config.automation === 'perfect') {
449
- // 완전 자동화: 간단한 성공 메시지
450
- console.log(chalk.green('━'.repeat(60)));
451
- console.log(chalk.green.bold('✓ 역할이 자동으로 설정됩니다'));
452
- console.log(chalk.green('━'.repeat(60)));
453
- console.log('');
454
- console.log(chalk.gray(`시스템 프롬프트: ${relativePromptPath}`));
658
+ if (!options.captureOutput) {
455
659
  console.log('');
456
- } else {
457
- // 수동 입력 필요: 명확한 안내
458
- console.log(chalk.yellow(''.repeat(60)));
459
- console.log(chalk.yellow.bold('⚠️ 중요: AI 도구 시작 후 다음을 입력하세요'));
460
- console.log(chalk.yellow(''.repeat(60)));
461
- console.log('');
462
- console.log(chalk.cyan.bold(` ${config.instruction}`));
463
- console.log('');
464
- console.log(chalk.gray('그 다음 Enter를 눌러 역할을 수행하도록 하세요.'));
465
- console.log('');
466
- console.log(chalk.yellow('━'.repeat(60)));
660
+ if (config.automation === 'perfect') {
661
+ console.log(chalk.green('━'.repeat(60)));
662
+ console.log(chalk.green.bold('✓ 역할이 자동으로 설정됩니다'));
663
+ console.log(chalk.green(''.repeat(60)));
664
+ console.log('');
665
+ console.log(chalk.gray(`시스템 프롬프트: ${relativePromptPath}`));
666
+ console.log('');
667
+ } else if (config.automation === 'manual') {
668
+ console.log(chalk.yellow(''.repeat(60)));
669
+ console.log(chalk.yellow.bold('⚠️ 수동 설정이 필요합니다'));
670
+ console.log(chalk.yellow('━'.repeat(60)));
671
+ console.log('');
672
+ console.log('CLI가 실행되면 아래 명령어를 복사해서 입력하세요:');
673
+ console.log('');
674
+ console.log(chalk.bgWhite.black.bold(` ${config.instruction} `));
675
+ console.log('');
676
+ }
677
+ console.log(chalk.green(`✓ ${tool} 실행 중...`));
467
678
  console.log('');
468
679
  }
469
680
 
470
- // CLI 실행
471
- console.log(chalk.green(`✓ ${tool} 실행 중...`));
472
- console.log('');
473
- logMessage('INFO', `${tool} CLI 실행 (automation: ${config.automation})`);
681
+ logMessage('INFO', `${tool} CLI 실행 (automation: ${config.automation}, captureOutput: ${!!options.captureOutput})`);
474
682
 
475
- // 환경 변수 병합 (도구별 커스텀 환경 변수 포함)
683
+ // 환경 변수 병합
476
684
  const envVars = {
477
685
  ...process.env,
478
686
  ADA_SYSTEM_PROMPT: systemPrompt,
479
- ...(config.env || {}) // 도구별 환경 변수 추가
687
+ ...(config.env || {})
480
688
  };
481
689
 
690
+ // 캡처 모드에 따라 stdio 설정 변경
691
+ const stdioConfig = options.captureOutput
692
+ ? (usePromptStdin ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'])
693
+ : 'inherit';
694
+
482
695
  const child = spawn(cmd, args, {
483
- stdio: 'inherit',
696
+ stdio: stdioConfig,
484
697
  shell: true,
485
698
  env: envVars
486
699
  });
487
700
 
701
+ if (typeof options.onSpawn === 'function') {
702
+ options.onSpawn(child);
703
+ }
704
+
705
+ let capturedOutput = '';
706
+ let capturedError = '';
707
+
708
+ if (options.captureOutput) {
709
+ child.stdout.on('data', (data) => {
710
+ capturedOutput += data.toString();
711
+ });
712
+ child.stderr.on('data', (data) => {
713
+ capturedError += data.toString();
714
+ });
715
+ }
716
+
717
+ if (options.captureOutput && usePromptStdin && child.stdin) {
718
+ const input = promptInput || systemPrompt;
719
+ child.stdin.write(input);
720
+ child.stdin.end();
721
+ }
722
+
488
723
  child.on('close', (code) => {
489
724
  if (code === 0) {
490
- resolve();
725
+ resolve(options.captureOutput ? capturedOutput : null);
491
726
  } else {
492
- reject(new Error(`${tool} exited with code ${code}`));
727
+ const errorMsg = options.captureOutput
728
+ ? `${tool} exited with code ${code}. Stderr: ${capturedError}`
729
+ : `${tool} exited with code ${code}`;
730
+ reject(new Error(errorMsg));
493
731
  }
494
732
  });
495
733