@litmers/cursorflow-orchestrator 0.1.20 → 0.1.28

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 +20 -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 +87 -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 +125 -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 +397 -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 +97 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +154 -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 +1136 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +1443 -1217
  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 +871 -499
  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
@@ -44,6 +44,7 @@ const state_1 = require("../utils/state");
44
44
  const doctor_1 = require("../utils/doctor");
45
45
  const path_1 = require("../utils/path");
46
46
  const enhanced_logger_1 = require("../utils/enhanced-logger");
47
+ const log_formatter_1 = require("../utils/log-formatter");
47
48
  function printHelp() {
48
49
  console.log(`
49
50
  Usage: cursorflow resume [lane] [options]
@@ -51,7 +52,7 @@ Usage: cursorflow resume [lane] [options]
51
52
  Resume interrupted or failed lanes.
52
53
 
53
54
  Options:
54
- <lane> Lane name to resume (single lane mode)
55
+ <lane> Lane name or tasks directory to resume
55
56
  --all Resume ALL incomplete/failed lanes
56
57
  --status Show status of all lanes in the run (no resume)
57
58
  --run-dir <path> Use a specific run directory (default: latest)
@@ -67,8 +68,8 @@ Examples:
67
68
  cursorflow resume --status # Check status of all lanes
68
69
  cursorflow resume --all # Resume all incomplete lanes
69
70
  cursorflow resume lane-1 # Resume single lane
71
+ cursorflow resume _cursorflow/tasks/feat1 # Resume all lanes in directory
70
72
  cursorflow resume --all --restart # Restart all incomplete lanes from task 0
71
- cursorflow resume --all --max-concurrent 2 # Resume with max 2 parallel lanes
72
73
  `);
73
74
  }
74
75
  function parseArgs(args) {
@@ -117,79 +118,138 @@ const STATUS_COLORS = {
117
118
  };
118
119
  const RESET = '\x1b[0m';
119
120
  /**
120
- * Format and print parsed message to console (copied from orchestrator.ts)
121
+ * Check if a process is alive by its PID
121
122
  */
122
- function handleParsedMessage(laneName, msg) {
123
- const ts = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false });
124
- const laneLabel = `[${laneName}]`.padEnd(12);
125
- let prefix = '';
126
- let content = msg.content;
127
- switch (msg.type) {
128
- case 'user':
129
- prefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
130
- content = content.replace(/\n/g, ' ');
131
- break;
132
- case 'assistant':
133
- prefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
134
- break;
135
- case 'tool':
136
- prefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
137
- const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
138
- if (toolMatch) {
139
- const [, name, args] = toolMatch;
140
- try {
141
- const parsedArgs = JSON.parse(args);
142
- let argStr = '';
143
- if (name === 'read_file' && parsedArgs.target_file)
144
- argStr = parsedArgs.target_file;
145
- else if (name === 'run_terminal_cmd' && parsedArgs.command)
146
- argStr = parsedArgs.command;
147
- else if (name === 'write' && parsedArgs.file_path)
148
- argStr = parsedArgs.file_path;
149
- else if (name === 'search_replace' && parsedArgs.file_path)
150
- argStr = parsedArgs.file_path;
151
- else {
152
- const keys = Object.keys(parsedArgs);
153
- if (keys.length > 0)
154
- argStr = String(parsedArgs[keys[0]]).substring(0, 50);
155
- }
156
- content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
157
- }
158
- catch {
159
- content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
160
- }
161
- }
162
- break;
163
- case 'tool_result':
164
- prefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
165
- const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
166
- content = resMatch ? `${resMatch[1]} OK` : 'result';
167
- break;
168
- case 'result':
169
- prefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
170
- break;
171
- case 'system':
172
- prefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
173
- break;
174
- case 'thinking':
175
- prefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
176
- break;
177
- }
178
- if (prefix) {
179
- const lines = content.split('\n');
180
- const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
181
- if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result' || msg.type === 'thinking') {
182
- const header = `${prefix} ┌${'─'.repeat(60)}`;
183
- process.stdout.write(`${tsPrefix} ${header}\n`);
184
- for (const line of lines) {
185
- process.stdout.write(`${tsPrefix} ${' '.repeat((0, enhanced_logger_1.stripAnsi)(prefix).length)} │ ${line}\n`);
123
+ function isProcessAlive(pid) {
124
+ try {
125
+ // On Unix-like systems, sending signal 0 checks if process exists
126
+ process.kill(pid, 0);
127
+ return true;
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ /**
134
+ * Check for zombie "running" lanes and fix them
135
+ * A zombie lane is one that has status "running" but its process is dead
136
+ */
137
+ function checkAndFixZombieLanes(runDir) {
138
+ const lanesDir = (0, path_1.safeJoin)(runDir, 'lanes');
139
+ if (!fs.existsSync(lanesDir)) {
140
+ return { fixed: [], pofCreated: false };
141
+ }
142
+ const fixed = [];
143
+ const zombieDetails = [];
144
+ const laneDirs = fs.readdirSync(lanesDir)
145
+ .filter(f => fs.statSync((0, path_1.safeJoin)(lanesDir, f)).isDirectory());
146
+ for (const laneName of laneDirs) {
147
+ const dir = (0, path_1.safeJoin)(lanesDir, laneName);
148
+ const statePath = (0, path_1.safeJoin)(dir, 'state.json');
149
+ if (!fs.existsSync(statePath))
150
+ continue;
151
+ const state = (0, state_1.loadState)(statePath);
152
+ if (!state)
153
+ continue;
154
+ // Check for zombie: status is "running" but process is dead
155
+ if (state.status === 'running' && state.pid) {
156
+ const alive = isProcessAlive(state.pid);
157
+ if (!alive) {
158
+ logger.warn(`🧟 Zombie lane detected: ${laneName} (PID ${state.pid} is dead)`);
159
+ // Update state to failed
160
+ const updatedState = {
161
+ ...state,
162
+ status: 'failed',
163
+ error: `Process terminated unexpectedly (PID ${state.pid} was running but is now dead)`,
164
+ endTime: Date.now(),
165
+ };
166
+ (0, state_1.saveState)(statePath, updatedState);
167
+ fixed.push(laneName);
168
+ zombieDetails.push({
169
+ name: laneName,
170
+ pid: state.pid,
171
+ taskIndex: state.currentTaskIndex,
172
+ totalTasks: state.totalTasks,
173
+ });
174
+ logger.info(` → Status changed to 'failed', ready for resume`);
186
175
  }
187
- process.stdout.write(`${tsPrefix} ${' '.repeat((0, enhanced_logger_1.stripAnsi)(prefix).length)} └${'─'.repeat(60)}\n`);
188
176
  }
189
- else {
190
- process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
177
+ }
178
+ // Create POF file if any zombies were found
179
+ let pofCreated = false;
180
+ if (zombieDetails.length > 0) {
181
+ const config = (0, config_1.loadConfig)();
182
+ const pofDir = (0, config_1.getPofDir)(config);
183
+ if (!fs.existsSync(pofDir)) {
184
+ fs.mkdirSync(pofDir, { recursive: true });
185
+ }
186
+ const runId = path.basename(runDir);
187
+ const pofPath = (0, path_1.safeJoin)(pofDir, `pof-${runId}.json`);
188
+ let existingPof = null;
189
+ try {
190
+ existingPof = JSON.parse(fs.readFileSync(pofPath, 'utf-8'));
191
+ }
192
+ catch {
193
+ // Ignore errors (file might not exist)
194
+ }
195
+ const pof = {
196
+ title: 'Run Failure Post-mortem',
197
+ runId: path.basename(runDir),
198
+ failureTime: new Date().toISOString(),
199
+ detectedAt: new Date().toISOString(),
200
+ summary: `${zombieDetails.length} lane(s) found with dead processes (zombie state)`,
201
+ rootCause: {
202
+ type: 'ZOMBIE_PROCESS',
203
+ description: 'Lane processes were marked as running but the processes are no longer alive',
204
+ symptoms: [
205
+ 'Process PIDs no longer exist in the system',
206
+ 'Lanes were stuck in "running" state',
207
+ 'No completion or error was recorded before process death',
208
+ ],
209
+ },
210
+ affectedLanes: zombieDetails.map(z => ({
211
+ name: z.name,
212
+ status: 'failed (was: running)',
213
+ task: `[${z.taskIndex + 1}/${z.totalTasks}]`,
214
+ taskIndex: z.taskIndex,
215
+ pid: z.pid,
216
+ reason: 'Process terminated unexpectedly',
217
+ })),
218
+ possibleCauses: [
219
+ 'System killed process due to memory pressure (OOM)',
220
+ 'User killed process manually (Ctrl+C, kill command)',
221
+ 'Agent timeout exceeded and process was terminated',
222
+ 'System restart or crash',
223
+ 'Agent hung and watchdog terminated it',
224
+ ],
225
+ recovery: {
226
+ command: `cursorflow resume --all --run-dir ${runDir}`,
227
+ description: 'Resume all failed lanes from their last checkpoint',
228
+ alternativeCommand: `cursorflow resume --all --restart --run-dir ${runDir}`,
229
+ alternativeDescription: 'Restart all failed lanes from the beginning',
230
+ },
231
+ // Merge with existing POF if present
232
+ previousFailures: existingPof ? [existingPof] : undefined,
233
+ };
234
+ // Use atomic write: write to temp file then rename
235
+ const tempPath = `${pofPath}.${Math.random().toString(36).substring(2, 7)}.tmp`;
236
+ try {
237
+ fs.writeFileSync(tempPath, JSON.stringify(pof, null, 2), 'utf8');
238
+ fs.renameSync(tempPath, pofPath);
239
+ pofCreated = true;
240
+ logger.info(`📋 POF file created: ${pofPath}`);
241
+ }
242
+ catch (err) {
243
+ // If temp file was created, try to clean it up
244
+ try {
245
+ if (fs.existsSync(tempPath))
246
+ fs.unlinkSync(tempPath);
247
+ }
248
+ catch { /* ignore */ }
249
+ throw err;
191
250
  }
192
251
  }
252
+ return { fixed, pofCreated };
193
253
  }
194
254
  /**
195
255
  * Get all lane statuses from a run directory
@@ -334,50 +394,25 @@ function spawnLaneResume(laneName, laneDir, state, options) {
334
394
  if (options.executor) {
335
395
  runnerArgs.push('--executor', options.executor);
336
396
  }
337
- const logManager = (0, enhanced_logger_1.createLogManager)(laneDir, laneName, options.enhancedLogConfig || {}, (msg) => handleParsedMessage(laneName, msg));
397
+ const logManager = (0, enhanced_logger_1.createLogManager)(laneDir, laneName, options.enhancedLogConfig || {}, (msg) => {
398
+ const formatted = (0, log_formatter_1.formatMessageForConsole)(msg, {
399
+ laneLabel: `[${laneName}]`,
400
+ includeTimestamp: true
401
+ });
402
+ process.stdout.write(formatted + '\n');
403
+ });
338
404
  const child = (0, child_process_1.spawn)('node', runnerArgs, {
339
405
  stdio: ['ignore', 'pipe', 'pipe'],
340
406
  env: process.env,
341
407
  });
342
- let lineBuffer = '';
343
408
  if (child.stdout) {
344
409
  child.stdout.on('data', (data) => {
345
410
  logManager.writeStdout(data);
346
- const str = data.toString();
347
- lineBuffer += str;
348
- const lines = lineBuffer.split('\n');
349
- lineBuffer = lines.pop() || '';
350
- for (const line of lines) {
351
- const trimmed = line.trim();
352
- if (trimmed &&
353
- !trimmed.startsWith('{') &&
354
- !trimmed.startsWith('[') &&
355
- !trimmed.includes('{"type"')) {
356
- process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${line}\n`);
357
- }
358
- }
359
411
  });
360
412
  }
361
413
  if (child.stderr) {
362
414
  child.stderr.on('data', (data) => {
363
415
  logManager.writeStderr(data);
364
- const str = data.toString();
365
- const lines = str.split('\n');
366
- for (const line of lines) {
367
- const trimmed = line.trim();
368
- if (trimmed) {
369
- const isStatus = trimmed.startsWith('Preparing worktree') ||
370
- trimmed.startsWith('Switched to a new branch') ||
371
- trimmed.startsWith('HEAD is now at') ||
372
- trimmed.includes('actual output');
373
- if (isStatus) {
374
- process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${trimmed}\n`);
375
- }
376
- else {
377
- process.stderr.write(`${logger.COLORS.red}[${laneName}] ERROR: ${trimmed}${logger.COLORS.reset}\n`);
378
- }
379
- }
380
- }
381
416
  });
382
417
  }
383
418
  child.on('exit', () => {
@@ -401,19 +436,7 @@ function waitForChild(child) {
401
436
  /**
402
437
  * Resume multiple lanes with concurrency control and dependency awareness
403
438
  */
404
- async function resumeAllLanes(runDir, options) {
405
- const allLanes = getAllLaneStatuses(runDir);
406
- const lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile);
407
- const missingTaskInfo = allLanes.filter(l => l.needsResume && !l.state?.tasksFile);
408
- if (missingTaskInfo.length > 0) {
409
- logger.warn(`Lanes that haven't started yet and have no task info: ${missingTaskInfo.map(l => l.name).join(', ')}`);
410
- logger.warn('These lanes cannot be resumed because their original task file paths were not recorded.');
411
- }
412
- if (lanesToResume.length === 0) {
413
- logger.success('All lanes are already completed! Nothing to resume.');
414
- return { succeeded: [], failed: [], skipped: [] };
415
- }
416
- // Check for lanes with unmet dependencies that can never be satisfied
439
+ async function resumeLanes(lanesToResume, allLanes, options) {
417
440
  const completedSet = new Set(allLanes.filter(l => l.isCompleted).map(l => l.name));
418
441
  const toResumeNames = new Set(lanesToResume.map(l => l.name));
419
442
  const skippedLanes = [];
@@ -436,16 +459,9 @@ async function resumeAllLanes(runDir, options) {
436
459
  logger.section(`🔁 Resuming ${resolvableLanes.length} Lane(s)`);
437
460
  logger.info(`Max concurrent: ${options.maxConcurrent}`);
438
461
  logger.info(`Mode: ${options.restart ? 'Restart from beginning' : 'Continue from last task'}`);
439
- // Show dependency order
440
- const lanesWithDeps = resolvableLanes.filter(l => l.dependsOn.length > 0);
441
- if (lanesWithDeps.length > 0) {
442
- logger.info(`Dependency-aware: ${lanesWithDeps.length} lane(s) have dependencies`);
443
- }
444
- console.log('');
445
462
  // Run doctor check once if needed (check git status)
446
463
  if (!options.skipDoctor) {
447
464
  logger.info('Running pre-flight checks...');
448
- // Use the first lane's tasksDir for doctor check
449
465
  const firstLane = resolvableLanes[0];
450
466
  const tasksDir = path.dirname(firstLane.state.tasksFile);
451
467
  const report = (0, doctor_1.runDoctor)({
@@ -466,15 +482,10 @@ async function resumeAllLanes(runDir, options) {
466
482
  }
467
483
  const succeeded = [];
468
484
  const failed = [];
469
- // Create a mutable set for tracking completed lanes (including those from this session)
470
485
  const sessionCompleted = new Set(completedSet);
471
- // Queue management with dependency awareness
472
486
  const pending = new Set(resolvableLanes.map(l => l.name));
473
487
  const active = new Map();
474
488
  const laneMap = new Map(resolvableLanes.map(l => [l.name, l]));
475
- /**
476
- * Find the next lane that can be started (all dependencies met)
477
- */
478
489
  const findReadyLane = () => {
479
490
  for (const laneName of pending) {
480
491
  const lane = laneMap.get(laneName);
@@ -484,21 +495,15 @@ async function resumeAllLanes(runDir, options) {
484
495
  }
485
496
  return null;
486
497
  };
487
- /**
488
- * Process lanes with dependency awareness
489
- */
490
498
  const processNext = () => {
491
499
  while (active.size < options.maxConcurrent) {
492
500
  const lane = findReadyLane();
493
501
  if (!lane) {
494
- // No lane ready to start
495
502
  if (pending.size > 0 && active.size === 0) {
496
- // Deadlock: pending lanes exist but none can start and none are running
497
503
  const pendingList = Array.from(pending).join(', ');
498
504
  logger.error(`Deadlock detected! Lanes waiting: ${pendingList}`);
499
- for (const ln of pending) {
505
+ for (const ln of pending)
500
506
  failed.push(ln);
501
- }
502
507
  pending.clear();
503
508
  }
504
509
  break;
@@ -513,13 +518,12 @@ async function resumeAllLanes(runDir, options) {
513
518
  enhancedLogConfig: options.enhancedLogConfig,
514
519
  });
515
520
  active.set(lane.name, child);
516
- // Handle completion
517
521
  waitForChild(child).then(code => {
518
522
  active.delete(lane.name);
519
523
  if (code === 0) {
520
524
  logger.success(`✓ ${lane.name} completed`);
521
525
  succeeded.push(lane.name);
522
- sessionCompleted.add(lane.name); // Mark as completed for dependency resolution
526
+ sessionCompleted.add(lane.name);
523
527
  }
524
528
  else if (code === 2) {
525
529
  logger.warn(`⚠ ${lane.name} blocked on dependency change`);
@@ -529,7 +533,6 @@ async function resumeAllLanes(runDir, options) {
529
533
  logger.error(`✗ ${lane.name} failed (exit ${code})`);
530
534
  failed.push(lane.name);
531
535
  }
532
- // Try to start more lanes now that one completed
533
536
  processNext();
534
537
  }).catch(err => {
535
538
  active.delete(lane.name);
@@ -539,26 +542,20 @@ async function resumeAllLanes(runDir, options) {
539
542
  });
540
543
  }
541
544
  };
542
- // Start initial batch
543
545
  processNext();
544
- // Wait for all to complete
545
546
  while (active.size > 0 || pending.size > 0) {
546
547
  await new Promise(resolve => setTimeout(resolve, 1000));
547
- // Check if we can start more (in case completion handlers haven't triggered processNext yet)
548
548
  if (active.size < options.maxConcurrent && pending.size > 0) {
549
549
  processNext();
550
550
  }
551
551
  }
552
- // Summary
553
552
  console.log('');
554
553
  logger.section('📊 Resume Summary');
555
554
  logger.info(`Succeeded: ${succeeded.length}`);
556
- if (failed.length > 0) {
555
+ if (failed.length > 0)
557
556
  logger.error(`Failed: ${failed.length} (${failed.join(', ')})`);
558
- }
559
- if (skippedLanes.length > 0) {
557
+ if (skippedLanes.length > 0)
560
558
  logger.warn(`Skipped: ${skippedLanes.length} (${skippedLanes.join(', ')})`);
561
- }
562
559
  return { succeeded, failed, skipped: skippedLanes };
563
560
  }
564
561
  async function resume(args) {
@@ -569,7 +566,6 @@ async function resume(args) {
569
566
  }
570
567
  const config = (0, config_1.loadConfig)();
571
568
  const logsDir = (0, config_1.getLogsDir)(config);
572
- // Find run directory
573
569
  let runDir = options.runDir;
574
570
  if (!runDir) {
575
571
  runDir = findLatestRunDir(logsDir);
@@ -577,110 +573,66 @@ async function resume(args) {
577
573
  if (!runDir || !fs.existsSync(runDir)) {
578
574
  throw new Error(`Run directory not found: ${runDir || 'latest'}. Have you run any tasks yet?`);
579
575
  }
580
- // Status mode: just show status and exit
581
- if (options.status) {
582
- printAllLaneStatus(runDir);
583
- return;
584
- }
585
- // All mode: resume all incomplete lanes
586
- if (options.all) {
587
- const result = await resumeAllLanes(runDir, {
588
- restart: options.restart,
589
- maxConcurrent: options.maxConcurrent,
590
- skipDoctor: options.skipDoctor,
591
- noGit: options.noGit,
592
- executor: options.executor,
593
- enhancedLogConfig: config.enhancedLogging,
594
- });
595
- if (result.failed.length > 0) {
596
- throw new Error(`${result.failed.length} lane(s) failed to complete`);
576
+ const allLanes = getAllLaneStatuses(runDir);
577
+ let lanesToResume = [];
578
+ // Check if the lane argument is actually a tasks directory
579
+ if (options.lane && fs.existsSync(options.lane) && fs.statSync(options.lane).isDirectory()) {
580
+ const tasksDir = path.resolve(options.lane);
581
+ lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile && path.resolve(l.state.tasksFile).startsWith(tasksDir));
582
+ if (lanesToResume.length > 0) {
583
+ logger.info(`📂 Task directory detected: ${options.lane}`);
584
+ logger.info(`Resuming ${lanesToResume.length} lane(s) from this directory.`);
585
+ }
586
+ else {
587
+ logger.warn(`No incomplete lanes found using tasks from directory: ${options.lane}`);
588
+ return;
597
589
  }
598
- return;
599
590
  }
600
- // Single lane mode (original behavior)
601
- if (!options.lane) {
602
- // Show status by default if no lane specified
603
- printAllLaneStatus(runDir);
604
- console.log('');
605
- console.log('Usage: cursorflow resume <lane> [options]');
606
- console.log(' cursorflow resume --all # Resume all incomplete lanes');
607
- return;
591
+ else if (options.all) {
592
+ lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile);
608
593
  }
609
- const laneDir = (0, path_1.safeJoin)(runDir, 'lanes', options.lane);
610
- const statePath = (0, path_1.safeJoin)(laneDir, 'state.json');
611
- if (!fs.existsSync(statePath)) {
612
- throw new Error(`Lane state not found at ${statePath}. Is the lane name correct?`);
594
+ else if (options.lane) {
595
+ const lane = allLanes.find(l => l.name === options.lane);
596
+ if (!lane) {
597
+ throw new Error(`Lane '${options.lane}' not found in run directory.`);
598
+ }
599
+ if (!lane.needsResume) {
600
+ logger.success(`Lane '${options.lane}' is already completed.`);
601
+ return;
602
+ }
603
+ lanesToResume = [lane];
613
604
  }
614
- const state = (0, state_1.loadState)(statePath);
615
- if (!state) {
616
- throw new Error(`Failed to load state from ${statePath}`);
605
+ // Check for zombie lanes
606
+ const zombieCheck = checkAndFixZombieLanes(runDir);
607
+ if (zombieCheck.fixed.length > 0) {
608
+ logger.section('🔧 Zombie Lane Recovery');
609
+ logger.info(`Fixed ${zombieCheck.fixed.length} zombie lane(s): ${zombieCheck.fixed.join(', ')}`);
610
+ console.log('');
617
611
  }
618
- if (!state.tasksFile || !fs.existsSync(state.tasksFile)) {
619
- throw new Error(`Original tasks file not found: ${state.tasksFile}. Resume impossible without task definition.`);
612
+ if (options.status) {
613
+ printAllLaneStatus(runDir);
614
+ return;
620
615
  }
621
- // Run doctor check before resuming (check branches, etc.)
622
- if (!options.skipDoctor) {
623
- const tasksDir = path.dirname(state.tasksFile);
624
- logger.info('Running pre-flight checks...');
625
- const report = (0, doctor_1.runDoctor)({
626
- cwd: process.cwd(),
627
- tasksDir,
628
- includeCursorAgentChecks: false, // Skip agent checks for resume
629
- });
630
- // Only show blocking errors for resume
631
- const blockingIssues = report.issues.filter(i => i.severity === 'error' &&
632
- (i.id.startsWith('branch.') || i.id.startsWith('git.')));
633
- if (blockingIssues.length > 0) {
634
- logger.section('🛑 Pre-resume check found issues');
635
- for (const issue of blockingIssues) {
636
- logger.error(`${issue.title} (${issue.id})`, '❌');
637
- console.log(` ${issue.message}`);
638
- if (issue.details)
639
- console.log(` Details: ${issue.details}`);
640
- if (issue.fixes?.length) {
641
- console.log(' Fix:');
642
- for (const fix of issue.fixes)
643
- console.log(` - ${fix}`);
644
- }
645
- console.log('');
646
- }
647
- throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass (not recommended).');
616
+ if (lanesToResume.length === 0) {
617
+ if (options.lane || options.all) {
618
+ logger.success('No lanes need to be resumed.');
648
619
  }
649
- // Show warnings but don't block
650
- const warnings = report.issues.filter(i => i.severity === 'warn' && i.id.startsWith('branch.'));
651
- if (warnings.length > 0) {
652
- logger.warn(`${warnings.length} warning(s) found. Run 'cursorflow doctor' for details.`);
620
+ else {
621
+ printAllLaneStatus(runDir);
653
622
  }
623
+ return;
654
624
  }
655
- logger.section(`🔁 Resuming Lane: ${options.lane}`);
656
- logger.info(`Run: ${path.basename(runDir)}`);
657
- logger.info(`Tasks: ${state.tasksFile}`);
658
- logger.info(`Starting from task index: ${options.restart ? 0 : state.currentTaskIndex}`);
659
- const { child } = spawnLaneResume(options.lane, laneDir, state, {
625
+ const result = await resumeLanes(lanesToResume, allLanes, {
660
626
  restart: options.restart,
627
+ maxConcurrent: options.maxConcurrent,
628
+ skipDoctor: options.skipDoctor,
661
629
  noGit: options.noGit,
662
630
  executor: options.executor,
663
631
  enhancedLogConfig: config.enhancedLogging,
664
632
  });
665
- logger.info(`Spawning runner process...`);
666
- return new Promise((resolve, reject) => {
667
- child.on('exit', (code) => {
668
- if (code === 0) {
669
- logger.success(`Lane ${options.lane} completed successfully`);
670
- resolve();
671
- }
672
- else if (code === 2) {
673
- logger.warn(`Lane ${options.lane} blocked on dependency change`);
674
- resolve();
675
- }
676
- else {
677
- reject(new Error(`Lane ${options.lane} failed with exit code ${code}`));
678
- }
679
- });
680
- child.on('error', (error) => {
681
- reject(new Error(`Failed to start runner: ${error.message}`));
682
- });
683
- });
633
+ if (result.failed.length > 0) {
634
+ throw new Error(`${result.failed.length} lane(s) failed to complete`);
635
+ }
684
636
  }
685
637
  module.exports = resume;
686
638
  //# sourceMappingURL=resume.js.map