@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
@@ -1,22 +1,30 @@
1
1
  /**
2
2
  * Core Runner - Execute tasks sequentially in a lane
3
3
  *
4
- * Adapted from sequential-agent-runner.js
4
+ * Features:
5
+ * - Enhanced retry with circuit breaker
6
+ * - Checkpoint system for recovery
7
+ * - State validation and repair
8
+ * - Improved dependency management
5
9
  */
6
10
 
7
11
  import * as fs from 'fs';
8
12
  import * as path from 'path';
9
- import { execSync, spawn, spawnSync } from 'child_process';
13
+ import { spawn, spawnSync } from 'child_process';
10
14
 
11
15
  import * as git from '../utils/git';
12
16
  import * as logger from '../utils/logger';
13
17
  import { ensureCursorAgent, checkCursorAuth, printAuthHelp } from '../utils/cursor-agent';
14
- import { saveState, appendLog, createConversationEntry } from '../utils/state';
18
+ import { saveState, appendLog, createConversationEntry, loadState, validateLaneState, repairLaneState, stateNeedsRecovery } from '../utils/state';
15
19
  import { events } from '../utils/events';
16
20
  import { loadConfig } from '../utils/config';
17
21
  import { registerWebhooks } from '../utils/webhook';
18
22
  import { runReviewLoop } from './reviewer';
19
23
  import { safeJoin } from '../utils/path';
24
+ import { analyzeFailure, RecoveryAction, logFailure, withRetry } from './failure-policy';
25
+ import { createCheckpoint, getLatestCheckpoint, restoreFromCheckpoint } from '../utils/checkpoint';
26
+ import { waitForTaskDependencies as waitForDeps, DependencyWaitOptions } from '../utils/dependency';
27
+ import { preflightCheck, printPreflightReport } from '../utils/health';
20
28
  import {
21
29
  RunnerConfig,
22
30
  Task,
@@ -25,7 +33,7 @@ import {
25
33
  DependencyPolicy,
26
34
  DependencyRequestPlan,
27
35
  LaneState
28
- } from '../utils/types';
36
+ } from '../types';
29
37
 
30
38
  /**
31
39
  * Execute cursor-agent command with timeout and better error handling
@@ -174,19 +182,18 @@ export function validateTaskConfig(config: RunnerConfig): void {
174
182
  }
175
183
 
176
184
  /**
177
- * Execute cursor-agent command with streaming and better error handling
185
+ * Internal: Execute cursor-agent command with streaming
178
186
  */
179
- export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }: {
187
+ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }: {
180
188
  workspaceDir: string;
181
189
  chatId: string;
182
190
  prompt: string;
183
191
  model?: string;
184
192
  signalDir?: string;
185
193
  timeout?: number;
186
- /** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
187
194
  enableIntervention?: boolean;
188
- /** Output format for cursor-agent (default: 'stream-json') */
189
195
  outputFormat?: 'stream-json' | 'json' | 'plain';
196
+ taskName?: string;
190
197
  }): Promise<AgentSendResult> {
191
198
  // Use stream-json format for structured output with tool calls and results
192
199
  const format = outputFormat || 'stream-json';
@@ -202,24 +209,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
202
209
  ];
203
210
 
204
211
  const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
205
- logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
206
212
 
207
213
  // Determine stdio mode based on intervention setting
208
- // When intervention is enabled, we pipe stdin for message injection
209
- // When disabled (default), we ignore stdin to avoid buffering issues
210
214
  const stdinMode = enableIntervention ? 'pipe' : 'ignore';
211
215
 
212
- if (enableIntervention) {
213
- logger.info('Intervention mode enabled (stdin piped)');
214
- }
215
-
216
216
  return new Promise((resolve) => {
217
217
  // Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
218
218
  const childEnv = { ...process.env };
219
219
 
220
- // Only filter out specific problematic NODE_OPTIONS, don't clear entirely
221
220
  if (childEnv.NODE_OPTIONS) {
222
- // Remove flags that might interfere with cursor-agent
223
221
  const filtered = childEnv.NODE_OPTIONS
224
222
  .split(' ')
225
223
  .filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
@@ -227,7 +225,6 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
227
225
  childEnv.NODE_OPTIONS = filtered;
228
226
  }
229
227
 
230
- // Disable Python buffering in case cursor-agent uses Python
231
228
  childEnv.PYTHONUNBUFFERED = '1';
232
229
 
233
230
  const child = spawn('cursor-agent', args, {
@@ -235,18 +232,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
235
232
  env: childEnv,
236
233
  });
237
234
 
238
- logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
239
-
240
- // Save PID to state if possible (avoid TOCTOU by reading directly)
235
+ // Save PID to state if possible
241
236
  if (child.pid && signalDir) {
242
237
  try {
243
238
  const statePath = safeJoin(signalDir, 'state.json');
244
- // Read directly without existence check to avoid race condition
245
239
  const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
246
240
  state.pid = child.pid;
247
241
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
248
242
  } catch {
249
- // Best effort - file may not exist yet
243
+ // Best effort
250
244
  }
251
245
  }
252
246
 
@@ -254,24 +248,23 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
254
248
  let fullStderr = '';
255
249
  let timeoutHandle: NodeJS.Timeout;
256
250
 
257
- // Heartbeat logging to show progress
251
+ // Heartbeat logging
258
252
  let lastHeartbeat = Date.now();
259
253
  let bytesReceived = 0;
254
+ const startTime = Date.now();
260
255
  const heartbeatInterval = setInterval(() => {
261
- const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
262
256
  const totalElapsed = Math.round((Date.now() - startTime) / 1000);
263
- logger.info(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
257
+ // Output without timestamp - orchestrator will add it
258
+ console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
264
259
  }, HEARTBEAT_INTERVAL_MS);
265
- const startTime = Date.now();
266
260
 
267
- // Watch for "intervention.txt" or "timeout.txt" signal files
261
+ // Signal watchers (intervention, timeout)
268
262
  const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
269
263
  const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
270
264
  let signalWatcher: fs.FSWatcher | null = null;
271
265
 
272
266
  if (signalDir && fs.existsSync(signalDir)) {
273
267
  signalWatcher = fs.watch(signalDir, (event, filename) => {
274
- // Handle intervention
275
268
  if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
276
269
  try {
277
270
  const message = fs.readFileSync(interventionPath, 'utf8').trim();
@@ -279,59 +272,48 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
279
272
  if (enableIntervention && child.stdin) {
280
273
  logger.info(`Injecting intervention: ${message}`);
281
274
  child.stdin.write(message + '\n');
275
+
276
+ // Log to conversation history for visibility in monitor/logs
277
+ if (signalDir) {
278
+ const convoPath = path.join(signalDir, 'conversation.jsonl');
279
+ appendLog(convoPath, createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${message}`, {
280
+ task: taskName || 'AGENT_TURN',
281
+ model: 'manual'
282
+ }));
283
+ }
282
284
  } else {
283
285
  logger.warn(`Intervention requested but stdin not available: ${message}`);
284
- logger.warn('To enable intervention, set enableIntervention: true in config');
285
286
  }
286
- fs.unlinkSync(interventionPath); // Clear it
287
+ fs.unlinkSync(interventionPath);
287
288
  }
288
- } catch (e) {
289
- logger.warn('Failed to read intervention file');
290
- }
289
+ } catch {}
291
290
  }
292
291
 
293
- // Handle dynamic timeout update
294
292
  if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
295
293
  try {
296
294
  const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
297
295
  const newTimeoutMs = parseInt(newTimeoutStr);
298
-
299
296
  if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
300
297
  logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
301
-
302
- // Clear old timeout
303
298
  if (timeoutHandle) clearTimeout(timeoutHandle);
304
-
305
- // Set new timeout based on total elapsed time
306
299
  const elapsed = Date.now() - startTime;
307
300
  const remaining = Math.max(1000, newTimeoutMs - elapsed);
308
-
309
301
  timeoutHandle = setTimeout(() => {
310
302
  clearInterval(heartbeatInterval);
311
303
  child.kill();
312
- const totalSec = Math.round(newTimeoutMs / 1000);
313
- resolve({
314
- ok: false,
315
- exitCode: -1,
316
- error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
317
- });
304
+ resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
318
305
  }, remaining);
319
-
320
- fs.unlinkSync(timeoutPath); // Clear it
306
+ fs.unlinkSync(timeoutPath);
321
307
  }
322
- } catch (e) {
323
- logger.warn('Failed to read timeout update file');
324
- }
308
+ } catch {}
325
309
  }
326
310
  });
327
311
  }
328
312
 
329
313
  if (child.stdout) {
330
314
  child.stdout.on('data', (data) => {
331
- const str = data.toString();
332
- fullStdout += str;
315
+ fullStdout += data.toString();
333
316
  bytesReceived += data.length;
334
- // Also pipe to our own stdout so it goes to terminal.log
335
317
  process.stdout.write(data);
336
318
  });
337
319
  }
@@ -339,7 +321,6 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
339
321
  if (child.stderr) {
340
322
  child.stderr.on('data', (data) => {
341
323
  fullStderr += data.toString();
342
- // Pipe to our own stderr so it goes to terminal.log
343
324
  process.stderr.write(data);
344
325
  });
345
326
  }
@@ -347,11 +328,10 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
347
328
  timeoutHandle = setTimeout(() => {
348
329
  clearInterval(heartbeatInterval);
349
330
  child.kill();
350
- const timeoutSec = Math.round(timeoutMs / 1000);
351
331
  resolve({
352
332
  ok: false,
353
333
  exitCode: -1,
354
- error: `cursor-agent timed out after ${timeoutSec} seconds. The LLM request may be taking too long or there may be network issues.`,
334
+ error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
355
335
  });
356
336
  }, timeoutMs);
357
337
 
@@ -364,21 +344,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
364
344
 
365
345
  if (code !== 0 || !json || json.type !== 'result') {
366
346
  let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
367
-
368
- // Check for common errors
369
- if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
370
- errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
371
- } else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
372
- errorMsg = 'API rate limit or quota exceeded.';
373
- } else if (errorMsg.includes('model')) {
374
- errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
375
- }
376
-
377
- resolve({
378
- ok: false,
379
- exitCode: code ?? -1,
380
- error: errorMsg,
381
- });
347
+ resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
382
348
  } else {
383
349
  resolve({
384
350
  ok: !json.is_error,
@@ -392,15 +358,35 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
392
358
  child.on('error', (err) => {
393
359
  clearTimeout(timeoutHandle);
394
360
  clearInterval(heartbeatInterval);
395
- resolve({
396
- ok: false,
397
- exitCode: -1,
398
- error: `Failed to start cursor-agent: ${err.message}`,
399
- });
361
+ resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
400
362
  });
401
363
  });
402
364
  }
403
365
 
366
+ /**
367
+ * Execute cursor-agent command with retries for transient errors
368
+ */
369
+ export async function cursorAgentSend(options: {
370
+ workspaceDir: string;
371
+ chatId: string;
372
+ prompt: string;
373
+ model?: string;
374
+ signalDir?: string;
375
+ timeout?: number;
376
+ enableIntervention?: boolean;
377
+ outputFormat?: 'stream-json' | 'json' | 'plain';
378
+ taskName?: string;
379
+ }): Promise<AgentSendResult> {
380
+ const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';
381
+
382
+ return withRetry(
383
+ laneName,
384
+ () => cursorAgentSendRaw(options),
385
+ (res) => ({ ok: res.ok, error: res.error }),
386
+ { maxRetries: 3 }
387
+ );
388
+ }
389
+
404
390
  /**
405
391
  * Extract dependency change request from agent response
406
392
  */
@@ -431,33 +417,155 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
431
417
  }
432
418
 
433
419
  /**
434
- * Wrap prompt with dependency policy
420
+ * Inter-task state file name
421
+ */
422
+ const LANE_STATE_FILE = '_cursorflow/lane-state.json';
423
+
424
+ /**
425
+ * Dependency request file name - agent writes here when dependency changes are needed
426
+ */
427
+ const DEPENDENCY_REQUEST_FILE = '_cursorflow/dependency-request.json';
428
+
429
+ /**
430
+ * Read dependency request from file if it exists
431
+ */
432
+ export function readDependencyRequestFile(worktreeDir: string): { required: boolean; plan?: DependencyRequestPlan } {
433
+ const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
434
+
435
+ if (!fs.existsSync(filePath)) {
436
+ return { required: false };
437
+ }
438
+
439
+ try {
440
+ const content = fs.readFileSync(filePath, 'utf8');
441
+ const plan = JSON.parse(content) as DependencyRequestPlan;
442
+
443
+ // Validate required fields
444
+ if (plan.reason && Array.isArray(plan.commands) && plan.commands.length > 0) {
445
+ logger.info(`📦 Dependency request file detected: ${filePath}`);
446
+ return { required: true, plan };
447
+ }
448
+
449
+ logger.warn(`Invalid dependency request file format: ${filePath}`);
450
+ return { required: false };
451
+ } catch (e) {
452
+ logger.warn(`Failed to parse dependency request file: ${e}`);
453
+ return { required: false };
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Clear dependency request file after processing
435
459
  */
436
- export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy, options: { noGit?: boolean } = {}): string {
437
- const { noGit = false } = options;
460
+ export function clearDependencyRequestFile(worktreeDir: string): void {
461
+ const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
438
462
 
439
- if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
463
+ if (fs.existsSync(filePath)) {
464
+ try {
465
+ fs.unlinkSync(filePath);
466
+ logger.info(`🗑️ Cleared dependency request file: ${filePath}`);
467
+ } catch (e) {
468
+ logger.warn(`Failed to clear dependency request file: ${e}`);
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Wrap prompt with dependency policy instructions (legacy, used by tests)
475
+ */
476
+ export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
477
+ if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
440
478
  return prompt;
441
479
  }
442
480
 
443
- let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
481
+ let wrapped = `### 📦 Dependency Policy\n`;
482
+ wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
483
+ wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
484
+ wrapped += prompt;
444
485
 
445
- rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
446
- rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
486
+ return wrapped;
487
+ }
488
+
489
+ /**
490
+ * Wrap prompt with global context, dependency policy, and worktree instructions
491
+ */
492
+ export function wrapPrompt(
493
+ prompt: string,
494
+ config: RunnerConfig,
495
+ options: {
496
+ noGit?: boolean;
497
+ isWorktree?: boolean;
498
+ previousState?: string | null;
499
+ } = {}
500
+ ): string {
501
+ const { noGit = false, isWorktree = true, previousState = null } = options;
502
+
503
+ // 1. PREFIX: Environment & Worktree context
504
+ let wrapped = `### 🛠 Environment & Context\n`;
505
+ wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
506
+ wrapped += `- **Path Rule**: 모든 파일 참조 및 터미널 명령어는 **현재 디렉토리(./)**를 기준으로 하세요.\n`;
507
+
508
+ if (isWorktree) {
509
+ wrapped += `- **File Availability**: Git 추적 파일만 존재합니다. (node_modules, .env 등은 기본적으로 없음)\n`;
510
+ }
511
+
512
+ // 2. Previous Task State (if available)
513
+ if (previousState) {
514
+ wrapped += `\n### 💡 Previous Task State\n`;
515
+ wrapped += `이전 태스크에서 전달된 상태 정보입니다:\n`;
516
+ wrapped += `\`\`\`json\n${previousState}\n\`\`\`\n`;
517
+ }
518
+
519
+ // 3. Dependency Policy (Integrated)
520
+ const policy = config.dependencyPolicy;
521
+ wrapped += `\n### 📦 Dependency Policy\n`;
522
+ wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
523
+ wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
447
524
 
448
525
  if (noGit) {
449
- rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
526
+ wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
450
527
  }
528
+
529
+ wrapped += `\n**📦 Dependency Change Rules:**\n`;
530
+ wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
531
+ wrapped += `2. 의존성 변경이 필요하다면:\n`;
532
+ wrapped += ` - **다른 파일을 절대 수정하지 마세요.**\n`;
533
+ wrapped += ` - 아래 JSON을 \`./${DEPENDENCY_REQUEST_FILE}\` 파일에 저장하세요:\n`;
534
+ wrapped += ` \`\`\`json\n`;
535
+ wrapped += ` {\n`;
536
+ wrapped += ` "reason": "왜 이 의존성이 필요한지 설명",\n`;
537
+ wrapped += ` "changes": ["add lodash@^4.17.21", "remove unused-pkg"],\n`;
538
+ wrapped += ` "commands": ["pnpm add lodash@^4.17.21", "pnpm remove unused-pkg"],\n`;
539
+ wrapped += ` "notes": "추가 참고사항 (선택)" \n`;
540
+ wrapped += ` }\n`;
541
+ wrapped += ` \`\`\`\n`;
542
+ wrapped += ` - 파일 저장 후 **즉시 작업을 종료**하세요. 오케스트레이터가 처리합니다.\n`;
543
+ wrapped += `3. 의존성 변경이 불필요하면 바로 본 작업을 진행하세요.\n`;
544
+
545
+ wrapped += `\n---\n\n${prompt}\n\n---\n`;
546
+
547
+ // 4. SUFFIX: Task Completion & Git Requirements
548
+ wrapped += `\n### 📝 Task Completion Requirements\n`;
549
+ wrapped += `**반드시 다음 순서로 작업을 마무리하세요:**\n\n`;
451
550
 
452
- rules += '\nRules:\n';
453
- rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
454
- rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
455
- rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
456
- rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
457
- rules += 'Then STOP.\n';
458
- rules += '- If dependency changes are NOT required, proceed normally.\n';
551
+ if (!noGit) {
552
+ wrapped += `1. **Git Commit & Push** (필수!):\n`;
553
+ wrapped += ` \`\`\`bash\n`;
554
+ wrapped += ` git add -A\n`;
555
+ wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
556
+ wrapped += ` git push origin HEAD\n`;
557
+ wrapped += ` \`\`\`\n`;
558
+ wrapped += ` ⚠️ 커밋과 푸시 없이 작업을 종료하면 변경사항이 손실됩니다!\n\n`;
559
+ }
459
560
 
460
- return `${rules}\n---\n\n${prompt}`;
561
+ wrapped += `2. **State Passing**: 다음 태스크로 전달할 정보가 있다면 \`./${LANE_STATE_FILE}\`에 JSON으로 저장하세요.\n\n`;
562
+ wrapped += `3. **Summary**: 작업 완료 후 다음을 요약해 주세요:\n`;
563
+ wrapped += ` - 생성/수정된 파일 목록\n`;
564
+ wrapped += ` - 주요 변경 사항\n`;
565
+ wrapped += ` - 커밋 해시 (git log --oneline -1)\n\n`;
566
+ wrapped += `4. 지시된 문서(docs/...)를 찾을 수 없다면 즉시 보고하세요.\n`;
567
+
568
+ return wrapped;
461
569
  }
462
570
 
463
571
  /**
@@ -490,49 +598,38 @@ export function applyDependencyFilePermissions(worktreeDir: string, policy: Depe
490
598
 
491
599
  /**
492
600
  * Wait for task-level dependencies to be completed by other lanes
601
+ * Now uses the enhanced dependency module with timeout support
493
602
  */
494
- export async function waitForTaskDependencies(deps: string[], runDir: string): Promise<void> {
603
+ export async function waitForTaskDependencies(
604
+ deps: string[],
605
+ runDir: string,
606
+ options: DependencyWaitOptions = {}
607
+ ): Promise<void> {
495
608
  if (!deps || deps.length === 0) return;
496
609
 
497
610
  const lanesRoot = path.dirname(runDir);
498
- const pendingDeps = new Set(deps);
499
-
500
- logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
501
-
502
- while (pendingDeps.size > 0) {
503
- for (const dep of pendingDeps) {
504
- const [laneName, taskName] = dep.split(':');
505
- if (!laneName || !taskName) {
506
- logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
507
- pendingDeps.delete(dep);
508
- continue;
509
- }
510
-
511
- const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
512
- if (fs.existsSync(depStatePath)) {
513
- try {
514
- const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
515
- if (state.completedTasks && state.completedTasks.includes(taskName)) {
516
- logger.info(`✓ Dependency met: ${dep}`);
517
- pendingDeps.delete(dep);
518
- } else if (state.status === 'failed') {
519
- throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
520
- }
521
- } catch (e: any) {
522
- if (e.message.includes('Dependency failed')) throw e;
523
- // Ignore parse errors, file might be being written
524
- }
611
+
612
+ const result = await waitForDeps(deps, lanesRoot, {
613
+ timeoutMs: options.timeoutMs || 30 * 60 * 1000, // 30 minutes default
614
+ pollIntervalMs: options.pollIntervalMs || 5000,
615
+ onTimeout: options.onTimeout || 'fail',
616
+ onProgress: (pending, completed) => {
617
+ if (completed.length > 0) {
618
+ logger.info(`Dependencies progress: ${completed.length}/${deps.length} completed`);
525
619
  }
620
+ },
621
+ });
622
+
623
+ if (!result.success) {
624
+ if (result.timedOut) {
625
+ throw new Error(`Dependency wait timed out after ${Math.round(result.elapsedMs / 1000)}s. Pending: ${result.failedDependencies.join(', ')}`);
526
626
  }
527
-
528
- if (pendingDeps.size > 0) {
529
- await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
530
- }
627
+ throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
531
628
  }
532
629
  }
533
630
 
534
631
  /**
535
- * Merge branches from dependency lanes
632
+ * Merge branches from dependency lanes with safe merge
536
633
  */
537
634
  export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string): Promise<void> {
538
635
  if (!deps || deps.length === 0) return;
@@ -545,21 +642,34 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
545
642
  if (!fs.existsSync(depStatePath)) continue;
546
643
 
547
644
  try {
548
- const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
549
- if (state.pipelineBranch) {
550
- logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
551
-
552
- // Ensure we have the latest
553
- git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
554
-
555
- git.merge(state.pipelineBranch, {
556
- cwd: worktreeDir,
557
- noFf: true,
558
- message: `chore: merge task dependency from ${laneName}`
559
- });
645
+ const state = loadState<LaneState>(depStatePath);
646
+ if (!state?.pipelineBranch) continue;
647
+
648
+ logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
649
+
650
+ // Ensure we have the latest
651
+ git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
652
+
653
+ // Use safe merge with conflict detection
654
+ const mergeResult = git.safeMerge(state.pipelineBranch, {
655
+ cwd: worktreeDir,
656
+ noFf: true,
657
+ message: `chore: merge task dependency from ${laneName}`,
658
+ abortOnConflict: true,
659
+ });
660
+
661
+ if (!mergeResult.success) {
662
+ if (mergeResult.conflict) {
663
+ logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
664
+ throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
665
+ }
666
+ throw new Error(mergeResult.error || 'Merge failed');
560
667
  }
668
+
669
+ logger.success(`✓ Merged ${laneName}`);
561
670
  } catch (e) {
562
671
  logger.error(`Failed to merge branch from ${laneName}: ${e}`);
672
+ throw e;
563
673
  }
564
674
  }
565
675
  }
@@ -614,10 +724,27 @@ export async function runTask({
614
724
  // Apply dependency permissions
615
725
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
616
726
 
617
- // Run prompt
618
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
727
+ // Read previous task state if available
728
+ let previousState: string | null = null;
729
+ const stateFilePath = safeJoin(worktreeDir, LANE_STATE_FILE);
730
+ if (fs.existsSync(stateFilePath)) {
731
+ try {
732
+ previousState = fs.readFileSync(stateFilePath, 'utf8');
733
+ logger.info('Loaded previous task state from _cursorflow/lane-state.json');
734
+ } catch (e) {
735
+ logger.warn(`Failed to read inter-task state: ${e}`);
736
+ }
737
+ }
738
+
739
+ // Wrap prompt with context, previous state, and completion instructions
740
+ const wrappedPrompt = wrapPrompt(task.prompt, config, {
741
+ noGit,
742
+ isWorktree: !noGit,
743
+ previousState
744
+ });
619
745
 
620
- appendLog(convoPath, createConversationEntry('user', prompt1, {
746
+ // Log ONLY the original prompt to keep logs clean
747
+ appendLog(convoPath, createConversationEntry('user', task.prompt, {
621
748
  task: task.name,
622
749
  model,
623
750
  }));
@@ -627,18 +754,19 @@ export async function runTask({
627
754
  events.emit('agent.prompt_sent', {
628
755
  taskName: task.name,
629
756
  model,
630
- promptLength: prompt1.length,
757
+ promptLength: wrappedPrompt.length,
631
758
  });
632
759
 
633
760
  const r1 = await cursorAgentSend({
634
761
  workspaceDir: worktreeDir,
635
762
  chatId,
636
- prompt: prompt1,
763
+ prompt: wrappedPrompt,
637
764
  model,
638
765
  signalDir: runDir,
639
766
  timeout,
640
767
  enableIntervention: config.enableIntervention,
641
768
  outputFormat: config.agentOutputFormat,
769
+ taskName: task.name,
642
770
  });
643
771
 
644
772
  const duration = Date.now() - startTime;
@@ -669,15 +797,31 @@ export async function runTask({
669
797
  };
670
798
  }
671
799
 
672
- // Check for dependency request
673
- const depReq = extractDependencyRequest(r1.resultText || '');
674
- if (depReq.required && !config.dependencyPolicy.allowDependencyChange) {
675
- return {
676
- taskName: task.name,
677
- taskBranch,
678
- status: 'BLOCKED_DEPENDENCY',
679
- dependencyRequest: depReq.plan || null,
680
- };
800
+ // Check for dependency request (file-based takes priority, then text-based)
801
+ const fileDepReq = readDependencyRequestFile(worktreeDir);
802
+ const textDepReq = extractDependencyRequest(r1.resultText || '');
803
+
804
+ // Determine which request to use (file-based is preferred as it's more structured)
805
+ const depReq = fileDepReq.required ? fileDepReq : textDepReq;
806
+
807
+ if (depReq.required) {
808
+ logger.info(`📦 Dependency change requested: ${depReq.plan?.reason || 'No reason provided'}`);
809
+
810
+ if (depReq.plan) {
811
+ logger.info(` Commands: ${depReq.plan.commands.join(', ')}`);
812
+ }
813
+
814
+ if (!config.dependencyPolicy.allowDependencyChange) {
815
+ // Clear the file so it doesn't persist after resolution
816
+ clearDependencyRequestFile(worktreeDir);
817
+
818
+ return {
819
+ taskName: task.name,
820
+ taskBranch,
821
+ status: 'BLOCKED_DEPENDENCY',
822
+ dependencyRequest: depReq.plan || null,
823
+ };
824
+ }
681
825
  }
682
826
 
683
827
  // Push task branch (skip in noGit mode)
@@ -732,7 +876,7 @@ export async function runTask({
732
876
  /**
733
877
  * Run all tasks in sequence
734
878
  */
735
- export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
879
+ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean; skipPreflight?: boolean } = {}): Promise<TaskExecutionResult[]> {
736
880
  const startIndex = options.startIndex || 0;
737
881
  const noGit = options.noGit || config.noGit || false;
738
882
 
@@ -751,6 +895,33 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
751
895
  throw validationError;
752
896
  }
753
897
 
898
+ // Run preflight checks (can be skipped for resume)
899
+ if (!options.skipPreflight && startIndex === 0) {
900
+ logger.info('Running preflight checks...');
901
+ const preflight = await preflightCheck({
902
+ requireRemote: !noGit,
903
+ requireAuth: true,
904
+ });
905
+
906
+ if (!preflight.canProceed) {
907
+ printPreflightReport(preflight);
908
+ throw new Error('Preflight check failed. Please fix the blockers above.');
909
+ }
910
+
911
+ if (preflight.warnings.length > 0) {
912
+ for (const warning of preflight.warnings) {
913
+ logger.warn(`⚠️ ${warning}`);
914
+ }
915
+ }
916
+
917
+ logger.success('✓ Preflight checks passed');
918
+ }
919
+
920
+ // Warn if baseBranch is set in config (it will be ignored)
921
+ if (config.baseBranch) {
922
+ logger.warn(`⚠️ config.baseBranch="${config.baseBranch}" will be ignored. Using current branch instead.`);
923
+ }
924
+
754
925
  // Ensure cursor-agent is installed
755
926
  ensureCursorAgent();
756
927
 
@@ -779,17 +950,47 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
779
950
  logger.success('✓ Cursor authentication OK');
780
951
 
781
952
  // In noGit mode, we don't need repoRoot - use current directory
782
- const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
953
+ const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
954
+
955
+ // ALWAYS use current branch as base - ignore config.baseBranch
956
+ // This ensures dependency structure is maintained in the worktree
957
+ const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
958
+ logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
783
959
 
784
960
  // Load existing state if resuming
785
961
  const statePath = safeJoin(runDir, 'state.json');
786
962
  let state: LaneState | null = null;
787
963
 
788
964
  if (fs.existsSync(statePath)) {
789
- try {
790
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
791
- } catch (e) {
792
- logger.warn(`Failed to load existing state from ${statePath}: ${e}`);
965
+ // Check if state needs recovery
966
+ if (stateNeedsRecovery(statePath)) {
967
+ logger.warn('State file indicates incomplete previous run. Attempting recovery...');
968
+ const repairedState = repairLaneState(statePath);
969
+ if (repairedState) {
970
+ state = repairedState;
971
+ logger.success('✓ State recovered');
972
+ } else {
973
+ logger.warn('Could not recover state. Starting fresh.');
974
+ }
975
+ } else {
976
+ state = loadState<LaneState>(statePath);
977
+
978
+ // Validate loaded state
979
+ if (state) {
980
+ const validation = validateLaneState(statePath, {
981
+ checkWorktree: !noGit,
982
+ checkBranch: !noGit,
983
+ autoRepair: true,
984
+ });
985
+
986
+ if (!validation.valid) {
987
+ logger.warn(`State validation issues: ${validation.issues.join(', ')}`);
988
+ if (validation.repaired) {
989
+ logger.info('State was auto-repaired');
990
+ state = validation.repairedState || state;
991
+ }
992
+ }
993
+ }
793
994
  }
794
995
  }
795
996
 
@@ -831,8 +1032,9 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
831
1032
  fs.mkdirSync(worktreeParent, { recursive: true });
832
1033
  }
833
1034
 
1035
+ // Always use the current branch (already captured at start) as the base branch
834
1036
  git.createWorktree(worktreeDir, pipelineBranch, {
835
- baseBranch: config.baseBranch || 'main',
1037
+ baseBranch: currentBranch,
836
1038
  cwd: repoRoot,
837
1039
  });
838
1040
  break; // Success
@@ -993,11 +1195,22 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
993
1195
 
994
1196
  // Run tasks
995
1197
  const results: TaskExecutionResult[] = [];
1198
+ const laneName = state.label || path.basename(runDir);
996
1199
 
997
1200
  for (let i = startIndex; i < config.tasks.length; i++) {
998
1201
  const task = config.tasks[i]!;
999
1202
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
1000
1203
 
1204
+ // Create checkpoint before each task
1205
+ try {
1206
+ await createCheckpoint(laneName, runDir, noGit ? null : worktreeDir, {
1207
+ description: `Before task ${i + 1}: ${task.name}`,
1208
+ maxCheckpoints: 5,
1209
+ });
1210
+ } catch (e: any) {
1211
+ logger.warn(`Failed to create checkpoint: ${e.message}`);
1212
+ }
1213
+
1001
1214
  // Handle task-level dependencies
1002
1215
  if (task.dependsOn && task.dependsOn.length > 0) {
1003
1216
  state.status = 'waiting';
@@ -1005,7 +1218,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
1005
1218
  saveState(statePath, state);
1006
1219
 
1007
1220
  try {
1008
- await waitForTaskDependencies(task.dependsOn, runDir);
1221
+ // Use enhanced dependency wait with timeout
1222
+ await waitForTaskDependencies(task.dependsOn, runDir, {
1223
+ timeoutMs: config.timeout || 30 * 60 * 1000,
1224
+ onTimeout: 'fail',
1225
+ });
1009
1226
 
1010
1227
  if (!noGit) {
1011
1228
  await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
@@ -1020,6 +1237,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
1020
1237
  state.error = e.message;
1021
1238
  saveState(statePath, state);
1022
1239
  logger.error(`Task dependency wait/merge failed: ${e.message}`);
1240
+
1241
+ // Try to restore from checkpoint
1242
+ const latestCheckpoint = getLatestCheckpoint(runDir);
1243
+ if (latestCheckpoint) {
1244
+ logger.info(`💾 Checkpoint available: ${latestCheckpoint.id}`);
1245
+ logger.info(` Resume with: cursorflow resume --checkpoint ${latestCheckpoint.id}`);
1246
+ }
1247
+
1023
1248
  process.exit(1);
1024
1249
  }
1025
1250
  }
@@ -1119,7 +1344,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
1119
1344
  logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
1120
1345
  } else {
1121
1346
  try {
1122
- const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
1347
+ // Always use current branch for comparison (already captured at start)
1348
+ const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
1123
1349
  if (stats) {
1124
1350
  logger.info('Final Workspace Summary (Git):\n' + stats);
1125
1351
  }