@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
package/src/cli/resume.ts CHANGED
@@ -1,626 +1,753 @@
1
- /**
2
- * CursorFlow resume command
3
- */
4
-
5
- import * as path from 'path';
6
- import * as fs from 'fs';
7
- import { spawn, ChildProcess } from 'child_process';
8
- import * as logger from '../utils/logger';
9
- import { loadConfig, getLogsDir } from '../utils/config';
10
- import { loadState } from '../utils/state';
11
- import { LaneState } from '../utils/types';
12
- import { runDoctor } from '../utils/doctor';
13
-
14
- interface ResumeOptions {
15
- lane: string | null;
16
- runDir: string | null;
17
- clean: boolean;
18
- restart: boolean;
19
- skipDoctor: boolean;
20
- all: boolean;
21
- status: boolean;
22
- maxConcurrent: number;
23
- help: boolean;
24
- }
25
-
26
- function printHelp(): void {
27
- console.log(`
28
- Usage: cursorflow resume [lane] [options]
29
-
30
- Resume interrupted or failed lanes.
31
-
32
- Options:
33
- <lane> Lane name to resume (single lane mode)
34
- --all Resume ALL incomplete/failed lanes
35
- --status Show status of all lanes in the run (no resume)
36
- --run-dir <path> Use a specific run directory (default: latest)
37
- --max-concurrent <n> Max lanes to run in parallel (default: 3)
38
- --clean Clean up existing worktree before resuming
39
- --restart Restart from the first task (index 0)
40
- --skip-doctor Skip environment/branch checks (not recommended)
41
- --help, -h Show help
42
-
43
- Examples:
44
- cursorflow resume --status # Check status of all lanes
45
- cursorflow resume --all # Resume all incomplete lanes
46
- cursorflow resume lane-1 # Resume single lane
47
- cursorflow resume --all --restart # Restart all incomplete lanes from task 0
48
- cursorflow resume --all --max-concurrent 2 # Resume with max 2 parallel lanes
49
- `);
50
- }
51
-
52
- function parseArgs(args: string[]): ResumeOptions {
53
- const runDirIdx = args.indexOf('--run-dir');
54
- const maxConcurrentIdx = args.indexOf('--max-concurrent');
55
-
56
- return {
57
- lane: args.find(a => !a.startsWith('--')) || null,
58
- runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
59
- clean: args.includes('--clean'),
60
- restart: args.includes('--restart'),
61
- skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
62
- all: args.includes('--all'),
63
- status: args.includes('--status'),
64
- maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '3') : 3,
65
- help: args.includes('--help') || args.includes('-h'),
66
- };
67
- }
68
-
69
- /**
70
- * Find the latest run directory
71
- */
72
- function findLatestRunDir(logsDir: string): string | null {
73
- const runsDir = path.join(logsDir, 'runs');
74
- if (!fs.existsSync(runsDir)) return null;
75
-
76
- const runs = fs.readdirSync(runsDir)
77
- .filter(d => d.startsWith('run-'))
78
- .sort()
79
- .reverse();
80
-
81
- return runs.length > 0 ? path.join(runsDir, runs[0]!) : null;
82
- }
83
-
84
- /**
85
- * Status indicator colors
86
- */
87
- const STATUS_COLORS: Record<string, string> = {
88
- completed: '\x1b[32m', // green
89
- running: '\x1b[36m', // cyan
90
- pending: '\x1b[33m', // yellow
91
- failed: '\x1b[31m', // red
92
- paused: '\x1b[35m', // magenta
93
- waiting: '\x1b[33m', // yellow
94
- reviewing: '\x1b[36m', // cyan
95
- unknown: '\x1b[90m', // gray
96
- };
97
- const RESET = '\x1b[0m';
98
-
99
- interface LaneInfo {
100
- name: string;
101
- dir: string;
102
- state: LaneState | null;
103
- needsResume: boolean;
104
- dependsOn: string[];
105
- isCompleted: boolean;
106
- }
107
-
108
- /**
109
- * Get all lane statuses from a run directory
110
- */
111
- function getAllLaneStatuses(runDir: string): LaneInfo[] {
112
- const lanesDir = path.join(runDir, 'lanes');
113
- if (!fs.existsSync(lanesDir)) {
114
- return [];
115
- }
116
-
117
- const lanes = fs.readdirSync(lanesDir)
118
- .filter(f => fs.statSync(path.join(lanesDir, f)).isDirectory())
119
- .map(name => {
120
- const dir = path.join(lanesDir, name);
121
- const statePath = path.join(dir, 'state.json');
122
- const state = fs.existsSync(statePath) ? loadState<LaneState>(statePath) : null;
123
-
124
- // Determine if lane needs resume
125
- const needsResume = state ? (
126
- state.status === 'failed' ||
127
- state.status === 'paused' ||
128
- state.status === 'running' || // If process crashed mid-run
129
- (state.status === 'pending' && state.currentTaskIndex > 0)
130
- ) : false;
131
-
132
- const isCompleted = state?.status === 'completed';
133
- const dependsOn = state?.dependsOn || [];
134
-
135
- return { name, dir, state, needsResume, dependsOn, isCompleted };
136
- });
137
-
138
- return lanes;
139
- }
140
-
141
- /**
142
- * Check if all dependencies of a lane are completed
143
- */
144
- function areDependenciesCompleted(
145
- lane: LaneInfo,
146
- allLanes: LaneInfo[],
147
- completedLanes: Set<string>
148
- ): boolean {
149
- if (!lane.dependsOn || lane.dependsOn.length === 0) {
150
- return true;
151
- }
152
-
153
- for (const depName of lane.dependsOn) {
154
- // Check if dependency is in completed set (already succeeded in this resume session)
155
- if (completedLanes.has(depName)) {
156
- continue;
157
- }
158
-
159
- // Check if dependency was already completed before this resume
160
- const depLane = allLanes.find(l => l.name === depName);
161
- if (!depLane || !depLane.isCompleted) {
162
- return false;
163
- }
164
- }
165
-
166
- return true;
167
- }
168
-
169
- /**
170
- * Print status of all lanes
171
- */
172
- function printAllLaneStatus(runDir: string): { total: number; completed: number; needsResume: number } {
173
- const lanes = getAllLaneStatuses(runDir);
174
-
175
- if (lanes.length === 0) {
176
- logger.warn('No lanes found in this run.');
177
- return { total: 0, completed: 0, needsResume: 0 };
178
- }
179
-
180
- logger.section(`📊 Lane Status (${path.basename(runDir)})`);
181
- console.log('');
182
-
183
- // Table header
184
- console.log(' ' +
185
- 'Lane'.padEnd(25) +
186
- 'Status'.padEnd(12) +
187
- 'Progress'.padEnd(12) +
188
- 'DependsOn'.padEnd(15) +
189
- 'Resumable'
190
- );
191
- console.log(' ' + '-'.repeat(75));
192
-
193
- let completedCount = 0;
194
- let needsResumeCount = 0;
195
- const completedSet = new Set<string>();
196
-
197
- // First pass: collect completed lanes
198
- for (const lane of lanes) {
199
- if (lane.isCompleted) {
200
- completedSet.add(lane.name);
201
- }
202
- }
203
-
204
- for (const lane of lanes) {
205
- const state = lane.state;
206
- const status = state?.status || 'unknown';
207
- const color = STATUS_COLORS[status] || STATUS_COLORS.unknown;
208
- const progress = state ? `${state.currentTaskIndex}/${state.totalTasks}` : '-/-';
209
- const dependsOnStr = lane.dependsOn.length > 0 ? lane.dependsOn.join(',').substring(0, 12) : '-';
210
-
211
- // Check if dependencies are met
212
- const depsCompleted = areDependenciesCompleted(lane, lanes, completedSet);
213
- const canResume = lane.needsResume && depsCompleted;
214
- const blockedByDep = lane.needsResume && !depsCompleted;
215
-
216
- if (status === 'completed') completedCount++;
217
- if (lane.needsResume) needsResumeCount++;
218
-
219
- let resumeIndicator = '';
220
- if (canResume) {
221
- resumeIndicator = '\x1b[33m✓\x1b[0m';
222
- } else if (blockedByDep) {
223
- resumeIndicator = '\x1b[90m⏳ waiting\x1b[0m';
224
- }
225
-
226
- console.log(' ' +
227
- lane.name.padEnd(25) +
228
- `${color}${status.padEnd(12)}${RESET}` +
229
- progress.padEnd(12) +
230
- dependsOnStr.padEnd(15) +
231
- resumeIndicator
232
- );
233
-
234
- // Show error if failed
235
- if (status === 'failed' && state?.error) {
236
- console.log(` ${''.padEnd(25)}\x1b[31m└─ ${state.error.substring(0, 50)}${state.error.length > 50 ? '...' : ''}\x1b[0m`);
237
- }
238
-
239
- // Show blocked dependency info
240
- if (blockedByDep) {
241
- const pendingDeps = lane.dependsOn.filter(d => !completedSet.has(d));
242
- console.log(` ${''.padEnd(25)}\x1b[90m└─ waiting for: ${pendingDeps.join(', ')}\x1b[0m`);
243
- }
244
- }
245
-
246
- console.log('');
247
- console.log(` Total: ${lanes.length} | Completed: ${completedCount} | Needs Resume: ${needsResumeCount}`);
248
-
249
- if (needsResumeCount > 0) {
250
- console.log('');
251
- console.log(' \x1b[33mTip:\x1b[0m Run \x1b[32mcursorflow resume --all\x1b[0m to resume all incomplete lanes');
252
- console.log(' Lanes with dependencies will wait until their dependencies complete.');
253
- }
254
-
255
- return { total: lanes.length, completed: completedCount, needsResume: needsResumeCount };
256
- }
257
-
258
- /**
259
- * Resume a single lane and return the child process
260
- */
261
- function spawnLaneResume(
262
- laneName: string,
263
- laneDir: string,
264
- state: LaneState,
265
- options: { restart: boolean }
266
- ): ChildProcess {
267
- const runnerPath = require.resolve('../core/runner');
268
- const startIndex = options.restart ? 0 : state.currentTaskIndex;
269
-
270
- const runnerArgs = [
271
- runnerPath,
272
- state.tasksFile!,
273
- '--run-dir', laneDir,
274
- '--start-index', String(startIndex),
275
- ];
276
-
277
- const child = spawn('node', runnerArgs, {
278
- stdio: 'inherit',
279
- env: process.env,
280
- });
281
-
282
- return child;
283
- }
284
-
285
- /**
286
- * Wait for a child process to exit
287
- */
288
- function waitForChild(child: ChildProcess): Promise<number> {
289
- return new Promise((resolve, reject) => {
290
- child.on('exit', (code) => {
291
- resolve(code ?? -1);
292
- });
293
- child.on('error', (err) => {
294
- reject(err);
295
- });
296
- });
297
- }
298
-
299
- /**
300
- * Resume multiple lanes with concurrency control and dependency awareness
301
- */
302
- async function resumeAllLanes(
303
- runDir: string,
304
- options: { restart: boolean; maxConcurrent: number; skipDoctor: boolean }
305
- ): Promise<{ succeeded: string[]; failed: string[]; skipped: string[] }> {
306
- const allLanes = getAllLaneStatuses(runDir);
307
- const lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile);
308
-
309
- if (lanesToResume.length === 0) {
310
- logger.success('All lanes are already completed! Nothing to resume.');
311
- return { succeeded: [], failed: [], skipped: [] };
312
- }
313
-
314
- // Check for lanes with unmet dependencies that can never be satisfied
315
- const completedSet = new Set<string>(allLanes.filter(l => l.isCompleted).map(l => l.name));
316
- const toResumeNames = new Set<string>(lanesToResume.map(l => l.name));
317
-
318
- const skippedLanes: string[] = [];
319
- const resolvableLanes: LaneInfo[] = [];
320
-
321
- for (const lane of lanesToResume) {
322
- // Check if all dependencies can be satisfied (either already completed or in the resume list)
323
- const unmetDeps = lane.dependsOn.filter(dep =>
324
- !completedSet.has(dep) && !toResumeNames.has(dep)
325
- );
326
-
327
- if (unmetDeps.length > 0) {
328
- logger.warn(`⏭ Skipping ${lane.name}: unresolvable dependencies (${unmetDeps.join(', ')})`);
329
- skippedLanes.push(lane.name);
330
- } else {
331
- resolvableLanes.push(lane);
332
- }
333
- }
334
-
335
- if (resolvableLanes.length === 0) {
336
- logger.warn('No lanes can be resumed due to dependency constraints.');
337
- return { succeeded: [], failed: [], skipped: skippedLanes };
338
- }
339
-
340
- logger.section(`🔁 Resuming ${resolvableLanes.length} Lane(s)`);
341
- logger.info(`Max concurrent: ${options.maxConcurrent}`);
342
- logger.info(`Mode: ${options.restart ? 'Restart from beginning' : 'Continue from last task'}`);
343
-
344
- // Show dependency order
345
- const lanesWithDeps = resolvableLanes.filter(l => l.dependsOn.length > 0);
346
- if (lanesWithDeps.length > 0) {
347
- logger.info(`Dependency-aware: ${lanesWithDeps.length} lane(s) have dependencies`);
348
- }
349
- console.log('');
350
-
351
- // Run doctor check once if needed (check git status)
352
- if (!options.skipDoctor) {
353
- logger.info('Running pre-flight checks...');
354
-
355
- // Use the first lane's tasksDir for doctor check
356
- const firstLane = resolvableLanes[0]!;
357
- const tasksDir = path.dirname(firstLane.state!.tasksFile!);
358
-
359
- const report = runDoctor({
360
- cwd: process.cwd(),
361
- tasksDir,
362
- includeCursorAgentChecks: false,
363
- });
364
-
365
- const blockingIssues = report.issues.filter(i =>
366
- i.severity === 'error' &&
367
- (i.id.startsWith('branch.') || i.id.startsWith('git.'))
368
- );
369
-
370
- if (blockingIssues.length > 0) {
371
- logger.section('🛑 Pre-resume check found issues');
372
- for (const issue of blockingIssues) {
373
- logger.error(`${issue.title} (${issue.id})`, '');
374
- console.log(` ${issue.message}`);
375
- }
376
- throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass.');
377
- }
378
- }
379
-
380
- const succeeded: string[] = [];
381
- const failed: string[] = [];
382
-
383
- // Create a mutable set for tracking completed lanes (including those from this session)
384
- const sessionCompleted = new Set<string>(completedSet);
385
-
386
- // Queue management with dependency awareness
387
- const pending = new Set<string>(resolvableLanes.map(l => l.name));
388
- const active: Map<string, ChildProcess> = new Map();
389
- const laneMap = new Map<string, LaneInfo>(resolvableLanes.map(l => [l.name, l]));
390
-
391
- /**
392
- * Find the next lane that can be started (all dependencies met)
393
- */
394
- const findReadyLane = (): LaneInfo | null => {
395
- for (const laneName of pending) {
396
- const lane = laneMap.get(laneName)!;
397
- if (areDependenciesCompleted(lane, allLanes, sessionCompleted)) {
398
- return lane;
399
- }
400
- }
401
- return null;
402
- };
403
-
404
- /**
405
- * Process lanes with dependency awareness
406
- */
407
- const processNext = (): void => {
408
- while (active.size < options.maxConcurrent) {
409
- const lane = findReadyLane();
410
-
411
- if (!lane) {
412
- // No lane ready to start
413
- if (pending.size > 0 && active.size === 0) {
414
- // Deadlock: pending lanes exist but none can start and none are running
415
- const pendingList = Array.from(pending).join(', ');
416
- logger.error(`Deadlock detected! Lanes waiting: ${pendingList}`);
417
- for (const ln of pending) {
418
- failed.push(ln);
419
- }
420
- pending.clear();
421
- }
422
- break;
423
- }
424
-
425
- pending.delete(lane.name);
426
-
427
- const depsInfo = lane.dependsOn.length > 0 ? ` (after: ${lane.dependsOn.join(', ')})` : '';
428
- logger.info(`Starting: ${lane.name} (task ${lane.state!.currentTaskIndex}/${lane.state!.totalTasks})${depsInfo}`);
429
-
430
- const child = spawnLaneResume(lane.name, lane.dir, lane.state!, {
431
- restart: options.restart,
432
- });
433
-
434
- active.set(lane.name, child);
435
-
436
- // Handle completion
437
- waitForChild(child).then(code => {
438
- active.delete(lane.name);
439
-
440
- if (code === 0) {
441
- logger.success(`✓ ${lane.name} completed`);
442
- succeeded.push(lane.name);
443
- sessionCompleted.add(lane.name); // Mark as completed for dependency resolution
444
- } else if (code === 2) {
445
- logger.warn(`⚠ ${lane.name} blocked on dependency change`);
446
- failed.push(lane.name);
447
- } else {
448
- logger.error(`✗ ${lane.name} failed (exit ${code})`);
449
- failed.push(lane.name);
450
- }
451
-
452
- // Try to start more lanes now that one completed
453
- processNext();
454
- }).catch(err => {
455
- active.delete(lane.name);
456
- logger.error(`✗ ${lane.name} error: ${err.message}`);
457
- failed.push(lane.name);
458
- processNext();
459
- });
460
- }
461
- };
462
-
463
- // Start initial batch
464
- processNext();
465
-
466
- // Wait for all to complete
467
- while (active.size > 0 || pending.size > 0) {
468
- await new Promise(resolve => setTimeout(resolve, 1000));
469
-
470
- // Check if we can start more (in case completion handlers haven't triggered processNext yet)
471
- if (active.size < options.maxConcurrent && pending.size > 0) {
472
- processNext();
473
- }
474
- }
475
-
476
- // Summary
477
- console.log('');
478
- logger.section('📊 Resume Summary');
479
- logger.info(`Succeeded: ${succeeded.length}`);
480
- if (failed.length > 0) {
481
- logger.error(`Failed: ${failed.length} (${failed.join(', ')})`);
482
- }
483
- if (skippedLanes.length > 0) {
484
- logger.warn(`Skipped: ${skippedLanes.length} (${skippedLanes.join(', ')})`);
485
- }
486
-
487
- return { succeeded, failed, skipped: skippedLanes };
488
- }
489
-
490
- async function resume(args: string[]): Promise<void> {
491
- const options = parseArgs(args);
492
-
493
- if (options.help) {
494
- printHelp();
495
- return;
496
- }
497
-
498
- const config = loadConfig();
499
- const logsDir = getLogsDir(config);
500
-
501
- // Find run directory
502
- let runDir = options.runDir;
503
- if (!runDir) {
504
- runDir = findLatestRunDir(logsDir);
505
- }
506
-
507
- if (!runDir || !fs.existsSync(runDir)) {
508
- throw new Error(`Run directory not found: ${runDir || 'latest'}. Have you run any tasks yet?`);
509
- }
510
-
511
- // Status mode: just show status and exit
512
- if (options.status) {
513
- printAllLaneStatus(runDir);
514
- return;
515
- }
516
-
517
- // All mode: resume all incomplete lanes
518
- if (options.all) {
519
- const result = await resumeAllLanes(runDir, {
520
- restart: options.restart,
521
- maxConcurrent: options.maxConcurrent,
522
- skipDoctor: options.skipDoctor,
523
- });
524
-
525
- if (result.failed.length > 0) {
526
- throw new Error(`${result.failed.length} lane(s) failed to complete`);
527
- }
528
- return;
529
- }
530
-
531
- // Single lane mode (original behavior)
532
- if (!options.lane) {
533
- // Show status by default if no lane specified
534
- printAllLaneStatus(runDir);
535
- console.log('');
536
- console.log('Usage: cursorflow resume <lane> [options]');
537
- console.log(' cursorflow resume --all # Resume all incomplete lanes');
538
- return;
539
- }
540
-
541
- const laneDir = path.join(runDir, 'lanes', options.lane);
542
- const statePath = path.join(laneDir, 'state.json');
543
-
544
- if (!fs.existsSync(statePath)) {
545
- throw new Error(`Lane state not found at ${statePath}. Is the lane name correct?`);
546
- }
547
-
548
- const state = loadState<LaneState>(statePath);
549
- if (!state) {
550
- throw new Error(`Failed to load state from ${statePath}`);
551
- }
552
-
553
- if (!state.tasksFile || !fs.existsSync(state.tasksFile)) {
554
- throw new Error(`Original tasks file not found: ${state.tasksFile}. Resume impossible without task definition.`);
555
- }
556
-
557
- // Run doctor check before resuming (check branches, etc.)
558
- if (!options.skipDoctor) {
559
- const tasksDir = path.dirname(state.tasksFile);
560
- logger.info('Running pre-flight checks...');
561
-
562
- const report = runDoctor({
563
- cwd: process.cwd(),
564
- tasksDir,
565
- includeCursorAgentChecks: false, // Skip agent checks for resume
566
- });
567
-
568
- // Only show blocking errors for resume
569
- const blockingIssues = report.issues.filter(i =>
570
- i.severity === 'error' &&
571
- (i.id.startsWith('branch.') || i.id.startsWith('git.'))
572
- );
573
-
574
- if (blockingIssues.length > 0) {
575
- logger.section('🛑 Pre-resume check found issues');
576
- for (const issue of blockingIssues) {
577
- logger.error(`${issue.title} (${issue.id})`, '❌');
578
- console.log(` ${issue.message}`);
579
- if (issue.details) console.log(` Details: ${issue.details}`);
580
- if (issue.fixes?.length) {
581
- console.log(' Fix:');
582
- for (const fix of issue.fixes) console.log(` - ${fix}`);
583
- }
584
- console.log('');
585
- }
586
- throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass (not recommended).');
587
- }
588
-
589
- // Show warnings but don't block
590
- const warnings = report.issues.filter(i => i.severity === 'warn' && i.id.startsWith('branch.'));
591
- if (warnings.length > 0) {
592
- logger.warn(`${warnings.length} warning(s) found. Run 'cursorflow doctor' for details.`);
593
- }
594
- }
595
-
596
- logger.section(`🔁 Resuming Lane: ${options.lane}`);
597
- logger.info(`Run: ${path.basename(runDir)}`);
598
- logger.info(`Tasks: ${state.tasksFile}`);
599
- logger.info(`Starting from task index: ${options.restart ? 0 : state.currentTaskIndex}`);
600
-
601
- const child = spawnLaneResume(options.lane, laneDir, state, {
602
- restart: options.restart,
603
- });
604
-
605
- logger.info(`Spawning runner process...`);
606
-
607
- return new Promise((resolve, reject) => {
608
- child.on('exit', (code) => {
609
- if (code === 0) {
610
- logger.success(`Lane ${options.lane} completed successfully`);
611
- resolve();
612
- } else if (code === 2) {
613
- logger.warn(`Lane ${options.lane} blocked on dependency change`);
614
- resolve();
615
- } else {
616
- reject(new Error(`Lane ${options.lane} failed with exit code ${code}`));
617
- }
618
- });
619
-
620
- child.on('error', (error) => {
621
- reject(new Error(`Failed to start runner: ${error.message}`));
622
- });
623
- });
624
- }
625
-
626
- export = resume;
1
+ /**
2
+ * CursorFlow resume command
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import { spawn, ChildProcess } from 'child_process';
8
+ import * as logger from '../utils/logger';
9
+ import { loadConfig, getLogsDir, getPofDir } from '../utils/config';
10
+ import { loadState, saveState } from '../utils/state';
11
+ import { LaneState } from '../types';
12
+ import { runDoctor } from '../utils/doctor';
13
+ import { safeJoin } from '../utils/path';
14
+ import {
15
+ EnhancedLogManager,
16
+ createLogManager,
17
+ ParsedMessage
18
+ } from '../utils/enhanced-logger';
19
+ import { formatMessageForConsole } from '../utils/log-formatter';
20
+
21
+ interface ResumeOptions {
22
+ lane: string | null;
23
+ runDir: string | null;
24
+ clean: boolean;
25
+ restart: boolean;
26
+ skipDoctor: boolean;
27
+ all: boolean;
28
+ status: boolean;
29
+ maxConcurrent: number;
30
+ help: boolean;
31
+ noGit: boolean;
32
+ executor: string | null;
33
+ }
34
+
35
+ function printHelp(): void {
36
+ console.log(`
37
+ Usage: cursorflow resume [lane] [options]
38
+
39
+ Resume interrupted or failed lanes.
40
+
41
+ Options:
42
+ <lane> Lane name or tasks directory to resume
43
+ --all Resume ALL incomplete/failed lanes
44
+ --status Show status of all lanes in the run (no resume)
45
+ --run-dir <path> Use a specific run directory (default: latest)
46
+ --max-concurrent <n> Max lanes to run in parallel (default: 3)
47
+ --clean Clean up existing worktree before resuming
48
+ --restart Restart from the first task (index 0)
49
+ --skip-doctor Skip environment/branch checks (not recommended)
50
+ --no-git Disable Git operations (must match original run)
51
+ --executor <type> Override executor (default: cursor-agent)
52
+ --help, -h Show help
53
+
54
+ Examples:
55
+ cursorflow resume --status # Check status of all lanes
56
+ cursorflow resume --all # Resume all incomplete lanes
57
+ cursorflow resume lane-1 # Resume single lane
58
+ cursorflow resume _cursorflow/tasks/feat1 # Resume all lanes in directory
59
+ cursorflow resume --all --restart # Restart all incomplete lanes from task 0
60
+ `);
61
+ }
62
+
63
+ function parseArgs(args: string[]): ResumeOptions {
64
+ const runDirIdx = args.indexOf('--run-dir');
65
+ const maxConcurrentIdx = args.indexOf('--max-concurrent');
66
+ const executorIdx = args.indexOf('--executor');
67
+
68
+ return {
69
+ lane: args.find(a => !a.startsWith('--')) || null,
70
+ runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
71
+ clean: args.includes('--clean'),
72
+ restart: args.includes('--restart'),
73
+ skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
74
+ all: args.includes('--all'),
75
+ status: args.includes('--status'),
76
+ maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '3') : 3,
77
+ help: args.includes('--help') || args.includes('-h'),
78
+ noGit: args.includes('--no-git'),
79
+ executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Find the latest run directory
85
+ */
86
+ function findLatestRunDir(logsDir: string): string | null {
87
+ const runsDir = safeJoin(logsDir, 'runs');
88
+ if (!fs.existsSync(runsDir)) return null;
89
+
90
+ const runs = fs.readdirSync(runsDir)
91
+ .filter(d => d.startsWith('run-'))
92
+ .sort()
93
+ .reverse();
94
+
95
+ return runs.length > 0 ? safeJoin(runsDir, runs[0]!) : null;
96
+ }
97
+
98
+ /**
99
+ * Status indicator colors
100
+ */
101
+ const STATUS_COLORS: Record<string, string> = {
102
+ completed: '\x1b[32m', // green
103
+ running: '\x1b[36m', // cyan
104
+ pending: '\x1b[33m', // yellow
105
+ failed: '\x1b[31m', // red
106
+ paused: '\x1b[35m', // magenta
107
+ waiting: '\x1b[33m', // yellow
108
+ reviewing: '\x1b[36m', // cyan
109
+ unknown: '\x1b[90m', // gray
110
+ };
111
+ const RESET = '\x1b[0m';
112
+
113
+ interface LaneInfo {
114
+ name: string;
115
+ dir: string;
116
+ state: LaneState | null;
117
+ needsResume: boolean;
118
+ dependsOn: string[];
119
+ isCompleted: boolean;
120
+ }
121
+
122
+ /**
123
+ * Check if a process is alive by its PID
124
+ */
125
+ function isProcessAlive(pid: number): boolean {
126
+ try {
127
+ // On Unix-like systems, sending signal 0 checks if process exists
128
+ process.kill(pid, 0);
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Check for zombie "running" lanes and fix them
137
+ * A zombie lane is one that has status "running" but its process is dead
138
+ */
139
+ function checkAndFixZombieLanes(runDir: string): { fixed: string[]; pofCreated: boolean } {
140
+ const lanesDir = safeJoin(runDir, 'lanes');
141
+ if (!fs.existsSync(lanesDir)) {
142
+ return { fixed: [], pofCreated: false };
143
+ }
144
+
145
+ const fixed: string[] = [];
146
+ const zombieDetails: Array<{
147
+ name: string;
148
+ pid: number;
149
+ taskIndex: number;
150
+ totalTasks: number;
151
+ }> = [];
152
+
153
+ const laneDirs = fs.readdirSync(lanesDir)
154
+ .filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory());
155
+
156
+ for (const laneName of laneDirs) {
157
+ const dir = safeJoin(lanesDir, laneName);
158
+ const statePath = safeJoin(dir, 'state.json');
159
+
160
+ if (!fs.existsSync(statePath)) continue;
161
+
162
+ const state = loadState<LaneState>(statePath);
163
+ if (!state) continue;
164
+
165
+ // Check for zombie: status is "running" but process is dead
166
+ if (state.status === 'running' && state.pid) {
167
+ const alive = isProcessAlive(state.pid);
168
+
169
+ if (!alive) {
170
+ logger.warn(`🧟 Zombie lane detected: ${laneName} (PID ${state.pid} is dead)`);
171
+
172
+ // Update state to failed
173
+ const updatedState: LaneState = {
174
+ ...state,
175
+ status: 'failed',
176
+ error: `Process terminated unexpectedly (PID ${state.pid} was running but is now dead)`,
177
+ endTime: Date.now(),
178
+ };
179
+
180
+ saveState(statePath, updatedState);
181
+ fixed.push(laneName);
182
+
183
+ zombieDetails.push({
184
+ name: laneName,
185
+ pid: state.pid,
186
+ taskIndex: state.currentTaskIndex,
187
+ totalTasks: state.totalTasks,
188
+ });
189
+
190
+ logger.info(` → Status changed to 'failed', ready for resume`);
191
+ }
192
+ }
193
+ }
194
+
195
+ // Create POF file if any zombies were found
196
+ let pofCreated = false;
197
+ if (zombieDetails.length > 0) {
198
+ const config = loadConfig();
199
+ const pofDir = getPofDir(config);
200
+ if (!fs.existsSync(pofDir)) {
201
+ fs.mkdirSync(pofDir, { recursive: true });
202
+ }
203
+
204
+ const runId = path.basename(runDir);
205
+ const pofPath = safeJoin(pofDir, `pof-${runId}.json`);
206
+
207
+ let existingPof = null;
208
+ try {
209
+ existingPof = JSON.parse(fs.readFileSync(pofPath, 'utf-8'));
210
+ } catch {
211
+ // Ignore errors (file might not exist)
212
+ }
213
+
214
+ const pof = {
215
+ title: 'Run Failure Post-mortem',
216
+ runId: path.basename(runDir),
217
+ failureTime: new Date().toISOString(),
218
+ detectedAt: new Date().toISOString(),
219
+ summary: `${zombieDetails.length} lane(s) found with dead processes (zombie state)`,
220
+
221
+ rootCause: {
222
+ type: 'ZOMBIE_PROCESS',
223
+ description: 'Lane processes were marked as running but the processes are no longer alive',
224
+ symptoms: [
225
+ 'Process PIDs no longer exist in the system',
226
+ 'Lanes were stuck in "running" state',
227
+ 'No completion or error was recorded before process death',
228
+ ],
229
+ },
230
+
231
+ affectedLanes: zombieDetails.map(z => ({
232
+ name: z.name,
233
+ status: 'failed (was: running)',
234
+ task: `[${z.taskIndex + 1}/${z.totalTasks}]`,
235
+ taskIndex: z.taskIndex,
236
+ pid: z.pid,
237
+ reason: 'Process terminated unexpectedly',
238
+ })),
239
+
240
+ possibleCauses: [
241
+ 'System killed process due to memory pressure (OOM)',
242
+ 'User killed process manually (Ctrl+C, kill command)',
243
+ 'Agent timeout exceeded and process was terminated',
244
+ 'System restart or crash',
245
+ 'Agent hung and watchdog terminated it',
246
+ ],
247
+
248
+ recovery: {
249
+ command: `cursorflow resume --all --run-dir ${runDir}`,
250
+ description: 'Resume all failed lanes from their last checkpoint',
251
+ alternativeCommand: `cursorflow resume --all --restart --run-dir ${runDir}`,
252
+ alternativeDescription: 'Restart all failed lanes from the beginning',
253
+ },
254
+
255
+ // Merge with existing POF if present
256
+ previousFailures: existingPof ? [existingPof] : undefined,
257
+ };
258
+
259
+ // Use atomic write: write to temp file then rename
260
+ const tempPath = `${pofPath}.${Math.random().toString(36).substring(2, 7)}.tmp`;
261
+ try {
262
+ fs.writeFileSync(tempPath, JSON.stringify(pof, null, 2), 'utf8');
263
+ fs.renameSync(tempPath, pofPath);
264
+ pofCreated = true;
265
+ logger.info(`📋 POF file created: ${pofPath}`);
266
+ } catch (err) {
267
+ // If temp file was created, try to clean it up
268
+ try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch { /* ignore */ }
269
+ throw err;
270
+ }
271
+ }
272
+
273
+ return { fixed, pofCreated };
274
+ }
275
+
276
+ /**
277
+ * Get all lane statuses from a run directory
278
+ */
279
+ function getAllLaneStatuses(runDir: string): LaneInfo[] {
280
+ const lanesDir = safeJoin(runDir, 'lanes');
281
+ if (!fs.existsSync(lanesDir)) {
282
+ return [];
283
+ }
284
+
285
+ const lanes = fs.readdirSync(lanesDir)
286
+ .filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory())
287
+ .map(name => {
288
+ const dir = safeJoin(lanesDir, name);
289
+ const statePath = safeJoin(dir, 'state.json');
290
+ const state = fs.existsSync(statePath) ? loadState<LaneState>(statePath) : null;
291
+
292
+ // Determine if lane needs resume: everything that is not completed
293
+ const needsResume = state ? (
294
+ state.status !== 'completed'
295
+ ) : true;
296
+
297
+ const isCompleted = state?.status === 'completed';
298
+ const dependsOn = state?.dependsOn || [];
299
+
300
+ return { name, dir, state, needsResume, dependsOn, isCompleted };
301
+ });
302
+
303
+ return lanes;
304
+ }
305
+
306
+ /**
307
+ * Check if all dependencies of a lane are completed
308
+ */
309
+ function areDependenciesCompleted(
310
+ lane: LaneInfo,
311
+ allLanes: LaneInfo[],
312
+ completedLanes: Set<string>
313
+ ): boolean {
314
+ if (!lane.dependsOn || lane.dependsOn.length === 0) {
315
+ return true;
316
+ }
317
+
318
+ for (const depName of lane.dependsOn) {
319
+ // Check if dependency is in completed set (already succeeded in this resume session)
320
+ if (completedLanes.has(depName)) {
321
+ continue;
322
+ }
323
+
324
+ // Check if dependency was already completed before this resume
325
+ const depLane = allLanes.find(l => l.name === depName);
326
+ if (!depLane || !depLane.isCompleted) {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ return true;
332
+ }
333
+
334
+ /**
335
+ * Print status of all lanes
336
+ */
337
+ function printAllLaneStatus(runDir: string): { total: number; completed: number; needsResume: number } {
338
+ const lanes = getAllLaneStatuses(runDir);
339
+
340
+ if (lanes.length === 0) {
341
+ logger.warn('No lanes found in this run.');
342
+ return { total: 0, completed: 0, needsResume: 0 };
343
+ }
344
+
345
+ logger.section(`📊 Lane Status (${path.basename(runDir)})`);
346
+ console.log('');
347
+
348
+ // Table header
349
+ console.log(' ' +
350
+ 'Lane'.padEnd(25) +
351
+ 'Status'.padEnd(12) +
352
+ 'Progress'.padEnd(12) +
353
+ 'DependsOn'.padEnd(15) +
354
+ 'Resumable'
355
+ );
356
+ console.log(' ' + '-'.repeat(75));
357
+
358
+ let completedCount = 0;
359
+ let needsResumeCount = 0;
360
+ const completedSet = new Set<string>();
361
+
362
+ // First pass: collect completed lanes
363
+ for (const lane of lanes) {
364
+ if (lane.isCompleted) {
365
+ completedSet.add(lane.name);
366
+ }
367
+ }
368
+
369
+ for (const lane of lanes) {
370
+ const state = lane.state;
371
+ const status = state?.status || 'unknown';
372
+ const color = STATUS_COLORS[status] || STATUS_COLORS.unknown;
373
+ const progress = state ? `${state.currentTaskIndex}/${state.totalTasks}` : '-/-';
374
+ const dependsOnStr = lane.dependsOn.length > 0 ? lane.dependsOn.join(',').substring(0, 12) : '-';
375
+
376
+ // Check if dependencies are met
377
+ const depsCompleted = areDependenciesCompleted(lane, lanes, completedSet);
378
+ const canResume = lane.needsResume && depsCompleted;
379
+ const blockedByDep = lane.needsResume && !depsCompleted;
380
+
381
+ if (status === 'completed') completedCount++;
382
+ if (lane.needsResume) needsResumeCount++;
383
+
384
+ let resumeIndicator = '';
385
+ if (canResume) {
386
+ resumeIndicator = '\x1b[33m✓\x1b[0m';
387
+ } else if (blockedByDep) {
388
+ resumeIndicator = '\x1b[90m⏳ waiting\x1b[0m';
389
+ }
390
+
391
+ console.log(' ' +
392
+ lane.name.padEnd(25) +
393
+ `${color}${status.padEnd(12)}${RESET}` +
394
+ progress.padEnd(12) +
395
+ dependsOnStr.padEnd(15) +
396
+ resumeIndicator
397
+ );
398
+
399
+ // Show error if failed
400
+ if (status === 'failed' && state?.error) {
401
+ console.log(` ${''.padEnd(25)}\x1b[31m└─ ${state.error.substring(0, 50)}${state.error.length > 50 ? '...' : ''}\x1b[0m`);
402
+ }
403
+
404
+ // Show blocked dependency info
405
+ if (blockedByDep) {
406
+ const pendingDeps = lane.dependsOn.filter(d => !completedSet.has(d));
407
+ console.log(` ${''.padEnd(25)}\x1b[90m└─ waiting for: ${pendingDeps.join(', ')}\x1b[0m`);
408
+ }
409
+ }
410
+
411
+ console.log('');
412
+ console.log(` Total: ${lanes.length} | Completed: ${completedCount} | Needs Resume: ${needsResumeCount}`);
413
+
414
+ if (needsResumeCount > 0) {
415
+ console.log('');
416
+ console.log(' \x1b[33mTip:\x1b[0m Run \x1b[32mcursorflow resume --all\x1b[0m to resume all incomplete lanes');
417
+ console.log(' Lanes with dependencies will wait until their dependencies complete.');
418
+ }
419
+
420
+ return { total: lanes.length, completed: completedCount, needsResume: needsResumeCount };
421
+ }
422
+
423
+ /**
424
+ * Resume a single lane and return the child process and its log manager
425
+ */
426
+ function spawnLaneResume(
427
+ laneName: string,
428
+ laneDir: string,
429
+ state: LaneState,
430
+ options: {
431
+ restart: boolean;
432
+ noGit?: boolean;
433
+ pipelineBranch?: string;
434
+ executor?: string | null;
435
+ enhancedLogConfig?: any;
436
+ }
437
+ ): { child: ChildProcess; logManager: EnhancedLogManager } {
438
+ const runnerPath = require.resolve('../core/runner');
439
+ const startIndex = options.restart ? 0 : state.currentTaskIndex;
440
+
441
+ const runnerArgs = [
442
+ runnerPath,
443
+ state.tasksFile!,
444
+ '--run-dir', laneDir,
445
+ '--start-index', String(startIndex),
446
+ ];
447
+
448
+ if (state.worktreeDir) {
449
+ runnerArgs.push('--worktree-dir', state.worktreeDir);
450
+ }
451
+
452
+ if (options.noGit) {
453
+ runnerArgs.push('--no-git');
454
+ }
455
+
456
+ // Explicitly pass pipeline branch if available (either from state or override)
457
+ const branch = options.pipelineBranch || state.pipelineBranch;
458
+ if (branch) {
459
+ runnerArgs.push('--pipeline-branch', branch);
460
+ }
461
+
462
+ // Pass executor if provided
463
+ if (options.executor) {
464
+ runnerArgs.push('--executor', options.executor);
465
+ }
466
+
467
+ const logManager = createLogManager(laneDir, laneName, options.enhancedLogConfig || {}, (msg) => {
468
+ const formatted = formatMessageForConsole(msg, {
469
+ laneLabel: `[${laneName}]`,
470
+ includeTimestamp: true
471
+ });
472
+ process.stdout.write(formatted + '\n');
473
+ });
474
+
475
+ const child = spawn('node', runnerArgs, {
476
+ stdio: ['ignore', 'pipe', 'pipe'],
477
+ env: process.env,
478
+ });
479
+
480
+ if (child.stdout) {
481
+ child.stdout.on('data', (data: Buffer) => {
482
+ logManager.writeStdout(data);
483
+ });
484
+ }
485
+
486
+ if (child.stderr) {
487
+ child.stderr.on('data', (data: Buffer) => {
488
+ logManager.writeStderr(data);
489
+ });
490
+ }
491
+
492
+ child.on('exit', () => {
493
+ logManager.close();
494
+ });
495
+
496
+ return { child, logManager };
497
+ }
498
+
499
+ /**
500
+ * Wait for a child process to exit
501
+ */
502
+ function waitForChild(child: ChildProcess): Promise<number> {
503
+ return new Promise((resolve, reject) => {
504
+ child.on('exit', (code) => {
505
+ resolve(code ?? -1);
506
+ });
507
+ child.on('error', (err) => {
508
+ reject(err);
509
+ });
510
+ });
511
+ }
512
+
513
+ /**
514
+ * Resume multiple lanes with concurrency control and dependency awareness
515
+ */
516
+ async function resumeLanes(
517
+ lanesToResume: LaneInfo[],
518
+ allLanes: LaneInfo[],
519
+ options: {
520
+ restart: boolean;
521
+ maxConcurrent: number;
522
+ skipDoctor: boolean;
523
+ noGit: boolean;
524
+ executor: string | null;
525
+ enhancedLogConfig?: any;
526
+ }
527
+ ): Promise<{ succeeded: string[]; failed: string[]; skipped: string[] }> {
528
+ const completedSet = new Set<string>(allLanes.filter(l => l.isCompleted).map(l => l.name));
529
+ const toResumeNames = new Set<string>(lanesToResume.map(l => l.name));
530
+
531
+ const skippedLanes: string[] = [];
532
+ const resolvableLanes: LaneInfo[] = [];
533
+
534
+ for (const lane of lanesToResume) {
535
+ // Check if all dependencies can be satisfied (either already completed or in the resume list)
536
+ const unmetDeps = lane.dependsOn.filter(dep =>
537
+ !completedSet.has(dep) && !toResumeNames.has(dep)
538
+ );
539
+
540
+ if (unmetDeps.length > 0) {
541
+ logger.warn(`⏭ Skipping ${lane.name}: unresolvable dependencies (${unmetDeps.join(', ')})`);
542
+ skippedLanes.push(lane.name);
543
+ } else {
544
+ resolvableLanes.push(lane);
545
+ }
546
+ }
547
+
548
+ if (resolvableLanes.length === 0) {
549
+ logger.warn('No lanes can be resumed due to dependency constraints.');
550
+ return { succeeded: [], failed: [], skipped: skippedLanes };
551
+ }
552
+
553
+ logger.section(`🔁 Resuming ${resolvableLanes.length} Lane(s)`);
554
+ logger.info(`Max concurrent: ${options.maxConcurrent}`);
555
+ logger.info(`Mode: ${options.restart ? 'Restart from beginning' : 'Continue from last task'}`);
556
+
557
+ // Run doctor check once if needed (check git status)
558
+ if (!options.skipDoctor) {
559
+ logger.info('Running pre-flight checks...');
560
+ const firstLane = resolvableLanes[0]!;
561
+ const tasksDir = path.dirname(firstLane.state!.tasksFile!);
562
+
563
+ const report = runDoctor({
564
+ cwd: process.cwd(),
565
+ tasksDir,
566
+ includeCursorAgentChecks: false,
567
+ });
568
+
569
+ const blockingIssues = report.issues.filter(i =>
570
+ i.severity === 'error' &&
571
+ (i.id.startsWith('branch.') || i.id.startsWith('git.'))
572
+ );
573
+
574
+ if (blockingIssues.length > 0) {
575
+ logger.section('🛑 Pre-resume check found issues');
576
+ for (const issue of blockingIssues) {
577
+ logger.error(`${issue.title} (${issue.id})`, '❌');
578
+ console.log(` ${issue.message}`);
579
+ }
580
+ throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass.');
581
+ }
582
+ }
583
+
584
+ const succeeded: string[] = [];
585
+ const failed: string[] = [];
586
+ const sessionCompleted = new Set<string>(completedSet);
587
+ const pending = new Set<string>(resolvableLanes.map(l => l.name));
588
+ const active: Map<string, ChildProcess> = new Map();
589
+ const laneMap = new Map<string, LaneInfo>(resolvableLanes.map(l => [l.name, l]));
590
+
591
+ const findReadyLane = (): LaneInfo | null => {
592
+ for (const laneName of pending) {
593
+ const lane = laneMap.get(laneName)!;
594
+ if (areDependenciesCompleted(lane, allLanes, sessionCompleted)) {
595
+ return lane;
596
+ }
597
+ }
598
+ return null;
599
+ };
600
+
601
+ const processNext = (): void => {
602
+ while (active.size < options.maxConcurrent) {
603
+ const lane = findReadyLane();
604
+ if (!lane) {
605
+ if (pending.size > 0 && active.size === 0) {
606
+ const pendingList = Array.from(pending).join(', ');
607
+ logger.error(`Deadlock detected! Lanes waiting: ${pendingList}`);
608
+ for (const ln of pending) failed.push(ln);
609
+ pending.clear();
610
+ }
611
+ break;
612
+ }
613
+
614
+ pending.delete(lane.name);
615
+ const depsInfo = lane.dependsOn.length > 0 ? ` (after: ${lane.dependsOn.join(', ')})` : '';
616
+ logger.info(`Starting: ${lane.name} (task ${lane.state!.currentTaskIndex}/${lane.state!.totalTasks})${depsInfo}`);
617
+
618
+ const { child } = spawnLaneResume(lane.name, lane.dir, lane.state!, {
619
+ restart: options.restart,
620
+ noGit: options.noGit,
621
+ executor: options.executor,
622
+ enhancedLogConfig: options.enhancedLogConfig,
623
+ });
624
+
625
+ active.set(lane.name, child);
626
+
627
+ waitForChild(child).then(code => {
628
+ active.delete(lane.name);
629
+ if (code === 0) {
630
+ logger.success(`✓ ${lane.name} completed`);
631
+ succeeded.push(lane.name);
632
+ sessionCompleted.add(lane.name);
633
+ } else if (code === 2) {
634
+ logger.warn(`⚠ ${lane.name} blocked on dependency change`);
635
+ failed.push(lane.name);
636
+ } else {
637
+ logger.error(`✗ ${lane.name} failed (exit ${code})`);
638
+ failed.push(lane.name);
639
+ }
640
+ processNext();
641
+ }).catch(err => {
642
+ active.delete(lane.name);
643
+ logger.error(`✗ ${lane.name} error: ${err.message}`);
644
+ failed.push(lane.name);
645
+ processNext();
646
+ });
647
+ }
648
+ };
649
+
650
+ processNext();
651
+
652
+ while (active.size > 0 || pending.size > 0) {
653
+ await new Promise(resolve => setTimeout(resolve, 1000));
654
+ if (active.size < options.maxConcurrent && pending.size > 0) {
655
+ processNext();
656
+ }
657
+ }
658
+
659
+ console.log('');
660
+ logger.section('📊 Resume Summary');
661
+ logger.info(`Succeeded: ${succeeded.length}`);
662
+ if (failed.length > 0) logger.error(`Failed: ${failed.length} (${failed.join(', ')})`);
663
+ if (skippedLanes.length > 0) logger.warn(`Skipped: ${skippedLanes.length} (${skippedLanes.join(', ')})`);
664
+
665
+ return { succeeded, failed, skipped: skippedLanes };
666
+ }
667
+
668
+ async function resume(args: string[]): Promise<void> {
669
+ const options = parseArgs(args);
670
+
671
+ if (options.help) {
672
+ printHelp();
673
+ return;
674
+ }
675
+
676
+ const config = loadConfig();
677
+ const logsDir = getLogsDir(config);
678
+
679
+ let runDir = options.runDir;
680
+ if (!runDir) {
681
+ runDir = findLatestRunDir(logsDir);
682
+ }
683
+
684
+ if (!runDir || !fs.existsSync(runDir)) {
685
+ throw new Error(`Run directory not found: ${runDir || 'latest'}. Have you run any tasks yet?`);
686
+ }
687
+
688
+ const allLanes = getAllLaneStatuses(runDir);
689
+ let lanesToResume: LaneInfo[] = [];
690
+
691
+ // Check if the lane argument is actually a tasks directory
692
+ if (options.lane && fs.existsSync(options.lane) && fs.statSync(options.lane).isDirectory()) {
693
+ const tasksDir = path.resolve(options.lane);
694
+ lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile && path.resolve(l.state.tasksFile).startsWith(tasksDir));
695
+
696
+ if (lanesToResume.length > 0) {
697
+ logger.info(`📂 Task directory detected: ${options.lane}`);
698
+ logger.info(`Resuming ${lanesToResume.length} lane(s) from this directory.`);
699
+ } else {
700
+ logger.warn(`No incomplete lanes found using tasks from directory: ${options.lane}`);
701
+ return;
702
+ }
703
+ } else if (options.all) {
704
+ lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile);
705
+ } else if (options.lane) {
706
+ const lane = allLanes.find(l => l.name === options.lane);
707
+ if (!lane) {
708
+ throw new Error(`Lane '${options.lane}' not found in run directory.`);
709
+ }
710
+ if (!lane.needsResume) {
711
+ logger.success(`Lane '${options.lane}' is already completed.`);
712
+ return;
713
+ }
714
+ lanesToResume = [lane];
715
+ }
716
+
717
+ // Check for zombie lanes
718
+ const zombieCheck = checkAndFixZombieLanes(runDir);
719
+ if (zombieCheck.fixed.length > 0) {
720
+ logger.section('🔧 Zombie Lane Recovery');
721
+ logger.info(`Fixed ${zombieCheck.fixed.length} zombie lane(s): ${zombieCheck.fixed.join(', ')}`);
722
+ console.log('');
723
+ }
724
+
725
+ if (options.status) {
726
+ printAllLaneStatus(runDir);
727
+ return;
728
+ }
729
+
730
+ if (lanesToResume.length === 0) {
731
+ if (options.lane || options.all) {
732
+ logger.success('No lanes need to be resumed.');
733
+ } else {
734
+ printAllLaneStatus(runDir);
735
+ }
736
+ return;
737
+ }
738
+
739
+ const result = await resumeLanes(lanesToResume, allLanes, {
740
+ restart: options.restart,
741
+ maxConcurrent: options.maxConcurrent,
742
+ skipDoctor: options.skipDoctor,
743
+ noGit: options.noGit,
744
+ executor: options.executor,
745
+ enhancedLogConfig: config.enhancedLogging,
746
+ });
747
+
748
+ if (result.failed.length > 0) {
749
+ throw new Error(`${result.failed.length} lane(s) failed to complete`);
750
+ }
751
+ }
752
+
753
+ export = resume;