@litmers/cursorflow-orchestrator 0.1.20 → 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 (224) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +4 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +8 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +392 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +4 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +9 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1131 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +388 -162
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +374 -2
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
@@ -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;
@@ -60,6 +67,10 @@ const config_1 = require("../utils/config");
60
67
  const webhook_1 = require("../utils/webhook");
61
68
  const reviewer_1 = require("./reviewer");
62
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");
63
74
  /**
64
75
  * Execute cursor-agent command with timeout and better error handling
65
76
  */
@@ -175,9 +186,9 @@ function validateTaskConfig(config) {
175
186
  }
176
187
  }
177
188
  /**
178
- * Execute cursor-agent command with streaming and better error handling
189
+ * Internal: Execute cursor-agent command with streaming
179
190
  */
180
- async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }) {
191
+ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }) {
181
192
  // Use stream-json format for structured output with tool calls and results
182
193
  const format = outputFormat || 'stream-json';
183
194
  const args = [
@@ -191,65 +202,53 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
191
202
  prompt,
192
203
  ];
193
204
  const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
194
- logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
195
205
  // Determine stdio mode based on intervention setting
196
- // When intervention is enabled, we pipe stdin for message injection
197
- // When disabled (default), we ignore stdin to avoid buffering issues
198
206
  const stdinMode = enableIntervention ? 'pipe' : 'ignore';
199
- if (enableIntervention) {
200
- logger.info('Intervention mode enabled (stdin piped)');
201
- }
202
207
  return new Promise((resolve) => {
203
208
  // Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
204
209
  const childEnv = { ...process.env };
205
- // Only filter out specific problematic NODE_OPTIONS, don't clear entirely
206
210
  if (childEnv.NODE_OPTIONS) {
207
- // Remove flags that might interfere with cursor-agent
208
211
  const filtered = childEnv.NODE_OPTIONS
209
212
  .split(' ')
210
213
  .filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
211
214
  .join(' ');
212
215
  childEnv.NODE_OPTIONS = filtered;
213
216
  }
214
- // Disable Python buffering in case cursor-agent uses Python
215
217
  childEnv.PYTHONUNBUFFERED = '1';
216
218
  const child = (0, child_process_1.spawn)('cursor-agent', args, {
217
219
  stdio: [stdinMode, 'pipe', 'pipe'],
218
220
  env: childEnv,
219
221
  });
220
- logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
221
- // Save PID to state if possible (avoid TOCTOU by reading directly)
222
+ // Save PID to state if possible
222
223
  if (child.pid && signalDir) {
223
224
  try {
224
225
  const statePath = (0, path_1.safeJoin)(signalDir, 'state.json');
225
- // Read directly without existence check to avoid race condition
226
226
  const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
227
227
  state.pid = child.pid;
228
228
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
229
229
  }
230
230
  catch {
231
- // Best effort - file may not exist yet
231
+ // Best effort
232
232
  }
233
233
  }
234
234
  let fullStdout = '';
235
235
  let fullStderr = '';
236
236
  let timeoutHandle;
237
- // Heartbeat logging to show progress
237
+ // Heartbeat logging
238
238
  let lastHeartbeat = Date.now();
239
239
  let bytesReceived = 0;
240
+ const startTime = Date.now();
240
241
  const heartbeatInterval = setInterval(() => {
241
- const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
242
242
  const totalElapsed = Math.round((Date.now() - startTime) / 1000);
243
- 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`);
244
245
  }, HEARTBEAT_INTERVAL_MS);
245
- const startTime = Date.now();
246
- // Watch for "intervention.txt" or "timeout.txt" signal files
246
+ // Signal watchers (intervention, timeout)
247
247
  const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
248
248
  const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
249
249
  let signalWatcher = null;
250
250
  if (signalDir && fs.existsSync(signalDir)) {
251
251
  signalWatcher = fs.watch(signalDir, (event, filename) => {
252
- // Handle intervention
253
252
  if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
254
253
  try {
255
254
  const message = fs.readFileSync(interventionPath, 'utf8').trim();
@@ -257,74 +256,65 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
257
256
  if (enableIntervention && child.stdin) {
258
257
  logger.info(`Injecting intervention: ${message}`);
259
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
+ }
260
267
  }
261
268
  else {
262
269
  logger.warn(`Intervention requested but stdin not available: ${message}`);
263
- logger.warn('To enable intervention, set enableIntervention: true in config');
264
270
  }
265
- fs.unlinkSync(interventionPath); // Clear it
271
+ fs.unlinkSync(interventionPath);
266
272
  }
267
273
  }
268
- catch (e) {
269
- logger.warn('Failed to read intervention file');
270
- }
274
+ catch { }
271
275
  }
272
- // Handle dynamic timeout update
273
276
  if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
274
277
  try {
275
278
  const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
276
279
  const newTimeoutMs = parseInt(newTimeoutStr);
277
280
  if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
278
281
  logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
279
- // Clear old timeout
280
282
  if (timeoutHandle)
281
283
  clearTimeout(timeoutHandle);
282
- // Set new timeout based on total elapsed time
283
284
  const elapsed = Date.now() - startTime;
284
285
  const remaining = Math.max(1000, newTimeoutMs - elapsed);
285
286
  timeoutHandle = setTimeout(() => {
286
287
  clearInterval(heartbeatInterval);
287
288
  child.kill();
288
- const totalSec = Math.round(newTimeoutMs / 1000);
289
- resolve({
290
- ok: false,
291
- exitCode: -1,
292
- error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
293
- });
289
+ resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
294
290
  }, remaining);
295
- fs.unlinkSync(timeoutPath); // Clear it
291
+ fs.unlinkSync(timeoutPath);
296
292
  }
297
293
  }
298
- catch (e) {
299
- logger.warn('Failed to read timeout update file');
300
- }
294
+ catch { }
301
295
  }
302
296
  });
303
297
  }
304
298
  if (child.stdout) {
305
299
  child.stdout.on('data', (data) => {
306
- const str = data.toString();
307
- fullStdout += str;
300
+ fullStdout += data.toString();
308
301
  bytesReceived += data.length;
309
- // Also pipe to our own stdout so it goes to terminal.log
310
302
  process.stdout.write(data);
311
303
  });
312
304
  }
313
305
  if (child.stderr) {
314
306
  child.stderr.on('data', (data) => {
315
307
  fullStderr += data.toString();
316
- // Pipe to our own stderr so it goes to terminal.log
317
308
  process.stderr.write(data);
318
309
  });
319
310
  }
320
311
  timeoutHandle = setTimeout(() => {
321
312
  clearInterval(heartbeatInterval);
322
313
  child.kill();
323
- const timeoutSec = Math.round(timeoutMs / 1000);
324
314
  resolve({
325
315
  ok: false,
326
316
  exitCode: -1,
327
- 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.`,
328
318
  });
329
319
  }, timeoutMs);
330
320
  child.on('close', (code) => {
@@ -335,21 +325,7 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
335
325
  const json = parseJsonFromStdout(fullStdout);
336
326
  if (code !== 0 || !json || json.type !== 'result') {
337
327
  let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
338
- // Check for common errors
339
- if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
340
- errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
341
- }
342
- else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
343
- errorMsg = 'API rate limit or quota exceeded.';
344
- }
345
- else if (errorMsg.includes('model')) {
346
- errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
347
- }
348
- resolve({
349
- ok: false,
350
- exitCode: code ?? -1,
351
- error: errorMsg,
352
- });
328
+ resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
353
329
  }
354
330
  else {
355
331
  resolve({
@@ -363,14 +339,17 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
363
339
  child.on('error', (err) => {
364
340
  clearTimeout(timeoutHandle);
365
341
  clearInterval(heartbeatInterval);
366
- resolve({
367
- ok: false,
368
- exitCode: -1,
369
- error: `Failed to start cursor-agent: ${err.message}`,
370
- });
342
+ resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
371
343
  });
372
344
  });
373
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
+ }
374
353
  /**
375
354
  * Extract dependency change request from agent response
376
355
  */
@@ -397,27 +376,126 @@ function extractDependencyRequest(text) {
397
376
  return { required: true, raw: t };
398
377
  }
399
378
  /**
400
- * 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
388
+ */
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)
401
427
  */
402
- function wrapPromptForDependencyPolicy(prompt, policy, options = {}) {
403
- const { noGit = false } = options;
404
- if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
428
+ function wrapPromptForDependencyPolicy(prompt, policy) {
429
+ if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
405
430
  return prompt;
406
431
  }
407
- let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
408
- rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
409
- 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`;
410
461
  if (noGit) {
411
- 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`;
412
491
  }
413
- rules += '\nRules:\n';
414
- rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
415
- rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
416
- rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
417
- rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
418
- rules += 'Then STOP.\n';
419
- rules += '- If dependency changes are NOT required, proceed normally.\n';
420
- 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;
421
499
  }
422
500
  /**
423
501
  * Apply file permissions based on dependency policy
@@ -446,47 +524,31 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
446
524
  }
447
525
  /**
448
526
  * Wait for task-level dependencies to be completed by other lanes
527
+ * Now uses the enhanced dependency module with timeout support
449
528
  */
450
- async function waitForTaskDependencies(deps, runDir) {
529
+ async function waitForTaskDependencies(deps, runDir, options = {}) {
451
530
  if (!deps || deps.length === 0)
452
531
  return;
453
532
  const lanesRoot = path.dirname(runDir);
454
- const pendingDeps = new Set(deps);
455
- logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
456
- while (pendingDeps.size > 0) {
457
- for (const dep of pendingDeps) {
458
- const [laneName, taskName] = dep.split(':');
459
- if (!laneName || !taskName) {
460
- logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
461
- pendingDeps.delete(dep);
462
- 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`);
463
540
  }
464
- const depStatePath = (0, path_1.safeJoin)(lanesRoot, laneName, 'state.json');
465
- if (fs.existsSync(depStatePath)) {
466
- try {
467
- const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
468
- if (state.completedTasks && state.completedTasks.includes(taskName)) {
469
- logger.info(`✓ Dependency met: ${dep}`);
470
- pendingDeps.delete(dep);
471
- }
472
- else if (state.status === 'failed') {
473
- throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
474
- }
475
- }
476
- catch (e) {
477
- if (e.message.includes('Dependency failed'))
478
- throw e;
479
- // Ignore parse errors, file might be being written
480
- }
481
- }
482
- }
483
- if (pendingDeps.size > 0) {
484
- 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(', ')}`);
485
546
  }
547
+ throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
486
548
  }
487
549
  }
488
550
  /**
489
- * Merge branches from dependency lanes
551
+ * Merge branches from dependency lanes with safe merge
490
552
  */
491
553
  async function mergeDependencyBranches(deps, runDir, worktreeDir) {
492
554
  if (!deps || deps.length === 0)
@@ -498,20 +560,31 @@ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
498
560
  if (!fs.existsSync(depStatePath))
499
561
  continue;
500
562
  try {
501
- const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
502
- if (state.pipelineBranch) {
503
- logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
504
- // Ensure we have the latest
505
- git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
506
- git.merge(state.pipelineBranch, {
507
- cwd: worktreeDir,
508
- noFf: true,
509
- message: `chore: merge task dependency from ${laneName}`
510
- });
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');
511
582
  }
583
+ logger.success(`✓ Merged ${laneName}`);
512
584
  }
513
585
  catch (e) {
514
586
  logger.error(`Failed to merge branch from ${laneName}: ${e}`);
587
+ throw e;
515
588
  }
516
589
  }
517
590
  }
@@ -541,9 +614,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
541
614
  }
542
615
  // Apply dependency permissions
543
616
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
544
- // Run prompt
545
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
546
- (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, {
547
637
  task: task.name,
548
638
  model,
549
639
  }));
@@ -552,17 +642,18 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
552
642
  events_1.events.emit('agent.prompt_sent', {
553
643
  taskName: task.name,
554
644
  model,
555
- promptLength: prompt1.length,
645
+ promptLength: wrappedPrompt.length,
556
646
  });
557
647
  const r1 = await cursorAgentSend({
558
648
  workspaceDir: worktreeDir,
559
649
  chatId,
560
- prompt: prompt1,
650
+ prompt: wrappedPrompt,
561
651
  model,
562
652
  signalDir: runDir,
563
653
  timeout,
564
654
  enableIntervention: config.enableIntervention,
565
655
  outputFormat: config.agentOutputFormat,
656
+ taskName: task.name,
566
657
  });
567
658
  const duration = Date.now() - startTime;
568
659
  events_1.events.emit('agent.response_received', {
@@ -589,15 +680,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
589
680
  error: r1.error,
590
681
  };
591
682
  }
592
- // Check for dependency request
593
- const depReq = extractDependencyRequest(r1.resultText || '');
594
- if (depReq.required && !config.dependencyPolicy.allowDependencyChange) {
595
- return {
596
- taskName: task.name,
597
- taskBranch,
598
- status: 'BLOCKED_DEPENDENCY',
599
- dependencyRequest: depReq.plan || null,
600
- };
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
+ }
601
703
  }
602
704
  // Push task branch (skip in noGit mode)
603
705
  if (!noGit) {
@@ -662,6 +764,28 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
662
764
  logger.error(` ${validationError.message}`);
663
765
  throw validationError;
664
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
+ }
665
789
  // Ensure cursor-agent is installed
666
790
  (0, cursor_agent_1.ensureCursorAgent)();
667
791
  // Check authentication before starting
@@ -682,16 +806,44 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
682
806
  }
683
807
  logger.success('✓ Cursor authentication OK');
684
808
  // In noGit mode, we don't need repoRoot - use current directory
685
- 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)`);
686
814
  // Load existing state if resuming
687
815
  const statePath = (0, path_1.safeJoin)(runDir, 'state.json');
688
816
  let state = null;
689
817
  if (fs.existsSync(statePath)) {
690
- try {
691
- 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
+ }
692
829
  }
693
- catch (e) {
694
- 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
+ }
695
847
  }
696
848
  }
697
849
  const randomSuffix = Math.random().toString(36).substring(2, 7);
@@ -728,8 +880,9 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
728
880
  if (!fs.existsSync(worktreeParent)) {
729
881
  fs.mkdirSync(worktreeParent, { recursive: true });
730
882
  }
883
+ // Always use the current branch (already captured at start) as the base branch
731
884
  git.createWorktree(worktreeDir, pipelineBranch, {
732
- baseBranch: config.baseBranch || 'main',
885
+ baseBranch: currentBranch,
733
886
  cwd: repoRoot,
734
887
  });
735
888
  break; // Success
@@ -876,16 +1029,31 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
876
1029
  }
877
1030
  // Run tasks
878
1031
  const results = [];
1032
+ const laneName = state.label || path.basename(runDir);
879
1033
  for (let i = startIndex; i < config.tasks.length; i++) {
880
1034
  const task = config.tasks[i];
881
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
+ }
882
1046
  // Handle task-level dependencies
883
1047
  if (task.dependsOn && task.dependsOn.length > 0) {
884
1048
  state.status = 'waiting';
885
1049
  state.waitingFor = task.dependsOn;
886
1050
  (0, state_1.saveState)(statePath, state);
887
1051
  try {
888
- 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
+ });
889
1057
  if (!noGit) {
890
1058
  await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
891
1059
  }
@@ -899,6 +1067,12 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
899
1067
  state.error = e.message;
900
1068
  (0, state_1.saveState)(statePath, state);
901
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
+ }
902
1076
  process.exit(1);
903
1077
  }
904
1078
  }
@@ -988,7 +1162,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
988
1162
  }
989
1163
  else {
990
1164
  try {
991
- 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 });
992
1167
  if (stats) {
993
1168
  logger.info('Final Workspace Summary (Git):\n' + stats);
994
1169
  }