create-claude-pipeline 0.4.2 → 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/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 +76 -5
- 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 +81 -5
- 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
|
@@ -22,6 +22,31 @@ export class SignalWatcher extends EventEmitter {
|
|
|
22
22
|
this.timer = null;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Atomically claim a signal file by renaming it, then read and delete.
|
|
27
|
+
* Returns null if file doesn't exist or another process claimed it.
|
|
28
|
+
*/
|
|
29
|
+
claimAndRead(filePath) {
|
|
30
|
+
const claimedPath = filePath + ".processing";
|
|
31
|
+
try {
|
|
32
|
+
fs.renameSync(filePath, claimedPath);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(claimedPath, "utf-8");
|
|
39
|
+
fs.unlinkSync(claimedPath);
|
|
40
|
+
return content;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(claimedPath);
|
|
45
|
+
}
|
|
46
|
+
catch { /* ignore */ }
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
25
50
|
poll() {
|
|
26
51
|
try {
|
|
27
52
|
this.processPhase();
|
|
@@ -36,16 +61,15 @@ export class SignalWatcher extends EventEmitter {
|
|
|
36
61
|
}
|
|
37
62
|
processPhase() {
|
|
38
63
|
const file = path.join(this.signalsDir, ".phase");
|
|
39
|
-
|
|
64
|
+
const content = this.claimAndRead(file);
|
|
65
|
+
if (content === null)
|
|
40
66
|
return;
|
|
41
|
-
const
|
|
42
|
-
const phase = parseInt(content, 10);
|
|
67
|
+
const phase = parseInt(content.trim(), 10);
|
|
43
68
|
if (!isNaN(phase) && phase >= 0 && phase <= 4) {
|
|
44
69
|
this.stateManager.setPhase(phase);
|
|
45
70
|
this.stateManager.addActivity("system", "info", `Phase ${phase} 시작`);
|
|
46
71
|
this.emit("phase", phase);
|
|
47
72
|
}
|
|
48
|
-
fs.unlinkSync(file);
|
|
49
73
|
}
|
|
50
74
|
processAgents() {
|
|
51
75
|
let entries;
|
|
@@ -58,43 +82,44 @@ export class SignalWatcher extends EventEmitter {
|
|
|
58
82
|
for (const name of entries) {
|
|
59
83
|
if (!name.startsWith(".agent_"))
|
|
60
84
|
continue;
|
|
85
|
+
// Skip .processing files from claimAndRead
|
|
86
|
+
if (name.endsWith(".processing"))
|
|
87
|
+
continue;
|
|
61
88
|
const agentId = name.slice(".agent_".length);
|
|
62
89
|
const file = path.join(this.signalsDir, name);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
// file may have been deleted between readdir and read
|
|
90
|
+
const content = this.claimAndRead(file);
|
|
91
|
+
if (content === null)
|
|
92
|
+
continue;
|
|
93
|
+
const status = content.trim();
|
|
94
|
+
if (status === "working" || status === "done" || status === "idle") {
|
|
95
|
+
this.stateManager.setAgentStatus(agentId, status);
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
99
|
processCheckpoint() {
|
|
76
100
|
const file = path.join(this.signalsDir, ".checkpoint");
|
|
77
|
-
|
|
101
|
+
const content = this.claimAndRead(file);
|
|
102
|
+
if (content === null)
|
|
78
103
|
return;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
if (pipeIdx === -1) {
|
|
82
|
-
fs.unlinkSync(file);
|
|
104
|
+
const pipeIdx = content.trim().indexOf("|");
|
|
105
|
+
if (pipeIdx === -1)
|
|
83
106
|
return;
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const description = content.slice(pipeIdx + 1);
|
|
107
|
+
const phase = parseInt(content.trim().slice(0, pipeIdx), 10);
|
|
108
|
+
const description = content.trim().slice(pipeIdx + 1);
|
|
87
109
|
if (!isNaN(phase)) {
|
|
88
110
|
this.stateManager.addActivity("system", "info", `Checkpoint Phase ${phase}: ${description}`);
|
|
89
111
|
this.emit("checkpoint", phase, description);
|
|
90
112
|
}
|
|
91
|
-
fs.unlinkSync(file);
|
|
92
113
|
}
|
|
93
114
|
processActivities() {
|
|
94
115
|
const file = path.join(this.signalsDir, ".activity");
|
|
95
|
-
|
|
116
|
+
let content;
|
|
117
|
+
try {
|
|
118
|
+
content = fs.readFileSync(file, "utf-8");
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
96
121
|
return;
|
|
97
|
-
|
|
122
|
+
}
|
|
98
123
|
const lines = content.split("\n").filter(Boolean);
|
|
99
124
|
for (let i = this.activityOffset; i < lines.length; i++) {
|
|
100
125
|
const parts = lines[i].split("|");
|
|
@@ -109,17 +134,23 @@ export class SignalWatcher extends EventEmitter {
|
|
|
109
134
|
}
|
|
110
135
|
}
|
|
111
136
|
this.activityOffset = lines.length;
|
|
112
|
-
// Truncate if file gets too large (> 100 lines)
|
|
113
137
|
if (lines.length > 100) {
|
|
114
|
-
|
|
138
|
+
try {
|
|
139
|
+
fs.truncateSync(file, 0);
|
|
140
|
+
}
|
|
141
|
+
catch { /* ignore */ }
|
|
115
142
|
this.activityOffset = 0;
|
|
116
143
|
}
|
|
117
144
|
}
|
|
118
145
|
processOutputs() {
|
|
119
146
|
const file = path.join(this.signalsDir, ".output");
|
|
120
|
-
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
content = fs.readFileSync(file, "utf-8");
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
121
152
|
return;
|
|
122
|
-
|
|
153
|
+
}
|
|
123
154
|
const lines = content.split("\n").filter(Boolean);
|
|
124
155
|
for (let i = this.outputOffset; i < lines.length; i++) {
|
|
125
156
|
const pipeIdx = lines[i].indexOf("|");
|
|
@@ -133,7 +164,10 @@ export class SignalWatcher extends EventEmitter {
|
|
|
133
164
|
}
|
|
134
165
|
this.outputOffset = lines.length;
|
|
135
166
|
if (lines.length > 100) {
|
|
136
|
-
|
|
167
|
+
try {
|
|
168
|
+
fs.truncateSync(file, 0);
|
|
169
|
+
}
|
|
170
|
+
catch { /* ignore */ }
|
|
137
171
|
this.outputOffset = 0;
|
|
138
172
|
}
|
|
139
173
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal-watcher.js","sourceRoot":"","sources":["../src/signal-watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAUtC,MAAM,OAAO,aAAc,SAAQ,YAAiC;IAOxD;IANF,UAAU,CAAS;IACnB,KAAK,GAA0C,IAAI,CAAC;IACpD,cAAc,GAAG,CAAC,CAAC;IACnB,YAAY,GAAG,CAAC,CAAC;IAEzB,YACU,YAA0B,EAClC,YAAoB,EACpB,UAAkB;QAElB,KAAK,EAAE,CAAC;QAJA,iBAAY,GAAZ,YAAY,CAAc;QAKlC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;QACjE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,UAAU,GAAG,GAAG;QACpB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAEO,IAAI;QACV,IAAI,CAAC;YACH,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAEO,YAAY;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAClD,
|
|
1
|
+
{"version":3,"file":"signal-watcher.js","sourceRoot":"","sources":["../src/signal-watcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,14 @@ export declare class StateManager {
|
|
|
4
4
|
private pipelineDir;
|
|
5
5
|
constructor(pipelinesDir: string, pipelineId: string);
|
|
6
6
|
read(): PipelineState | null;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Atomic update: read → modify → write via temp file + rename.
|
|
9
|
+
*
|
|
10
|
+
* INVARIANT: Only ONE process should write to state.json.
|
|
11
|
+
* Currently Runner and ContextWatcher share a single Node.js process,
|
|
12
|
+
* and all I/O is synchronous, so the event loop serializes updates.
|
|
13
|
+
* If the architecture changes to multi-process, add file locking.
|
|
14
|
+
*/
|
|
8
15
|
update(updater: (state: PipelineState) => PipelineState): void;
|
|
9
16
|
setStatus(status: PipelineState["status"]): void;
|
|
10
17
|
setPhase(phase: number): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
+
const MAX_ACTIVITIES = 200;
|
|
4
5
|
export class StateManager {
|
|
5
6
|
stateFile;
|
|
6
7
|
pipelineDir;
|
|
@@ -17,7 +18,14 @@ export class StateManager {
|
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Atomic update: read → modify → write via temp file + rename.
|
|
23
|
+
*
|
|
24
|
+
* INVARIANT: Only ONE process should write to state.json.
|
|
25
|
+
* Currently Runner and ContextWatcher share a single Node.js process,
|
|
26
|
+
* and all I/O is synchronous, so the event loop serializes updates.
|
|
27
|
+
* If the architecture changes to multi-process, add file locking.
|
|
28
|
+
*/
|
|
21
29
|
update(updater) {
|
|
22
30
|
const state = this.read();
|
|
23
31
|
if (!state)
|
|
@@ -25,7 +33,16 @@ export class StateManager {
|
|
|
25
33
|
const updated = updater(state);
|
|
26
34
|
const tmpFile = path.join(this.pipelineDir, `.state.tmp.${Date.now()}`);
|
|
27
35
|
fs.writeFileSync(tmpFile, JSON.stringify(updated, null, 2));
|
|
28
|
-
|
|
36
|
+
try {
|
|
37
|
+
fs.renameSync(tmpFile, this.stateFile);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(tmpFile);
|
|
42
|
+
}
|
|
43
|
+
catch { /* ignore */ }
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
29
46
|
}
|
|
30
47
|
setStatus(status) {
|
|
31
48
|
this.update((s) => ({ ...s, status }));
|
|
@@ -43,19 +60,20 @@ export class StateManager {
|
|
|
43
60
|
}));
|
|
44
61
|
}
|
|
45
62
|
addActivity(agentId, type, message) {
|
|
46
|
-
this.update((s) =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
this.update((s) => {
|
|
64
|
+
const newActivity = {
|
|
65
|
+
id: crypto.randomUUID(),
|
|
66
|
+
agentId,
|
|
67
|
+
message,
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
type,
|
|
70
|
+
};
|
|
71
|
+
const activities = [...s.activities, newActivity];
|
|
72
|
+
const trimmed = activities.length > MAX_ACTIVITIES
|
|
73
|
+
? activities.slice(activities.length - MAX_ACTIVITIES)
|
|
74
|
+
: activities;
|
|
75
|
+
return { ...s, activities: trimmed };
|
|
76
|
+
});
|
|
59
77
|
}
|
|
60
78
|
addOutput(filename, phase) {
|
|
61
79
|
this.update((s) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../src/state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B,MAAM,OAAO,YAAY;IACf,SAAS,CAAS;IAClB,WAAW,CAAS;IAE5B,YAAY,YAAoB,EAAE,UAAkB;QAClD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI;QACF,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACrD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../src/state-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,7 @@ import type { CheckpointResponse } from "./types.js";
|
|
|
9
9
|
export function waitForCheckpoint(
|
|
10
10
|
pipelinesDir: string,
|
|
11
11
|
pipelineId: string,
|
|
12
|
+
expectedPhase: number,
|
|
12
13
|
signal?: AbortSignal,
|
|
13
14
|
): Promise<CheckpointResponse> {
|
|
14
15
|
const filePath = path.join(pipelinesDir, pipelineId, "checkpoint_response.json");
|
|
@@ -24,20 +25,28 @@ export function waitForCheckpoint(
|
|
|
24
25
|
try {
|
|
25
26
|
if (!fs.existsSync(filePath)) return;
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
-
const response = JSON.parse(raw) as CheckpointResponse;
|
|
29
|
-
|
|
30
|
-
// Delete the file after reading
|
|
28
|
+
const claimedPath = filePath + ".processing";
|
|
31
29
|
try {
|
|
32
|
-
fs.
|
|
30
|
+
fs.renameSync(filePath, claimedPath);
|
|
33
31
|
} catch {
|
|
34
|
-
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const raw = fs.readFileSync(claimedPath, "utf-8");
|
|
36
|
+
const response = JSON.parse(raw) as CheckpointResponse;
|
|
37
|
+
|
|
38
|
+
fs.unlinkSync(claimedPath);
|
|
39
|
+
|
|
40
|
+
if (response.phase !== undefined && response.phase !== expectedPhase) {
|
|
41
|
+
console.log(`[Runner] Discarding orphan checkpoint response for phase ${response.phase} (expected ${expectedPhase})`);
|
|
42
|
+
return;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
clearInterval(timer);
|
|
38
46
|
resolve(response);
|
|
39
47
|
} catch {
|
|
40
|
-
|
|
48
|
+
const claimedPath = filePath + ".processing";
|
|
49
|
+
try { fs.unlinkSync(claimedPath); } catch { /* ignore */ }
|
|
41
50
|
}
|
|
42
51
|
}, POLL_INTERVAL_MS);
|
|
43
52
|
|
|
@@ -33,6 +33,7 @@ export class ContextWatcher {
|
|
|
33
33
|
private seenFiles = new Set<string>();
|
|
34
34
|
private lastSignalTime = 0;
|
|
35
35
|
private intervals: ReturnType<typeof setInterval>[] = [];
|
|
36
|
+
private pendingCopies = new Map<string, number>();
|
|
36
37
|
|
|
37
38
|
constructor(
|
|
38
39
|
private stateManager: StateManager,
|
|
@@ -40,8 +41,16 @@ export class ContextWatcher {
|
|
|
40
41
|
pipelineId: string,
|
|
41
42
|
) {
|
|
42
43
|
this.pipelineContextDir = path.join(pipelinesDir, pipelineId, "context");
|
|
43
|
-
// Project root is one level up from pipelines dir
|
|
44
44
|
this.rootContextDir = path.join(pipelinesDir, "..", "context");
|
|
45
|
+
|
|
46
|
+
// Restore seenFiles from existing state outputs to prevent duplicate processing on restart
|
|
47
|
+
const state = stateManager.read();
|
|
48
|
+
if (state) {
|
|
49
|
+
for (const output of state.outputs) {
|
|
50
|
+
const basename = path.basename(output.filename);
|
|
51
|
+
this.seenFiles.add(basename);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
notifySignalProcessed(): void {
|
|
@@ -96,23 +105,37 @@ export class ContextWatcher {
|
|
|
96
105
|
if (this.seenFiles.has(filename)) return;
|
|
97
106
|
|
|
98
107
|
const filePath = path.join(sourceDir, filename);
|
|
99
|
-
|
|
108
|
+
let stat: fs.Stats;
|
|
109
|
+
try {
|
|
110
|
+
stat = fs.statSync(filePath);
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// For root fallback copies, wait for file size to stabilize
|
|
116
|
+
if (isRootFallback) {
|
|
117
|
+
const prevSize = this.pendingCopies.get(filename);
|
|
118
|
+
if (prevSize === undefined || prevSize !== stat.size) {
|
|
119
|
+
this.pendingCopies.set(filename, stat.size);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.pendingCopies.delete(filename);
|
|
123
|
+
}
|
|
100
124
|
|
|
101
125
|
this.seenFiles.add(filename);
|
|
102
126
|
|
|
103
|
-
// If found at project root, copy to pipeline context dir
|
|
104
127
|
if (isRootFallback) {
|
|
105
128
|
const destPath = path.join(this.pipelineContextDir, filename);
|
|
106
129
|
if (!fs.existsSync(destPath)) {
|
|
107
130
|
try {
|
|
108
131
|
fs.copyFileSync(filePath, destPath);
|
|
109
132
|
} catch {
|
|
110
|
-
|
|
133
|
+
this.seenFiles.delete(filename);
|
|
134
|
+
return;
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
|
|
115
|
-
// Skip state update if SignalWatcher was active recently
|
|
116
139
|
if (Date.now() - this.lastSignalTime < 5000) return;
|
|
117
140
|
|
|
118
141
|
const phase = CONTEXT_FILE_PHASES[filename];
|
|
@@ -22,6 +22,14 @@ if (!REQUIREMENTS) {
|
|
|
22
22
|
const projectRoot = path.resolve(PIPELINES_DIR, "..");
|
|
23
23
|
const contextDir = path.join(PIPELINES_DIR, PIPELINE_ID, "context");
|
|
24
24
|
|
|
25
|
+
const PHASE_TIMEOUTS: Record<number, number> = {
|
|
26
|
+
0: 5 * 60_000, // 5min
|
|
27
|
+
1: 10 * 60_000, // 10min
|
|
28
|
+
2: 15 * 60_000, // 15min
|
|
29
|
+
3: 30 * 60_000, // 30min
|
|
30
|
+
4: 20 * 60_000, // 20min
|
|
31
|
+
};
|
|
32
|
+
|
|
25
33
|
// ── Pre-check ───────────────────────────────────────────────────────
|
|
26
34
|
function checkClaudeCLI(): boolean {
|
|
27
35
|
try {
|
|
@@ -33,7 +41,11 @@ function checkClaudeCLI(): boolean {
|
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
// ── Run a single Claude -p call and return stdout ───────────────────
|
|
36
|
-
function runClaude(
|
|
44
|
+
function runClaude(
|
|
45
|
+
prompt: string,
|
|
46
|
+
timeoutMs: number,
|
|
47
|
+
signal?: AbortSignal,
|
|
48
|
+
): Promise<{ stdout: string; code: number }> {
|
|
37
49
|
return new Promise((resolve) => {
|
|
38
50
|
const child = spawn("claude", ["-p", prompt], {
|
|
39
51
|
cwd: projectRoot,
|
|
@@ -43,11 +55,30 @@ function runClaude(prompt: string): Promise<{ stdout: string; code: number }> {
|
|
|
43
55
|
|
|
44
56
|
let stdout = "";
|
|
45
57
|
let stderr = "";
|
|
58
|
+
let killed = false;
|
|
59
|
+
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
killed = true;
|
|
62
|
+
child.kill("SIGTERM");
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
65
|
+
}, 5000);
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
|
|
68
|
+
if (signal) {
|
|
69
|
+
signal.addEventListener("abort", () => {
|
|
70
|
+
killed = true;
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
child.kill("SIGTERM");
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
75
|
+
}, 5000);
|
|
76
|
+
}, { once: true });
|
|
77
|
+
}
|
|
46
78
|
|
|
47
79
|
child.stdout.on("data", (data: Buffer) => {
|
|
48
80
|
const text = data.toString();
|
|
49
81
|
stdout += text;
|
|
50
|
-
// Print lines as they come
|
|
51
82
|
for (const line of text.split("\n")) {
|
|
52
83
|
if (line.trim()) console.log(`[Claude] ${line}`);
|
|
53
84
|
}
|
|
@@ -58,11 +89,17 @@ function runClaude(prompt: string): Promise<{ stdout: string; code: number }> {
|
|
|
58
89
|
});
|
|
59
90
|
|
|
60
91
|
child.on("close", (code) => {
|
|
92
|
+
clearTimeout(timer);
|
|
61
93
|
if (stderr.trim()) console.error(`[Claude:err] ${stderr.trim()}`);
|
|
62
|
-
|
|
94
|
+
if (killed) {
|
|
95
|
+
resolve({ stdout, code: -1 });
|
|
96
|
+
} else {
|
|
97
|
+
resolve({ stdout, code: code ?? 1 });
|
|
98
|
+
}
|
|
63
99
|
});
|
|
64
100
|
|
|
65
101
|
child.on("error", () => {
|
|
102
|
+
clearTimeout(timer);
|
|
66
103
|
resolve({ stdout, code: 1 });
|
|
67
104
|
});
|
|
68
105
|
});
|
|
@@ -295,6 +332,25 @@ async function main(): Promise<void> {
|
|
|
295
332
|
process.exit(1);
|
|
296
333
|
}
|
|
297
334
|
|
|
335
|
+
const pidFile = path.join(PIPELINES_DIR!, PIPELINE_ID!, "runner.pid");
|
|
336
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
337
|
+
|
|
338
|
+
const heartbeatFile = path.join(PIPELINES_DIR!, PIPELINE_ID!, "heartbeat");
|
|
339
|
+
fs.writeFileSync(heartbeatFile, String(Date.now()));
|
|
340
|
+
|
|
341
|
+
const heartbeatTimer = setInterval(() => {
|
|
342
|
+
try {
|
|
343
|
+
fs.writeFileSync(heartbeatFile, String(Date.now()));
|
|
344
|
+
} catch { /* ignore */ }
|
|
345
|
+
}, 10_000);
|
|
346
|
+
|
|
347
|
+
function cleanup(): void {
|
|
348
|
+
clearInterval(heartbeatTimer);
|
|
349
|
+
try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
|
|
350
|
+
try { fs.unlinkSync(heartbeatFile); } catch { /* ignore */ }
|
|
351
|
+
}
|
|
352
|
+
process.on("exit", cleanup);
|
|
353
|
+
|
|
298
354
|
// Ensure context directory exists
|
|
299
355
|
fs.mkdirSync(contextDir, { recursive: true });
|
|
300
356
|
|
|
@@ -302,6 +358,17 @@ async function main(): Promise<void> {
|
|
|
302
358
|
const contextWatcher = new ContextWatcher(stateManager, PIPELINES_DIR!, PIPELINE_ID!);
|
|
303
359
|
contextWatcher.start();
|
|
304
360
|
|
|
361
|
+
const abortController = new AbortController();
|
|
362
|
+
const { signal } = abortController;
|
|
363
|
+
|
|
364
|
+
const gracefulShutdown = (sig: string) => {
|
|
365
|
+
console.log(`[Runner] ${sig} received, shutting down...`);
|
|
366
|
+
abortController.abort();
|
|
367
|
+
setTimeout(() => process.exit(1), 10_000);
|
|
368
|
+
};
|
|
369
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
370
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
371
|
+
|
|
305
372
|
stateManager.setStatus("running");
|
|
306
373
|
stateManager.addActivity("system", "info", "파이프라인 시작");
|
|
307
374
|
|
|
@@ -329,7 +396,16 @@ async function main(): Promise<void> {
|
|
|
329
396
|
prompt += `\n\n## 사용자 피드백 (수정 요청)\n${lastFeedback}\n\n위 피드백을 반영하여 이 Phase를 다시 수행해주세요.`;
|
|
330
397
|
}
|
|
331
398
|
|
|
332
|
-
const
|
|
399
|
+
const timeoutMs = PHASE_TIMEOUTS[phase] ?? 15 * 60_000;
|
|
400
|
+
const result = await runClaude(prompt, timeoutMs, signal);
|
|
401
|
+
|
|
402
|
+
if (result.code === -1) {
|
|
403
|
+
stateManager.setStatus("failed");
|
|
404
|
+
stateManager.addActivity("system", "error",
|
|
405
|
+
`Phase ${phase} 타임아웃 (${timeoutMs / 60_000}분 초과)`);
|
|
406
|
+
contextWatcher.stop();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
333
409
|
|
|
334
410
|
if (result.code !== 0) {
|
|
335
411
|
stateManager.setStatus("failed");
|
|
@@ -364,7 +440,7 @@ async function main(): Promise<void> {
|
|
|
364
440
|
console.log(`[Runner] Checkpoint Phase ${phase}: waiting for approval...`);
|
|
365
441
|
|
|
366
442
|
try {
|
|
367
|
-
const response = await waitForCheckpoint(PIPELINES_DIR!, PIPELINE_ID
|
|
443
|
+
const response = await waitForCheckpoint(PIPELINES_DIR!, PIPELINE_ID!, phase, signal);
|
|
368
444
|
|
|
369
445
|
if (response.action === "approve") {
|
|
370
446
|
const msg = response.message
|