@silbaram/artifact-driven-agent 0.1.7 → 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 +220 -41
  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 +367 -182
  58. package/src/commands/sessions.js +633 -694
  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 +470 -336
  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
@@ -1,707 +1,646 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import readline from 'readline';
5
- import { spawnSync } from 'child_process';
6
- import { getSessionsDir, isWorkspaceSetup, getWorkspaceDir, getPackageRoot } from '../utils/files.js';
7
- import {
8
- getActiveSessions,
9
- getPendingQuestions,
10
- readStatus,
11
- getStatusFilePath,
12
- cleanupZombieSessions,
13
- answerQuestion
14
- } from '../utils/sessionState.js';
15
-
16
- export async function sessions(options = {}) {
17
- if (!isWorkspaceSetup()) {
18
- console.log(chalk.red('? 먼저 setup을 실행하세요.'));
19
- process.exit(1);
20
- }
21
-
22
- // Watch 모드
23
- if (options.watch) {
24
- return watchSessions();
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import readline from 'readline';
5
+ import { getSessionsDir, isWorkspaceSetup, getWorkspaceDir } from '../utils/files.js';
6
+ import {
7
+ getActiveSessions,
8
+ getPendingQuestions,
9
+ readStatus,
10
+ getStatusFilePath,
11
+ cleanupZombieSessions,
12
+ answerQuestion
13
+ } from '../utils/sessionState.js';
14
+
15
+ export async function sessions(options = {}) {
16
+ if (!isWorkspaceSetup()) {
17
+ console.log(chalk.red('? 먼저 setup을 실행하세요.'));
18
+ process.exit(1);
19
+ }
20
+
21
+ // Watch 모드
22
+ if (options.watch) {
23
+ return watchSessions();
24
+ }
25
+
26
+ // Clean 모드
27
+ if (options.clean) {
28
+ return cleanupCompletedSessions();
25
29
  }
26
-
30
+
31
+ const sessionsDir = getSessionsDir();
32
+
33
+ // 좀비 세션 정리 (비활성/오래된 세션)
34
+ const removedCount = cleanupZombieSessions(60);
35
+
36
+ console.log('');
37
+ console.log(chalk.cyan('━'.repeat(60)));
38
+ console.log(chalk.cyan.bold('세션 상태'));
39
+ console.log(chalk.cyan('━'.repeat(60)));
40
+ console.log('');
41
+
42
+ if (removedCount > 0) {
43
+ console.log(chalk.yellow(`좀비 세션 ${removedCount}개 정리됨 (비활성/오래됨)`));
44
+ console.log('');
45
+ }
46
+
47
+ // 1. 실시간 활성 세션 표시
48
+ const activeSessions = getActiveSessions();
49
+ if (activeSessions.length > 0) {
50
+ console.log(chalk.yellow.bold('활성 세션 (실시간)'));
51
+ console.log('');
52
+ console.log(chalk.gray(' 역할 도구 시작 시간 상태'));
53
+ console.log(chalk.gray(' ' + '─'.repeat(56)));
54
+
55
+ activeSessions.forEach(session => {
56
+ const role = (session.role || '-').padEnd(10);
57
+ const tool = (session.tool || '-').padEnd(8);
58
+ const startTime = new Date(session.startedAt).toLocaleString('ko-KR');
59
+ const status = session.status || 'active';
60
+ const statusIcon = status === 'active' ? '+' : '-';
61
+
62
+ console.log(` ${role} ${tool} ${startTime} ${statusIcon} ${status}`);
63
+ });
64
+ console.log('');
65
+ } else {
66
+ console.log(chalk.gray(' 현재 활성 세션 없음'));
67
+ console.log('');
68
+ }
69
+
70
+ // 2. 대기 중인 질문 표시
71
+ const pendingQuestions = getPendingQuestions();
72
+ if (pendingQuestions.length > 0) {
73
+ console.log(chalk.yellow.bold('대기 질문'));
74
+ console.log('');
75
+
76
+ pendingQuestions.forEach(q => {
77
+ console.log(chalk.yellow(` [${q.id}] ${q.from} → ${q.to}`));
78
+ console.log(chalk.white(` 질문: ${q.question}`));
79
+ if (q.options && q.options.length > 0) {
80
+ console.log(chalk.gray(` 옵션: ${q.options.join(', ')}`));
81
+ }
82
+ console.log('');
83
+ });
84
+ }
85
+
86
+ // 3. Task 진행 상황 표시
87
+ const status = readStatus();
88
+ const taskProgress = status.taskProgress || {};
89
+ const activeTasks = Object.entries(taskProgress).filter(([_, info]) => {
90
+ const s = (info.status || '').toUpperCase();
91
+ return s && s !== 'DONE' && s !== 'REJECTED';
92
+ });
93
+
94
+ if (activeTasks.length > 0) {
95
+ console.log(chalk.cyan.bold('진행 중인 Task'));
96
+ console.log('');
97
+
98
+ activeTasks.forEach(([taskId, info]) => {
99
+ const normalizedStatus = info.status === 'IN_QA' ? 'IN_REVIEW' : info.status;
100
+ const statusColor = normalizedStatus === 'DONE' ? chalk.green :
101
+ normalizedStatus === 'IN_DEV' ? chalk.yellow :
102
+ normalizedStatus === 'IN_REVIEW' ? chalk.blue :
103
+ chalk.white;
104
+
105
+ console.log(` ${taskId}: [${statusColor(normalizedStatus)}]`);
106
+ if (info.assignee) {
107
+ console.log(chalk.gray(` 담당: ${info.assignee}`));
108
+ }
109
+ if (info.note) {
110
+ console.log(chalk.gray(` 메모: ${info.note}`));
111
+ }
112
+ });
113
+ console.log('');
114
+ }
115
+
116
+ console.log(chalk.cyan('━'.repeat(60)));
117
+ console.log('');
118
+
119
+ // 4. 최근 세션 기록 (히스토리)
120
+ if (!fs.existsSync(sessionsDir)) {
121
+ console.log(chalk.gray(' 세션 기록 없음'));
122
+ console.log('');
123
+ return;
124
+ }
125
+
126
+ const sessionDirs = fs.readdirSync(sessionsDir)
127
+ .filter(f => fs.statSync(path.join(sessionsDir, f)).isDirectory())
128
+ .sort()
129
+ .reverse();
130
+
131
+ if (sessionDirs.length === 0) {
132
+ console.log(chalk.gray(' 세션 기록 없음'));
133
+ console.log('');
134
+ return;
135
+ }
136
+
137
+ console.log(chalk.cyan.bold('최근 세션 기록'));
138
+ console.log('');
139
+ console.log(chalk.gray(' 세션 ID 역할 도구 상태'));
140
+ console.log(chalk.gray(' ' + '─'.repeat(56)));
141
+
142
+ for (const sessionId of sessionDirs.slice(0, 10)) {
143
+ const sessionFile = path.join(sessionsDir, sessionId, 'session.json');
144
+
145
+ if (fs.existsSync(sessionFile)) {
146
+ try {
147
+ const session = JSON.parse(fs.readFileSync(sessionFile, 'utf-8'));
148
+ const role = (session.role || '-').padEnd(10);
149
+ const tool = (session.tool || '-').padEnd(8);
150
+ const status = session.status || 'unknown';
151
+
152
+ const statusColor = status === 'completed' ? chalk.green :
153
+ status === 'active' ? chalk.yellow :
154
+ status === 'error' ? chalk.red :
155
+ chalk.gray;
156
+
157
+ console.log(` ${sessionId} ${role} ${tool} ${statusColor(status)}`);
158
+ } catch (e) {
159
+ console.log(` ${sessionId} ${chalk.gray('(읽기 실패)')}`);
160
+ }
161
+ } else {
162
+ console.log(` ${sessionId} ${chalk.gray('(정보 없음)')}`);
163
+ }
164
+ }
165
+
166
+ console.log('');
167
+
168
+ if (sessionDirs.length > 10) {
169
+ console.log(chalk.gray(` ... 그 외 ${sessionDirs.length - 10}개 세션 (ada logs 명령어로 확인)`));
170
+ console.log('');
171
+ }
172
+ }
173
+
174
+ // Watch 모드: 실시간 세션 모니터링
175
+ async function watchSessions() {
176
+ const statusFile = getStatusFilePath();
177
+ let lastUpdate = '';
178
+ let isWatching = true;
179
+ let isPrompting = false;
180
+ const promptQueue = [];
181
+ const promptedQuestions = new Set();
182
+
183
+ function pauseWatch() {
184
+ isWatching = false;
185
+ }
186
+
187
+ function resumeWatch() {
188
+ isWatching = true;
189
+ drawScreen();
190
+ lastUpdate = new Date().toLocaleTimeString('ko-KR');
191
+ }
192
+
193
+ function disableKeypressHandling() {
194
+ if (process.stdin.isTTY) {
195
+ process.stdin.setRawMode(false);
196
+ }
197
+ process.stdin.removeListener('keypress', keyHandler);
198
+ }
199
+
200
+ function enableKeypressHandling() {
201
+ if (process.stdin.isTTY) {
202
+ readline.emitKeypressEvents(process.stdin);
203
+ process.stdin.setRawMode(true);
204
+ }
205
+ process.stdin.on('keypress', keyHandler);
206
+ }
207
+
208
+ function enqueueQuestions(questions) {
209
+ questions.forEach(question => {
210
+ if (!promptedQuestions.has(question.id)) {
211
+ promptQueue.push(question);
212
+ promptedQuestions.add(question.id);
213
+ }
214
+ });
215
+ }
216
+
217
+ async function processPromptQueue() {
218
+ if (isPrompting || promptQueue.length === 0) {
219
+ return;
220
+ }
221
+
222
+ isPrompting = true;
223
+ pauseWatch();
224
+ disableKeypressHandling();
225
+
226
+ while (promptQueue.length > 0) {
227
+ const nextQuestion = promptQueue.shift();
228
+ await promptQuestion(nextQuestion);
229
+ }
230
+
231
+ enableKeypressHandling();
232
+ isPrompting = false;
233
+ resumeWatch();
234
+ }
235
+
236
+ async function promptQuestion(question) {
237
+ const status = readStatus();
238
+ const currentQuestion = status.pendingQuestions?.find(q => q.id === question.id);
239
+
240
+ if (!currentQuestion || currentQuestion.status !== 'waiting') {
241
+ return;
242
+ }
243
+
244
+ console.log('');
245
+ console.log(chalk.yellow('━'.repeat(60)));
246
+ console.log(chalk.yellow.bold('질문 응답 필요'));
247
+ console.log(chalk.yellow('━'.repeat(60)));
248
+ console.log(chalk.white(` ID: ${currentQuestion.id}`));
249
+ console.log(chalk.white(` 요청: ${currentQuestion.from} → ${currentQuestion.to}`));
250
+ console.log(chalk.white(` 질문: ${currentQuestion.question}`));
251
+
252
+ if (currentQuestion.options && currentQuestion.options.length > 0) {
253
+ console.log(chalk.gray(' 옵션:'));
254
+ currentQuestion.options.forEach((option, index) => {
255
+ console.log(chalk.gray(` ${index + 1}) ${option}`));
256
+ });
257
+ }
258
+
259
+ const promptText = currentQuestion.options && currentQuestion.options.length > 0
260
+ ? ' 답변(번호 또는 직접 입력): '
261
+ : ' 답변: ';
262
+
263
+ const answerInput = await askInput(chalk.cyan(promptText));
264
+ let answer = (answerInput || '').trim();
265
+
266
+ if (currentQuestion.options && currentQuestion.options.length > 0) {
267
+ const optionIndex = Number.parseInt(answer, 10);
268
+ if (!Number.isNaN(optionIndex) && optionIndex >= 1 && optionIndex <= currentQuestion.options.length) {
269
+ answer = currentQuestion.options[optionIndex - 1];
270
+ }
271
+ }
272
+
273
+ if (!answer) {
274
+ answer = '(응답 없음)';
275
+ }
276
+
277
+ answerQuestion(currentQuestion.id, answer);
278
+
279
+ // 처리 완료 메시지
280
+ console.log(chalk.green(` ✓ 저장 완료: ${currentQuestion.id}`));
281
+ console.log(chalk.gray(` ✓ 관련 알림 읽음 처리됨`));
282
+ console.log('');
283
+ }
284
+
285
+ function askInput(prompt) {
286
+ return new Promise(resolve => {
287
+ const rl = readline.createInterface({
288
+ input: process.stdin,
289
+ output: process.stdout
290
+ });
291
+
292
+ rl.question(prompt, answer => {
293
+ rl.close();
294
+ resolve(answer);
295
+ });
296
+ });
297
+ }
298
+
299
+ // 화면 그리기 함수
300
+ function drawScreen() {
301
+ console.clear();
302
+
303
+ const now = new Date();
304
+ const timeString = now.toLocaleTimeString('ko-KR');
305
+ let pendingQuestions = [];
306
+
307
+ // 헤더
308
+ console.log('');
309
+ console.log(chalk.cyan('┌' + '─'.repeat(78) + '┐'));
310
+ console.log(chalk.cyan('│') + chalk.bold.white(' Manager Watch Mode'.padEnd(78)) + chalk.cyan('│'));
311
+ console.log(chalk.cyan('│') + chalk.gray(` 시간: ${timeString}`.padEnd(78)) + chalk.cyan('│'));
312
+ console.log(chalk.cyan('└' + '─'.repeat(78) + '┘'));
313
+ console.log('');
314
+
315
+ try {
316
+ const status = readStatus();
317
+ const activeSessions = status.activeSessions || [];
318
+ pendingQuestions = status.pendingQuestions?.filter(q => q.status === 'waiting') || [];
319
+ const taskProgress = status.taskProgress || {};
320
+ const notifications = status.notifications || [];
321
+
322
+ // 통계 패널
323
+ console.log(chalk.bgBlue.white.bold(' 통계 '));
324
+ console.log('');
325
+ console.log(chalk.white(` 활성 세션: ${chalk.yellow(activeSessions.length)}개`));
326
+ console.log(chalk.white(` 대기 질문: ${pendingQuestions.length > 0 ? chalk.red(pendingQuestions.length) : chalk.green('0')}개`));
327
+
328
+ const activeTasks = Object.keys(taskProgress).filter(
329
+ taskId => taskProgress[taskId].status !== 'DONE'
330
+ );
331
+ console.log(chalk.white(` 진행 Task: ${chalk.cyan(activeTasks.length)}개`));
332
+ console.log(chalk.white(` 읽지 않은 알림: ${notifications.filter(n => !n.read).length}개`));
333
+ console.log('');
334
+
335
+ // 활성 세션
336
+ if (activeSessions.length > 0) {
337
+ console.log(chalk.bgGreen.black.bold(' 활성 세션 '));
338
+ console.log('');
339
+
340
+ activeSessions.forEach((session, index) => {
341
+ const startTime = new Date(session.startedAt);
342
+ const duration = Math.floor((now - startTime) / 1000 / 60); // 분
343
+ const statusIcon = session.status === 'active' ? '+' : '-';
344
+
345
+ console.log(chalk.white(` ${index + 1}. ${statusIcon} ${chalk.bold(session.role)}`));
346
+ console.log(chalk.gray(` 도구: ${session.tool}`));
347
+ console.log(chalk.gray(` 실행 시간: ${duration}분`));
348
+
349
+ if (index < activeSessions.length - 1) {
350
+ console.log('');
351
+ }
352
+ });
353
+ console.log('');
354
+ } else {
355
+ console.log(chalk.gray(' 현재 활성 세션이 없습니다.'));
356
+ console.log('');
357
+ }
358
+
359
+ // 대기 질문 (강조)
360
+ if (pendingQuestions.length > 0) {
361
+ console.log(chalk.bgYellow.black.bold(' 대기 질문 '));
362
+ console.log('');
363
+
364
+ pendingQuestions.slice(0, 3).forEach((q, index) => {
365
+ console.log(chalk.yellow(` [${q.id}] ${q.from} → ${q.to}`));
366
+ console.log(chalk.white(` 질문: ${q.question}`));
367
+
368
+ if (q.options && q.options.length > 0) {
369
+ console.log(chalk.gray(` 옵션: ${q.options.join(' / ')}`));
370
+ }
371
+
372
+ const elapsed = Math.floor((now - new Date(q.createdAt)) / 1000 / 60);
373
+ console.log(chalk.gray(` 대기 시간: ${elapsed}분`));
374
+
375
+ if (index < Math.min(pendingQuestions.length, 3) - 1) {
376
+ console.log('');
377
+ }
378
+ });
379
+
380
+ if (pendingQuestions.length > 3) {
381
+ console.log('');
382
+ console.log(chalk.gray(` ... 그 외 ${pendingQuestions.length - 3}개 질문`));
383
+ }
384
+ console.log('');
385
+ }
386
+
387
+ // 진행 중인 Task
388
+ if (activeTasks.length > 0) {
389
+ console.log(chalk.bgCyan.black.bold(' 진행 중인 Task '));
390
+ console.log('');
391
+
392
+ activeTasks.slice(0, 5).forEach((taskId, index) => {
393
+ const task = taskProgress[taskId];
394
+
395
+ const statusColors = {
396
+ 'IN_DEV': chalk.blue,
397
+ 'IN_REVIEW': chalk.yellow,
398
+ 'READY': chalk.gray,
399
+ 'IN_SPRINT': chalk.cyan
400
+ };
401
+
402
+ const normalizedStatus = task.status === 'IN_QA' ? 'IN_REVIEW' : task.status;
403
+ const statusColor = statusColors[normalizedStatus] || chalk.white;
404
+
405
+ console.log(chalk.white(` ${taskId}: [${statusColor(normalizedStatus)}]`));
406
+ if (task.assignee) {
407
+ console.log(chalk.gray(` 담당: ${task.assignee}`));
408
+ }
409
+
410
+ if (task.note) {
411
+ console.log(chalk.gray(` 메모: ${task.note}`));
412
+ }
413
+
414
+ if (index < Math.min(activeTasks.length, 5) - 1) {
415
+ console.log('');
416
+ }
417
+ });
418
+
419
+ if (activeTasks.length > 5) {
420
+ console.log('');
421
+ console.log(chalk.gray(` ... 그 외 ${activeTasks.length - 5}개 Task`));
422
+ }
423
+ console.log('');
424
+ }
425
+
426
+ // 최근 알림
427
+ const recentNotifications = notifications.slice(-3).reverse();
428
+ if (recentNotifications.length > 0) {
429
+ console.log(chalk.bgMagenta.white.bold(' 최근 알림 '));
430
+ console.log('');
431
+
432
+ recentNotifications.forEach((notif, index) => {
433
+ const typeIcons = {
434
+ 'info': 'i',
435
+ 'warning': '!',
436
+ 'error': 'x',
437
+ 'question': '?',
438
+ 'complete': 'v'
439
+ };
440
+
441
+ const icon = typeIcons[notif.type] || '?';
442
+ const readStatus = notif.read ? chalk.gray('[읽음]') : chalk.yellow('[안읽음]');
443
+
444
+ console.log(chalk.white(` ${icon} ${readStatus} ${notif.message}`));
445
+ console.log(chalk.gray(` from: ${notif.from}`));
446
+
447
+ if (index < recentNotifications.length - 1) {
448
+ console.log('');
449
+ }
450
+ });
451
+ console.log('');
452
+ }
453
+
454
+ } catch (error) {
455
+ console.log(chalk.red(' 상태 파일 읽기 오류'));
456
+ console.log(chalk.gray(` ${error.message}`));
457
+ console.log('');
458
+ }
459
+
460
+ // 푸터
461
+ console.log(chalk.cyan('─'.repeat(80)));
462
+ console.log(chalk.gray(' [q] 종료 [r] 새로고침 [c] 화면 지우기 [h] 도움말'));
463
+ console.log(chalk.cyan('─'.repeat(80)));
464
+
465
+ if (lastUpdate) {
466
+ console.log(chalk.gray(` 마지막 업데이트: ${lastUpdate}`));
467
+ }
468
+
469
+ enqueueQuestions(pendingQuestions);
470
+ processPromptQueue();
471
+ }
472
+
473
+ // 키보드 입력 처리
474
+ const keyHandler = (str, key) => {
475
+ if (key.ctrl && key.name === 'c') {
476
+ cleanup();
477
+ } else if (key.name === 'q') {
478
+ cleanup();
479
+ } else if (key.name === 'r') {
480
+ drawScreen();
481
+ lastUpdate = new Date().toLocaleTimeString('ko-KR');
482
+ } else if (key.name === 'c') {
483
+ console.clear();
484
+ drawScreen();
485
+ } else if (key.name === 'h') {
486
+ showHelp();
487
+ }
488
+ };
489
+
490
+ enableKeypressHandling();
491
+
492
+ // 초기 화면 그리기
493
+ drawScreen();
494
+ lastUpdate = new Date().toLocaleTimeString('ko-KR');
495
+
496
+ // 파일 감시
497
+ let watcher;
498
+ if (fs.existsSync(statusFile)) {
499
+ watcher = fs.watch(statusFile, (eventType) => {
500
+ if (eventType === 'change' && isWatching) {
501
+ drawScreen();
502
+ lastUpdate = new Date().toLocaleTimeString('ko-KR');
503
+ }
504
+ });
505
+ }
506
+
507
+ // 2초마다 화면 갱신 (시간 표시 업데이트)
508
+ // 30초마다 좀비 세션 정리 (15번째 호출마다)
509
+ let tickCount = 0;
510
+ const intervalId = setInterval(() => {
511
+ if (isWatching) {
512
+ tickCount++;
513
+
514
+ // 30초마다 좀비 세션 정리 (2초 × 15 = 30초)
515
+ if (tickCount % 15 === 0) {
516
+ const removedCount = cleanupZombieSessions(60); // 비활성/오래된 세션 제거
517
+ if (removedCount > 0) {
518
+ lastUpdate = `${new Date().toLocaleTimeString('ko-KR')} (좀비 세션 ${removedCount}개 정리됨)`;
519
+ }
520
+ }
521
+
522
+ drawScreen();
523
+ }
524
+ }, 2000);
525
+
526
+ // 정리 함수
527
+ function cleanup() {
528
+ isWatching = false;
529
+ clearInterval(intervalId);
530
+ if (watcher) watcher.close();
531
+
532
+ disableKeypressHandling();
533
+
534
+ console.log('');
535
+ console.log(chalk.cyan('Watch 모드를 종료합니다.'));
536
+ console.log('');
537
+ process.exit(0);
538
+ }
539
+
540
+ // 도움말 표시
541
+ function showHelp() {
542
+ console.clear();
543
+ console.log('');
544
+ console.log(chalk.cyan('┌' + '─'.repeat(78) + '┐'));
545
+ console.log(chalk.cyan('│') + chalk.bold.white(' Watch 모드 도움말'.padEnd(78)) + chalk.cyan('│'));
546
+ console.log(chalk.cyan('└' + '─'.repeat(78) + '┘'));
547
+ console.log('');
548
+ console.log(chalk.white(' Watch 모드는 실시간으로 세션 상태를 모니터링합니다.'));
549
+ console.log('');
550
+ console.log(chalk.yellow(' 키보드 단축키:'));
551
+ console.log(chalk.white(' q - 종료'));
552
+ console.log(chalk.white(' r - 수동 새로고침'));
553
+ console.log(chalk.white(' c - 화면 지우기'));
554
+ console.log(chalk.white(' h - 이 도움말'));
555
+ console.log(chalk.white(' Ctrl+C - 강제 종료'));
556
+ console.log('');
557
+ console.log(chalk.yellow(' 자동 갱신:'));
558
+ console.log(chalk.white(' - .ada-status.json 파일 변경 시 즉시 갱신'));
559
+ console.log(chalk.white(' - 2초마다 시간 정보 자동 갱신'));
560
+ console.log('');
561
+ console.log(chalk.yellow(' Manager 역할:'));
562
+ console.log(chalk.white(' - 대기 질문 확인 및 응답'));
563
+ console.log(chalk.white(' - 질문 발생 시 자동 응답 프롬프트 표시'));
564
+ console.log(chalk.white(' - Task 진행 상황 모니터링'));
565
+ console.log(chalk.white(' - 세션 상태 실시간 추적'));
566
+ console.log('');
567
+ console.log(chalk.gray(' 아무 키나 눌러서 계속...'));
568
+
569
+ process.stdin.once('keypress', () => {
570
+ drawScreen();
571
+ });
572
+ }
573
+
574
+ // 프로세스 종료 처리
575
+ process.on('SIGINT', cleanup);
576
+ process.on('SIGTERM', cleanup);
577
+ }
578
+
579
+ /**
580
+ * 완료된 세션 정리
581
+ */
582
+ async function cleanupCompletedSessions() {
27
583
  const sessionsDir = getSessionsDir();
28
584
 
29
- // 좀비 세션 정리 (60분 이상 된 세션)
30
- const removedCount = cleanupZombieSessions(60);
31
-
585
+ if (!fs.existsSync(sessionsDir)) {
586
+ console.log(chalk.yellow('⚠️ 세션 디렉토리가 없습니다.'));
587
+ return;
588
+ }
589
+
32
590
  console.log('');
33
591
  console.log(chalk.cyan('━'.repeat(60)));
34
- console.log(chalk.cyan.bold('?? 세션 상태'));
592
+ console.log(chalk.cyan.bold('완료된 세션 정리 (status: completed)'));
35
593
  console.log(chalk.cyan('━'.repeat(60)));
36
594
  console.log('');
37
595
 
38
- if (removedCount > 0) {
39
- console.log(chalk.yellow(`?? 좀비 세션 ${removedCount}개 정리됨 (60분 이상 경과)`));
40
- console.log('');
41
- }
42
-
43
- // 1. 실시간 활성 세션 표시
44
- const activeSessions = getActiveSessions();
45
- if (activeSessions.length > 0) {
46
- console.log(chalk.yellow.bold('?? 활성 세션 (실시간)'));
47
- console.log('');
48
- console.log(chalk.gray(' 역할 도구 시작 시간 상태'));
49
- console.log(chalk.gray(' ' + '─'.repeat(56)));
50
-
51
- activeSessions.forEach(session => {
52
- const role = (session.role || '-').padEnd(10);
53
- const tool = (session.tool || '-').padEnd(8);
54
- const startTime = new Date(session.startedAt).toLocaleString('ko-KR');
55
- const status = session.status || 'active';
56
- const statusIcon = status === 'active' ? '??' : '??';
57
-
58
- console.log(` ${role} ${tool} ${startTime} ${statusIcon} ${status}`);
59
- });
60
- console.log('');
61
- } else {
62
- console.log(chalk.gray(' 현재 활성 세션 없음'));
63
- console.log('');
64
- }
65
-
66
- // 2. 대기 중인 질문 표시
67
- const pendingQuestions = getPendingQuestions();
68
- if (pendingQuestions.length > 0) {
69
- console.log(chalk.yellow.bold('?? 대기 질문'));
70
- console.log('');
71
-
72
- pendingQuestions.forEach(q => {
73
- console.log(chalk.yellow(` [${q.id}] ${q.from} → ${q.to}`));
74
- console.log(chalk.white(` 질문: ${q.question}`));
75
- if (q.options && q.options.length > 0) {
76
- console.log(chalk.gray(` 옵션: ${q.options.join(', ')}`));
77
- }
78
- console.log('');
79
- });
80
- }
81
-
82
- // 3. Task 진행 상황 표시
83
- const status = readStatus();
84
- const taskProgress = status.taskProgress || {};
85
- const activeTasks = Object.entries(taskProgress).filter(([_, info]) =>
86
- info.status && info.status !== 'DONE'
87
- );
88
-
89
- if (activeTasks.length > 0) {
90
- console.log(chalk.cyan.bold('?? 진행 중인 Task'));
91
- console.log('');
92
-
93
- activeTasks.forEach(([taskId, info]) => {
94
- const progress = info.progress || 0;
95
- const progressBar = '?'.repeat(Math.floor(progress / 10)) + '?'.repeat(10 - Math.floor(progress / 10));
96
- console.log(` ${taskId}: ${progressBar} ${progress}% (${info.status})`);
97
- if (info.assignee) {
98
- console.log(chalk.gray(` 담당: ${info.assignee}`));
99
- }
100
- if (info.note) {
101
- console.log(chalk.gray(` 메모: ${info.note}`));
102
- }
103
- });
104
- console.log('');
105
- }
106
-
107
- console.log(chalk.cyan('━'.repeat(60)));
108
- console.log('');
109
-
110
- // 4. 최근 세션 기록 (히스토리)
111
- if (!fs.existsSync(sessionsDir)) {
112
- console.log(chalk.gray(' 세션 기록 없음'));
113
- console.log('');
114
- return;
115
- }
116
-
117
596
  const sessionDirs = fs.readdirSync(sessionsDir)
118
597
  .filter(f => fs.statSync(path.join(sessionsDir, f)).isDirectory())
119
- .sort()
120
- .reverse();
121
-
122
- if (sessionDirs.length === 0) {
123
- console.log(chalk.gray(' 세션 기록 없음'));
124
- console.log('');
125
- return;
126
- }
127
-
128
- console.log(chalk.cyan.bold('?? 최근 세션 기록'));
129
- console.log('');
130
- console.log(chalk.gray(' 세션 ID 역할 도구 상태'));
131
- console.log(chalk.gray(' ' + '─'.repeat(56)));
132
-
133
- for (const sessionId of sessionDirs.slice(0, 10)) {
134
- const sessionFile = path.join(sessionsDir, sessionId, 'session.json');
135
-
136
- if (fs.existsSync(sessionFile)) {
137
- try {
138
- const session = JSON.parse(fs.readFileSync(sessionFile, 'utf-8'));
139
- const role = (session.role || '-').padEnd(10);
140
- const tool = (session.tool || '-').padEnd(8);
141
- const status = session.status || 'unknown';
142
-
143
- const statusColor = status === 'completed' ? chalk.green :
144
- status === 'active' ? chalk.yellow :
145
- status === 'error' ? chalk.red :
146
- chalk.gray;
147
-
148
- console.log(` ${sessionId} ${role} ${tool} ${statusColor(status)}`);
149
- } catch (e) {
150
- console.log(` ${sessionId} ${chalk.gray('(읽기 실패)')}`);
598
+ .sort();
599
+
600
+ let completedCount = 0;
601
+ let skippedCount = 0;
602
+ let errorCount = 0;
603
+
604
+ for (const sessionId of sessionDirs) {
605
+ const sessionPath = path.join(sessionsDir, sessionId);
606
+ const sessionFile = path.join(sessionPath, 'session.json');
607
+
608
+ try {
609
+ // session.json 확인
610
+ if (!fs.existsSync(sessionFile)) {
611
+ console.log(chalk.gray(` ${sessionId}: session.json 없음 (건너뜀)`));
612
+ skippedCount++;
613
+ continue;
614
+ }
615
+
616
+ // 세션 정보 읽기
617
+ const session = JSON.parse(fs.readFileSync(sessionFile, 'utf-8'));
618
+
619
+ // 완료 상태가 아니면 건너뜀
620
+ if (session.status !== 'completed') {
621
+ console.log(chalk.yellow(` ${sessionId}: 완료 아님 (${session.status || 'unknown'}) - 유지`));
622
+ skippedCount++;
623
+ continue;
151
624
  }
152
- } else {
153
- console.log(` ${sessionId} ${chalk.gray('(정보 없음)')}`);
154
- }
155
- }
156
-
157
- console.log('');
158
-
159
- if (sessionDirs.length > 10) {
160
- console.log(chalk.gray(` ... 그 외 ${sessionDirs.length - 10}개 세션 (ada logs 명령어로 확인)`));
161
- console.log('');
162
- }
163
- }
164
-
165
- // Watch 모드: 실시간 세션 모니터링
166
- async function watchSessions() {
167
- const statusFile = getStatusFilePath();
168
- let lastUpdate = '';
169
- let isWatching = true;
170
- let isPrompting = false;
171
- const promptQueue = [];
172
- const promptedQuestions = new Set();
173
-
174
- function pauseWatch() {
175
- isWatching = false;
176
- }
177
-
178
- function resumeWatch() {
179
- isWatching = true;
180
- drawScreen();
181
- lastUpdate = new Date().toLocaleTimeString('ko-KR');
182
- }
183
-
184
- function disableKeypressHandling() {
185
- if (process.stdin.isTTY) {
186
- process.stdin.setRawMode(false);
187
- }
188
- process.stdin.removeListener('keypress', keyHandler);
189
- }
190
-
191
- function enableKeypressHandling() {
192
- if (process.stdin.isTTY) {
193
- readline.emitKeypressEvents(process.stdin);
194
- process.stdin.setRawMode(true);
195
- }
196
- process.stdin.on('keypress', keyHandler);
197
- }
198
-
199
- function enqueueQuestions(questions) {
200
- questions.forEach(question => {
201
- if (!promptedQuestions.has(question.id)) {
202
- promptQueue.push(question);
203
- promptedQuestions.add(question.id);
204
- }
205
- });
206
- }
207
-
208
- async function processPromptQueue() {
209
- if (isPrompting || promptQueue.length === 0) {
210
- return;
211
- }
212
-
213
- isPrompting = true;
214
- pauseWatch();
215
- disableKeypressHandling();
216
-
217
- while (promptQueue.length > 0) {
218
- const nextQuestion = promptQueue.shift();
219
- await promptQuestion(nextQuestion);
220
- }
221
-
222
- enableKeypressHandling();
223
- isPrompting = false;
224
- resumeWatch();
225
- }
226
-
227
- async function promptQuestion(question) {
228
- const status = readStatus();
229
- const currentQuestion = status.pendingQuestions?.find(q => q.id === question.id);
230
-
231
- if (!currentQuestion || currentQuestion.status !== 'waiting') {
232
- return;
233
- }
234
-
235
- console.log('');
236
- console.log(chalk.yellow('━'.repeat(60)));
237
- console.log(chalk.yellow.bold('?? 질문 응답 필요'));
238
- console.log(chalk.yellow('━'.repeat(60)));
239
- console.log(chalk.white(` ID: ${currentQuestion.id}`));
240
- console.log(chalk.white(` 요청: ${currentQuestion.from} → ${currentQuestion.to}`));
241
- console.log(chalk.white(` 질문: ${currentQuestion.question}`));
242
-
243
- if (currentQuestion.options && currentQuestion.options.length > 0) {
244
- console.log(chalk.gray(' 옵션:'));
245
- currentQuestion.options.forEach((option, index) => {
246
- console.log(chalk.gray(` ${index + 1}) ${option}`));
247
- });
248
- }
249
-
250
- const promptText = currentQuestion.options && currentQuestion.options.length > 0
251
- ? ' 답변(번호 또는 직접 입력): '
252
- : ' 답변: ';
253
-
254
- const answerInput = await askInput(chalk.cyan(promptText));
255
- let answer = (answerInput || '').trim();
256
-
257
- if (currentQuestion.options && currentQuestion.options.length > 0) {
258
- const optionIndex = Number.parseInt(answer, 10);
259
- if (!Number.isNaN(optionIndex) && optionIndex >= 1 && optionIndex <= currentQuestion.options.length) {
260
- answer = currentQuestion.options[optionIndex - 1];
261
- }
262
- }
263
-
264
- if (!answer) {
265
- answer = '(응답 없음)';
266
- }
267
-
268
- answerQuestion(currentQuestion.id, answer);
269
- await handleQuestionAction(currentQuestion, answer);
270
-
271
- console.log(chalk.green(` 저장 완료: ${currentQuestion.id}`));
272
- console.log('');
273
- }
274
-
275
- function askInput(prompt) {
276
- return new Promise(resolve => {
277
- const rl = readline.createInterface({
278
- input: process.stdin,
279
- output: process.stdout
280
- });
281
-
282
- rl.question(prompt, answer => {
283
- rl.close();
284
- resolve(answer);
285
- });
286
- });
287
- }
288
-
289
- async function handleQuestionAction(question, answer) {
290
- if (!question.action) {
291
- return;
292
- }
293
-
294
- if (question.action === 'sprint_setup') {
295
- if (!shouldProceed(answer)) {
296
- console.log(chalk.gray(' 처리하지 않음'));
297
- return;
298
- }
299
-
300
- await runSprintSetup();
301
- }
302
- }
303
-
304
- function shouldProceed(answer) {
305
- const normalized = (answer || '').trim().toLowerCase();
306
- if (!normalized) {
307
- return false;
308
- }
309
-
310
- if (normalized.includes('아니오') || normalized.includes('거부') || normalized === 'no' || normalized === 'n') {
311
- return false;
312
- }
313
-
314
- return (
315
- normalized.startsWith('예') ||
316
- normalized.includes('처리') ||
317
- normalized === 'yes' ||
318
- normalized === 'y'
319
- );
320
- }
321
-
322
- async function runSprintSetup() {
323
- const workspace = getWorkspaceDir();
324
- const backlogDir = path.join(workspace, 'artifacts', 'backlog');
325
-
326
- console.log('');
327
- console.log(chalk.cyan('━'.repeat(60)));
328
- console.log(chalk.cyan.bold('?? 스프린트 생성/Task 할당'));
329
- console.log(chalk.cyan('━'.repeat(60)));
330
-
331
- if (!runAdaCommand(['sprint', 'create'])) {
332
- console.log(chalk.red(' 스프린트 생성에 실패했습니다.'));
333
- console.log('');
334
- return;
335
- }
336
-
337
- if (!fs.existsSync(backlogDir) || !fs.statSync(backlogDir).isDirectory()) {
338
- console.log(chalk.yellow(' backlog/ 디렉토리가 없습니다.'));
339
- console.log('');
340
- return;
341
- }
342
-
343
- const taskIds = fs.readdirSync(backlogDir)
344
- .filter(file => /^task-\d+\.md$/i.test(file))
345
- .map(file => file.replace(/\.md$/i, ''));
346
-
347
- if (taskIds.length === 0) {
348
- console.log(chalk.yellow(' backlog에 Task가 없습니다.'));
349
- console.log('');
350
- return;
351
- }
352
-
353
- console.log(chalk.gray(' backlog Task 목록:'));
354
- taskIds.forEach(taskId => {
355
- console.log(chalk.gray(` - ${taskId}`));
356
- });
357
- console.log('');
358
-
359
- const answer = await askInput(chalk.cyan(' 추가할 Task (all/skip/공백 구분): '));
360
- const selected = selectTaskIds(taskIds, answer);
361
-
362
- if (selected.length === 0) {
363
- console.log(chalk.yellow(' Task 추가를 건너뜁니다.'));
364
- console.log('');
365
- return;
366
- }
367
-
368
- const ok = runAdaCommand(['sprint', 'add', ...selected]);
369
- if (!ok) {
370
- console.log(chalk.red(' Task 추가에 실패했습니다.'));
371
- }
372
- console.log('');
373
- }
374
-
375
- function selectTaskIds(taskIds, answer) {
376
- const input = (answer || '').trim();
377
- if (!input) {
378
- return [];
379
- }
380
-
381
- const normalized = input.toLowerCase();
382
- if (normalized === 'skip' || normalized === 'none' || normalized === 'n') {
383
- return [];
384
- }
385
-
386
- if (normalized === 'all') {
387
- return [...taskIds];
388
- }
389
-
390
- const selected = input.split(/[\s,]+/)
391
- .map(token => token.trim())
392
- .filter(Boolean)
393
- .map(token => token.startsWith('task-') ? token : `task-${token}`);
394
-
395
- const taskSet = new Set(taskIds);
396
- return [...new Set(selected)].filter(taskId => taskSet.has(taskId));
397
- }
398
-
399
- function resolveCliPath() {
400
- const argvPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
401
- const packageCliPath = path.join(getPackageRoot(), 'bin', 'cli.js');
402
- const candidates = [argvPath, packageCliPath].filter(Boolean);
403
-
404
- for (const candidate of candidates) {
405
- if (candidate && fs.existsSync(candidate)) {
406
- return candidate;
407
- }
408
- }
409
-
410
- return null;
411
- }
412
-
413
- function runAdaCommand(args) {
414
- const cliPath = resolveCliPath();
415
-
416
- if (!cliPath) {
417
- console.log(chalk.red(' CLI 경로를 찾을 수 없습니다.'));
418
- return false;
419
- }
420
-
421
- const result = spawnSync(process.execPath, [cliPath, ...args], {
422
- stdio: 'inherit',
423
- cwd: process.cwd()
424
- });
425
- return result.status === 0;
426
- }
427
-
428
- // 화면 그리기 함수
429
- function drawScreen() {
430
- console.clear();
431
-
432
- const now = new Date();
433
- const timeString = now.toLocaleTimeString('ko-KR');
434
- let pendingQuestions = [];
435
-
436
- // 헤더
437
- console.log('');
438
- console.log(chalk.cyan('┌' + '─'.repeat(78) + '┐'));
439
- console.log(chalk.cyan('│') + chalk.bold.white(' ?? Manager Watch Mode'.padEnd(78)) + chalk.cyan('│'));
440
- console.log(chalk.cyan('│') + chalk.gray(` ? ${timeString}`.padEnd(78)) + chalk.cyan('│'));
441
- console.log(chalk.cyan('└' + '─'.repeat(78) + '┘'));
442
- console.log('');
443
-
444
- try {
445
- const status = readStatus();
446
- const activeSessions = status.activeSessions || [];
447
- pendingQuestions = status.pendingQuestions?.filter(q => q.status === 'waiting') || [];
448
- const taskProgress = status.taskProgress || {};
449
- const notifications = status.notifications || [];
450
-
451
- // 통계 패널
452
- console.log(chalk.bgBlue.white.bold(' ?? 통계 '));
453
- console.log('');
454
- console.log(chalk.white(` 활성 세션: ${chalk.yellow(activeSessions.length)}개`));
455
- console.log(chalk.white(` 대기 질문: ${pendingQuestions.length > 0 ? chalk.red(pendingQuestions.length) : chalk.green('0')}개`));
456
-
457
- const activeTasks = Object.keys(taskProgress).filter(
458
- taskId => taskProgress[taskId].status !== 'DONE'
459
- );
460
- console.log(chalk.white(` 진행 Task: ${chalk.cyan(activeTasks.length)}개`));
461
- console.log(chalk.white(` 읽지 않은 알림: ${notifications.filter(n => !n.read).length}개`));
462
- console.log('');
463
-
464
- // 활성 세션
465
- if (activeSessions.length > 0) {
466
- console.log(chalk.bgGreen.black.bold(' ?? 활성 세션 '));
467
- console.log('');
468
-
469
- activeSessions.forEach((session, index) => {
470
- const startTime = new Date(session.startedAt);
471
- const duration = Math.floor((now - startTime) / 1000 / 60); // 분
472
- const statusIcon = session.status === 'active' ? '??' : '??';
473
-
474
- console.log(chalk.white(` ${index + 1}. ${statusIcon} ${chalk.bold(session.role)}`));
475
- console.log(chalk.gray(` 도구: ${session.tool}`));
476
- console.log(chalk.gray(` 실행 시간: ${duration}분`));
477
-
478
- if (index < activeSessions.length - 1) {
479
- console.log('');
480
- }
481
- });
482
- console.log('');
483
- } else {
484
- console.log(chalk.gray(' 현재 활성 세션이 없습니다.'));
485
- console.log('');
486
- }
487
-
488
- // 대기 질문 (강조)
489
- if (pendingQuestions.length > 0) {
490
- console.log(chalk.bgYellow.black.bold(' ?? 대기 질문 '));
491
- console.log('');
492
-
493
- pendingQuestions.slice(0, 3).forEach((q, index) => {
494
- console.log(chalk.yellow(` [${q.id}] ${q.from} → ${q.to}`));
495
- console.log(chalk.white(` 질문: ${q.question}`));
496
-
497
- if (q.options && q.options.length > 0) {
498
- console.log(chalk.gray(` 옵션: ${q.options.join(' / ')}`));
499
- }
500
-
501
- const elapsed = Math.floor((now - new Date(q.createdAt)) / 1000 / 60);
502
- console.log(chalk.gray(` 대기 시간: ${elapsed}분`));
503
-
504
- if (index < Math.min(pendingQuestions.length, 3) - 1) {
505
- console.log('');
506
- }
507
- });
508
-
509
- if (pendingQuestions.length > 3) {
510
- console.log('');
511
- console.log(chalk.gray(` ... 그 외 ${pendingQuestions.length - 3}개 질문`));
512
- }
513
- console.log('');
514
- }
515
-
516
- // 진행 중인 Task
517
- if (activeTasks.length > 0) {
518
- console.log(chalk.bgCyan.black.bold(' ?? 진행 중인 Task '));
519
- console.log('');
520
-
521
- activeTasks.slice(0, 5).forEach((taskId, index) => {
522
- const task = taskProgress[taskId];
523
- const progress = task.progress || 0;
524
- const bars = Math.floor(progress / 5);
525
- const progressBar = '?'.repeat(bars) + '?'.repeat(20 - bars);
526
-
527
- const statusColors = {
528
- 'IN_DEV': chalk.blue,
529
- 'IN_REVIEW': chalk.yellow,
530
- 'IN_QA': chalk.magenta,
531
- 'READY': chalk.gray,
532
- 'IN_SPRINT': chalk.cyan
533
- };
534
-
535
- const statusColor = statusColors[task.status] || chalk.white;
536
-
537
- console.log(chalk.white(` ${taskId}: ${progressBar} ${progress}%`));
538
- console.log(chalk.gray(` 상태: ${statusColor(task.status)} ${task.assignee ? `| 담당: ${task.assignee}` : ''}`));
539
-
540
- if (task.note) {
541
- console.log(chalk.gray(` 메모: ${task.note}`));
542
- }
543
-
544
- if (index < Math.min(activeTasks.length, 5) - 1) {
545
- console.log('');
546
- }
547
- });
548
-
549
- if (activeTasks.length > 5) {
550
- console.log('');
551
- console.log(chalk.gray(` ... 그 외 ${activeTasks.length - 5}개 Task`));
552
- }
553
- console.log('');
554
- }
555
-
556
- // 최근 알림
557
- const recentNotifications = notifications.slice(-3).reverse();
558
- if (recentNotifications.length > 0) {
559
- console.log(chalk.bgMagenta.white.bold(' ?? 최근 알림 '));
560
- console.log('');
561
-
562
- recentNotifications.forEach((notif, index) => {
563
- const typeIcons = {
564
- 'info': '??',
565
- 'warning': '??',
566
- 'error': '?',
567
- 'question': '?',
568
- 'complete': '?'
569
- };
570
-
571
- const icon = typeIcons[notif.type] || '??';
572
- const readStatus = notif.read ? chalk.gray('[읽음]') : chalk.yellow('[안읽음]');
573
-
574
- console.log(chalk.white(` ${icon} ${readStatus} ${notif.message}`));
575
- console.log(chalk.gray(` from: ${notif.from}`));
576
-
577
- if (index < recentNotifications.length - 1) {
578
- console.log('');
579
- }
580
- });
581
- console.log('');
582
- }
583
-
584
- } catch (error) {
585
- console.log(chalk.red(' 상태 파일 읽기 오류'));
586
- console.log(chalk.gray(` ${error.message}`));
587
- console.log('');
588
- }
589
-
590
- // 푸터
591
- console.log(chalk.cyan('─'.repeat(80)));
592
- console.log(chalk.gray(' [q] 종료 [r] 새로고침 [c] 화면 지우기 [h] 도움말'));
593
- console.log(chalk.cyan('─'.repeat(80)));
594
-
595
- if (lastUpdate) {
596
- console.log(chalk.gray(` 마지막 업데이트: ${lastUpdate}`));
597
- }
598
-
599
- enqueueQuestions(pendingQuestions);
600
- processPromptQueue();
601
- }
602
-
603
- // 키보드 입력 처리
604
- const keyHandler = (str, key) => {
605
- if (key.ctrl && key.name === 'c') {
606
- cleanup();
607
- } else if (key.name === 'q') {
608
- cleanup();
609
- } else if (key.name === 'r') {
610
- drawScreen();
611
- lastUpdate = new Date().toLocaleTimeString('ko-KR');
612
- } else if (key.name === 'c') {
613
- console.clear();
614
- drawScreen();
615
- } else if (key.name === 'h') {
616
- showHelp();
617
- }
618
- };
619
-
620
- enableKeypressHandling();
621
-
622
- // 초기 화면 그리기
623
- drawScreen();
624
- lastUpdate = new Date().toLocaleTimeString('ko-KR');
625
-
626
- // 파일 감시
627
- let watcher;
628
- if (fs.existsSync(statusFile)) {
629
- watcher = fs.watch(statusFile, (eventType) => {
630
- if (eventType === 'change' && isWatching) {
631
- drawScreen();
632
- lastUpdate = new Date().toLocaleTimeString('ko-KR');
633
- }
634
- });
635
- }
636
-
637
- // 2초마다 화면 갱신 (시간 표시 업데이트)
638
- // 30초마다 좀비 세션 정리 (15번째 호출마다)
639
- let tickCount = 0;
640
- const intervalId = setInterval(() => {
641
- if (isWatching) {
642
- tickCount++;
643
-
644
- // 30초마다 좀비 세션 정리 (2초 × 15 = 30초)
645
- if (tickCount % 15 === 0) {
646
- const removedCount = cleanupZombieSessions(60); // 60분 이상 된 세션 제거
647
- if (removedCount > 0) {
648
- lastUpdate = `${new Date().toLocaleTimeString('ko-KR')} (좀비 세션 ${removedCount}개 정리됨)`;
649
- }
650
- }
651
-
652
- drawScreen();
653
- }
654
- }, 2000);
655
-
656
- // 정리 함수
657
- function cleanup() {
658
- isWatching = false;
659
- clearInterval(intervalId);
660
- if (watcher) watcher.close();
661
-
662
- disableKeypressHandling();
663
-
664
- console.log('');
665
- console.log(chalk.cyan('?? Watch 모드를 종료합니다.'));
666
- console.log('');
667
- process.exit(0);
668
- }
669
-
670
- // 도움말 표시
671
- function showHelp() {
672
- console.clear();
673
- console.log('');
674
- console.log(chalk.cyan('┌' + '─'.repeat(78) + '┐'));
675
- console.log(chalk.cyan('│') + chalk.bold.white(' ?? Watch 모드 도움말'.padEnd(78)) + chalk.cyan('│'));
676
- console.log(chalk.cyan('└' + '─'.repeat(78) + '┘'));
677
- console.log('');
678
- console.log(chalk.white(' Watch 모드는 실시간으로 세션 상태를 모니터링합니다.'));
679
- console.log('');
680
- console.log(chalk.yellow(' 키보드 단축키:'));
681
- console.log(chalk.white(' q - 종료'));
682
- console.log(chalk.white(' r - 수동 새로고침'));
683
- console.log(chalk.white(' c - 화면 지우기'));
684
- console.log(chalk.white(' h - 이 도움말'));
685
- console.log(chalk.white(' Ctrl+C - 강제 종료'));
686
- console.log('');
687
- console.log(chalk.yellow(' 자동 갱신:'));
688
- console.log(chalk.white(' - .ada-status.json 파일 변경 시 즉시 갱신'));
689
- console.log(chalk.white(' - 2초마다 시간 정보 자동 갱신'));
690
- console.log('');
691
- console.log(chalk.yellow(' Manager 역할:'));
692
- console.log(chalk.white(' - 대기 질문 확인 및 응답'));
693
- console.log(chalk.white(' - 질문 발생 시 자동 응답 프롬프트 표시'));
694
- console.log(chalk.white(' - Task 진행 상황 모니터링'));
695
- console.log(chalk.white(' - 세션 상태 실시간 추적'));
696
- console.log('');
697
- console.log(chalk.gray(' 아무 키나 눌러서 계속...'));
698
-
699
- process.stdin.once('keypress', () => {
700
- drawScreen();
701
- });
702
- }
703
625
 
704
- // 프로세스 종료 처리
705
- process.on('SIGINT', cleanup);
706
- process.on('SIGTERM', cleanup);
626
+ // 세션 디렉토리 삭제
627
+ fs.removeSync(sessionPath);
628
+ console.log(chalk.green(` ✓ ${sessionId}: 삭제됨 (${session.status})`));
629
+ completedCount++;
630
+
631
+ } catch (error) {
632
+ console.log(chalk.red(` ✗ ${sessionId}: 오류 - ${error.message}`));
633
+ errorCount++;
634
+ }
635
+ }
636
+
637
+ console.log('');
638
+ console.log(chalk.cyan('━'.repeat(60)));
639
+ console.log(chalk.white(` 삭제됨: ${chalk.green(completedCount)}개`));
640
+ console.log(chalk.white(` 유지됨: ${chalk.gray(skippedCount)}개`));
641
+ if (errorCount > 0) {
642
+ console.log(chalk.white(` 오류: ${chalk.red(errorCount)}개`));
643
+ }
644
+ console.log(chalk.cyan('━'.repeat(60)));
645
+ console.log('');
707
646
  }