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.
Files changed (27) 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/lib/pipelines.ts +21 -4
  7. package/template/.claude-pipeline/runner/dist/checkpoint-waiter.d.ts +1 -1
  8. package/template/.claude-pipeline/runner/dist/checkpoint-waiter.js +16 -7
  9. package/template/.claude-pipeline/runner/dist/checkpoint-waiter.js.map +1 -1
  10. package/template/.claude-pipeline/runner/dist/context-watcher.d.ts +1 -0
  11. package/template/.claude-pipeline/runner/dist/context-watcher.js +26 -5
  12. package/template/.claude-pipeline/runner/dist/context-watcher.js.map +1 -1
  13. package/template/.claude-pipeline/runner/dist/pipeline-runner.js +76 -5
  14. package/template/.claude-pipeline/runner/dist/pipeline-runner.js.map +1 -1
  15. package/template/.claude-pipeline/runner/dist/signal-watcher.d.ts +5 -0
  16. package/template/.claude-pipeline/runner/dist/signal-watcher.js +63 -29
  17. package/template/.claude-pipeline/runner/dist/signal-watcher.js.map +1 -1
  18. package/template/.claude-pipeline/runner/dist/state-manager.d.ts +8 -1
  19. package/template/.claude-pipeline/runner/dist/state-manager.js +33 -15
  20. package/template/.claude-pipeline/runner/dist/state-manager.js.map +1 -1
  21. package/template/.claude-pipeline/runner/dist/types.d.ts +1 -0
  22. package/template/.claude-pipeline/runner/src/checkpoint-waiter.ts +16 -7
  23. package/template/.claude-pipeline/runner/src/context-watcher.ts +28 -5
  24. package/template/.claude-pipeline/runner/src/pipeline-runner.ts +81 -5
  25. package/template/.claude-pipeline/runner/src/signal-watcher.ts +57 -33
  26. package/template/.claude-pipeline/runner/src/state-manager.ts +30 -15
  27. 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
- if (!fs.existsSync(file))
64
+ const content = this.claimAndRead(file);
65
+ if (content === null)
40
66
  return;
41
- const content = fs.readFileSync(file, "utf-8").trim();
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
- try {
64
- const status = fs.readFileSync(file, "utf-8").trim();
65
- if (status === "working" || status === "done" || status === "idle") {
66
- this.stateManager.setAgentStatus(agentId, status);
67
- }
68
- fs.unlinkSync(file);
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
- if (!fs.existsSync(file))
101
+ const content = this.claimAndRead(file);
102
+ if (content === null)
78
103
  return;
79
- const content = fs.readFileSync(file, "utf-8").trim();
80
- const pipeIdx = content.indexOf("|");
81
- if (pipeIdx === -1) {
82
- fs.unlinkSync(file);
104
+ const pipeIdx = content.trim().indexOf("|");
105
+ if (pipeIdx === -1)
83
106
  return;
84
- }
85
- const phase = parseInt(content.slice(0, pipeIdx), 10);
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
- if (!fs.existsSync(file))
116
+ let content;
117
+ try {
118
+ content = fs.readFileSync(file, "utf-8");
119
+ }
120
+ catch {
96
121
  return;
97
- const content = fs.readFileSync(file, "utf-8");
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
- fs.unlinkSync(file);
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
- if (!fs.existsSync(file))
147
+ let content;
148
+ try {
149
+ content = fs.readFileSync(file, "utf-8");
150
+ }
151
+ catch {
121
152
  return;
122
- const content = fs.readFileSync(file, "utf-8");
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
- fs.unlinkSync(file);
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,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO;QAEjC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,KAAK,KAAK,CAAC,CAAC;YACrE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC5B,CAAC;QACD,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IAEO,aAAa;QACnB,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;gBAAE,SAAS;YAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAE9C,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;gBACrD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;oBACnE,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBACpD,CAAC;gBACD,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,sDAAsD;YACxD,CAAC;QACH,CAAC;IACH,CAAC;IAEO,iBAAiB;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QACvD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO;QAEjC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACtD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC;YACnB,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACtD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QAE/C,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YAClB,IAAI,CAAC,YAAY,CAAC,WAAW,CAC3B,QAAQ,EACR,MAAM,EACN,oBAAoB,KAAK,KAAK,WAAW,EAAE,CAC5C,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;QAC9C,CAAC;QACD,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IAEO,iBAAiB;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACrD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO;QAEjC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAElD,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;gBACtB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAsB,CAAC;gBACjD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBAChD,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;gBAC5D,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9B,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBACxD,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC;QAEnC,gDAAgD;QAChD,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACvB,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACpB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAEO,cAAc;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO;QAEjC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAElD,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACtC,IAAI,OAAO,KAAK,CAAC,CAAC;gBAAE,SAAS;YAE7B,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YAC/D,IAAI,QAAQ,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9B,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC;QAEjC,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACvB,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACpB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;CACF"}
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
- /** Atomic update: read → modify → write via temp file + rename */
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
- /** Atomic update: read → modify → write via temp file + rename */
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
- fs.renameSync(tmpFile, this.stateFile);
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
- ...s,
48
- activities: [
49
- ...s.activities,
50
- {
51
- id: crypto.randomUUID(),
52
- agentId,
53
- message,
54
- timestamp: new Date().toISOString(),
55
- type,
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":"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"}
1
+ {"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../src/state-manager.ts"],"names":[],"mappings":"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"}
@@ -31,4 +31,5 @@ export interface CheckpointResponse {
31
31
  action: "approve" | "reject";
32
32
  message: string;
33
33
  timestamp: string;
34
+ phase: number;
34
35
  }
@@ -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 raw = fs.readFileSync(filePath, "utf-8");
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.unlinkSync(filePath);
30
+ fs.renameSync(filePath, claimedPath);
33
31
  } catch {
34
- // ignore delete errors
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
- // JSON parse error or read error — file may be mid-write, retry next poll
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
- if (!fs.existsSync(filePath)) return;
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
- // copy may fail if file is being written
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(prompt: string): Promise<{ stdout: string; code: number }> {
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
- resolve({ stdout, code: code ?? 1 });
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 result = await runClaude(prompt);
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