@silbaram/artifact-driven-agent 0.1.2 → 0.1.4

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.
@@ -0,0 +1,350 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { getWorkspaceDir } from './files.js';
4
+
5
+ /**
6
+ * .ada-status.json 파일 경로 반환
7
+ */
8
+ export function getStatusFilePath() {
9
+ return path.join(getWorkspaceDir(), '.ada-status.json');
10
+ }
11
+
12
+ /**
13
+ * 상태 파일 템플릿
14
+ */
15
+ function getEmptyStatus() {
16
+ return {
17
+ version: '1.0',
18
+ updatedAt: new Date().toISOString(),
19
+ currentPhase: 'planning',
20
+ activeSessions: [],
21
+ pendingQuestions: [],
22
+ taskProgress: {},
23
+ notifications: [],
24
+ locks: {}
25
+ };
26
+ }
27
+
28
+ /**
29
+ * 상태 파일 읽기 (없으면 초기화)
30
+ * 동시 접근 시 재시도 로직 포함
31
+ */
32
+ export function readStatus(retries = 3) {
33
+ const statusFile = getStatusFilePath();
34
+
35
+ for (let i = 0; i < retries; i++) {
36
+ try {
37
+ if (!fs.existsSync(statusFile)) {
38
+ const initialStatus = getEmptyStatus();
39
+ fs.writeFileSync(statusFile, JSON.stringify(initialStatus, null, 2));
40
+ return initialStatus;
41
+ }
42
+
43
+ const content = fs.readFileSync(statusFile, 'utf-8');
44
+ const status = JSON.parse(content);
45
+
46
+ // 스키마 검증 및 누락된 필드 추가
47
+ const emptyStatus = getEmptyStatus();
48
+ Object.keys(emptyStatus).forEach(key => {
49
+ if (status[key] === undefined) {
50
+ status[key] = emptyStatus[key];
51
+ }
52
+ });
53
+
54
+ return status;
55
+ } catch (err) {
56
+ if (i === retries - 1) {
57
+ console.error('⚠️ 상태 파일 읽기 실패:', err.message);
58
+ return getEmptyStatus();
59
+ }
60
+ // 짧은 대기 후 재시도
61
+ const delay = Math.random() * 100;
62
+ const start = Date.now();
63
+ while (Date.now() - start < delay) { /* busy wait */ }
64
+ }
65
+ }
66
+
67
+ return getEmptyStatus();
68
+ }
69
+
70
+ /**
71
+ * 상태 파일 쓰기
72
+ * 동시 접근 시 재시도 로직 포함
73
+ */
74
+ export function writeStatus(status, retries = 3) {
75
+ const statusFile = getStatusFilePath();
76
+ status.updatedAt = new Date().toISOString();
77
+
78
+ for (let i = 0; i < retries; i++) {
79
+ try {
80
+ // 임시 파일에 먼저 쓰고 atomic rename
81
+ const tempFile = `${statusFile}.tmp`;
82
+ fs.writeFileSync(tempFile, JSON.stringify(status, null, 2));
83
+ fs.renameSync(tempFile, statusFile);
84
+ return true;
85
+ } catch (err) {
86
+ if (i === retries - 1) {
87
+ console.error('⚠️ 상태 파일 쓰기 실패:', err.message);
88
+ return false;
89
+ }
90
+ // 짧은 대기 후 재시도
91
+ const delay = Math.random() * 100;
92
+ const start = Date.now();
93
+ while (Date.now() - start < delay) { /* busy wait */ }
94
+ }
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * 세션 등록
102
+ */
103
+ export function registerSession(sessionId, role, tool) {
104
+ const status = readStatus();
105
+
106
+ // 기존 세션 중복 확인
107
+ const existing = status.activeSessions.find(s => s.sessionId === sessionId);
108
+ if (existing) {
109
+ return status;
110
+ }
111
+
112
+ status.activeSessions.push({
113
+ sessionId,
114
+ role,
115
+ tool,
116
+ startedAt: new Date().toISOString(),
117
+ status: 'active'
118
+ });
119
+
120
+ writeStatus(status);
121
+ return status;
122
+ }
123
+
124
+ /**
125
+ * 세션 제거
126
+ */
127
+ export function unregisterSession(sessionId) {
128
+ const status = readStatus();
129
+
130
+ status.activeSessions = status.activeSessions.filter(s => s.sessionId !== sessionId);
131
+
132
+ // 해당 세션이 보유한 잠금 해제
133
+ Object.keys(status.locks).forEach(file => {
134
+ if (status.locks[file].holder === sessionId) {
135
+ delete status.locks[file];
136
+ }
137
+ });
138
+
139
+ writeStatus(status);
140
+ return status;
141
+ }
142
+
143
+ /**
144
+ * 세션 상태 업데이트
145
+ */
146
+ export function updateSessionStatus(sessionId, newStatus) {
147
+ const status = readStatus();
148
+
149
+ const session = status.activeSessions.find(s => s.sessionId === sessionId);
150
+ if (session) {
151
+ session.status = newStatus;
152
+ session.lastUpdate = new Date().toISOString();
153
+ writeStatus(status);
154
+ }
155
+
156
+ return status;
157
+ }
158
+
159
+ /**
160
+ * 질문 추가
161
+ */
162
+ export function addQuestion(from, to, question, options = [], priority = 'normal') {
163
+ const status = readStatus();
164
+
165
+ const questionId = `Q${from.substring(0, 1).toUpperCase()}${String(status.pendingQuestions.length + 1).padStart(3, '0')}`;
166
+
167
+ const newQuestion = {
168
+ id: questionId,
169
+ from,
170
+ to,
171
+ question,
172
+ options,
173
+ priority,
174
+ status: 'waiting',
175
+ createdAt: new Date().toISOString()
176
+ };
177
+
178
+ status.pendingQuestions.push(newQuestion);
179
+
180
+ // 알림 추가
181
+ addNotificationInternal(status, 'question', from, `새 질문 [${questionId}]: ${question}`);
182
+
183
+ writeStatus(status);
184
+ return questionId;
185
+ }
186
+
187
+ /**
188
+ * 질문 응답
189
+ */
190
+ export function answerQuestion(questionId, answer) {
191
+ const status = readStatus();
192
+
193
+ const question = status.pendingQuestions.find(q => q.id === questionId);
194
+ if (question) {
195
+ question.status = 'answered';
196
+ question.answer = answer;
197
+ question.answeredAt = new Date().toISOString();
198
+
199
+ // 알림 추가
200
+ addNotificationInternal(status, 'info', 'manager', `질문 ${questionId} 응답됨: ${answer}`);
201
+
202
+ writeStatus(status);
203
+ }
204
+
205
+ return status;
206
+ }
207
+
208
+ /**
209
+ * Task 진행률 업데이트
210
+ */
211
+ export function updateTaskProgress(taskId, updates) {
212
+ const status = readStatus();
213
+
214
+ if (!status.taskProgress[taskId]) {
215
+ status.taskProgress[taskId] = {};
216
+ }
217
+
218
+ Object.assign(status.taskProgress[taskId], updates, {
219
+ lastUpdate: new Date().toISOString()
220
+ });
221
+
222
+ writeStatus(status);
223
+ return status;
224
+ }
225
+
226
+ /**
227
+ * 알림 추가 (내부용)
228
+ */
229
+ function addNotificationInternal(status, type, from, message) {
230
+ const notificationId = `N${String(status.notifications.length + 1).padStart(3, '0')}`;
231
+
232
+ status.notifications.push({
233
+ id: notificationId,
234
+ type,
235
+ from,
236
+ message,
237
+ read: false,
238
+ createdAt: new Date().toISOString()
239
+ });
240
+
241
+ // 알림은 최근 50개만 유지
242
+ if (status.notifications.length > 50) {
243
+ status.notifications = status.notifications.slice(-50);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * 알림 추가 (외부용)
249
+ */
250
+ export function addNotification(type, from, message) {
251
+ const status = readStatus();
252
+ addNotificationInternal(status, type, from, message);
253
+ writeStatus(status);
254
+ return status;
255
+ }
256
+
257
+ /**
258
+ * 파일 잠금 획득
259
+ */
260
+ export function acquireLock(sessionId, filePath, timeoutMs = 30000) {
261
+ const status = readStatus();
262
+
263
+ // 기존 잠금 확인
264
+ if (status.locks[filePath]) {
265
+ const lock = status.locks[filePath];
266
+ const lockAge = Date.now() - new Date(lock.acquiredAt).getTime();
267
+
268
+ // 타임아웃 초과 시 강제 해제
269
+ if (lockAge > timeoutMs) {
270
+ addNotificationInternal(status, 'warning', 'system',
271
+ `파일 잠금 타임아웃: ${filePath} (${lock.holder})`);
272
+ delete status.locks[filePath];
273
+ } else {
274
+ // 다른 세션이 잠금 보유 중
275
+ return false;
276
+ }
277
+ }
278
+
279
+ // 잠금 획득
280
+ status.locks[filePath] = {
281
+ holder: sessionId,
282
+ acquiredAt: new Date().toISOString()
283
+ };
284
+
285
+ writeStatus(status);
286
+ return true;
287
+ }
288
+
289
+ /**
290
+ * 파일 잠금 해제
291
+ */
292
+ export function releaseLock(sessionId, filePath) {
293
+ const status = readStatus();
294
+
295
+ if (status.locks[filePath] && status.locks[filePath].holder === sessionId) {
296
+ delete status.locks[filePath];
297
+ writeStatus(status);
298
+ return true;
299
+ }
300
+
301
+ return false;
302
+ }
303
+
304
+ /**
305
+ * 읽지 않은 알림 개수
306
+ */
307
+ export function getUnreadNotificationCount() {
308
+ const status = readStatus();
309
+ return status.notifications.filter(n => !n.read).length;
310
+ }
311
+
312
+ /**
313
+ * 활성 세션 목록
314
+ */
315
+ export function getActiveSessions() {
316
+ const status = readStatus();
317
+ return status.activeSessions;
318
+ }
319
+
320
+ /**
321
+ * 대기 중인 질문 목록
322
+ */
323
+ export function getPendingQuestions() {
324
+ const status = readStatus();
325
+ return status.pendingQuestions.filter(q => q.status === 'waiting');
326
+ }
327
+
328
+ /**
329
+ * 좀비 세션 정리 (오래된 세션 제거)
330
+ */
331
+ export function cleanupZombieSessions(maxAgeMinutes = 60) {
332
+ const status = readStatus();
333
+ const now = Date.now();
334
+
335
+ const originalCount = status.activeSessions.length;
336
+
337
+ status.activeSessions = status.activeSessions.filter(session => {
338
+ const age = now - new Date(session.startedAt).getTime();
339
+ return age < maxAgeMinutes * 60 * 1000;
340
+ });
341
+
342
+ const removedCount = originalCount - status.activeSessions.length;
343
+
344
+ if (removedCount > 0) {
345
+ addNotificationInternal(status, 'info', 'system', `좀비 세션 ${removedCount}개 정리됨`);
346
+ writeStatus(status);
347
+ }
348
+
349
+ return removedCount;
350
+ }
@@ -1,36 +0,0 @@
1
- # Feature 단위 Artifacts 구조
2
-
3
- > 대규모 프로젝트에서 병렬 개발을 위한 Feature 분리 구조
4
-
5
- ## 📁 구조
6
-
7
- ```
8
- ai-dev-team/artifacts/features/
9
- ├── F001-user-auth/
10
- │ ├── spec.md # Feature 스펙
11
- │ ├── api.md # Feature API
12
- │ ├── ui.md # Feature UI
13
- │ ├── review.md # 리뷰 기록
14
- │ └── qa.md # QA 기록
15
- └── _template/ # 템플릿
16
- ```
17
-
18
- ## 🎯 언제 사용하는가?
19
-
20
- ### 사용 권장
21
- - 프로젝트 규모 M 이상
22
- - 기능 3개 이상
23
- - 병렬 개발 필요
24
-
25
- ### 사용 불필요
26
- - 프로젝트 규모 S
27
- - 기능 2개 이하
28
- - 순차 개발
29
-
30
- ## 📋 명명 규칙
31
-
32
- ```
33
- <Feature-ID>-<feature-name>/
34
- ```
35
-
36
- 예시: `F001-user-auth/`, `F002-dashboard/`