create-claude-pipeline 0.4.1 → 0.4.3

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 (28) hide show
  1. package/package.json +1 -1
  2. package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/checkpoint/route.ts +1 -1
  3. package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/events/route.ts +25 -3
  4. package/template/.claude-pipeline/dashboard/src/app/api/pipelines/route.ts +21 -14
  5. package/template/.claude-pipeline/dashboard/src/app/api/pipelines/stream/route.ts +5 -1
  6. package/template/.claude-pipeline/dashboard/src/components/checkpoint-banner.tsx +9 -2
  7. package/template/.claude-pipeline/dashboard/src/lib/pipelines.ts +21 -4
  8. package/template/.claude-pipeline/runner/dist/checkpoint-waiter.d.ts +1 -1
  9. package/template/.claude-pipeline/runner/dist/checkpoint-waiter.js +16 -7
  10. package/template/.claude-pipeline/runner/dist/checkpoint-waiter.js.map +1 -1
  11. package/template/.claude-pipeline/runner/dist/context-watcher.d.ts +1 -0
  12. package/template/.claude-pipeline/runner/dist/context-watcher.js +26 -5
  13. package/template/.claude-pipeline/runner/dist/context-watcher.js.map +1 -1
  14. package/template/.claude-pipeline/runner/dist/pipeline-runner.js +144 -53
  15. package/template/.claude-pipeline/runner/dist/pipeline-runner.js.map +1 -1
  16. package/template/.claude-pipeline/runner/dist/signal-watcher.d.ts +5 -0
  17. package/template/.claude-pipeline/runner/dist/signal-watcher.js +63 -29
  18. package/template/.claude-pipeline/runner/dist/signal-watcher.js.map +1 -1
  19. package/template/.claude-pipeline/runner/dist/state-manager.d.ts +8 -1
  20. package/template/.claude-pipeline/runner/dist/state-manager.js +33 -15
  21. package/template/.claude-pipeline/runner/dist/state-manager.js.map +1 -1
  22. package/template/.claude-pipeline/runner/dist/types.d.ts +1 -0
  23. package/template/.claude-pipeline/runner/src/checkpoint-waiter.ts +16 -7
  24. package/template/.claude-pipeline/runner/src/context-watcher.ts +28 -5
  25. package/template/.claude-pipeline/runner/src/pipeline-runner.ts +157 -66
  26. package/template/.claude-pipeline/runner/src/signal-watcher.ts +57 -33
  27. package/template/.claude-pipeline/runner/src/state-manager.ts +30 -15
  28. package/template/.claude-pipeline/runner/src/types.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-pipeline",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Claude Code 파이프라인 시스템을 프로젝트에 설치하고 대시보드를 실행합니다",
5
5
  "bin": {
6
6
  "create-claude-pipeline": "./bin/cli.js"
@@ -14,7 +14,7 @@ export async function POST(request: Request, { params }: { params: { id: string
14
14
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
15
15
  }
16
16
 
17
- const success = writeCheckpointResponse(params.id, action, message);
17
+ const success = writeCheckpointResponse(params.id, action, message, state.currentPhase);
18
18
  if (!success) {
19
19
  return NextResponse.json({ error: "Failed to write checkpoint response" }, { status: 500 });
20
20
  }
@@ -26,7 +26,11 @@ export async function GET(
26
26
  // Send initial state
27
27
  const initialState = readPipelineState(id);
28
28
  if (initialState) {
29
- writer.write("pipeline:updated", { id, state: initialState });
29
+ const { activities: initialActivities, ...initialMeta } = initialState;
30
+ writer.write("pipeline:updated", { id, state: { ...initialMeta, activitiesCount: initialActivities.length } });
31
+ for (const activity of initialActivities) {
32
+ writer.write("pipeline:activity", { id, activity });
33
+ }
30
34
  prevActivitiesCount = initialState.activities.length;
31
35
 
32
36
  const checkpoint = detectCheckpoint(initialState.activities);
@@ -52,8 +56,9 @@ export async function GET(
52
56
  const state = readPipelineState(id);
53
57
  if (!state) return;
54
58
 
55
- // Send full state update
56
- writer.write("pipeline:updated", { id, state });
59
+ // Send full state update (without activities array)
60
+ const { activities, ...stateMeta } = state;
61
+ writer.write("pipeline:updated", { id, state: { ...stateMeta, activitiesCount: activities.length } });
57
62
 
58
63
  // Send new activities individually
59
64
  const newActivities = state.activities.slice(prevActivitiesCount);
@@ -67,6 +72,23 @@ export async function GET(
67
72
  if (checkpoint) {
68
73
  writer.write("pipeline:checkpoint", { id, checkpoint });
69
74
  }
75
+
76
+ if (state.status === "running" || state.status === "paused") {
77
+ const heartbeatPath = path.join(pipelineDir, "heartbeat");
78
+ try {
79
+ const hbStat = fs.statSync(heartbeatPath);
80
+ const staleMs = Date.now() - hbStat.mtimeMs;
81
+ if (staleMs > 30_000) {
82
+ writer.write("pipeline:runner_stale", {
83
+ id,
84
+ lastHeartbeat: hbStat.mtimeMs,
85
+ staleMs,
86
+ });
87
+ }
88
+ } catch {
89
+ // heartbeat file doesn't exist — old runner or not started yet
90
+ }
91
+ }
70
92
  } catch {
71
93
  // state.json may be mid-write or pipeline removed
72
94
  if (!fs.existsSync(pipelineDir)) {
@@ -54,21 +54,15 @@ export async function POST(request: Request) {
54
54
  JSON.stringify(initialState, null, 2)
55
55
  );
56
56
 
57
- // Spawn pipeline-runner (wrapper that manages Claude CLI + state)
58
- try {
59
- // process.cwd() = .claude-pipeline/dashboard/
60
- // .. = .claude-pipeline/
61
- // ../.. = project root
62
- const projectRoot = path.resolve(process.cwd(), "..", "..");
63
- const runnerScript = path.resolve(
64
- projectRoot,
65
- ".claude-pipeline",
66
- "runner",
67
- "dist",
68
- "pipeline-runner.js",
69
- );
57
+ // Spawn pipeline-runner
58
+ const projectRoot = path.resolve(process.cwd(), "..", "..");
59
+ const runnerScript = path.resolve(
60
+ projectRoot, ".claude-pipeline", "runner", "dist", "pipeline-runner.js",
61
+ );
70
62
 
71
- const child = spawn("node", [runnerScript], {
63
+ let child;
64
+ try {
65
+ child = spawn("node", [runnerScript], {
72
66
  cwd: projectRoot,
73
67
  detached: true,
74
68
  stdio: "ignore",
@@ -81,7 +75,20 @@ export async function POST(request: Request) {
81
75
  });
82
76
  child.unref();
83
77
  } catch (e) {
78
+ const failedState = { ...initialState, status: "failed" };
79
+ fs.writeFileSync(
80
+ path.join(pipelineDir, "state.json"),
81
+ JSON.stringify(failedState, null, 2)
82
+ );
84
83
  console.error("Failed to spawn pipeline runner:", e);
84
+ return NextResponse.json(
85
+ { id, status: "failed", error: "Runner 실행 실패" },
86
+ { status: 500 },
87
+ );
88
+ }
89
+
90
+ if (child.pid) {
91
+ fs.writeFileSync(path.join(pipelineDir, "runner.pid"), String(child.pid));
85
92
  }
86
93
 
87
94
  return NextResponse.json({ id, status: "running" }, { status: 201 });
@@ -44,7 +44,11 @@ export async function GET() {
44
44
  lastMtimes.set(entry.name, mtime);
45
45
  const state = readPipelineState(entry.name);
46
46
  if (state) {
47
- writer.write("pipeline:updated", { id: entry.name, state });
47
+ const { activities, ...summary } = state;
48
+ writer.write("pipeline:updated", {
49
+ id: entry.name,
50
+ state: { ...summary, activitiesCount: activities.length },
51
+ });
48
52
  }
49
53
  }
50
54
  } catch {
@@ -20,7 +20,7 @@ export function CheckpointBanner({ checkpoint, onRespond }: CheckpointBannerProp
20
20
  value={feedback}
21
21
  onChange={(e) => setFeedback(e.target.value)}
22
22
  className="w-full bg-[#111827] border border-border rounded-lg p-2 text-text-primary text-sm resize-none h-20 focus:outline-none focus:border-accent-purple"
23
- placeholder="피드백을 입력하세요..."
23
+ placeholder="추가 지시사항을 입력하세요..."
24
24
  autoFocus
25
25
  />
26
26
  <div className="flex justify-end gap-2 mt-2">
@@ -29,9 +29,16 @@ export function CheckpointBanner({ checkpoint, onRespond }: CheckpointBannerProp
29
29
  </button>
30
30
  <button
31
31
  onClick={() => onRespond("reject", feedback)}
32
+ disabled={!feedback.trim()}
33
+ className="px-3 py-1 text-[11px] text-white bg-red-500/80 rounded-md disabled:opacity-50"
34
+ >
35
+ 거절 + 재작업
36
+ </button>
37
+ <button
38
+ onClick={() => onRespond("approve", feedback)}
32
39
  className="px-3 py-1 text-[11px] text-white bg-gradient-to-r from-accent-purple to-accent-purple-light rounded-md"
33
40
  >
34
- 전송
41
+ 피드백과 함께 승인
35
42
  </button>
36
43
  </div>
37
44
  </div>
@@ -4,7 +4,14 @@ import type { PipelineState, PipelineSummary } from "@/types/pipeline";
4
4
 
5
5
  // process.cwd() = .claude-pipeline/dashboard/ → ../.. = project root
6
6
  const PIPELINES_DIR = process.env.PIPELINES_DIR
7
- || path.resolve(process.cwd(), "..", "..", "pipelines");
7
+ || (() => {
8
+ const fallback = path.resolve(process.cwd(), "..", "..", "pipelines");
9
+ console.warn(
10
+ `[pipelines] PIPELINES_DIR not set, using cwd-based fallback: ${fallback}. ` +
11
+ `Set PIPELINES_DIR env var for reliable path resolution.`
12
+ );
13
+ return fallback;
14
+ })();
8
15
 
9
16
  export function getPipelinesDir(): string {
10
17
  return path.resolve(PIPELINES_DIR);
@@ -75,16 +82,26 @@ export function readOutputFile(pipelineId: string, filepath: string): { content:
75
82
  }
76
83
  }
77
84
 
78
- export function writeCheckpointResponse(pipelineId: string, action: string, message?: string): boolean {
79
- const filePath = path.join(getPipelineDir(pipelineId), "checkpoint_response.json");
85
+ export function writeCheckpointResponse(
86
+ pipelineId: string,
87
+ action: string,
88
+ message?: string,
89
+ phase?: number,
90
+ ): boolean {
91
+ const dir = getPipelineDir(pipelineId);
92
+ const filePath = path.join(dir, "checkpoint_response.json");
93
+ const tmpPath = filePath + `.tmp.${Date.now()}`;
80
94
  try {
81
- fs.writeFileSync(filePath, JSON.stringify({
95
+ fs.writeFileSync(tmpPath, JSON.stringify({
82
96
  action,
83
97
  message: message || "",
98
+ phase: phase ?? -1,
84
99
  timestamp: new Date().toISOString(),
85
100
  }));
101
+ fs.renameSync(tmpPath, filePath);
86
102
  return true;
87
103
  } catch {
104
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
88
105
  return false;
89
106
  }
90
107
  }
@@ -3,4 +3,4 @@ import type { CheckpointResponse } from "./types.js";
3
3
  * Polls for checkpoint_response.json and resolves when the user responds.
4
4
  * Waits indefinitely until the file appears or the abort signal fires.
5
5
  */
6
- export declare function waitForCheckpoint(pipelinesDir: string, pipelineId: string, signal?: AbortSignal): Promise<CheckpointResponse>;
6
+ export declare function waitForCheckpoint(pipelinesDir: string, pipelineId: string, expectedPhase: number, signal?: AbortSignal): Promise<CheckpointResponse>;
@@ -4,7 +4,7 @@ import path from "path";
4
4
  * Polls for checkpoint_response.json and resolves when the user responds.
5
5
  * Waits indefinitely until the file appears or the abort signal fires.
6
6
  */
7
- export function waitForCheckpoint(pipelinesDir, pipelineId, signal) {
7
+ export function waitForCheckpoint(pipelinesDir, pipelineId, expectedPhase, signal) {
8
8
  const filePath = path.join(pipelinesDir, pipelineId, "checkpoint_response.json");
9
9
  const POLL_INTERVAL_MS = 2000;
10
10
  return new Promise((resolve, reject) => {
@@ -16,20 +16,29 @@ export function waitForCheckpoint(pipelinesDir, pipelineId, signal) {
16
16
  try {
17
17
  if (!fs.existsSync(filePath))
18
18
  return;
19
- const raw = fs.readFileSync(filePath, "utf-8");
20
- const response = JSON.parse(raw);
21
- // Delete the file after reading
19
+ const claimedPath = filePath + ".processing";
22
20
  try {
23
- fs.unlinkSync(filePath);
21
+ fs.renameSync(filePath, claimedPath);
24
22
  }
25
23
  catch {
26
- // ignore delete errors
24
+ return;
25
+ }
26
+ const raw = fs.readFileSync(claimedPath, "utf-8");
27
+ const response = JSON.parse(raw);
28
+ fs.unlinkSync(claimedPath);
29
+ if (response.phase !== undefined && response.phase !== expectedPhase) {
30
+ console.log(`[Runner] Discarding orphan checkpoint response for phase ${response.phase} (expected ${expectedPhase})`);
31
+ return;
27
32
  }
28
33
  clearInterval(timer);
29
34
  resolve(response);
30
35
  }
31
36
  catch {
32
- // JSON parse error or read error — file may be mid-write, retry next poll
37
+ const claimedPath = filePath + ".processing";
38
+ try {
39
+ fs.unlinkSync(claimedPath);
40
+ }
41
+ catch { /* ignore */ }
33
42
  }
34
43
  }, POLL_INTERVAL_MS);
35
44
  if (signal) {
@@ -1 +1 @@
1
- {"version":3,"file":"checkpoint-waiter.js","sourceRoot":"","sources":["../src/checkpoint-waiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAoB,EACpB,UAAkB,EAClB,MAAoB;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,0BAA0B,CAAC,CAAC;IACjF,MAAM,gBAAgB,GAAG,IAAI,CAAC;IAE9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC7B,IAAI,CAAC;gBACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;oBAAE,OAAO;gBAErC,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;gBAEvD,gCAAgC;gBAChC,IAAI,CAAC;oBACH,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;gBAC1B,CAAC;gBAAC,MAAM,CAAC;oBACP,uBAAuB;gBACzB,CAAC;gBAED,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrB,OAAO,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;YAAC,MAAM,CAAC;gBACP,0EAA0E;YAC5E,CAAC;QACH,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAErB,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBACpC,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC/B,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"checkpoint-waiter.js","sourceRoot":"","sources":["../src/checkpoint-waiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAoB,EACpB,UAAkB,EAClB,aAAqB,EACrB,MAAoB;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,0BAA0B,CAAC,CAAC;IACjF,MAAM,gBAAgB,GAAG,IAAI,CAAC;IAE9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC7B,IAAI,CAAC;gBACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;oBAAE,OAAO;gBAErC,MAAM,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;gBAC7C,IAAI,CAAC;oBACH,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;gBACvC,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO;gBACT,CAAC;gBAED,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;gBAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;gBAEvD,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;gBAE3B,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,IAAI,QAAQ,CAAC,KAAK,KAAK,aAAa,EAAE,CAAC;oBACrE,OAAO,CAAC,GAAG,CAAC,4DAA4D,QAAQ,CAAC,KAAK,cAAc,aAAa,GAAG,CAAC,CAAC;oBACtH,OAAO;gBACT,CAAC;gBAED,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrB,OAAO,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;gBAC7C,IAAI,CAAC;oBAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAErB,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBACpC,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC/B,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -13,6 +13,7 @@ export declare class ContextWatcher {
13
13
  private seenFiles;
14
14
  private lastSignalTime;
15
15
  private intervals;
16
+ private pendingCopies;
16
17
  constructor(stateManager: StateManager, pipelinesDir: string, pipelineId: string);
17
18
  notifySignalProcessed(): void;
18
19
  start(): void;
@@ -31,11 +31,19 @@ export class ContextWatcher {
31
31
  seenFiles = new Set();
32
32
  lastSignalTime = 0;
33
33
  intervals = [];
34
+ pendingCopies = new Map();
34
35
  constructor(stateManager, pipelinesDir, pipelineId) {
35
36
  this.stateManager = stateManager;
36
37
  this.pipelineContextDir = path.join(pipelinesDir, pipelineId, "context");
37
- // Project root is one level up from pipelines dir
38
38
  this.rootContextDir = path.join(pipelinesDir, "..", "context");
39
+ // Restore seenFiles from existing state outputs to prevent duplicate processing on restart
40
+ const state = stateManager.read();
41
+ if (state) {
42
+ for (const output of state.outputs) {
43
+ const basename = path.basename(output.filename);
44
+ this.seenFiles.add(basename);
45
+ }
46
+ }
39
47
  }
40
48
  notifySignalProcessed() {
41
49
  this.lastSignalTime = Date.now();
@@ -82,10 +90,23 @@ export class ContextWatcher {
82
90
  if (this.seenFiles.has(filename))
83
91
  return;
84
92
  const filePath = path.join(sourceDir, filename);
85
- if (!fs.existsSync(filePath))
93
+ let stat;
94
+ try {
95
+ stat = fs.statSync(filePath);
96
+ }
97
+ catch {
86
98
  return;
99
+ }
100
+ // For root fallback copies, wait for file size to stabilize
101
+ if (isRootFallback) {
102
+ const prevSize = this.pendingCopies.get(filename);
103
+ if (prevSize === undefined || prevSize !== stat.size) {
104
+ this.pendingCopies.set(filename, stat.size);
105
+ return;
106
+ }
107
+ this.pendingCopies.delete(filename);
108
+ }
87
109
  this.seenFiles.add(filename);
88
- // If found at project root, copy to pipeline context dir
89
110
  if (isRootFallback) {
90
111
  const destPath = path.join(this.pipelineContextDir, filename);
91
112
  if (!fs.existsSync(destPath)) {
@@ -93,11 +114,11 @@ export class ContextWatcher {
93
114
  fs.copyFileSync(filePath, destPath);
94
115
  }
95
116
  catch {
96
- // copy may fail if file is being written
117
+ this.seenFiles.delete(filename);
118
+ return;
97
119
  }
98
120
  }
99
121
  }
100
- // Skip state update if SignalWatcher was active recently
101
122
  if (Date.now() - this.lastSignalTime < 5000)
102
123
  return;
103
124
  const phase = CONTEXT_FILE_PHASES[filename];
@@ -1 +1 @@
1
- {"version":3,"file":"context-watcher.js","sourceRoot":"","sources":["../src/context-watcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-watcher.js","sourceRoot":"","sources":["../src/context-watcher.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,13 @@ if (!REQUIREMENTS) {
17
17
  }
18
18
  const projectRoot = path.resolve(PIPELINES_DIR, "..");
19
19
  const contextDir = path.join(PIPELINES_DIR, PIPELINE_ID, "context");
20
+ const PHASE_TIMEOUTS = {
21
+ 0: 5 * 60_000, // 5min
22
+ 1: 10 * 60_000, // 10min
23
+ 2: 15 * 60_000, // 15min
24
+ 3: 30 * 60_000, // 30min
25
+ 4: 20 * 60_000, // 20min
26
+ };
20
27
  // ── Pre-check ───────────────────────────────────────────────────────
21
28
  function checkClaudeCLI() {
22
29
  try {
@@ -28,7 +35,7 @@ function checkClaudeCLI() {
28
35
  }
29
36
  }
30
37
  // ── Run a single Claude -p call and return stdout ───────────────────
31
- function runClaude(prompt) {
38
+ function runClaude(prompt, timeoutMs, signal) {
32
39
  return new Promise((resolve) => {
33
40
  const child = spawn("claude", ["-p", prompt], {
34
41
  cwd: projectRoot,
@@ -37,10 +44,29 @@ function runClaude(prompt) {
37
44
  });
38
45
  let stdout = "";
39
46
  let stderr = "";
47
+ let killed = false;
48
+ const timer = setTimeout(() => {
49
+ killed = true;
50
+ child.kill("SIGTERM");
51
+ setTimeout(() => {
52
+ if (!child.killed)
53
+ child.kill("SIGKILL");
54
+ }, 5000);
55
+ }, timeoutMs);
56
+ if (signal) {
57
+ signal.addEventListener("abort", () => {
58
+ killed = true;
59
+ clearTimeout(timer);
60
+ child.kill("SIGTERM");
61
+ setTimeout(() => {
62
+ if (!child.killed)
63
+ child.kill("SIGKILL");
64
+ }, 5000);
65
+ }, { once: true });
66
+ }
40
67
  child.stdout.on("data", (data) => {
41
68
  const text = data.toString();
42
69
  stdout += text;
43
- // Print lines as they come
44
70
  for (const line of text.split("\n")) {
45
71
  if (line.trim())
46
72
  console.log(`[Claude] ${line}`);
@@ -50,11 +76,18 @@ function runClaude(prompt) {
50
76
  stderr += data.toString();
51
77
  });
52
78
  child.on("close", (code) => {
79
+ clearTimeout(timer);
53
80
  if (stderr.trim())
54
81
  console.error(`[Claude:err] ${stderr.trim()}`);
55
- resolve({ stdout, code: code ?? 1 });
82
+ if (killed) {
83
+ resolve({ stdout, code: -1 });
84
+ }
85
+ else {
86
+ resolve({ stdout, code: code ?? 1 });
87
+ }
56
88
  });
57
89
  child.on("error", () => {
90
+ clearTimeout(timer);
58
91
  resolve({ stdout, code: 1 });
59
92
  });
60
93
  });
@@ -261,71 +294,129 @@ async function main() {
261
294
  stateManager.addActivity("system", "error", "Claude CLI를 찾을 수 없거나 로그인되어 있지 않습니다.");
262
295
  process.exit(1);
263
296
  }
297
+ const pidFile = path.join(PIPELINES_DIR, PIPELINE_ID, "runner.pid");
298
+ fs.writeFileSync(pidFile, String(process.pid));
299
+ const heartbeatFile = path.join(PIPELINES_DIR, PIPELINE_ID, "heartbeat");
300
+ fs.writeFileSync(heartbeatFile, String(Date.now()));
301
+ const heartbeatTimer = setInterval(() => {
302
+ try {
303
+ fs.writeFileSync(heartbeatFile, String(Date.now()));
304
+ }
305
+ catch { /* ignore */ }
306
+ }, 10_000);
307
+ function cleanup() {
308
+ clearInterval(heartbeatTimer);
309
+ try {
310
+ fs.unlinkSync(pidFile);
311
+ }
312
+ catch { /* ignore */ }
313
+ try {
314
+ fs.unlinkSync(heartbeatFile);
315
+ }
316
+ catch { /* ignore */ }
317
+ }
318
+ process.on("exit", cleanup);
264
319
  // Ensure context directory exists
265
320
  fs.mkdirSync(contextDir, { recursive: true });
266
321
  // Start context watcher (fallback: copies root context/ to pipeline context/)
267
322
  const contextWatcher = new ContextWatcher(stateManager, PIPELINES_DIR, PIPELINE_ID);
268
323
  contextWatcher.start();
324
+ const abortController = new AbortController();
325
+ const { signal } = abortController;
326
+ const gracefulShutdown = (sig) => {
327
+ console.log(`[Runner] ${sig} received, shutting down...`);
328
+ abortController.abort();
329
+ setTimeout(() => process.exit(1), 10_000);
330
+ };
331
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
332
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
269
333
  stateManager.setStatus("running");
270
334
  stateManager.addActivity("system", "info", "파이프라인 시작");
335
+ let lastFeedback = "";
271
336
  // ── Run phases sequentially ─────────────────────────────────────
272
337
  for (const phaseConfig of PHASES) {
273
338
  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}`);
339
+ const MAX_RETRIES = 3;
340
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
341
+ const isRetry = attempt > 0;
342
+ console.log(`\n[Runner] ── Phase ${phase}: ${name}${isRetry ? ` (재시도 #${attempt})` : ""} ──`);
343
+ stateManager.setPhase(phase);
344
+ stateManager.setStatus("running");
345
+ if (!isRetry) {
346
+ stateManager.addActivity("system", "info", `Phase ${phase} 시작: ${name}`);
295
347
  }
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);
348
+ // Build prompt (on retry, append feedback)
349
+ let prompt = buildPrompt();
350
+ if (isRetry && lastFeedback) {
351
+ prompt += `\n\n## 사용자 피드백 (수정 요청)\n${lastFeedback}\n\n위 피드백을 반영하여 이 Phase를 다시 수행해주세요.`;
303
352
  }
304
- }
305
- // ── Checkpoint: wait for user approval ──────────────────────
306
- stateManager.addActivity("system", "info", `Checkpoint Phase ${phase}: ${checkpoint}`);
307
- stateManager.setStatus("paused");
308
- console.log(`[Runner] Checkpoint Phase ${phase}: waiting for approval...`);
309
- try {
310
- const response = await waitForCheckpoint(PIPELINES_DIR, PIPELINE_ID);
311
- if (response.action === "approve") {
312
- stateManager.addActivity("system", "success", `Checkpoint Phase ${phase} approved`);
313
- stateManager.setStatus("running");
314
- console.log(`[Runner] Phase ${phase} approved`);
353
+ const timeoutMs = PHASE_TIMEOUTS[phase] ?? 15 * 60_000;
354
+ const result = await runClaude(prompt, timeoutMs, signal);
355
+ if (result.code === -1) {
356
+ stateManager.setStatus("failed");
357
+ stateManager.addActivity("system", "error", `Phase ${phase} 타임아웃 (${timeoutMs / 60_000}분 초과)`);
358
+ contextWatcher.stop();
359
+ return;
315
360
  }
316
- else {
317
- const feedback = response.message || "수정 요청";
318
- stateManager.addActivity("system", "info", `Checkpoint Phase ${phase} rejected: ${feedback}`);
361
+ if (result.code !== 0) {
319
362
  stateManager.setStatus("failed");
320
- stateManager.addActivity("system", "error", `Phase ${phase}에서 사용자가 거절함`);
321
- console.log(`[Runner] Phase ${phase} rejected: ${feedback}`);
322
- break;
363
+ stateManager.addActivity("system", "error", `Phase ${phase} 실패 (exit code: ${result.code})`);
364
+ contextWatcher.stop();
365
+ return;
366
+ }
367
+ // Log Claude's output as activity (truncated)
368
+ const summary = result.stdout.trim().slice(0, 200);
369
+ if (summary) {
370
+ stateManager.addActivity("system", "progress", summary + (result.stdout.length > 200 ? "..." : ""));
371
+ }
372
+ // Register output files
373
+ for (const file of expectedFiles) {
374
+ if (fs.existsSync(path.join(contextDir, file))) {
375
+ stateManager.addOutput(`context/${file}`, phase);
376
+ stateManager.addActivity("system", "success", `산출물 생성: ${file}`);
377
+ }
378
+ }
379
+ for (const file of listContextFiles()) {
380
+ const existing = stateManager.read();
381
+ if (existing && !existing.outputs.some((o) => o.filename === `context/${file}`)) {
382
+ stateManager.addOutput(`context/${file}`, phase);
383
+ }
384
+ }
385
+ // ── Checkpoint: wait for user approval ──────────────────────
386
+ stateManager.addActivity("system", "info", `Checkpoint Phase ${phase}: ${checkpoint}`);
387
+ stateManager.setStatus("paused");
388
+ console.log(`[Runner] Checkpoint Phase ${phase}: waiting for approval...`);
389
+ try {
390
+ const response = await waitForCheckpoint(PIPELINES_DIR, PIPELINE_ID, phase, signal);
391
+ if (response.action === "approve") {
392
+ const msg = response.message
393
+ ? `Checkpoint Phase ${phase} approved (피드백: ${response.message})`
394
+ : `Checkpoint Phase ${phase} approved`;
395
+ stateManager.addActivity("system", "success", msg);
396
+ console.log(`[Runner] Phase ${phase} approved`);
397
+ lastFeedback = "";
398
+ break; // Exit retry loop, proceed to next phase
399
+ }
400
+ else {
401
+ // Reject → retry this phase with feedback
402
+ lastFeedback = response.message || "수정 요청";
403
+ stateManager.addActivity("system", "info", `Phase ${phase} 수정 요청: ${lastFeedback}`);
404
+ console.log(`[Runner] Phase ${phase} revision requested: ${lastFeedback}`);
405
+ if (attempt === MAX_RETRIES) {
406
+ stateManager.setStatus("failed");
407
+ stateManager.addActivity("system", "error", `Phase ${phase}: 최대 재시도 횟수(${MAX_RETRIES}) 초과`);
408
+ contextWatcher.stop();
409
+ return;
410
+ }
411
+ // Continue retry loop
412
+ }
413
+ }
414
+ catch (err) {
415
+ console.error("[Runner] Checkpoint error:", err);
416
+ stateManager.setStatus("failed");
417
+ contextWatcher.stop();
418
+ return;
323
419
  }
324
- }
325
- catch (err) {
326
- console.error("[Runner] Checkpoint error:", err);
327
- stateManager.setStatus("failed");
328
- break;
329
420
  }
330
421
  }
331
422
  // ── Finalize ──────────────────────────────────────────────────