create-claude-pipeline 0.3.2 → 0.4.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.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, execSync } from "child_process";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
3
4
|
import { StateManager } from "./state-manager.js";
|
|
4
|
-
import { SignalWatcher } from "./signal-watcher.js";
|
|
5
5
|
import { ContextWatcher } from "./context-watcher.js";
|
|
6
6
|
import { waitForCheckpoint } from "./checkpoint-waiter.js";
|
|
7
7
|
const PIPELINE_ID = process.env.PIPELINE_ID;
|
|
@@ -16,8 +16,8 @@ if (!REQUIREMENTS) {
|
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
const projectRoot = path.resolve(PIPELINES_DIR, "..");
|
|
19
|
-
const
|
|
20
|
-
// ── Pre-check
|
|
19
|
+
const contextDir = path.join(PIPELINES_DIR, PIPELINE_ID, "context");
|
|
20
|
+
// ── Pre-check ───────────────────────────────────────────────────────
|
|
21
21
|
function checkClaudeCLI() {
|
|
22
22
|
try {
|
|
23
23
|
execSync("claude --version", { timeout: 10000, stdio: "pipe" });
|
|
@@ -27,202 +27,315 @@ function checkClaudeCLI() {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
// ──
|
|
31
|
-
function
|
|
30
|
+
// ── Run a single Claude -p call and return stdout ───────────────────
|
|
31
|
+
function runClaude(prompt) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const child = spawn("claude", ["-p", prompt], {
|
|
34
|
+
cwd: projectRoot,
|
|
35
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
36
|
+
env: { ...process.env, PIPELINE_ID: PIPELINE_ID },
|
|
37
|
+
});
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
child.stdout.on("data", (data) => {
|
|
41
|
+
const text = data.toString();
|
|
42
|
+
stdout += text;
|
|
43
|
+
// Print lines as they come
|
|
44
|
+
for (const line of text.split("\n")) {
|
|
45
|
+
if (line.trim())
|
|
46
|
+
console.log(`[Claude] ${line}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
child.stderr.on("data", (data) => {
|
|
50
|
+
stderr += data.toString();
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => {
|
|
53
|
+
if (stderr.trim())
|
|
54
|
+
console.error(`[Claude:err] ${stderr.trim()}`);
|
|
55
|
+
resolve({ stdout, code: code ?? 1 });
|
|
56
|
+
});
|
|
57
|
+
child.on("error", () => {
|
|
58
|
+
resolve({ stdout, code: 1 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ── Read context file if it exists ──────────────────────────────────
|
|
63
|
+
function readContextFile(filename) {
|
|
64
|
+
const filePath = path.join(contextDir, filename);
|
|
65
|
+
try {
|
|
66
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── List existing context files ─────────────────────────────────────
|
|
73
|
+
function listContextFiles() {
|
|
74
|
+
try {
|
|
75
|
+
return fs.readdirSync(contextDir).sort();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ── Build phase-specific prompts ────────────────────────────────────
|
|
82
|
+
const COMMON_INSTRUCTIONS = [
|
|
83
|
+
`PIPELINE_ID: ${PIPELINE_ID}`,
|
|
84
|
+
"",
|
|
85
|
+
"모든 산출물은 반드시 아래 경로에 저장:",
|
|
86
|
+
` pipelines/${PIPELINE_ID}/context/`,
|
|
87
|
+
"",
|
|
88
|
+
"예시:",
|
|
89
|
+
` pipelines/${PIPELINE_ID}/context/00_requirements.md`,
|
|
90
|
+
` pipelines/${PIPELINE_ID}/context/01_plan.md`,
|
|
91
|
+
"",
|
|
92
|
+
"절대 프로젝트 루트의 context/ 폴더에 파일을 만들지 마세요.",
|
|
93
|
+
'CLAUDE.md의 파이프라인 가이드를 따르세요.',
|
|
94
|
+
].join("\n");
|
|
95
|
+
function buildPhase0Prompt() {
|
|
32
96
|
return [
|
|
33
|
-
"
|
|
97
|
+
"## PHASE 0: 인풋 수신",
|
|
34
98
|
"",
|
|
35
|
-
|
|
99
|
+
"아래 요구사항을 분석해서 PM(Alex)으로서 작업 범위를 파악해주세요.",
|
|
36
100
|
"",
|
|
37
101
|
"요구사항:",
|
|
102
|
+
REQUIREMENTS,
|
|
103
|
+
"",
|
|
104
|
+
"수행할 작업:",
|
|
105
|
+
"1. 신규 기능인지 기존 기능 수정인지 판단",
|
|
106
|
+
"2. 영향 범위 파악 (FE / BE / Infra / 전체)",
|
|
107
|
+
"3. 필요한 Agent 역할 목록 결정",
|
|
108
|
+
"4. 예상 작업 순서 설계",
|
|
109
|
+
"",
|
|
110
|
+
`결과를 pipelines/${PIPELINE_ID}/context/00_requirements.md 에 저장해주세요.`,
|
|
111
|
+
"",
|
|
112
|
+
COMMON_INSTRUCTIONS,
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
function buildPhase1Prompt() {
|
|
116
|
+
const requirements = readContextFile("00_requirements.md") || REQUIREMENTS;
|
|
117
|
+
return [
|
|
118
|
+
"## PHASE 1: 기획",
|
|
119
|
+
"",
|
|
120
|
+
"기획자(Mina)로서 아래 요구사항을 바탕으로 기획안을 작성해주세요.",
|
|
121
|
+
"",
|
|
122
|
+
"=== 요구사항 ===",
|
|
123
|
+
requirements,
|
|
124
|
+
"=== 끝 ===",
|
|
125
|
+
"",
|
|
126
|
+
"기획안에 포함할 내용:",
|
|
127
|
+
"1. 개요 (목적, 핵심 가치, 작업 범위)",
|
|
128
|
+
"2. 유저 스토리",
|
|
129
|
+
"3. 기능 명세 (표 형식)",
|
|
130
|
+
"4. 화면 목록 (표 형식)",
|
|
131
|
+
"5. API 초안 (Method / Path / 설명 / 인증 여부)",
|
|
132
|
+
"6. 엣지케이스 & 예외 처리",
|
|
133
|
+
"7. 비기능 요구사항",
|
|
134
|
+
"",
|
|
135
|
+
`결과를 pipelines/${PIPELINE_ID}/context/01_plan.md 에 저장해주세요.`,
|
|
136
|
+
"",
|
|
137
|
+
COMMON_INSTRUCTIONS,
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
function buildPhase2Prompt() {
|
|
141
|
+
const requirements = readContextFile("00_requirements.md") || REQUIREMENTS;
|
|
142
|
+
const plan = readContextFile("01_plan.md") || "";
|
|
143
|
+
return [
|
|
144
|
+
"## PHASE 2: 설계",
|
|
145
|
+
"",
|
|
146
|
+
"디자이너(Lena)와 BE 설계자(Sam)로서 설계를 수행해주세요.",
|
|
147
|
+
"",
|
|
148
|
+
"=== 요구사항 ===",
|
|
38
149
|
requirements,
|
|
150
|
+
"=== 기획안 ===",
|
|
151
|
+
plan,
|
|
152
|
+
"=== 끝 ===",
|
|
153
|
+
"",
|
|
154
|
+
"디자이너 산출물 (02_design_spec.md):",
|
|
155
|
+
"- 디자인 토큰, 공통 컴포넌트, 화면별 레이아웃, 인터랙션, 접근성",
|
|
156
|
+
"",
|
|
157
|
+
"BE 설계 산출물 (03_api_spec.md + 03_erd.md):",
|
|
158
|
+
"- ERD, API 명세 상세, 인증/권한 설계",
|
|
159
|
+
"",
|
|
160
|
+
`모든 파일을 pipelines/${PIPELINE_ID}/context/ 에 저장해주세요.`,
|
|
161
|
+
"",
|
|
162
|
+
COMMON_INSTRUCTIONS,
|
|
163
|
+
].join("\n");
|
|
164
|
+
}
|
|
165
|
+
function buildPhase3Prompt() {
|
|
166
|
+
const contextFiles = listContextFiles();
|
|
167
|
+
let contextSummary = "";
|
|
168
|
+
for (const file of contextFiles) {
|
|
169
|
+
const content = readContextFile(file);
|
|
170
|
+
if (content) {
|
|
171
|
+
contextSummary += `\n=== ${file} ===\n${content}\n`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [
|
|
175
|
+
"## PHASE 3: 구현",
|
|
39
176
|
"",
|
|
40
|
-
"
|
|
41
|
-
'CLAUDE.md의 "Pipeline Dashboard Integration" 섹션을 반드시 따르세요.',
|
|
177
|
+
"FE(Jay), BE(Sam), Infra(Dex)로서 구현을 수행해주세요.",
|
|
42
178
|
"",
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`- 예: pipelines/${pipelineId}/context/00_requirements.md`,
|
|
47
|
-
`- 예: pipelines/${pipelineId}/context/01_plan.md`,
|
|
179
|
+
"지금까지의 산출물:",
|
|
180
|
+
contextSummary,
|
|
181
|
+
"=== 끝 ===",
|
|
48
182
|
"",
|
|
49
|
-
"
|
|
183
|
+
"기획안과 설계 명세를 바탕으로 코드를 구현해주세요.",
|
|
184
|
+
"각 Agent는 자신의 담당 파일만 수정합니다.",
|
|
185
|
+
"",
|
|
186
|
+
COMMON_INSTRUCTIONS,
|
|
187
|
+
].join("\n");
|
|
188
|
+
}
|
|
189
|
+
function buildPhase4Prompt() {
|
|
190
|
+
const contextFiles = listContextFiles();
|
|
191
|
+
let contextSummary = "";
|
|
192
|
+
for (const file of contextFiles) {
|
|
193
|
+
const content = readContextFile(file);
|
|
194
|
+
if (content) {
|
|
195
|
+
contextSummary += `\n=== ${file} ===\n${content}\n`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return [
|
|
199
|
+
"## PHASE 4: QA + 통합",
|
|
200
|
+
"",
|
|
201
|
+
"QA(Eva), 보안 리뷰어(Rex), 코드 리뷰어(Nora)로서 검증을 수행해주세요.",
|
|
202
|
+
"",
|
|
203
|
+
"지금까지의 산출물:",
|
|
204
|
+
contextSummary,
|
|
205
|
+
"=== 끝 ===",
|
|
206
|
+
"",
|
|
207
|
+
"산출물:",
|
|
208
|
+
`- pipelines/${PIPELINE_ID}/context/qa_report.md`,
|
|
209
|
+
`- pipelines/${PIPELINE_ID}/context/security_report.md`,
|
|
210
|
+
"",
|
|
211
|
+
COMMON_INSTRUCTIONS,
|
|
50
212
|
].join("\n");
|
|
51
213
|
}
|
|
214
|
+
const PHASES = [
|
|
215
|
+
{
|
|
216
|
+
phase: 0,
|
|
217
|
+
name: "인풋 수신",
|
|
218
|
+
buildPrompt: buildPhase0Prompt,
|
|
219
|
+
expectedFiles: ["00_requirements.md"],
|
|
220
|
+
checkpoint: "요구사항 분석 결과를 확인해주세요.",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
phase: 1,
|
|
224
|
+
name: "기획",
|
|
225
|
+
buildPrompt: buildPhase1Prompt,
|
|
226
|
+
expectedFiles: ["01_plan.md"],
|
|
227
|
+
checkpoint: "기획안을 검토해주세요.",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
phase: 2,
|
|
231
|
+
name: "설계",
|
|
232
|
+
buildPrompt: buildPhase2Prompt,
|
|
233
|
+
expectedFiles: ["02_design_spec.md", "03_api_spec.md"],
|
|
234
|
+
checkpoint: "디자인 명세 + API 명세를 검토해주세요.",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
phase: 3,
|
|
238
|
+
name: "구현",
|
|
239
|
+
buildPrompt: buildPhase3Prompt,
|
|
240
|
+
expectedFiles: [],
|
|
241
|
+
checkpoint: "구현 결과를 확인해주세요.",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
phase: 4,
|
|
245
|
+
name: "QA + 통합",
|
|
246
|
+
buildPrompt: buildPhase4Prompt,
|
|
247
|
+
expectedFiles: ["qa_report.md", "security_report.md"],
|
|
248
|
+
checkpoint: "QA 보고서 + 보안 보고서를 확인해주세요.",
|
|
249
|
+
},
|
|
250
|
+
];
|
|
52
251
|
// ── Main ────────────────────────────────────────────────────────────
|
|
53
252
|
async function main() {
|
|
54
253
|
const stateManager = new StateManager(PIPELINES_DIR, PIPELINE_ID);
|
|
55
|
-
// Verify state.json exists (created by dashboard)
|
|
56
254
|
const initialState = stateManager.read();
|
|
57
255
|
if (!initialState) {
|
|
58
256
|
console.error(`state.json not found for pipeline ${PIPELINE_ID}`);
|
|
59
257
|
process.exit(1);
|
|
60
258
|
}
|
|
61
|
-
// Pre-check Claude CLI
|
|
62
259
|
if (!checkClaudeCLI()) {
|
|
63
260
|
stateManager.setStatus("failed");
|
|
64
|
-
stateManager.addActivity("system", "error", "Claude CLI를 찾을 수 없거나 로그인되어 있지 않습니다.
|
|
261
|
+
stateManager.addActivity("system", "error", "Claude CLI를 찾을 수 없거나 로그인되어 있지 않습니다.");
|
|
65
262
|
process.exit(1);
|
|
66
263
|
}
|
|
67
|
-
//
|
|
68
|
-
|
|
264
|
+
// Ensure context directory exists
|
|
265
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
266
|
+
// Start context watcher (fallback: copies root context/ to pipeline context/)
|
|
69
267
|
const contextWatcher = new ContextWatcher(stateManager, PIPELINES_DIR, PIPELINE_ID);
|
|
70
|
-
// Wire up: notify contextWatcher when signals are processed
|
|
71
|
-
signalWatcher.on("phase", () => contextWatcher.notifySignalProcessed());
|
|
72
|
-
signalWatcher.on("checkpoint", () => contextWatcher.notifySignalProcessed());
|
|
73
|
-
signalWatcher.start(500);
|
|
74
268
|
contextWatcher.start();
|
|
75
269
|
stateManager.setStatus("running");
|
|
76
270
|
stateManager.addActivity("system", "info", "파이프라인 시작");
|
|
77
|
-
// ──
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
271
|
+
// ── Run phases sequentially ─────────────────────────────────────
|
|
272
|
+
for (const phaseConfig of PHASES) {
|
|
273
|
+
const { phase, name, buildPrompt, expectedFiles, checkpoint } = phaseConfig;
|
|
274
|
+
console.log(`\n[Runner] ── Phase ${phase}: ${name} ──`);
|
|
275
|
+
stateManager.setPhase(phase);
|
|
276
|
+
stateManager.addActivity("system", "info", `Phase ${phase} 시작: ${name}`);
|
|
277
|
+
// Build and run prompt
|
|
278
|
+
const prompt = buildPrompt();
|
|
279
|
+
const result = await runClaude(prompt);
|
|
280
|
+
if (result.code !== 0) {
|
|
281
|
+
stateManager.setStatus("failed");
|
|
282
|
+
stateManager.addActivity("system", "error", `Phase ${phase} 실패 (exit code: ${result.code})`);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
// Log Claude's output as activity (truncated)
|
|
286
|
+
const summary = result.stdout.trim().slice(0, 200);
|
|
287
|
+
if (summary) {
|
|
288
|
+
stateManager.addActivity("system", "progress", summary + (result.stdout.length > 200 ? "..." : ""));
|
|
289
|
+
}
|
|
290
|
+
// Register output files
|
|
291
|
+
for (const file of expectedFiles) {
|
|
292
|
+
if (fs.existsSync(path.join(contextDir, file))) {
|
|
293
|
+
stateManager.addOutput(`context/${file}`, phase);
|
|
294
|
+
stateManager.addActivity("system", "success", `산출물 생성: ${file}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Also register any unexpected context files
|
|
298
|
+
for (const file of listContextFiles()) {
|
|
299
|
+
const existing = stateManager.read();
|
|
300
|
+
if (existing && !existing.outputs.some((o) => o.filename === `context/${file}`)) {
|
|
301
|
+
const filePhase = phase;
|
|
302
|
+
stateManager.addOutput(`context/${file}`, filePhase);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ── Checkpoint: wait for user approval ──────────────────────
|
|
306
|
+
stateManager.addActivity("system", "info", `Checkpoint Phase ${phase}: ${checkpoint}`);
|
|
87
307
|
stateManager.setStatus("paused");
|
|
308
|
+
console.log(`[Runner] Checkpoint Phase ${phase}: waiting for approval...`);
|
|
88
309
|
try {
|
|
89
|
-
const response = await waitForCheckpoint(PIPELINES_DIR, PIPELINE_ID
|
|
310
|
+
const response = await waitForCheckpoint(PIPELINES_DIR, PIPELINE_ID);
|
|
90
311
|
if (response.action === "approve") {
|
|
91
312
|
stateManager.addActivity("system", "success", `Checkpoint Phase ${phase} approved`);
|
|
313
|
+
stateManager.setStatus("running");
|
|
314
|
+
console.log(`[Runner] Phase ${phase} approved`);
|
|
92
315
|
}
|
|
93
316
|
else {
|
|
94
|
-
const feedback = response.message || "
|
|
317
|
+
const feedback = response.message || "수정 요청";
|
|
95
318
|
stateManager.addActivity("system", "info", `Checkpoint Phase ${phase} rejected: ${feedback}`);
|
|
319
|
+
stateManager.setStatus("failed");
|
|
320
|
+
stateManager.addActivity("system", "error", `Phase ${phase}에서 사용자가 거절함`);
|
|
321
|
+
console.log(`[Runner] Phase ${phase} rejected: ${feedback}`);
|
|
322
|
+
break;
|
|
96
323
|
}
|
|
97
|
-
// Note: Claude runs in -p (print) mode with stdin ignored.
|
|
98
|
-
// Checkpoint responses are recorded in state.json activities.
|
|
99
|
-
// Claude reads checkpoint_response.json via signal protocol.
|
|
100
|
-
stateManager.setStatus("running");
|
|
101
324
|
}
|
|
102
325
|
catch (err) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
// ── Stream stdout/stderr → parse for activity logging ─────────────
|
|
109
|
-
// Even if Claude doesn't write signal files, we extract progress
|
|
110
|
-
// from stdout to keep the dashboard alive with activity updates.
|
|
111
|
-
const AGENT_KEYWORDS = {
|
|
112
|
-
"PM": "alex", "Alex": "alex",
|
|
113
|
-
"기획": "mina", "Mina": "mina",
|
|
114
|
-
"디자이너": "lena", "Lena": "lena", "디자인": "lena",
|
|
115
|
-
"FE": "jay", "Jay": "jay", "프론트": "jay",
|
|
116
|
-
"BE": "sam", "Sam": "sam", "백엔드": "sam",
|
|
117
|
-
"인프라": "dex", "Dex": "dex", "Infra": "dex", "Docker": "dex",
|
|
118
|
-
"QA": "eva", "Eva": "eva", "테스트": "eva",
|
|
119
|
-
"보안": "rex", "Rex": "rex", "Security": "rex",
|
|
120
|
-
"리뷰": "nora", "Nora": "nora", "코드 리뷰": "nora",
|
|
121
|
-
};
|
|
122
|
-
const PHASE_PATTERNS = [
|
|
123
|
-
{ pattern: /PHASE\s*0|인풋\s*수신|요구사항\s*분석/i, phase: 0 },
|
|
124
|
-
{ pattern: /PHASE\s*1|기획|plan/i, phase: 1 },
|
|
125
|
-
{ pattern: /PHASE\s*2|설계|design|API\s*명세/i, phase: 2 },
|
|
126
|
-
{ pattern: /PHASE\s*3|구현|implement/i, phase: 3 },
|
|
127
|
-
{ pattern: /PHASE\s*4|QA|통합|보안\s*리뷰/i, phase: 4 },
|
|
128
|
-
];
|
|
129
|
-
let lastActivityTime = 0;
|
|
130
|
-
const ACTIVITY_THROTTLE_MS = 3000; // Don't spam: max 1 activity per 3s from stdout
|
|
131
|
-
function detectAgentFromLine(line) {
|
|
132
|
-
for (const [keyword, agentId] of Object.entries(AGENT_KEYWORDS)) {
|
|
133
|
-
if (line.includes(keyword))
|
|
134
|
-
return agentId;
|
|
326
|
+
console.error("[Runner] Checkpoint error:", err);
|
|
327
|
+
stateManager.setStatus("failed");
|
|
328
|
+
break;
|
|
135
329
|
}
|
|
136
|
-
return "system";
|
|
137
330
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("```"))
|
|
144
|
-
return;
|
|
145
|
-
if (/^[-=_]{3,}$/.test(trimmed))
|
|
146
|
-
return;
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
// Phase detection (always process, no throttle)
|
|
149
|
-
for (const { pattern, phase } of PHASE_PATTERNS) {
|
|
150
|
-
if (pattern.test(trimmed)) {
|
|
151
|
-
const currentState = stateManager.read();
|
|
152
|
-
if (currentState && currentState.currentPhase < phase) {
|
|
153
|
-
stateManager.setPhase(phase);
|
|
154
|
-
stateManager.addActivity("system", "info", `Phase ${phase} 시작`);
|
|
155
|
-
contextWatcher.notifySignalProcessed();
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// Checkpoint detection from stdout
|
|
161
|
-
if (/체크포인트|checkpoint/i.test(trimmed) && /승인|확인|검토|approve|review/i.test(trimmed)) {
|
|
162
|
-
// Don't duplicate if signal-watcher already caught it
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// Throttled activity logging
|
|
166
|
-
if (now - lastActivityTime < ACTIVITY_THROTTLE_MS)
|
|
167
|
-
return;
|
|
168
|
-
lastActivityTime = now;
|
|
169
|
-
// Log meaningful lines as activities
|
|
170
|
-
const agentId = detectAgentFromLine(trimmed);
|
|
171
|
-
// Truncate long lines
|
|
172
|
-
const message = trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed;
|
|
173
|
-
stateManager.addActivity(agentId, "progress", message);
|
|
331
|
+
// ── Finalize ──────────────────────────────────────────────────
|
|
332
|
+
const finalState = stateManager.read();
|
|
333
|
+
if (finalState && finalState.status === "running") {
|
|
334
|
+
stateManager.setStatus("completed");
|
|
335
|
+
stateManager.addActivity("system", "success", "파이프라인 완료");
|
|
174
336
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
stdoutBuffer += data.toString();
|
|
178
|
-
const lines = stdoutBuffer.split("\n");
|
|
179
|
-
stdoutBuffer = lines.pop() || "";
|
|
180
|
-
for (const line of lines) {
|
|
181
|
-
if (line.trim()) {
|
|
182
|
-
console.log(`[Claude] ${line}`);
|
|
183
|
-
processStdoutLine(line);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
claude.stderr.on("data", (data) => {
|
|
188
|
-
const text = data.toString().trim();
|
|
189
|
-
if (text) {
|
|
190
|
-
console.error(`[Claude:err] ${text}`);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
// ── Handle process exit ───────────────────────────────────────────
|
|
194
|
-
claude.on("close", (code) => {
|
|
195
|
-
console.log(`[Runner] Claude process exited with code ${code}`);
|
|
196
|
-
abortController.abort();
|
|
197
|
-
if (code === 0) {
|
|
198
|
-
stateManager.setStatus("completed");
|
|
199
|
-
stateManager.addActivity("system", "success", "파이프라인 완료");
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
stateManager.setStatus("failed");
|
|
203
|
-
stateManager.addActivity("system", "error", `Claude 프로세스가 비정상 종료되었습니다 (exit code: ${code})`);
|
|
204
|
-
}
|
|
205
|
-
signalWatcher.stop();
|
|
206
|
-
contextWatcher.stop();
|
|
207
|
-
});
|
|
208
|
-
claude.on("error", (err) => {
|
|
209
|
-
console.error("[Runner] Failed to spawn Claude:", err);
|
|
210
|
-
stateManager.setStatus("failed");
|
|
211
|
-
stateManager.addActivity("system", "error", `Claude 실행 실패: ${err.message}`);
|
|
212
|
-
signalWatcher.stop();
|
|
213
|
-
contextWatcher.stop();
|
|
214
|
-
});
|
|
215
|
-
// ── Graceful shutdown ─────────────────────────────────────────────
|
|
216
|
-
const cleanup = () => {
|
|
217
|
-
abortController.abort();
|
|
218
|
-
signalWatcher.stop();
|
|
219
|
-
contextWatcher.stop();
|
|
220
|
-
if (!claude.killed) {
|
|
221
|
-
claude.kill("SIGTERM");
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
process.on("SIGINT", cleanup);
|
|
225
|
-
process.on("SIGTERM", cleanup);
|
|
337
|
+
contextWatcher.stop();
|
|
338
|
+
console.log("[Runner] Pipeline finished");
|
|
226
339
|
}
|
|
227
340
|
main().catch((err) => {
|
|
228
341
|
console.error("[Runner] Fatal error:", err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-runner.js","sourceRoot":"","sources":["../src/pipeline-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"pipeline-runner.js","sourceRoot":"","sources":["../src/pipeline-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAgB,MAAM,eAAe,CAAC;AAC9D,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;AAC5C,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;AAChD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAEhF,IAAI,CAAC,WAAW,IAAI,CAAC,aAAa,EAAE,CAAC;IACnC,OAAO,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;AACtD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;AAEpE,uEAAuE;AACvE,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,uEAAuE;AACvE,SAAS,SAAS,CAAC,MAAc;IAC/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE;YAC5C,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;YACjC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,WAAY,EAAE;SACnD,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,IAAI,CAAC;YACf,2BAA2B;YAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpC,IAAI,IAAI,CAAC,IAAI,EAAE;oBAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;YACnD,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACvC,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,IAAI,MAAM,CAAC,IAAI,EAAE;gBAAE,OAAO,CAAC,KAAK,CAAC,gBAAgB,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAClE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACrB,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,uEAAuE;AACvE,SAAS,eAAe,CAAC,QAAgB;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,uEAAuE;AACvE,SAAS,gBAAgB;IACvB,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,uEAAuE;AAEvE,MAAM,mBAAmB,GAAG;IAC1B,gBAAgB,WAAW,EAAE;IAC7B,EAAE;IACF,wBAAwB;IACxB,eAAe,WAAW,WAAW;IACrC,EAAE;IACF,KAAK;IACL,eAAe,WAAW,6BAA6B;IACvD,eAAe,WAAW,qBAAqB;IAC/C,EAAE;IACF,uCAAuC;IACvC,6BAA6B;CAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,SAAS,iBAAiB;IACxB,OAAO;QACL,mBAAmB;QACnB,EAAE;QACF,0CAA0C;QAC1C,EAAE;QACF,OAAO;QACP,YAAY;QACZ,EAAE;QACF,SAAS;QACT,0BAA0B;QAC1B,oCAAoC;QACpC,uBAAuB;QACvB,gBAAgB;QAChB,EAAE;QACF,iBAAiB,WAAW,uCAAuC;QACnE,EAAE;QACF,mBAAmB;KACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,YAAY,GAAG,eAAe,CAAC,oBAAoB,CAAC,IAAI,YAAY,CAAC;IAC3E,OAAO;QACL,gBAAgB;QAChB,EAAE;QACF,wCAAwC;QACxC,EAAE;QACF,cAAc;QACd,YAAY;QACZ,WAAW;QACX,EAAE;QACF,cAAc;QACd,0BAA0B;QAC1B,WAAW;QACX,iBAAiB;QACjB,iBAAiB;QACjB,wCAAwC;QACxC,kBAAkB;QAClB,aAAa;QACb,EAAE;QACF,iBAAiB,WAAW,+BAA+B;QAC3D,EAAE;QACF,mBAAmB;KACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,YAAY,GAAG,eAAe,CAAC,oBAAoB,CAAC,IAAI,YAAY,CAAC;IAC3E,MAAM,IAAI,GAAG,eAAe,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO;QACL,gBAAgB;QAChB,EAAE;QACF,uCAAuC;QACvC,EAAE;QACF,cAAc;QACd,YAAY;QACZ,aAAa;QACb,IAAI;QACJ,WAAW;QACX,EAAE;QACF,+BAA+B;QAC/B,wCAAwC;QACxC,EAAE;QACF,yCAAyC;QACzC,4BAA4B;QAC5B,EAAE;QACF,oBAAoB,WAAW,qBAAqB;QACpD,EAAE;QACF,mBAAmB;KACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAC;IACxC,IAAI,cAAc,GAAG,EAAE,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,EAAE,CAAC;YACZ,cAAc,IAAI,SAAS,IAAI,SAAS,OAAO,IAAI,CAAC;QACtD,CAAC;IACH,CAAC;IACD,OAAO;QACL,gBAAgB;QAChB,EAAE;QACF,4CAA4C;QAC5C,EAAE;QACF,YAAY;QACZ,cAAc;QACd,WAAW;QACX,EAAE;QACF,8BAA8B;QAC9B,4BAA4B;QAC5B,EAAE;QACF,mBAAmB;KACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAC;IACxC,IAAI,cAAc,GAAG,EAAE,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,EAAE,CAAC;YACZ,cAAc,IAAI,SAAS,IAAI,SAAS,OAAO,IAAI,CAAC;QACtD,CAAC;IACH,CAAC;IACD,OAAO;QACL,qBAAqB;QACrB,EAAE;QACF,kDAAkD;QAClD,EAAE;QACF,YAAY;QACZ,cAAc;QACd,WAAW;QACX,EAAE;QACF,MAAM;QACN,eAAe,WAAW,uBAAuB;QACjD,eAAe,WAAW,6BAA6B;QACvD,EAAE;QACF,mBAAmB;KACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAYD,MAAM,MAAM,GAAkB;IAC5B;QACE,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,iBAAiB;QAC9B,aAAa,EAAE,CAAC,oBAAoB,CAAC;QACrC,UAAU,EAAE,qBAAqB;KAClC;IACD;QACE,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,iBAAiB;QAC9B,aAAa,EAAE,CAAC,YAAY,CAAC;QAC7B,UAAU,EAAE,cAAc;KAC3B;IACD;QACE,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,iBAAiB;QAC9B,aAAa,EAAE,CAAC,mBAAmB,EAAE,gBAAgB,CAAC;QACtD,UAAU,EAAE,0BAA0B;KACvC;IACD;QACE,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,iBAAiB;QAC9B,aAAa,EAAE,EAAE;QACjB,UAAU,EAAE,gBAAgB;KAC7B;IACD;QACE,KAAK,EAAE,CAAC;QACR,IAAI,EAAE,SAAS;QACf,WAAW,EAAE,iBAAiB;QAC9B,aAAa,EAAE,CAAC,cAAc,EAAE,oBAAoB,CAAC;QACrD,UAAU,EAAE,0BAA0B;KACvC;CACF,CAAC;AAEF,uEAAuE;AACvE,KAAK,UAAU,IAAI;IACjB,MAAM,YAAY,GAAG,IAAI,YAAY,CAAC,aAAc,EAAE,WAAY,CAAC,CAAC;IAEpE,MAAM,YAAY,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC;IACzC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,qCAAqC,WAAW,EAAE,CAAC,CAAC;QAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;QACtB,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACjC,YAAY,CAAC,WAAW,CACtB,QAAQ,EACR,OAAO,EACP,qCAAqC,CACtC,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,kCAAkC;IAClC,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,8EAA8E;IAC9E,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,YAAY,EAAE,aAAc,EAAE,WAAY,CAAC,CAAC;IACtF,cAAc,CAAC,KAAK,EAAE,CAAC;IAEvB,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAEvD,mEAAmE;IACnE,KAAK,MAAM,WAAW,IAAI,MAAM,EAAE,CAAC;QACjC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,WAAW,CAAC;QAE5E,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC;QACxD,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC7B,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,IAAI,EAAE,CAAC,CAAC;QAEzE,uBAAuB;QACvB,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC;QAEvC,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACtB,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YACjC,YAAY,CAAC,WAAW,CACtB,QAAQ,EACR,OAAO,EACP,SAAS,KAAK,mBAAmB,MAAM,CAAC,IAAI,GAAG,CAChD,CAAC;YACF,MAAM;QACR,CAAC;QAED,8CAA8C;QAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACnD,IAAI,OAAO,EAAE,CAAC;YACZ,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACtG,CAAC;QAED,wBAAwB;QACxB,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;gBAC/C,YAAY,CAAC,SAAS,CAAC,WAAW,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;gBACjD,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QAED,6CAA6C;QAC7C,KAAK,MAAM,IAAI,IAAI,gBAAgB,EAAE,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,QAAQ,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,WAAW,IAAI,EAAE,CAAC,EAAE,CAAC;gBAChF,MAAM,SAAS,GAAG,KAAK,CAAC;gBACxB,YAAY,CAAC,SAAS,CAAC,WAAW,IAAI,EAAE,EAAE,SAAS,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;QAED,+DAA+D;QAC/D,YAAY,CAAC,WAAW,CACtB,QAAQ,EACR,MAAM,EACN,oBAAoB,KAAK,KAAK,UAAU,EAAE,CAC3C,CAAC;QACF,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAEjC,OAAO,CAAC,GAAG,CAAC,6BAA6B,KAAK,2BAA2B,CAAC,CAAC;QAE3E,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,aAAc,EAAE,WAAY,CAAC,CAAC;YAEvE,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAClC,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,oBAAoB,KAAK,WAAW,CAAC,CAAC;gBACpF,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBAClC,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,WAAW,CAAC,CAAC;YAClD,CAAC;iBAAM,CAAC;gBACN,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC;gBAC7C,YAAY,CAAC,WAAW,CACtB,QAAQ,EACR,MAAM,EACN,oBAAoB,KAAK,cAAc,QAAQ,EAAE,CAClD,CAAC;gBACF,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;gBACjC,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,KAAK,aAAa,CAAC,CAAC;gBACzE,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,cAAc,QAAQ,EAAE,CAAC,CAAC;gBAC7D,MAAM;YACR,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,GAAG,CAAC,CAAC;YACjD,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YACjC,MAAM;QACR,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAClD,YAAY,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QACpC,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;IAED,cAAc,CAAC,IAAI,EAAE,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;AAC5C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { spawn, execSync } from "child_process";
|
|
1
|
+
import { spawn, execSync, ChildProcess } from "child_process";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { StateManager } from "./state-manager.js";
|
|
5
|
-
import { SignalWatcher } from "./signal-watcher.js";
|
|
6
5
|
import { ContextWatcher } from "./context-watcher.js";
|
|
7
6
|
import { waitForCheckpoint } from "./checkpoint-waiter.js";
|
|
8
7
|
|
|
@@ -21,9 +20,9 @@ if (!REQUIREMENTS) {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
const projectRoot = path.resolve(PIPELINES_DIR, "..");
|
|
24
|
-
const
|
|
23
|
+
const contextDir = path.join(PIPELINES_DIR, PIPELINE_ID, "context");
|
|
25
24
|
|
|
26
|
-
// ── Pre-check
|
|
25
|
+
// ── Pre-check ───────────────────────────────────────────────────────
|
|
27
26
|
function checkClaudeCLI(): boolean {
|
|
28
27
|
try {
|
|
29
28
|
execSync("claude --version", { timeout: 10000, stdio: "pipe" });
|
|
@@ -33,243 +32,369 @@ function checkClaudeCLI(): boolean {
|
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
// ──
|
|
37
|
-
function
|
|
35
|
+
// ── Run a single Claude -p call and return stdout ───────────────────
|
|
36
|
+
function runClaude(prompt: string): Promise<{ stdout: string; code: number }> {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const child = spawn("claude", ["-p", prompt], {
|
|
39
|
+
cwd: projectRoot,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
env: { ...process.env, PIPELINE_ID: PIPELINE_ID! },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let stdout = "";
|
|
45
|
+
let stderr = "";
|
|
46
|
+
|
|
47
|
+
child.stdout.on("data", (data: Buffer) => {
|
|
48
|
+
const text = data.toString();
|
|
49
|
+
stdout += text;
|
|
50
|
+
// Print lines as they come
|
|
51
|
+
for (const line of text.split("\n")) {
|
|
52
|
+
if (line.trim()) console.log(`[Claude] ${line}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.stderr.on("data", (data: Buffer) => {
|
|
57
|
+
stderr += data.toString();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.on("close", (code) => {
|
|
61
|
+
if (stderr.trim()) console.error(`[Claude:err] ${stderr.trim()}`);
|
|
62
|
+
resolve({ stdout, code: code ?? 1 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
child.on("error", () => {
|
|
66
|
+
resolve({ stdout, code: 1 });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Read context file if it exists ──────────────────────────────────
|
|
72
|
+
function readContextFile(filename: string): string | null {
|
|
73
|
+
const filePath = path.join(contextDir, filename);
|
|
74
|
+
try {
|
|
75
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── List existing context files ─────────────────────────────────────
|
|
82
|
+
function listContextFiles(): string[] {
|
|
83
|
+
try {
|
|
84
|
+
return fs.readdirSync(contextDir).sort();
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Build phase-specific prompts ────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const COMMON_INSTRUCTIONS = [
|
|
93
|
+
`PIPELINE_ID: ${PIPELINE_ID}`,
|
|
94
|
+
"",
|
|
95
|
+
"모든 산출물은 반드시 아래 경로에 저장:",
|
|
96
|
+
` pipelines/${PIPELINE_ID}/context/`,
|
|
97
|
+
"",
|
|
98
|
+
"예시:",
|
|
99
|
+
` pipelines/${PIPELINE_ID}/context/00_requirements.md`,
|
|
100
|
+
` pipelines/${PIPELINE_ID}/context/01_plan.md`,
|
|
101
|
+
"",
|
|
102
|
+
"절대 프로젝트 루트의 context/ 폴더에 파일을 만들지 마세요.",
|
|
103
|
+
'CLAUDE.md의 파이프라인 가이드를 따르세요.',
|
|
104
|
+
].join("\n");
|
|
105
|
+
|
|
106
|
+
function buildPhase0Prompt(): string {
|
|
38
107
|
return [
|
|
39
|
-
"
|
|
108
|
+
"## PHASE 0: 인풋 수신",
|
|
40
109
|
"",
|
|
41
|
-
|
|
110
|
+
"아래 요구사항을 분석해서 PM(Alex)으로서 작업 범위를 파악해주세요.",
|
|
42
111
|
"",
|
|
43
112
|
"요구사항:",
|
|
113
|
+
REQUIREMENTS,
|
|
114
|
+
"",
|
|
115
|
+
"수행할 작업:",
|
|
116
|
+
"1. 신규 기능인지 기존 기능 수정인지 판단",
|
|
117
|
+
"2. 영향 범위 파악 (FE / BE / Infra / 전체)",
|
|
118
|
+
"3. 필요한 Agent 역할 목록 결정",
|
|
119
|
+
"4. 예상 작업 순서 설계",
|
|
120
|
+
"",
|
|
121
|
+
`결과를 pipelines/${PIPELINE_ID}/context/00_requirements.md 에 저장해주세요.`,
|
|
122
|
+
"",
|
|
123
|
+
COMMON_INSTRUCTIONS,
|
|
124
|
+
].join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildPhase1Prompt(): string {
|
|
128
|
+
const requirements = readContextFile("00_requirements.md") || REQUIREMENTS;
|
|
129
|
+
return [
|
|
130
|
+
"## PHASE 1: 기획",
|
|
131
|
+
"",
|
|
132
|
+
"기획자(Mina)로서 아래 요구사항을 바탕으로 기획안을 작성해주세요.",
|
|
133
|
+
"",
|
|
134
|
+
"=== 요구사항 ===",
|
|
44
135
|
requirements,
|
|
136
|
+
"=== 끝 ===",
|
|
45
137
|
"",
|
|
46
|
-
"
|
|
47
|
-
|
|
138
|
+
"기획안에 포함할 내용:",
|
|
139
|
+
"1. 개요 (목적, 핵심 가치, 작업 범위)",
|
|
140
|
+
"2. 유저 스토리",
|
|
141
|
+
"3. 기능 명세 (표 형식)",
|
|
142
|
+
"4. 화면 목록 (표 형식)",
|
|
143
|
+
"5. API 초안 (Method / Path / 설명 / 인증 여부)",
|
|
144
|
+
"6. 엣지케이스 & 예외 처리",
|
|
145
|
+
"7. 비기능 요구사항",
|
|
48
146
|
"",
|
|
49
|
-
|
|
50
|
-
`- 모든 산출물(context 파일)은 pipelines/${pipelineId}/context/ 에 생성`,
|
|
51
|
-
`- 시그널 파일은 pipelines/${pipelineId}/signals/ 에 생성`,
|
|
52
|
-
`- 예: pipelines/${pipelineId}/context/00_requirements.md`,
|
|
53
|
-
`- 예: pipelines/${pipelineId}/context/01_plan.md`,
|
|
147
|
+
`결과를 pipelines/${PIPELINE_ID}/context/01_plan.md 에 저장해주세요.`,
|
|
54
148
|
"",
|
|
55
|
-
|
|
149
|
+
COMMON_INSTRUCTIONS,
|
|
56
150
|
].join("\n");
|
|
57
151
|
}
|
|
58
152
|
|
|
153
|
+
function buildPhase2Prompt(): string {
|
|
154
|
+
const requirements = readContextFile("00_requirements.md") || REQUIREMENTS;
|
|
155
|
+
const plan = readContextFile("01_plan.md") || "";
|
|
156
|
+
return [
|
|
157
|
+
"## PHASE 2: 설계",
|
|
158
|
+
"",
|
|
159
|
+
"디자이너(Lena)와 BE 설계자(Sam)로서 설계를 수행해주세요.",
|
|
160
|
+
"",
|
|
161
|
+
"=== 요구사항 ===",
|
|
162
|
+
requirements,
|
|
163
|
+
"=== 기획안 ===",
|
|
164
|
+
plan,
|
|
165
|
+
"=== 끝 ===",
|
|
166
|
+
"",
|
|
167
|
+
"디자이너 산출물 (02_design_spec.md):",
|
|
168
|
+
"- 디자인 토큰, 공통 컴포넌트, 화면별 레이아웃, 인터랙션, 접근성",
|
|
169
|
+
"",
|
|
170
|
+
"BE 설계 산출물 (03_api_spec.md + 03_erd.md):",
|
|
171
|
+
"- ERD, API 명세 상세, 인증/권한 설계",
|
|
172
|
+
"",
|
|
173
|
+
`모든 파일을 pipelines/${PIPELINE_ID}/context/ 에 저장해주세요.`,
|
|
174
|
+
"",
|
|
175
|
+
COMMON_INSTRUCTIONS,
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildPhase3Prompt(): string {
|
|
180
|
+
const contextFiles = listContextFiles();
|
|
181
|
+
let contextSummary = "";
|
|
182
|
+
for (const file of contextFiles) {
|
|
183
|
+
const content = readContextFile(file);
|
|
184
|
+
if (content) {
|
|
185
|
+
contextSummary += `\n=== ${file} ===\n${content}\n`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [
|
|
189
|
+
"## PHASE 3: 구현",
|
|
190
|
+
"",
|
|
191
|
+
"FE(Jay), BE(Sam), Infra(Dex)로서 구현을 수행해주세요.",
|
|
192
|
+
"",
|
|
193
|
+
"지금까지의 산출물:",
|
|
194
|
+
contextSummary,
|
|
195
|
+
"=== 끝 ===",
|
|
196
|
+
"",
|
|
197
|
+
"기획안과 설계 명세를 바탕으로 코드를 구현해주세요.",
|
|
198
|
+
"각 Agent는 자신의 담당 파일만 수정합니다.",
|
|
199
|
+
"",
|
|
200
|
+
COMMON_INSTRUCTIONS,
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildPhase4Prompt(): string {
|
|
205
|
+
const contextFiles = listContextFiles();
|
|
206
|
+
let contextSummary = "";
|
|
207
|
+
for (const file of contextFiles) {
|
|
208
|
+
const content = readContextFile(file);
|
|
209
|
+
if (content) {
|
|
210
|
+
contextSummary += `\n=== ${file} ===\n${content}\n`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [
|
|
214
|
+
"## PHASE 4: QA + 통합",
|
|
215
|
+
"",
|
|
216
|
+
"QA(Eva), 보안 리뷰어(Rex), 코드 리뷰어(Nora)로서 검증을 수행해주세요.",
|
|
217
|
+
"",
|
|
218
|
+
"지금까지의 산출물:",
|
|
219
|
+
contextSummary,
|
|
220
|
+
"=== 끝 ===",
|
|
221
|
+
"",
|
|
222
|
+
"산출물:",
|
|
223
|
+
`- pipelines/${PIPELINE_ID}/context/qa_report.md`,
|
|
224
|
+
`- pipelines/${PIPELINE_ID}/context/security_report.md`,
|
|
225
|
+
"",
|
|
226
|
+
COMMON_INSTRUCTIONS,
|
|
227
|
+
].join("\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Phase definitions ───────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
interface PhaseConfig {
|
|
233
|
+
phase: number;
|
|
234
|
+
name: string;
|
|
235
|
+
buildPrompt: () => string;
|
|
236
|
+
expectedFiles: string[];
|
|
237
|
+
checkpoint: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const PHASES: PhaseConfig[] = [
|
|
241
|
+
{
|
|
242
|
+
phase: 0,
|
|
243
|
+
name: "인풋 수신",
|
|
244
|
+
buildPrompt: buildPhase0Prompt,
|
|
245
|
+
expectedFiles: ["00_requirements.md"],
|
|
246
|
+
checkpoint: "요구사항 분석 결과를 확인해주세요.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
phase: 1,
|
|
250
|
+
name: "기획",
|
|
251
|
+
buildPrompt: buildPhase1Prompt,
|
|
252
|
+
expectedFiles: ["01_plan.md"],
|
|
253
|
+
checkpoint: "기획안을 검토해주세요.",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
phase: 2,
|
|
257
|
+
name: "설계",
|
|
258
|
+
buildPrompt: buildPhase2Prompt,
|
|
259
|
+
expectedFiles: ["02_design_spec.md", "03_api_spec.md"],
|
|
260
|
+
checkpoint: "디자인 명세 + API 명세를 검토해주세요.",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
phase: 3,
|
|
264
|
+
name: "구현",
|
|
265
|
+
buildPrompt: buildPhase3Prompt,
|
|
266
|
+
expectedFiles: [],
|
|
267
|
+
checkpoint: "구현 결과를 확인해주세요.",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
phase: 4,
|
|
271
|
+
name: "QA + 통합",
|
|
272
|
+
buildPrompt: buildPhase4Prompt,
|
|
273
|
+
expectedFiles: ["qa_report.md", "security_report.md"],
|
|
274
|
+
checkpoint: "QA 보고서 + 보안 보고서를 확인해주세요.",
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
|
|
59
278
|
// ── Main ────────────────────────────────────────────────────────────
|
|
60
279
|
async function main(): Promise<void> {
|
|
61
280
|
const stateManager = new StateManager(PIPELINES_DIR!, PIPELINE_ID!);
|
|
62
281
|
|
|
63
|
-
// Verify state.json exists (created by dashboard)
|
|
64
282
|
const initialState = stateManager.read();
|
|
65
283
|
if (!initialState) {
|
|
66
284
|
console.error(`state.json not found for pipeline ${PIPELINE_ID}`);
|
|
67
285
|
process.exit(1);
|
|
68
286
|
}
|
|
69
287
|
|
|
70
|
-
// Pre-check Claude CLI
|
|
71
288
|
if (!checkClaudeCLI()) {
|
|
72
289
|
stateManager.setStatus("failed");
|
|
73
290
|
stateManager.addActivity(
|
|
74
291
|
"system",
|
|
75
292
|
"error",
|
|
76
|
-
"Claude CLI를 찾을 수 없거나 로그인되어 있지 않습니다.
|
|
293
|
+
"Claude CLI를 찾을 수 없거나 로그인되어 있지 않습니다.",
|
|
77
294
|
);
|
|
78
295
|
process.exit(1);
|
|
79
296
|
}
|
|
80
297
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
const contextWatcher = new ContextWatcher(stateManager, PIPELINES_DIR!, PIPELINE_ID!);
|
|
84
|
-
|
|
85
|
-
// Wire up: notify contextWatcher when signals are processed
|
|
86
|
-
signalWatcher.on("phase", () => contextWatcher.notifySignalProcessed());
|
|
87
|
-
signalWatcher.on("checkpoint", () => contextWatcher.notifySignalProcessed());
|
|
298
|
+
// Ensure context directory exists
|
|
299
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
88
300
|
|
|
89
|
-
|
|
301
|
+
// Start context watcher (fallback: copies root context/ to pipeline context/)
|
|
302
|
+
const contextWatcher = new ContextWatcher(stateManager, PIPELINES_DIR!, PIPELINE_ID!);
|
|
90
303
|
contextWatcher.start();
|
|
91
304
|
|
|
92
305
|
stateManager.setStatus("running");
|
|
93
306
|
stateManager.addActivity("system", "info", "파이프라인 시작");
|
|
94
307
|
|
|
95
|
-
// ──
|
|
96
|
-
const
|
|
308
|
+
// ── Run phases sequentially ─────────────────────────────────────
|
|
309
|
+
for (const phaseConfig of PHASES) {
|
|
310
|
+
const { phase, name, buildPrompt, expectedFiles, checkpoint } = phaseConfig;
|
|
97
311
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
312
|
+
console.log(`\n[Runner] ── Phase ${phase}: ${name} ──`);
|
|
313
|
+
stateManager.setPhase(phase);
|
|
314
|
+
stateManager.addActivity("system", "info", `Phase ${phase} 시작: ${name}`);
|
|
315
|
+
|
|
316
|
+
// Build and run prompt
|
|
317
|
+
const prompt = buildPrompt();
|
|
318
|
+
const result = await runClaude(prompt);
|
|
319
|
+
|
|
320
|
+
if (result.code !== 0) {
|
|
321
|
+
stateManager.setStatus("failed");
|
|
322
|
+
stateManager.addActivity(
|
|
323
|
+
"system",
|
|
324
|
+
"error",
|
|
325
|
+
`Phase ${phase} 실패 (exit code: ${result.code})`,
|
|
326
|
+
);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
103
329
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
330
|
+
// Log Claude's output as activity (truncated)
|
|
331
|
+
const summary = result.stdout.trim().slice(0, 200);
|
|
332
|
+
if (summary) {
|
|
333
|
+
stateManager.addActivity("system", "progress", summary + (result.stdout.length > 200 ? "..." : ""));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Register output files
|
|
337
|
+
for (const file of expectedFiles) {
|
|
338
|
+
if (fs.existsSync(path.join(contextDir, file))) {
|
|
339
|
+
stateManager.addOutput(`context/${file}`, phase);
|
|
340
|
+
stateManager.addActivity("system", "success", `산출물 생성: ${file}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Also register any unexpected context files
|
|
345
|
+
for (const file of listContextFiles()) {
|
|
346
|
+
const existing = stateManager.read();
|
|
347
|
+
if (existing && !existing.outputs.some((o) => o.filename === `context/${file}`)) {
|
|
348
|
+
const filePhase = phase;
|
|
349
|
+
stateManager.addOutput(`context/${file}`, filePhase);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Checkpoint: wait for user approval ──────────────────────
|
|
354
|
+
stateManager.addActivity(
|
|
355
|
+
"system",
|
|
356
|
+
"info",
|
|
357
|
+
`Checkpoint Phase ${phase}: ${checkpoint}`,
|
|
358
|
+
);
|
|
107
359
|
stateManager.setStatus("paused");
|
|
108
360
|
|
|
361
|
+
console.log(`[Runner] Checkpoint Phase ${phase}: waiting for approval...`);
|
|
362
|
+
|
|
109
363
|
try {
|
|
110
|
-
const response = await waitForCheckpoint(
|
|
111
|
-
PIPELINES_DIR!,
|
|
112
|
-
PIPELINE_ID!,
|
|
113
|
-
abortController.signal,
|
|
114
|
-
);
|
|
364
|
+
const response = await waitForCheckpoint(PIPELINES_DIR!, PIPELINE_ID!);
|
|
115
365
|
|
|
116
366
|
if (response.action === "approve") {
|
|
117
367
|
stateManager.addActivity("system", "success", `Checkpoint Phase ${phase} approved`);
|
|
368
|
+
stateManager.setStatus("running");
|
|
369
|
+
console.log(`[Runner] Phase ${phase} approved`);
|
|
118
370
|
} else {
|
|
119
|
-
const feedback = response.message || "
|
|
371
|
+
const feedback = response.message || "수정 요청";
|
|
120
372
|
stateManager.addActivity(
|
|
121
373
|
"system",
|
|
122
374
|
"info",
|
|
123
375
|
`Checkpoint Phase ${phase} rejected: ${feedback}`,
|
|
124
376
|
);
|
|
377
|
+
stateManager.setStatus("failed");
|
|
378
|
+
stateManager.addActivity("system", "error", `Phase ${phase}에서 사용자가 거절함`);
|
|
379
|
+
console.log(`[Runner] Phase ${phase} rejected: ${feedback}`);
|
|
380
|
+
break;
|
|
125
381
|
}
|
|
126
|
-
// Note: Claude runs in -p (print) mode with stdin ignored.
|
|
127
|
-
// Checkpoint responses are recorded in state.json activities.
|
|
128
|
-
// Claude reads checkpoint_response.json via signal protocol.
|
|
129
|
-
|
|
130
|
-
stateManager.setStatus("running");
|
|
131
382
|
} catch (err) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// ── Stream stdout/stderr → parse for activity logging ─────────────
|
|
139
|
-
// Even if Claude doesn't write signal files, we extract progress
|
|
140
|
-
// from stdout to keep the dashboard alive with activity updates.
|
|
141
|
-
|
|
142
|
-
const AGENT_KEYWORDS: Record<string, string> = {
|
|
143
|
-
"PM": "alex", "Alex": "alex",
|
|
144
|
-
"기획": "mina", "Mina": "mina",
|
|
145
|
-
"디자이너": "lena", "Lena": "lena", "디자인": "lena",
|
|
146
|
-
"FE": "jay", "Jay": "jay", "프론트": "jay",
|
|
147
|
-
"BE": "sam", "Sam": "sam", "백엔드": "sam",
|
|
148
|
-
"인프라": "dex", "Dex": "dex", "Infra": "dex", "Docker": "dex",
|
|
149
|
-
"QA": "eva", "Eva": "eva", "테스트": "eva",
|
|
150
|
-
"보안": "rex", "Rex": "rex", "Security": "rex",
|
|
151
|
-
"리뷰": "nora", "Nora": "nora", "코드 리뷰": "nora",
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const PHASE_PATTERNS = [
|
|
155
|
-
{ pattern: /PHASE\s*0|인풋\s*수신|요구사항\s*분석/i, phase: 0 },
|
|
156
|
-
{ pattern: /PHASE\s*1|기획|plan/i, phase: 1 },
|
|
157
|
-
{ pattern: /PHASE\s*2|설계|design|API\s*명세/i, phase: 2 },
|
|
158
|
-
{ pattern: /PHASE\s*3|구현|implement/i, phase: 3 },
|
|
159
|
-
{ pattern: /PHASE\s*4|QA|통합|보안\s*리뷰/i, phase: 4 },
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
let lastActivityTime = 0;
|
|
163
|
-
const ACTIVITY_THROTTLE_MS = 3000; // Don't spam: max 1 activity per 3s from stdout
|
|
164
|
-
|
|
165
|
-
function detectAgentFromLine(line: string): string {
|
|
166
|
-
for (const [keyword, agentId] of Object.entries(AGENT_KEYWORDS)) {
|
|
167
|
-
if (line.includes(keyword)) return agentId;
|
|
383
|
+
console.error("[Runner] Checkpoint error:", err);
|
|
384
|
+
stateManager.setStatus("failed");
|
|
385
|
+
break;
|
|
168
386
|
}
|
|
169
|
-
return "system";
|
|
170
387
|
}
|
|
171
388
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("```")) return;
|
|
178
|
-
if (/^[-=_]{3,}$/.test(trimmed)) return;
|
|
179
|
-
|
|
180
|
-
const now = Date.now();
|
|
181
|
-
|
|
182
|
-
// Phase detection (always process, no throttle)
|
|
183
|
-
for (const { pattern, phase } of PHASE_PATTERNS) {
|
|
184
|
-
if (pattern.test(trimmed)) {
|
|
185
|
-
const currentState = stateManager.read();
|
|
186
|
-
if (currentState && currentState.currentPhase < phase) {
|
|
187
|
-
stateManager.setPhase(phase);
|
|
188
|
-
stateManager.addActivity("system", "info", `Phase ${phase} 시작`);
|
|
189
|
-
contextWatcher.notifySignalProcessed();
|
|
190
|
-
}
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Checkpoint detection from stdout
|
|
196
|
-
if (/체크포인트|checkpoint/i.test(trimmed) && /승인|확인|검토|approve|review/i.test(trimmed)) {
|
|
197
|
-
// Don't duplicate if signal-watcher already caught it
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Throttled activity logging
|
|
202
|
-
if (now - lastActivityTime < ACTIVITY_THROTTLE_MS) return;
|
|
203
|
-
lastActivityTime = now;
|
|
204
|
-
|
|
205
|
-
// Log meaningful lines as activities
|
|
206
|
-
const agentId = detectAgentFromLine(trimmed);
|
|
207
|
-
// Truncate long lines
|
|
208
|
-
const message = trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed;
|
|
209
|
-
stateManager.addActivity(agentId, "progress", message);
|
|
389
|
+
// ── Finalize ──────────────────────────────────────────────────
|
|
390
|
+
const finalState = stateManager.read();
|
|
391
|
+
if (finalState && finalState.status === "running") {
|
|
392
|
+
stateManager.setStatus("completed");
|
|
393
|
+
stateManager.addActivity("system", "success", "파이프라인 완료");
|
|
210
394
|
}
|
|
211
395
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
stdoutBuffer += data.toString();
|
|
215
|
-
const lines = stdoutBuffer.split("\n");
|
|
216
|
-
stdoutBuffer = lines.pop() || "";
|
|
217
|
-
for (const line of lines) {
|
|
218
|
-
if (line.trim()) {
|
|
219
|
-
console.log(`[Claude] ${line}`);
|
|
220
|
-
processStdoutLine(line);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
claude.stderr.on("data", (data: Buffer) => {
|
|
226
|
-
const text = data.toString().trim();
|
|
227
|
-
if (text) {
|
|
228
|
-
console.error(`[Claude:err] ${text}`);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ── Handle process exit ───────────────────────────────────────────
|
|
233
|
-
claude.on("close", (code) => {
|
|
234
|
-
console.log(`[Runner] Claude process exited with code ${code}`);
|
|
235
|
-
abortController.abort();
|
|
236
|
-
|
|
237
|
-
if (code === 0) {
|
|
238
|
-
stateManager.setStatus("completed");
|
|
239
|
-
stateManager.addActivity("system", "success", "파이프라인 완료");
|
|
240
|
-
} else {
|
|
241
|
-
stateManager.setStatus("failed");
|
|
242
|
-
stateManager.addActivity(
|
|
243
|
-
"system",
|
|
244
|
-
"error",
|
|
245
|
-
`Claude 프로세스가 비정상 종료되었습니다 (exit code: ${code})`,
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
signalWatcher.stop();
|
|
250
|
-
contextWatcher.stop();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
claude.on("error", (err) => {
|
|
254
|
-
console.error("[Runner] Failed to spawn Claude:", err);
|
|
255
|
-
stateManager.setStatus("failed");
|
|
256
|
-
stateManager.addActivity("system", "error", `Claude 실행 실패: ${err.message}`);
|
|
257
|
-
signalWatcher.stop();
|
|
258
|
-
contextWatcher.stop();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// ── Graceful shutdown ─────────────────────────────────────────────
|
|
262
|
-
const cleanup = () => {
|
|
263
|
-
abortController.abort();
|
|
264
|
-
signalWatcher.stop();
|
|
265
|
-
contextWatcher.stop();
|
|
266
|
-
if (!claude.killed) {
|
|
267
|
-
claude.kill("SIGTERM");
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
process.on("SIGINT", cleanup);
|
|
272
|
-
process.on("SIGTERM", cleanup);
|
|
396
|
+
contextWatcher.stop();
|
|
397
|
+
console.log("[Runner] Pipeline finished");
|
|
273
398
|
}
|
|
274
399
|
|
|
275
400
|
main().catch((err) => {
|