@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26

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 (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
@@ -2,7 +2,11 @@
2
2
  /**
3
3
  * Core Runner - Execute tasks sequentially in a lane
4
4
  *
5
- * Adapted from sequential-agent-runner.js
5
+ * Features:
6
+ * - Enhanced retry with circuit breaker
7
+ * - Checkpoint system for recovery
8
+ * - State validation and repair
9
+ * - Improved dependency management
6
10
  */
7
11
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
12
  if (k2 === undefined) k2 = k;
@@ -42,7 +46,10 @@ exports.cursorAgentCreateChat = cursorAgentCreateChat;
42
46
  exports.validateTaskConfig = validateTaskConfig;
43
47
  exports.cursorAgentSend = cursorAgentSend;
44
48
  exports.extractDependencyRequest = extractDependencyRequest;
49
+ exports.readDependencyRequestFile = readDependencyRequestFile;
50
+ exports.clearDependencyRequestFile = clearDependencyRequestFile;
45
51
  exports.wrapPromptForDependencyPolicy = wrapPromptForDependencyPolicy;
52
+ exports.wrapPrompt = wrapPrompt;
46
53
  exports.applyDependencyFilePermissions = applyDependencyFilePermissions;
47
54
  exports.waitForTaskDependencies = waitForTaskDependencies;
48
55
  exports.mergeDependencyBranches = mergeDependencyBranches;
@@ -59,6 +66,11 @@ const events_1 = require("../utils/events");
59
66
  const config_1 = require("../utils/config");
60
67
  const webhook_1 = require("../utils/webhook");
61
68
  const reviewer_1 = require("./reviewer");
69
+ const path_1 = require("../utils/path");
70
+ const failure_policy_1 = require("./failure-policy");
71
+ const checkpoint_1 = require("../utils/checkpoint");
72
+ const dependency_1 = require("../utils/dependency");
73
+ const health_1 = require("../utils/health");
62
74
  /**
63
75
  * Execute cursor-agent command with timeout and better error handling
64
76
  */
@@ -174,9 +186,9 @@ function validateTaskConfig(config) {
174
186
  }
175
187
  }
176
188
  /**
177
- * Execute cursor-agent command with streaming and better error handling
189
+ * Internal: Execute cursor-agent command with streaming
178
190
  */
179
- async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }) {
191
+ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }) {
180
192
  // Use stream-json format for structured output with tool calls and results
181
193
  const format = outputFormat || 'stream-json';
182
194
  const args = [
@@ -190,65 +202,53 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
190
202
  prompt,
191
203
  ];
192
204
  const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
193
- logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
194
205
  // Determine stdio mode based on intervention setting
195
- // When intervention is enabled, we pipe stdin for message injection
196
- // When disabled (default), we ignore stdin to avoid buffering issues
197
206
  const stdinMode = enableIntervention ? 'pipe' : 'ignore';
198
- if (enableIntervention) {
199
- logger.info('Intervention mode enabled (stdin piped)');
200
- }
201
207
  return new Promise((resolve) => {
202
208
  // Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
203
209
  const childEnv = { ...process.env };
204
- // Only filter out specific problematic NODE_OPTIONS, don't clear entirely
205
210
  if (childEnv.NODE_OPTIONS) {
206
- // Remove flags that might interfere with cursor-agent
207
211
  const filtered = childEnv.NODE_OPTIONS
208
212
  .split(' ')
209
213
  .filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
210
214
  .join(' ');
211
215
  childEnv.NODE_OPTIONS = filtered;
212
216
  }
213
- // Disable Python buffering in case cursor-agent uses Python
214
217
  childEnv.PYTHONUNBUFFERED = '1';
215
218
  const child = (0, child_process_1.spawn)('cursor-agent', args, {
216
219
  stdio: [stdinMode, 'pipe', 'pipe'],
217
220
  env: childEnv,
218
221
  });
219
- logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
220
- // Save PID to state if possible (avoid TOCTOU by reading directly)
222
+ // Save PID to state if possible
221
223
  if (child.pid && signalDir) {
222
224
  try {
223
- const statePath = path.join(signalDir, 'state.json');
224
- // Read directly without existence check to avoid race condition
225
+ const statePath = (0, path_1.safeJoin)(signalDir, 'state.json');
225
226
  const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
226
227
  state.pid = child.pid;
227
228
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
228
229
  }
229
230
  catch {
230
- // Best effort - file may not exist yet
231
+ // Best effort
231
232
  }
232
233
  }
233
234
  let fullStdout = '';
234
235
  let fullStderr = '';
235
236
  let timeoutHandle;
236
- // Heartbeat logging to show progress
237
+ // Heartbeat logging
237
238
  let lastHeartbeat = Date.now();
238
239
  let bytesReceived = 0;
240
+ const startTime = Date.now();
239
241
  const heartbeatInterval = setInterval(() => {
240
- const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
241
242
  const totalElapsed = Math.round((Date.now() - startTime) / 1000);
242
- logger.info(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
243
+ // Output without timestamp - orchestrator will add it
244
+ console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
243
245
  }, HEARTBEAT_INTERVAL_MS);
244
- const startTime = Date.now();
245
- // Watch for "intervention.txt" or "timeout.txt" signal files
246
+ // Signal watchers (intervention, timeout)
246
247
  const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
247
248
  const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
248
249
  let signalWatcher = null;
249
250
  if (signalDir && fs.existsSync(signalDir)) {
250
251
  signalWatcher = fs.watch(signalDir, (event, filename) => {
251
- // Handle intervention
252
252
  if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
253
253
  try {
254
254
  const message = fs.readFileSync(interventionPath, 'utf8').trim();
@@ -256,74 +256,65 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
256
256
  if (enableIntervention && child.stdin) {
257
257
  logger.info(`Injecting intervention: ${message}`);
258
258
  child.stdin.write(message + '\n');
259
+ // Log to conversation history for visibility in monitor/logs
260
+ if (signalDir) {
261
+ const convoPath = path.join(signalDir, 'conversation.jsonl');
262
+ (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('intervention', `[HUMAN INTERVENTION]: ${message}`, {
263
+ task: taskName || 'AGENT_TURN',
264
+ model: 'manual'
265
+ }));
266
+ }
259
267
  }
260
268
  else {
261
269
  logger.warn(`Intervention requested but stdin not available: ${message}`);
262
- logger.warn('To enable intervention, set enableIntervention: true in config');
263
270
  }
264
- fs.unlinkSync(interventionPath); // Clear it
271
+ fs.unlinkSync(interventionPath);
265
272
  }
266
273
  }
267
- catch (e) {
268
- logger.warn('Failed to read intervention file');
269
- }
274
+ catch { }
270
275
  }
271
- // Handle dynamic timeout update
272
276
  if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
273
277
  try {
274
278
  const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
275
279
  const newTimeoutMs = parseInt(newTimeoutStr);
276
280
  if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
277
281
  logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
278
- // Clear old timeout
279
282
  if (timeoutHandle)
280
283
  clearTimeout(timeoutHandle);
281
- // Set new timeout based on total elapsed time
282
284
  const elapsed = Date.now() - startTime;
283
285
  const remaining = Math.max(1000, newTimeoutMs - elapsed);
284
286
  timeoutHandle = setTimeout(() => {
285
287
  clearInterval(heartbeatInterval);
286
288
  child.kill();
287
- const totalSec = Math.round(newTimeoutMs / 1000);
288
- resolve({
289
- ok: false,
290
- exitCode: -1,
291
- error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
292
- });
289
+ resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
293
290
  }, remaining);
294
- fs.unlinkSync(timeoutPath); // Clear it
291
+ fs.unlinkSync(timeoutPath);
295
292
  }
296
293
  }
297
- catch (e) {
298
- logger.warn('Failed to read timeout update file');
299
- }
294
+ catch { }
300
295
  }
301
296
  });
302
297
  }
303
298
  if (child.stdout) {
304
299
  child.stdout.on('data', (data) => {
305
- const str = data.toString();
306
- fullStdout += str;
300
+ fullStdout += data.toString();
307
301
  bytesReceived += data.length;
308
- // Also pipe to our own stdout so it goes to terminal.log
309
302
  process.stdout.write(data);
310
303
  });
311
304
  }
312
305
  if (child.stderr) {
313
306
  child.stderr.on('data', (data) => {
314
307
  fullStderr += data.toString();
315
- // Pipe to our own stderr so it goes to terminal.log
316
308
  process.stderr.write(data);
317
309
  });
318
310
  }
319
311
  timeoutHandle = setTimeout(() => {
320
312
  clearInterval(heartbeatInterval);
321
313
  child.kill();
322
- const timeoutSec = Math.round(timeoutMs / 1000);
323
314
  resolve({
324
315
  ok: false,
325
316
  exitCode: -1,
326
- error: `cursor-agent timed out after ${timeoutSec} seconds. The LLM request may be taking too long or there may be network issues.`,
317
+ error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
327
318
  });
328
319
  }, timeoutMs);
329
320
  child.on('close', (code) => {
@@ -334,21 +325,7 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
334
325
  const json = parseJsonFromStdout(fullStdout);
335
326
  if (code !== 0 || !json || json.type !== 'result') {
336
327
  let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
337
- // Check for common errors
338
- if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
339
- errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
340
- }
341
- else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
342
- errorMsg = 'API rate limit or quota exceeded.';
343
- }
344
- else if (errorMsg.includes('model')) {
345
- errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
346
- }
347
- resolve({
348
- ok: false,
349
- exitCode: code ?? -1,
350
- error: errorMsg,
351
- });
328
+ resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
352
329
  }
353
330
  else {
354
331
  resolve({
@@ -362,14 +339,17 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
362
339
  child.on('error', (err) => {
363
340
  clearTimeout(timeoutHandle);
364
341
  clearInterval(heartbeatInterval);
365
- resolve({
366
- ok: false,
367
- exitCode: -1,
368
- error: `Failed to start cursor-agent: ${err.message}`,
369
- });
342
+ resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
370
343
  });
371
344
  });
372
345
  }
346
+ /**
347
+ * Execute cursor-agent command with retries for transient errors
348
+ */
349
+ async function cursorAgentSend(options) {
350
+ const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';
351
+ return (0, failure_policy_1.withRetry)(laneName, () => cursorAgentSendRaw(options), (res) => ({ ok: res.ok, error: res.error }), { maxRetries: 3 });
352
+ }
373
353
  /**
374
354
  * Extract dependency change request from agent response
375
355
  */
@@ -396,27 +376,126 @@ function extractDependencyRequest(text) {
396
376
  return { required: true, raw: t };
397
377
  }
398
378
  /**
399
- * Wrap prompt with dependency policy
379
+ * Inter-task state file name
380
+ */
381
+ const LANE_STATE_FILE = '_cursorflow/lane-state.json';
382
+ /**
383
+ * Dependency request file name - agent writes here when dependency changes are needed
384
+ */
385
+ const DEPENDENCY_REQUEST_FILE = '_cursorflow/dependency-request.json';
386
+ /**
387
+ * Read dependency request from file if it exists
400
388
  */
401
- function wrapPromptForDependencyPolicy(prompt, policy, options = {}) {
402
- const { noGit = false } = options;
403
- if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
389
+ function readDependencyRequestFile(worktreeDir) {
390
+ const filePath = (0, path_1.safeJoin)(worktreeDir, DEPENDENCY_REQUEST_FILE);
391
+ if (!fs.existsSync(filePath)) {
392
+ return { required: false };
393
+ }
394
+ try {
395
+ const content = fs.readFileSync(filePath, 'utf8');
396
+ const plan = JSON.parse(content);
397
+ // Validate required fields
398
+ if (plan.reason && Array.isArray(plan.commands) && plan.commands.length > 0) {
399
+ logger.info(`📦 Dependency request file detected: ${filePath}`);
400
+ return { required: true, plan };
401
+ }
402
+ logger.warn(`Invalid dependency request file format: ${filePath}`);
403
+ return { required: false };
404
+ }
405
+ catch (e) {
406
+ logger.warn(`Failed to parse dependency request file: ${e}`);
407
+ return { required: false };
408
+ }
409
+ }
410
+ /**
411
+ * Clear dependency request file after processing
412
+ */
413
+ function clearDependencyRequestFile(worktreeDir) {
414
+ const filePath = (0, path_1.safeJoin)(worktreeDir, DEPENDENCY_REQUEST_FILE);
415
+ if (fs.existsSync(filePath)) {
416
+ try {
417
+ fs.unlinkSync(filePath);
418
+ logger.info(`🗑️ Cleared dependency request file: ${filePath}`);
419
+ }
420
+ catch (e) {
421
+ logger.warn(`Failed to clear dependency request file: ${e}`);
422
+ }
423
+ }
424
+ }
425
+ /**
426
+ * Wrap prompt with dependency policy instructions (legacy, used by tests)
427
+ */
428
+ function wrapPromptForDependencyPolicy(prompt, policy) {
429
+ if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
404
430
  return prompt;
405
431
  }
406
- let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
407
- rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
408
- rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
432
+ let wrapped = `### 📦 Dependency Policy\n`;
433
+ wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
434
+ wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
435
+ wrapped += prompt;
436
+ return wrapped;
437
+ }
438
+ /**
439
+ * Wrap prompt with global context, dependency policy, and worktree instructions
440
+ */
441
+ function wrapPrompt(prompt, config, options = {}) {
442
+ const { noGit = false, isWorktree = true, previousState = null } = options;
443
+ // 1. PREFIX: Environment & Worktree context
444
+ let wrapped = `### 🛠 Environment & Context\n`;
445
+ wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
446
+ wrapped += `- **Path Rule**: 모든 파일 참조 및 터미널 명령어는 **현재 디렉토리(./)**를 기준으로 하세요.\n`;
447
+ if (isWorktree) {
448
+ wrapped += `- **File Availability**: Git 추적 파일만 존재합니다. (node_modules, .env 등은 기본적으로 없음)\n`;
449
+ }
450
+ // 2. Previous Task State (if available)
451
+ if (previousState) {
452
+ wrapped += `\n### 💡 Previous Task State\n`;
453
+ wrapped += `이전 태스크에서 전달된 상태 정보입니다:\n`;
454
+ wrapped += `\`\`\`json\n${previousState}\n\`\`\`\n`;
455
+ }
456
+ // 3. Dependency Policy (Integrated)
457
+ const policy = config.dependencyPolicy;
458
+ wrapped += `\n### 📦 Dependency Policy\n`;
459
+ wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
460
+ wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
409
461
  if (noGit) {
410
- rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
462
+ wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
463
+ }
464
+ wrapped += `\n**📦 Dependency Change Rules:**\n`;
465
+ wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
466
+ wrapped += `2. 의존성 변경이 필요하다면:\n`;
467
+ wrapped += ` - **다른 파일을 절대 수정하지 마세요.**\n`;
468
+ wrapped += ` - 아래 JSON을 \`./${DEPENDENCY_REQUEST_FILE}\` 파일에 저장하세요:\n`;
469
+ wrapped += ` \`\`\`json\n`;
470
+ wrapped += ` {\n`;
471
+ wrapped += ` "reason": "왜 이 의존성이 필요한지 설명",\n`;
472
+ wrapped += ` "changes": ["add lodash@^4.17.21", "remove unused-pkg"],\n`;
473
+ wrapped += ` "commands": ["pnpm add lodash@^4.17.21", "pnpm remove unused-pkg"],\n`;
474
+ wrapped += ` "notes": "추가 참고사항 (선택)" \n`;
475
+ wrapped += ` }\n`;
476
+ wrapped += ` \`\`\`\n`;
477
+ wrapped += ` - 파일 저장 후 **즉시 작업을 종료**하세요. 오케스트레이터가 처리합니다.\n`;
478
+ wrapped += `3. 의존성 변경이 불필요하면 바로 본 작업을 진행하세요.\n`;
479
+ wrapped += `\n---\n\n${prompt}\n\n---\n`;
480
+ // 4. SUFFIX: Task Completion & Git Requirements
481
+ wrapped += `\n### 📝 Task Completion Requirements\n`;
482
+ wrapped += `**반드시 다음 순서로 작업을 마무리하세요:**\n\n`;
483
+ if (!noGit) {
484
+ wrapped += `1. **Git Commit & Push** (필수!):\n`;
485
+ wrapped += ` \`\`\`bash\n`;
486
+ wrapped += ` git add -A\n`;
487
+ wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
488
+ wrapped += ` git push origin HEAD\n`;
489
+ wrapped += ` \`\`\`\n`;
490
+ wrapped += ` ⚠️ 커밋과 푸시 없이 작업을 종료하면 변경사항이 손실됩니다!\n\n`;
411
491
  }
412
- rules += '\nRules:\n';
413
- rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
414
- rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
415
- rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
416
- rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
417
- rules += 'Then STOP.\n';
418
- rules += '- If dependency changes are NOT required, proceed normally.\n';
419
- return `${rules}\n---\n\n${prompt}`;
492
+ wrapped += `2. **State Passing**: 다음 태스크로 전달할 정보가 있다면 \`./${LANE_STATE_FILE}\`에 JSON으로 저장하세요.\n\n`;
493
+ wrapped += `3. **Summary**: 작업 완료 다음을 요약해 주세요:\n`;
494
+ wrapped += ` - 생성/수정된 파일 목록\n`;
495
+ wrapped += ` - 주요 변경 사항\n`;
496
+ wrapped += ` - 커밋 해시 (git log --oneline -1)\n\n`;
497
+ wrapped += `4. 지시된 문서(docs/...)를 찾을 수 없다면 즉시 보고하세요.\n`;
498
+ return wrapped;
420
499
  }
421
500
  /**
422
501
  * Apply file permissions based on dependency policy
@@ -430,7 +509,7 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
430
509
  targets.push('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock');
431
510
  }
432
511
  for (const file of targets) {
433
- const filePath = path.join(worktreeDir, file);
512
+ const filePath = (0, path_1.safeJoin)(worktreeDir, file);
434
513
  if (!fs.existsSync(filePath))
435
514
  continue;
436
515
  try {
@@ -445,47 +524,31 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
445
524
  }
446
525
  /**
447
526
  * Wait for task-level dependencies to be completed by other lanes
527
+ * Now uses the enhanced dependency module with timeout support
448
528
  */
449
- async function waitForTaskDependencies(deps, runDir) {
529
+ async function waitForTaskDependencies(deps, runDir, options = {}) {
450
530
  if (!deps || deps.length === 0)
451
531
  return;
452
532
  const lanesRoot = path.dirname(runDir);
453
- const pendingDeps = new Set(deps);
454
- logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
455
- while (pendingDeps.size > 0) {
456
- for (const dep of pendingDeps) {
457
- const [laneName, taskName] = dep.split(':');
458
- if (!laneName || !taskName) {
459
- logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
460
- pendingDeps.delete(dep);
461
- continue;
533
+ const result = await (0, dependency_1.waitForTaskDependencies)(deps, lanesRoot, {
534
+ timeoutMs: options.timeoutMs || 30 * 60 * 1000, // 30 minutes default
535
+ pollIntervalMs: options.pollIntervalMs || 5000,
536
+ onTimeout: options.onTimeout || 'fail',
537
+ onProgress: (pending, completed) => {
538
+ if (completed.length > 0) {
539
+ logger.info(`Dependencies progress: ${completed.length}/${deps.length} completed`);
462
540
  }
463
- const depStatePath = path.join(lanesRoot, laneName, 'state.json');
464
- if (fs.existsSync(depStatePath)) {
465
- try {
466
- const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
467
- if (state.completedTasks && state.completedTasks.includes(taskName)) {
468
- logger.info(`✓ Dependency met: ${dep}`);
469
- pendingDeps.delete(dep);
470
- }
471
- else if (state.status === 'failed') {
472
- throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
473
- }
474
- }
475
- catch (e) {
476
- if (e.message.includes('Dependency failed'))
477
- throw e;
478
- // Ignore parse errors, file might be being written
479
- }
480
- }
481
- }
482
- if (pendingDeps.size > 0) {
483
- await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
541
+ },
542
+ });
543
+ if (!result.success) {
544
+ if (result.timedOut) {
545
+ throw new Error(`Dependency wait timed out after ${Math.round(result.elapsedMs / 1000)}s. Pending: ${result.failedDependencies.join(', ')}`);
484
546
  }
547
+ throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
485
548
  }
486
549
  }
487
550
  /**
488
- * Merge branches from dependency lanes
551
+ * Merge branches from dependency lanes with safe merge
489
552
  */
490
553
  async function mergeDependencyBranches(deps, runDir, worktreeDir) {
491
554
  if (!deps || deps.length === 0)
@@ -493,24 +556,35 @@ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
493
556
  const lanesRoot = path.dirname(runDir);
494
557
  const lanesToMerge = new Set(deps.map(d => d.split(':')[0]));
495
558
  for (const laneName of lanesToMerge) {
496
- const depStatePath = path.join(lanesRoot, laneName, 'state.json');
559
+ const depStatePath = (0, path_1.safeJoin)(lanesRoot, laneName, 'state.json');
497
560
  if (!fs.existsSync(depStatePath))
498
561
  continue;
499
562
  try {
500
- const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
501
- if (state.pipelineBranch) {
502
- logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
503
- // Ensure we have the latest
504
- git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
505
- git.merge(state.pipelineBranch, {
506
- cwd: worktreeDir,
507
- noFf: true,
508
- message: `chore: merge task dependency from ${laneName}`
509
- });
563
+ const state = (0, state_1.loadState)(depStatePath);
564
+ if (!state?.pipelineBranch)
565
+ continue;
566
+ logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
567
+ // Ensure we have the latest
568
+ git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
569
+ // Use safe merge with conflict detection
570
+ const mergeResult = git.safeMerge(state.pipelineBranch, {
571
+ cwd: worktreeDir,
572
+ noFf: true,
573
+ message: `chore: merge task dependency from ${laneName}`,
574
+ abortOnConflict: true,
575
+ });
576
+ if (!mergeResult.success) {
577
+ if (mergeResult.conflict) {
578
+ logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
579
+ throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
580
+ }
581
+ throw new Error(mergeResult.error || 'Merge failed');
510
582
  }
583
+ logger.success(`✓ Merged ${laneName}`);
511
584
  }
512
585
  catch (e) {
513
586
  logger.error(`Failed to merge branch from ${laneName}: ${e}`);
587
+ throw e;
514
588
  }
515
589
  }
516
590
  }
@@ -520,7 +594,7 @@ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
520
594
  async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskBranch, chatId, runDir, noGit = false, }) {
521
595
  const model = task.model || config.model || 'sonnet-4.5';
522
596
  const timeout = task.timeout || config.timeout;
523
- const convoPath = path.join(runDir, 'conversation.jsonl');
597
+ const convoPath = (0, path_1.safeJoin)(runDir, 'conversation.jsonl');
524
598
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
525
599
  logger.info(`Model: ${model}`);
526
600
  if (noGit) {
@@ -540,9 +614,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
540
614
  }
541
615
  // Apply dependency permissions
542
616
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
543
- // Run prompt
544
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
545
- (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('user', prompt1, {
617
+ // Read previous task state if available
618
+ let previousState = null;
619
+ const stateFilePath = (0, path_1.safeJoin)(worktreeDir, LANE_STATE_FILE);
620
+ if (fs.existsSync(stateFilePath)) {
621
+ try {
622
+ previousState = fs.readFileSync(stateFilePath, 'utf8');
623
+ logger.info('Loaded previous task state from _cursorflow/lane-state.json');
624
+ }
625
+ catch (e) {
626
+ logger.warn(`Failed to read inter-task state: ${e}`);
627
+ }
628
+ }
629
+ // Wrap prompt with context, previous state, and completion instructions
630
+ const wrappedPrompt = wrapPrompt(task.prompt, config, {
631
+ noGit,
632
+ isWorktree: !noGit,
633
+ previousState
634
+ });
635
+ // Log ONLY the original prompt to keep logs clean
636
+ (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('user', task.prompt, {
546
637
  task: task.name,
547
638
  model,
548
639
  }));
@@ -551,17 +642,18 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
551
642
  events_1.events.emit('agent.prompt_sent', {
552
643
  taskName: task.name,
553
644
  model,
554
- promptLength: prompt1.length,
645
+ promptLength: wrappedPrompt.length,
555
646
  });
556
647
  const r1 = await cursorAgentSend({
557
648
  workspaceDir: worktreeDir,
558
649
  chatId,
559
- prompt: prompt1,
650
+ prompt: wrappedPrompt,
560
651
  model,
561
652
  signalDir: runDir,
562
653
  timeout,
563
654
  enableIntervention: config.enableIntervention,
564
655
  outputFormat: config.agentOutputFormat,
656
+ taskName: task.name,
565
657
  });
566
658
  const duration = Date.now() - startTime;
567
659
  events_1.events.emit('agent.response_received', {
@@ -588,15 +680,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
588
680
  error: r1.error,
589
681
  };
590
682
  }
591
- // Check for dependency request
592
- const depReq = extractDependencyRequest(r1.resultText || '');
593
- if (depReq.required && !config.dependencyPolicy.allowDependencyChange) {
594
- return {
595
- taskName: task.name,
596
- taskBranch,
597
- status: 'BLOCKED_DEPENDENCY',
598
- dependencyRequest: depReq.plan || null,
599
- };
683
+ // Check for dependency request (file-based takes priority, then text-based)
684
+ const fileDepReq = readDependencyRequestFile(worktreeDir);
685
+ const textDepReq = extractDependencyRequest(r1.resultText || '');
686
+ // Determine which request to use (file-based is preferred as it's more structured)
687
+ const depReq = fileDepReq.required ? fileDepReq : textDepReq;
688
+ if (depReq.required) {
689
+ logger.info(`📦 Dependency change requested: ${depReq.plan?.reason || 'No reason provided'}`);
690
+ if (depReq.plan) {
691
+ logger.info(` Commands: ${depReq.plan.commands.join(', ')}`);
692
+ }
693
+ if (!config.dependencyPolicy.allowDependencyChange) {
694
+ // Clear the file so it doesn't persist after resolution
695
+ clearDependencyRequestFile(worktreeDir);
696
+ return {
697
+ taskName: task.name,
698
+ taskBranch,
699
+ status: 'BLOCKED_DEPENDENCY',
700
+ dependencyRequest: depReq.plan || null,
701
+ };
702
+ }
600
703
  }
601
704
  // Push task branch (skip in noGit mode)
602
705
  if (!noGit) {
@@ -661,6 +764,28 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
661
764
  logger.error(` ${validationError.message}`);
662
765
  throw validationError;
663
766
  }
767
+ // Run preflight checks (can be skipped for resume)
768
+ if (!options.skipPreflight && startIndex === 0) {
769
+ logger.info('Running preflight checks...');
770
+ const preflight = await (0, health_1.preflightCheck)({
771
+ requireRemote: !noGit,
772
+ requireAuth: true,
773
+ });
774
+ if (!preflight.canProceed) {
775
+ (0, health_1.printPreflightReport)(preflight);
776
+ throw new Error('Preflight check failed. Please fix the blockers above.');
777
+ }
778
+ if (preflight.warnings.length > 0) {
779
+ for (const warning of preflight.warnings) {
780
+ logger.warn(`⚠️ ${warning}`);
781
+ }
782
+ }
783
+ logger.success('✓ Preflight checks passed');
784
+ }
785
+ // Warn if baseBranch is set in config (it will be ignored)
786
+ if (config.baseBranch) {
787
+ logger.warn(`⚠️ config.baseBranch="${config.baseBranch}" will be ignored. Using current branch instead.`);
788
+ }
664
789
  // Ensure cursor-agent is installed
665
790
  (0, cursor_agent_1.ensureCursorAgent)();
666
791
  // Check authentication before starting
@@ -681,24 +806,53 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
681
806
  }
682
807
  logger.success('✓ Cursor authentication OK');
683
808
  // In noGit mode, we don't need repoRoot - use current directory
684
- const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
809
+ const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
810
+ // ALWAYS use current branch as base - ignore config.baseBranch
811
+ // This ensures dependency structure is maintained in the worktree
812
+ const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
813
+ logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
685
814
  // Load existing state if resuming
686
- const statePath = path.join(runDir, 'state.json');
815
+ const statePath = (0, path_1.safeJoin)(runDir, 'state.json');
687
816
  let state = null;
688
817
  if (fs.existsSync(statePath)) {
689
- try {
690
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
818
+ // Check if state needs recovery
819
+ if ((0, state_1.stateNeedsRecovery)(statePath)) {
820
+ logger.warn('State file indicates incomplete previous run. Attempting recovery...');
821
+ const repairedState = (0, state_1.repairLaneState)(statePath);
822
+ if (repairedState) {
823
+ state = repairedState;
824
+ logger.success('✓ State recovered');
825
+ }
826
+ else {
827
+ logger.warn('Could not recover state. Starting fresh.');
828
+ }
691
829
  }
692
- catch (e) {
693
- logger.warn(`Failed to load existing state from ${statePath}: ${e}`);
830
+ else {
831
+ state = (0, state_1.loadState)(statePath);
832
+ // Validate loaded state
833
+ if (state) {
834
+ const validation = (0, state_1.validateLaneState)(statePath, {
835
+ checkWorktree: !noGit,
836
+ checkBranch: !noGit,
837
+ autoRepair: true,
838
+ });
839
+ if (!validation.valid) {
840
+ logger.warn(`State validation issues: ${validation.issues.join(', ')}`);
841
+ if (validation.repaired) {
842
+ logger.info('State was auto-repaired');
843
+ state = validation.repairedState || state;
844
+ }
845
+ }
846
+ }
694
847
  }
695
848
  }
696
849
  const randomSuffix = Math.random().toString(36).substring(2, 7);
697
850
  const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
698
851
  // In noGit mode, use a simple local directory instead of worktree
699
- const worktreeDir = state?.worktreeDir || (noGit
700
- ? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
701
- : path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
852
+ // Flatten the path by replacing slashes with hyphens to avoid race conditions in parent directory creation
853
+ const worktreeDir = state?.worktreeDir || config.worktreeDir || (noGit
854
+ ? (0, path_1.safeJoin)(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
855
+ : (0, path_1.safeJoin)(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-')));
702
856
  if (startIndex === 0) {
703
857
  logger.section('🚀 Starting Pipeline');
704
858
  }
@@ -716,10 +870,36 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
716
870
  fs.mkdirSync(worktreeDir, { recursive: true });
717
871
  }
718
872
  else {
719
- git.createWorktree(worktreeDir, pipelineBranch, {
720
- baseBranch: config.baseBranch || 'main',
721
- cwd: repoRoot,
722
- });
873
+ // Use a simple retry mechanism for Git worktree creation to handle potential race conditions
874
+ let retries = 3;
875
+ let lastError = null;
876
+ while (retries > 0) {
877
+ try {
878
+ // Ensure parent directory exists before calling git worktree
879
+ const worktreeParent = path.dirname(worktreeDir);
880
+ if (!fs.existsSync(worktreeParent)) {
881
+ fs.mkdirSync(worktreeParent, { recursive: true });
882
+ }
883
+ // Always use the current branch (already captured at start) as the base branch
884
+ git.createWorktree(worktreeDir, pipelineBranch, {
885
+ baseBranch: currentBranch,
886
+ cwd: repoRoot,
887
+ });
888
+ break; // Success
889
+ }
890
+ catch (e) {
891
+ lastError = e;
892
+ retries--;
893
+ if (retries > 0) {
894
+ const delay = Math.floor(Math.random() * 1000) + 500;
895
+ logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
896
+ await new Promise(resolve => setTimeout(resolve, delay));
897
+ }
898
+ }
899
+ }
900
+ if (retries === 0 && lastError) {
901
+ throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
902
+ }
723
903
  }
724
904
  }
725
905
  else if (!noGit) {
@@ -759,6 +939,9 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
759
939
  state.status = 'running';
760
940
  state.error = null;
761
941
  state.dependencyRequest = null;
942
+ state.pipelineBranch = pipelineBranch;
943
+ state.worktreeDir = worktreeDir;
944
+ state.label = state.label || pipelineBranch;
762
945
  state.dependsOn = config.dependsOn || [];
763
946
  state.completedTasks = state.completedTasks || [];
764
947
  }
@@ -769,8 +952,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
769
952
  // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
770
953
  const lanesRoot = path.dirname(runDir);
771
954
  for (const depName of config.dependsOn) {
772
- const depRunDir = path.join(lanesRoot, depName);
773
- const depStatePath = path.join(depRunDir, 'state.json');
955
+ const depRunDir = path.join(lanesRoot, depName); // nosemgrep
956
+ const depStatePath = path.join(depRunDir, 'state.json'); // nosemgrep
774
957
  if (!fs.existsSync(depStatePath)) {
775
958
  logger.warn(`Dependency state not found for ${depName} at ${depStatePath}`);
776
959
  continue;
@@ -809,8 +992,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
809
992
  // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
810
993
  const lanesRoot = path.dirname(runDir);
811
994
  for (const depName of config.dependsOn) {
812
- const depRunDir = path.join(lanesRoot, depName);
813
- const depStatePath = path.join(depRunDir, 'state.json');
995
+ const depRunDir = (0, path_1.safeJoin)(lanesRoot, depName);
996
+ const depStatePath = (0, path_1.safeJoin)(depRunDir, 'state.json');
814
997
  if (!fs.existsSync(depStatePath)) {
815
998
  continue;
816
999
  }
@@ -826,8 +1009,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
826
1009
  for (const entry of entries) {
827
1010
  if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
828
1011
  continue;
829
- const srcPath = path.join(src, entry.name);
830
- const destPath = path.join(dest, entry.name);
1012
+ const srcPath = (0, path_1.safeJoin)(src, entry.name);
1013
+ const destPath = (0, path_1.safeJoin)(dest, entry.name);
831
1014
  if (entry.isDirectory()) {
832
1015
  copyFiles(srcPath, destPath);
833
1016
  }
@@ -846,16 +1029,31 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
846
1029
  }
847
1030
  // Run tasks
848
1031
  const results = [];
1032
+ const laneName = state.label || path.basename(runDir);
849
1033
  for (let i = startIndex; i < config.tasks.length; i++) {
850
1034
  const task = config.tasks[i];
851
1035
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
1036
+ // Create checkpoint before each task
1037
+ try {
1038
+ await (0, checkpoint_1.createCheckpoint)(laneName, runDir, noGit ? null : worktreeDir, {
1039
+ description: `Before task ${i + 1}: ${task.name}`,
1040
+ maxCheckpoints: 5,
1041
+ });
1042
+ }
1043
+ catch (e) {
1044
+ logger.warn(`Failed to create checkpoint: ${e.message}`);
1045
+ }
852
1046
  // Handle task-level dependencies
853
1047
  if (task.dependsOn && task.dependsOn.length > 0) {
854
1048
  state.status = 'waiting';
855
1049
  state.waitingFor = task.dependsOn;
856
1050
  (0, state_1.saveState)(statePath, state);
857
1051
  try {
858
- await waitForTaskDependencies(task.dependsOn, runDir);
1052
+ // Use enhanced dependency wait with timeout
1053
+ await waitForTaskDependencies(task.dependsOn, runDir, {
1054
+ timeoutMs: config.timeout || 30 * 60 * 1000,
1055
+ onTimeout: 'fail',
1056
+ });
859
1057
  if (!noGit) {
860
1058
  await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
861
1059
  }
@@ -869,6 +1067,12 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
869
1067
  state.error = e.message;
870
1068
  (0, state_1.saveState)(statePath, state);
871
1069
  logger.error(`Task dependency wait/merge failed: ${e.message}`);
1070
+ // Try to restore from checkpoint
1071
+ const latestCheckpoint = (0, checkpoint_1.getLatestCheckpoint)(runDir);
1072
+ if (latestCheckpoint) {
1073
+ logger.info(`💾 Checkpoint available: ${latestCheckpoint.id}`);
1074
+ logger.info(` Resume with: cursorflow resume --checkpoint ${latestCheckpoint.id}`);
1075
+ }
872
1076
  process.exit(1);
873
1077
  }
874
1078
  }
@@ -943,7 +1147,7 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
943
1147
  continue;
944
1148
  if (entry.isDirectory()) {
945
1149
  stats.dirs++;
946
- const sub = getFileSummary(path.join(dir, entry.name));
1150
+ const sub = getFileSummary((0, path_1.safeJoin)(dir, entry.name));
947
1151
  stats.files += sub.files;
948
1152
  stats.dirs += sub.dirs;
949
1153
  }
@@ -958,7 +1162,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
958
1162
  }
959
1163
  else {
960
1164
  try {
961
- const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
1165
+ // Always use current branch for comparison (already captured at start)
1166
+ const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
962
1167
  if (stats) {
963
1168
  logger.info('Final Workspace Summary (Git):\n' + stats);
964
1169
  }
@@ -983,10 +1188,12 @@ if (require.main === module) {
983
1188
  const runDirIdx = args.indexOf('--run-dir');
984
1189
  const startIdxIdx = args.indexOf('--start-index');
985
1190
  const pipelineBranchIdx = args.indexOf('--pipeline-branch');
1191
+ const worktreeDirIdx = args.indexOf('--worktree-dir');
986
1192
  const noGit = args.includes('--no-git');
987
1193
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1] : '.';
988
1194
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
989
1195
  const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
1196
+ const forcedWorktreeDir = worktreeDirIdx >= 0 ? args[worktreeDirIdx + 1] : null;
990
1197
  // Extract runId from runDir (format: .../runs/run-123/lanes/lane-name)
991
1198
  const parts = runDir.split(path.sep);
992
1199
  const runsIdx = parts.lastIndexOf('runs');
@@ -1014,6 +1221,9 @@ if (require.main === module) {
1014
1221
  if (forcedPipelineBranch) {
1015
1222
  config.pipelineBranch = forcedPipelineBranch;
1016
1223
  }
1224
+ if (forcedWorktreeDir) {
1225
+ config.worktreeDir = forcedWorktreeDir;
1226
+ }
1017
1227
  }
1018
1228
  catch (error) {
1019
1229
  console.error(`Failed to load tasks file: ${error.message}`);