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.
- package/package.json +1 -1
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/checkpoint/route.ts +1 -1
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/events/route.ts +25 -3
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/route.ts +21 -14
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/stream/route.ts +5 -1
- package/template/.claude-pipeline/dashboard/src/components/checkpoint-banner.tsx +9 -2
- package/template/.claude-pipeline/dashboard/src/lib/pipelines.ts +21 -4
- package/template/.claude-pipeline/runner/dist/checkpoint-waiter.d.ts +1 -1
- package/template/.claude-pipeline/runner/dist/checkpoint-waiter.js +16 -7
- package/template/.claude-pipeline/runner/dist/checkpoint-waiter.js.map +1 -1
- package/template/.claude-pipeline/runner/dist/context-watcher.d.ts +1 -0
- package/template/.claude-pipeline/runner/dist/context-watcher.js +26 -5
- package/template/.claude-pipeline/runner/dist/context-watcher.js.map +1 -1
- package/template/.claude-pipeline/runner/dist/pipeline-runner.js +144 -53
- package/template/.claude-pipeline/runner/dist/pipeline-runner.js.map +1 -1
- package/template/.claude-pipeline/runner/dist/signal-watcher.d.ts +5 -0
- package/template/.claude-pipeline/runner/dist/signal-watcher.js +63 -29
- package/template/.claude-pipeline/runner/dist/signal-watcher.js.map +1 -1
- package/template/.claude-pipeline/runner/dist/state-manager.d.ts +8 -1
- package/template/.claude-pipeline/runner/dist/state-manager.js +33 -15
- package/template/.claude-pipeline/runner/dist/state-manager.js.map +1 -1
- package/template/.claude-pipeline/runner/dist/types.d.ts +1 -0
- package/template/.claude-pipeline/runner/src/checkpoint-waiter.ts +16 -7
- package/template/.claude-pipeline/runner/src/context-watcher.ts +28 -5
- package/template/.claude-pipeline/runner/src/pipeline-runner.ts +157 -66
- package/template/.claude-pipeline/runner/src/signal-watcher.ts +57 -33
- package/template/.claude-pipeline/runner/src/state-manager.ts +30 -15
- package/template/.claude-pipeline/runner/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
||
|
|
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(
|
|
79
|
-
|
|
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(
|
|
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
|
|
20
|
-
const response = JSON.parse(raw);
|
|
21
|
-
// Delete the file after reading
|
|
19
|
+
const claimedPath = filePath + ".processing";
|
|
22
20
|
try {
|
|
23
|
-
fs.
|
|
21
|
+
fs.renameSync(filePath, claimedPath);
|
|
24
22
|
}
|
|
25
23
|
catch {
|
|
26
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,+DAA+D;AAC/D,MAAM,mBAAmB,GAA2B;IAClD,oBAAoB,EAAE,CAAC;IACvB,YAAY,EAAE,CAAC;IACf,cAAc,EAAE,CAAC;IACjB,mBAAmB,EAAE,CAAC;IACtB,gBAAgB,EAAE,CAAC;IACnB,WAAW,EAAE,CAAC;IACd,eAAe,EAAE,CAAC;IAClB,eAAe,EAAE,CAAC;IAClB,kBAAkB,EAAE,CAAC;IACrB,eAAe,EAAE,CAAC;IAClB,gBAAgB,EAAE,CAAC;IACnB,mBAAmB,EAAE,CAAC;IACtB,cAAc,EAAE,CAAC;IACjB,oBAAoB,EAAE,CAAC;CACxB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,OAAO,cAAc;
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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 ──────────────────────────────────────────────────
|