@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
@@ -2,7 +2,11 @@
2
2
  /**
3
3
  * Orchestrator - Parallel lane execution with dependency management
4
4
  *
5
- * Adapted from admin-domains-orchestrator.js
5
+ * Features:
6
+ * - Multi-layer stall detection
7
+ * - Cyclic dependency detection
8
+ * - Enhanced recovery strategies
9
+ * - Health checks before start
6
10
  */
7
11
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
12
  if (k2 === undefined) k2 = k;
@@ -53,11 +57,26 @@ const webhook_1 = require("../utils/webhook");
53
57
  const config_1 = require("../utils/config");
54
58
  const git = __importStar(require("../utils/git"));
55
59
  const child_process_2 = require("child_process");
60
+ const path_1 = require("../utils/path");
56
61
  const enhanced_logger_1 = require("../utils/enhanced-logger");
62
+ const log_formatter_1 = require("../utils/log-formatter");
63
+ const failure_policy_1 = require("./failure-policy");
64
+ const auto_recovery_1 = require("./auto-recovery");
65
+ const dependency_1 = require("../utils/dependency");
66
+ const health_1 = require("../utils/health");
67
+ const checkpoint_1 = require("../utils/checkpoint");
68
+ const lock_1 = require("../utils/lock");
69
+ /** Default stall detection configuration - 1 minute idle timeout for fast recovery */
70
+ const DEFAULT_ORCHESTRATOR_STALL_CONFIG = {
71
+ ...failure_policy_1.DEFAULT_STALL_CONFIG,
72
+ idleTimeoutMs: 60 * 1000, // 1 minute (quick detection for continue signal)
73
+ progressTimeoutMs: 10 * 60 * 1000, // 10 minutes
74
+ maxRestarts: 2,
75
+ };
57
76
  /**
58
77
  * Spawn a lane process
59
78
  */
60
- function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0, pipelineBranch, enhancedLogConfig, noGit = false, }) {
79
+ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0, pipelineBranch, worktreeDir, enhancedLogConfig, noGit = false, onActivity, }) {
61
80
  fs.mkdirSync(laneRunDir, { recursive: true });
62
81
  // Use extension-less resolve to handle both .ts (dev) and .js (dist)
63
82
  const runnerPath = require.resolve('./runner');
@@ -71,6 +90,9 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
71
90
  if (pipelineBranch) {
72
91
  args.push('--pipeline-branch', pipelineBranch);
73
92
  }
93
+ if (worktreeDir) {
94
+ args.push('--worktree-dir', worktreeDir);
95
+ }
74
96
  if (noGit) {
75
97
  args.push('--no-git');
76
98
  }
@@ -86,86 +108,13 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
86
108
  if (logConfig.enabled) {
87
109
  // Create callback for clean console output
88
110
  const onParsedMessage = (msg) => {
89
- // Print a clean, colored version of the message to the console
90
- const ts = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false });
91
- const laneLabel = `[${laneName}]`.padEnd(12);
92
- let prefix = '';
93
- let content = msg.content;
94
- switch (msg.type) {
95
- case 'user':
96
- prefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
97
- // No truncation for user prompt to ensure full command visibility
98
- content = content.replace(/\n/g, ' ');
99
- break;
100
- case 'assistant':
101
- prefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
102
- break;
103
- case 'tool':
104
- prefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
105
- // Simplify tool call: [Tool: read_file] {"target_file":"..."} -> read_file(target_file: ...)
106
- const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
107
- if (toolMatch) {
108
- const [, name, args] = toolMatch;
109
- try {
110
- const parsedArgs = JSON.parse(args);
111
- let argStr = '';
112
- if (name === 'read_file' && parsedArgs.target_file) {
113
- argStr = parsedArgs.target_file;
114
- }
115
- else if (name === 'run_terminal_cmd' && parsedArgs.command) {
116
- argStr = parsedArgs.command;
117
- }
118
- else if (name === 'write' && parsedArgs.file_path) {
119
- argStr = parsedArgs.file_path;
120
- }
121
- else if (name === 'search_replace' && parsedArgs.file_path) {
122
- argStr = parsedArgs.file_path;
123
- }
124
- else {
125
- // Generic summary for other tools
126
- const keys = Object.keys(parsedArgs);
127
- if (keys.length > 0) {
128
- argStr = String(parsedArgs[keys[0]]).substring(0, 50);
129
- }
130
- }
131
- content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
132
- }
133
- catch {
134
- content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
135
- }
136
- }
137
- break;
138
- case 'tool_result':
139
- prefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
140
- // Simplify tool result: [Tool Result: read_file] ... -> read_file OK
141
- const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
142
- content = resMatch ? `${resMatch[1]} OK` : 'result';
143
- break;
144
- case 'result':
145
- prefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
146
- break;
147
- case 'system':
148
- prefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
149
- break;
150
- case 'thinking':
151
- prefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
152
- break;
153
- }
154
- if (prefix) {
155
- const lines = content.split('\n');
156
- const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
157
- if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result' || msg.type === 'thinking') {
158
- const header = `${prefix} ┌${'─'.repeat(60)}`;
159
- process.stdout.write(`${tsPrefix} ${header}\n`);
160
- for (const line of lines) {
161
- process.stdout.write(`${tsPrefix} ${' '.repeat((0, enhanced_logger_1.stripAnsi)(prefix).length)} │ ${line}\n`);
162
- }
163
- process.stdout.write(`${tsPrefix} ${' '.repeat((0, enhanced_logger_1.stripAnsi)(prefix).length)} └${'─'.repeat(60)}\n`);
164
- }
165
- else {
166
- process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
167
- }
168
- }
111
+ if (onActivity)
112
+ onActivity();
113
+ const formatted = (0, log_formatter_1.formatMessageForConsole)(msg, {
114
+ laneLabel: `[${laneName}]`,
115
+ includeTimestamp: true
116
+ });
117
+ process.stdout.write(formatted + '\n');
169
118
  };
170
119
  logManager = (0, enhanced_logger_1.createLogManager)(laneRunDir, laneName, logConfig, onParsedMessage);
171
120
  logPath = logManager.getLogPaths().clean;
@@ -188,12 +137,23 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
188
137
  lineBuffer = lines.pop() || '';
189
138
  for (const line of lines) {
190
139
  const trimmed = line.trim();
191
- // Only print if NOT a noisy line
192
- if (trimmed &&
193
- !trimmed.startsWith('{') &&
194
- !trimmed.startsWith('[') &&
195
- !trimmed.includes('{"type"')) {
196
- 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`);
140
+ // Show if it's a timestamped log line (starts with [YYYY-MM-DD... or [HH:MM:SS])
141
+ // or if it's NOT a noisy JSON line
142
+ const hasTimestamp = /^\[\d{4}-\d{2}-\d{2}T|\^\[\d{2}:\d{2}:\d{2}\]/.test(trimmed);
143
+ const isJson = trimmed.startsWith('{') || trimmed.includes('{"type"');
144
+ if (trimmed && !isJson) {
145
+ if (onActivity)
146
+ onActivity();
147
+ // If line already has timestamp format, just add lane prefix
148
+ if (hasTimestamp) {
149
+ // Insert lane name after first timestamp
150
+ const formatted = trimmed.replace(/^(\[[^\]]+\])/, `$1 ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset}`);
151
+ process.stdout.write(formatted + '\n');
152
+ }
153
+ else {
154
+ // Add full prefix: timestamp + lane
155
+ process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset} ${line}\n`);
156
+ }
197
157
  }
198
158
  }
199
159
  });
@@ -211,11 +171,14 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
211
171
  trimmed.startsWith('Switched to a new branch') ||
212
172
  trimmed.startsWith('HEAD is now at') ||
213
173
  trimmed.includes('actual output');
174
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
214
175
  if (isStatus) {
215
- 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`);
176
+ process.stdout.write(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset} ${trimmed}\n`);
216
177
  }
217
178
  else {
218
- process.stderr.write(`${logger.COLORS.red}[${laneName}] ERROR: ${trimmed}${logger.COLORS.reset}\n`);
179
+ if (onActivity)
180
+ onActivity();
181
+ process.stderr.write(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset} ${logger.COLORS.red}❌ ERR ${trimmed}${logger.COLORS.reset}\n`);
219
182
  }
220
183
  }
221
184
  }
@@ -228,7 +191,7 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
228
191
  }
229
192
  else {
230
193
  // Fallback to simple file logging
231
- logPath = path.join(laneRunDir, 'terminal.log');
194
+ logPath = (0, path_1.safeJoin)(laneRunDir, 'terminal.log');
232
195
  const logFd = fs.openSync(logPath, 'a');
233
196
  child = (0, child_process_1.spawn)('node', args, {
234
197
  stdio: ['ignore', logFd, logFd],
@@ -269,7 +232,7 @@ function listLaneFiles(tasksDir) {
269
232
  .filter(f => f.endsWith('.json'))
270
233
  .sort()
271
234
  .map(f => {
272
- const filePath = path.join(tasksDir, f);
235
+ const filePath = (0, path_1.safeJoin)(tasksDir, f);
273
236
  const name = path.basename(f, '.json');
274
237
  let dependsOn = [];
275
238
  try {
@@ -294,7 +257,7 @@ function printLaneStatus(lanes, laneRunDirs) {
294
257
  const dir = laneRunDirs[lane.name];
295
258
  if (!dir)
296
259
  return { lane: lane.name, status: '(unknown)', task: '-' };
297
- const statePath = path.join(dir, 'state.json');
260
+ const statePath = (0, path_1.safeJoin)(dir, 'state.json');
298
261
  const state = (0, state_1.loadState)(statePath);
299
262
  if (!state) {
300
263
  const isWaiting = lane.dependsOn.length > 0;
@@ -331,12 +294,12 @@ async function resolveAllDependencies(blockedLanes, allLanes, laneRunDirs, pipel
331
294
  return;
332
295
  // 2. Setup a temporary worktree for resolution if needed, or use the first available one
333
296
  const firstLaneName = Array.from(blockedLanes.keys())[0];
334
- const statePath = path.join(laneRunDirs[firstLaneName], 'state.json');
297
+ const statePath = (0, path_1.safeJoin)(laneRunDirs[firstLaneName], 'state.json');
335
298
  const state = (0, state_1.loadState)(statePath);
336
- const worktreeDir = state?.worktreeDir || path.join(runRoot, 'resolution-worktree');
299
+ const worktreeDir = state?.worktreeDir || (0, path_1.safeJoin)(runRoot, 'resolution-worktree');
337
300
  if (!fs.existsSync(worktreeDir)) {
338
301
  logger.info(`Creating resolution worktree at ${worktreeDir}`);
339
- git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: 'main' });
302
+ git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: git.getCurrentBranch() });
340
303
  }
341
304
  // 3. Resolve on pipeline branch
342
305
  logger.info(`Resolving dependencies on ${pipelineBranch}`);
@@ -367,7 +330,7 @@ async function resolveAllDependencies(blockedLanes, allLanes, laneRunDirs, pipel
367
330
  const laneDir = laneRunDirs[lane.name];
368
331
  if (!laneDir)
369
332
  continue;
370
- const laneState = (0, state_1.loadState)(path.join(laneDir, 'state.json'));
333
+ const laneState = (0, state_1.loadState)((0, path_1.safeJoin)(laneDir, 'state.json'));
371
334
  if (!laneState || laneState.status === 'completed' || laneState.status === 'failed')
372
335
  continue;
373
336
  // Merge pipelineBranch into the lane's current task branch
@@ -406,16 +369,82 @@ async function orchestrate(tasksDir, options = {}) {
406
369
  if (lanes.length === 0) {
407
370
  throw new Error(`No lane task files found in ${tasksDir}`);
408
371
  }
372
+ // Run preflight checks
373
+ if (!options.skipPreflight) {
374
+ logger.section('🔍 Preflight Checks');
375
+ const preflight = await (0, health_1.preflightCheck)({
376
+ requireRemote: !options.noGit,
377
+ requireAuth: true,
378
+ });
379
+ if (!preflight.canProceed) {
380
+ (0, health_1.printPreflightReport)(preflight);
381
+ throw new Error('Preflight check failed. Please fix the blockers above.');
382
+ }
383
+ // Auto-repair if there are warnings
384
+ if (preflight.warnings.length > 0) {
385
+ logger.info('Attempting auto-repair...');
386
+ const repair = await (0, health_1.autoRepair)();
387
+ if (repair.repaired.length > 0) {
388
+ for (const r of repair.repaired) {
389
+ logger.success(`✓ ${r}`);
390
+ }
391
+ }
392
+ }
393
+ logger.success('✓ Preflight checks passed');
394
+ }
395
+ // Validate dependencies and detect cycles
396
+ logger.section('📊 Dependency Analysis');
397
+ const depInfos = lanes.map(l => ({
398
+ name: l.name,
399
+ dependsOn: l.dependsOn,
400
+ }));
401
+ const depValidation = (0, dependency_1.validateDependencies)(depInfos);
402
+ if (!depValidation.valid) {
403
+ logger.error('❌ Dependency validation failed:');
404
+ for (const err of depValidation.errors) {
405
+ logger.error(` • ${err}`);
406
+ }
407
+ throw new Error('Invalid dependency configuration');
408
+ }
409
+ if (depValidation.warnings.length > 0) {
410
+ for (const warn of depValidation.warnings) {
411
+ logger.warn(`⚠️ ${warn}`);
412
+ }
413
+ }
414
+ // Print dependency graph
415
+ (0, dependency_1.printDependencyGraph)(depInfos);
409
416
  const config = (0, config_1.loadConfig)();
410
417
  const logsDir = (0, config_1.getLogsDir)(config);
411
418
  const runId = `run-${Date.now()}`;
412
419
  // Use absolute path for runRoot to avoid issues with subfolders
413
420
  const runRoot = options.runDir
414
- ? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir))
415
- : path.join(logsDir, 'runs', runId);
421
+ ? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir)) // nosemgrep
422
+ : (0, path_1.safeJoin)(logsDir, 'runs', runId);
416
423
  fs.mkdirSync(runRoot, { recursive: true });
424
+ // Clean stale locks before starting
425
+ try {
426
+ const lockDir = (0, lock_1.getLockDir)(git.getRepoRoot());
427
+ const cleaned = (0, lock_1.cleanStaleLocks)(lockDir);
428
+ if (cleaned > 0) {
429
+ logger.info(`Cleaned ${cleaned} stale lock(s)`);
430
+ }
431
+ }
432
+ catch {
433
+ // Ignore lock cleanup errors
434
+ }
417
435
  const randomSuffix = Math.random().toString(36).substring(2, 7);
418
436
  const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}-${randomSuffix}`;
437
+ // Stall detection configuration
438
+ const stallConfig = {
439
+ ...DEFAULT_ORCHESTRATOR_STALL_CONFIG,
440
+ ...options.stallConfig,
441
+ };
442
+ // Initialize auto-recovery manager
443
+ const autoRecoveryManager = (0, auto_recovery_1.getAutoRecoveryManager)({
444
+ ...auto_recovery_1.DEFAULT_AUTO_RECOVERY_CONFIG,
445
+ idleTimeoutMs: stallConfig.idleTimeoutMs, // Sync with stall config
446
+ ...options.autoRecoveryConfig,
447
+ });
419
448
  // Initialize event system
420
449
  events_1.events.setRunId(runId);
421
450
  if (options.webhooks) {
@@ -436,11 +465,38 @@ async function orchestrate(tasksDir, options = {}) {
436
465
  // Track start index for each lane (initially 0)
437
466
  for (const lane of lanes) {
438
467
  lane.startIndex = 0;
468
+ lane.restartCount = 0;
439
469
  }
440
470
  const laneRunDirs = {};
471
+ const laneWorktreeDirs = {};
472
+ const repoRoot = git.getRepoRoot();
441
473
  for (const lane of lanes) {
442
- laneRunDirs[lane.name] = path.join(runRoot, 'lanes', lane.name);
474
+ laneRunDirs[lane.name] = (0, path_1.safeJoin)(runRoot, 'lanes', lane.name);
443
475
  fs.mkdirSync(laneRunDirs[lane.name], { recursive: true });
476
+ // Create initial state for ALL lanes so resume can find them even if they didn't start
477
+ try {
478
+ const taskConfig = JSON.parse(fs.readFileSync(lane.path, 'utf8'));
479
+ // Calculate unique branch and worktree for this lane
480
+ const lanePipelineBranch = `${pipelineBranch}/${lane.name}`;
481
+ // Use a flat worktree directory name to avoid race conditions in parent directory creation
482
+ // repoRoot/_cursorflow/worktrees/cursorflow-run-xxx-lane-name
483
+ const laneWorktreeDir = (0, path_1.safeJoin)(repoRoot, taskConfig.worktreeRoot || '_cursorflow/worktrees', lanePipelineBranch.replace(/\//g, '-'));
484
+ // Ensure the parent directory exists before spawning the runner
485
+ // to avoid race conditions in git worktree add or fs operations
486
+ const worktreeParent = path.dirname(laneWorktreeDir);
487
+ if (!fs.existsSync(worktreeParent)) {
488
+ fs.mkdirSync(worktreeParent, { recursive: true });
489
+ }
490
+ laneWorktreeDirs[lane.name] = laneWorktreeDir;
491
+ const initialState = (0, state_1.createLaneState)(lane.name, taskConfig, lane.path, {
492
+ pipelineBranch: lanePipelineBranch,
493
+ worktreeDir: laneWorktreeDir
494
+ });
495
+ (0, state_1.saveState)((0, path_1.safeJoin)(laneRunDirs[lane.name], 'state.json'), initialState);
496
+ }
497
+ catch (e) {
498
+ logger.warn(`Failed to create initial state for lane ${lane.name}: ${e}`);
499
+ }
444
500
  }
445
501
  logger.section('🧭 Starting Orchestration');
446
502
  logger.info(`Tasks directory: ${tasksDir}`);
@@ -497,7 +553,14 @@ async function orchestrate(tasksDir, options = {}) {
497
553
  for (const lane of readyToStart) {
498
554
  if (running.size >= maxConcurrent)
499
555
  break;
556
+ const laneStatePath = (0, path_1.safeJoin)(laneRunDirs[lane.name], 'state.json');
557
+ // Validate and repair state before starting
558
+ const validation = (0, state_1.validateLaneState)(laneStatePath, { autoRepair: true });
559
+ if (!validation.valid && !validation.repaired) {
560
+ logger.warn(`[${lane.name}] State validation issues: ${validation.issues.join(', ')}`);
561
+ }
500
562
  logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
563
+ let lastOutput = '';
501
564
  const spawnResult = spawnLane({
502
565
  laneName: lane.name,
503
566
  tasksFile: lane.path,
@@ -505,25 +568,280 @@ async function orchestrate(tasksDir, options = {}) {
505
568
  executor: options.executor || 'cursor-agent',
506
569
  startIndex: lane.startIndex,
507
570
  pipelineBranch: `${pipelineBranch}/${lane.name}`,
571
+ worktreeDir: laneWorktreeDirs[lane.name],
508
572
  enhancedLogConfig: options.enhancedLogging,
509
573
  noGit: options.noGit,
574
+ onActivity: () => {
575
+ const info = running.get(lane.name);
576
+ if (info) {
577
+ info.lastActivity = Date.now();
578
+ }
579
+ }
510
580
  });
511
- running.set(lane.name, spawnResult);
581
+ // Track last output and bytes received for long operation and stall detection
582
+ if (spawnResult.child.stdout) {
583
+ spawnResult.child.stdout.on('data', (data) => {
584
+ const info = running.get(lane.name);
585
+ if (info) {
586
+ info.lastOutput = data.toString().trim().split('\n').pop() || '';
587
+ info.bytesReceived += data.length;
588
+ // Update auto-recovery manager
589
+ autoRecoveryManager.recordActivity(lane.name, data.length, info.lastOutput);
590
+ }
591
+ });
592
+ }
593
+ const now = Date.now();
594
+ running.set(lane.name, {
595
+ ...spawnResult,
596
+ lastActivity: now,
597
+ lastStateUpdate: now,
598
+ stallPhase: 0,
599
+ taskStartTime: now,
600
+ lastOutput: '',
601
+ statePath: laneStatePath,
602
+ bytesReceived: 0,
603
+ lastBytesCheck: 0,
604
+ continueSignalsSent: 0,
605
+ });
606
+ // Register lane with auto-recovery manager
607
+ autoRecoveryManager.registerLane(lane.name);
608
+ // Update lane tracking
609
+ lane.taskStartTime = now;
512
610
  events_1.events.emit('lane.started', {
513
611
  laneName: lane.name,
514
612
  pid: spawnResult.child.pid,
515
613
  logPath: spawnResult.logPath,
516
614
  });
517
615
  }
518
- // 3. Wait for any running lane to finish
616
+ // 3. Wait for any running lane to finish OR check for stalls
519
617
  if (running.size > 0) {
618
+ // Polling timeout for stall detection
619
+ let pollTimeout;
620
+ const pollPromise = new Promise(resolve => {
621
+ pollTimeout = setTimeout(() => resolve({ name: '__poll__', code: 0 }), 10000);
622
+ });
520
623
  const promises = Array.from(running.entries()).map(async ([name, { child }]) => {
521
624
  const code = await waitChild(child);
522
625
  return { name, code };
523
626
  });
524
- const finished = await Promise.race(promises);
627
+ const result = await Promise.race([...promises, pollPromise]);
628
+ if (pollTimeout)
629
+ clearTimeout(pollTimeout);
630
+ if (result.name === '__poll__') {
631
+ // Periodic stall check with multi-layer detection and escalating recovery
632
+ for (const [laneName, info] of running.entries()) {
633
+ const now = Date.now();
634
+ const idleTime = now - info.lastActivity;
635
+ const lane = lanes.find(l => l.name === laneName);
636
+ // Check state file for progress updates
637
+ let progressTime = 0;
638
+ try {
639
+ const stateStat = fs.statSync(info.statePath);
640
+ const stateUpdateTime = stateStat.mtimeMs;
641
+ if (stateUpdateTime > info.lastStateUpdate) {
642
+ info.lastStateUpdate = stateUpdateTime;
643
+ }
644
+ progressTime = now - info.lastStateUpdate;
645
+ }
646
+ catch {
647
+ // State file might not exist yet
648
+ }
649
+ // Calculate bytes received since last check
650
+ const bytesDelta = info.bytesReceived - info.lastBytesCheck;
651
+ info.lastBytesCheck = info.bytesReceived;
652
+ // Use multi-layer stall analysis with enhanced context
653
+ const analysis = (0, failure_policy_1.analyzeStall)({
654
+ stallPhase: info.stallPhase,
655
+ idleTimeMs: idleTime,
656
+ progressTimeMs: progressTime,
657
+ lastOutput: info.lastOutput,
658
+ restartCount: lane.restartCount || 0,
659
+ taskStartTimeMs: info.taskStartTime,
660
+ bytesReceived: bytesDelta, // Bytes since last check
661
+ continueSignalsSent: info.continueSignalsSent,
662
+ }, stallConfig);
663
+ // Only act if action is not NONE
664
+ if (analysis.action !== failure_policy_1.RecoveryAction.NONE) {
665
+ (0, failure_policy_1.logFailure)(laneName, analysis);
666
+ info.logManager?.log('error', analysis.message);
667
+ if (analysis.action === failure_policy_1.RecoveryAction.CONTINUE_SIGNAL) {
668
+ const interventionPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'intervention.txt');
669
+ try {
670
+ fs.writeFileSync(interventionPath, 'continue');
671
+ info.stallPhase = 1;
672
+ info.lastActivity = now;
673
+ info.continueSignalsSent++;
674
+ logger.info(`[${laneName}] Sent continue signal (#${info.continueSignalsSent})`);
675
+ events_1.events.emit('recovery.continue_signal', {
676
+ laneName,
677
+ idleSeconds: Math.round(idleTime / 1000),
678
+ signalCount: info.continueSignalsSent,
679
+ });
680
+ }
681
+ catch (e) {
682
+ logger.error(`Failed to write intervention file for ${laneName}: ${e}`);
683
+ }
684
+ }
685
+ else if (analysis.action === failure_policy_1.RecoveryAction.STRONGER_PROMPT) {
686
+ const interventionPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'intervention.txt');
687
+ const strongerPrompt = `[SYSTEM INTERVENTION] You seem to be stuck. Please continue with your current task immediately. If you're waiting for something, explain what you need and proceed with what you can do now. If you've completed the task, summarize your work and finish.`;
688
+ try {
689
+ fs.writeFileSync(interventionPath, strongerPrompt);
690
+ info.stallPhase = 2;
691
+ info.lastActivity = now;
692
+ logger.warn(`[${laneName}] Sent stronger prompt after continue signal failed`);
693
+ events_1.events.emit('recovery.stronger_prompt', { laneName });
694
+ }
695
+ catch (e) {
696
+ logger.error(`Failed to write intervention file for ${laneName}: ${e}`);
697
+ }
698
+ }
699
+ else if (analysis.action === failure_policy_1.RecoveryAction.KILL_AND_RESTART ||
700
+ analysis.action === failure_policy_1.RecoveryAction.RESTART_LANE ||
701
+ analysis.action === failure_policy_1.RecoveryAction.RESTART_LANE_FROM_CHECKPOINT) {
702
+ lane.restartCount = (lane.restartCount || 0) + 1;
703
+ info.stallPhase = 3;
704
+ // Try to get checkpoint info
705
+ const checkpoint = (0, checkpoint_1.getLatestCheckpoint)(laneRunDirs[laneName]);
706
+ if (checkpoint) {
707
+ logger.info(`[${laneName}] Checkpoint available: ${checkpoint.id} (task ${checkpoint.taskIndex})`);
708
+ }
709
+ // Kill the process
710
+ try {
711
+ info.child.kill('SIGKILL');
712
+ }
713
+ catch {
714
+ // Process might already be dead
715
+ }
716
+ logger.warn(`[${laneName}] Killing and restarting lane (restart #${lane.restartCount})`);
717
+ events_1.events.emit('recovery.restart', {
718
+ laneName,
719
+ restartCount: lane.restartCount,
720
+ maxRestarts: stallConfig.maxRestarts,
721
+ });
722
+ }
723
+ else if (analysis.action === failure_policy_1.RecoveryAction.RUN_DOCTOR) {
724
+ info.stallPhase = 4;
725
+ // Run diagnostics
726
+ logger.error(`[${laneName}] Running diagnostics due to persistent failures...`);
727
+ // Import health check dynamically to avoid circular dependency
728
+ const { checkAgentHealth, checkAuthHealth } = await Promise.resolve().then(() => __importStar(require('../utils/health')));
729
+ const [agentHealth, authHealth] = await Promise.all([
730
+ checkAgentHealth(),
731
+ checkAuthHealth(),
732
+ ]);
733
+ const issues = [];
734
+ if (!agentHealth.ok)
735
+ issues.push(`Agent: ${agentHealth.message}`);
736
+ if (!authHealth.ok)
737
+ issues.push(`Auth: ${authHealth.message}`);
738
+ if (issues.length > 0) {
739
+ logger.error(`[${laneName}] Diagnostic issues found:\n ${issues.join('\n ')}`);
740
+ }
741
+ else {
742
+ logger.warn(`[${laneName}] No obvious issues found. The problem may be with the AI model or network.`);
743
+ }
744
+ // Save diagnostic to file
745
+ const diagnosticPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'diagnostic.json');
746
+ fs.writeFileSync(diagnosticPath, JSON.stringify({
747
+ timestamp: Date.now(),
748
+ agentHealthy: agentHealth.ok,
749
+ authHealthy: authHealth.ok,
750
+ issues,
751
+ analysis,
752
+ }, null, 2));
753
+ // Kill the process
754
+ try {
755
+ info.child.kill('SIGKILL');
756
+ }
757
+ catch {
758
+ // Process might already be dead
759
+ }
760
+ logger.error(`[${laneName}] Aborting lane after diagnostic. Check ${diagnosticPath} for details.`);
761
+ // Save POF for failed recovery
762
+ const recoveryState = autoRecoveryManager.getState(laneName);
763
+ if (recoveryState) {
764
+ try {
765
+ const laneStatePath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'state.json');
766
+ const laneState = (0, state_1.loadState)(laneStatePath);
767
+ const pofDir = (0, path_1.safeJoin)(runRoot, '..', '..', 'pof');
768
+ const diagnosticInfo = {
769
+ timestamp: Date.now(),
770
+ agentHealthy: agentHealth.ok,
771
+ authHealthy: authHealth.ok,
772
+ systemHealthy: true,
773
+ suggestedAction: issues.length > 0 ? 'Fix the issues above and retry' : 'Try with a different model',
774
+ details: issues.join('\n') || 'No obvious issues found',
775
+ };
776
+ const pofEntry = (0, auto_recovery_1.createPOFFromRecoveryState)(runId, runRoot, laneName, recoveryState, laneState, diagnosticInfo);
777
+ (0, auto_recovery_1.savePOF)(runId, pofDir, pofEntry);
778
+ }
779
+ catch (pofError) {
780
+ logger.warn(`[${laneName}] Failed to save POF: ${pofError.message}`);
781
+ }
782
+ }
783
+ events_1.events.emit('recovery.diagnosed', {
784
+ laneName,
785
+ diagnostic: { agentHealthy: agentHealth.ok, authHealthy: authHealth.ok, issues },
786
+ });
787
+ }
788
+ else if (analysis.action === failure_policy_1.RecoveryAction.ABORT_LANE) {
789
+ info.stallPhase = 5;
790
+ try {
791
+ info.child.kill('SIGKILL');
792
+ }
793
+ catch {
794
+ // Process might already be dead
795
+ }
796
+ logger.error(`[${laneName}] Aborting lane due to repeated stalls`);
797
+ // Save POF for failed recovery
798
+ const recoveryState = autoRecoveryManager.getState(laneName);
799
+ if (recoveryState) {
800
+ try {
801
+ const laneStatePath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'state.json');
802
+ const laneState = (0, state_1.loadState)(laneStatePath);
803
+ const pofDir = (0, path_1.safeJoin)(runRoot, '..', '..', 'pof');
804
+ const pofEntry = (0, auto_recovery_1.createPOFFromRecoveryState)(runId, runRoot, laneName, recoveryState, laneState, recoveryState.diagnosticInfo);
805
+ (0, auto_recovery_1.savePOF)(runId, pofDir, pofEntry);
806
+ }
807
+ catch (pofError) {
808
+ logger.warn(`[${laneName}] Failed to save POF: ${pofError.message}`);
809
+ }
810
+ }
811
+ }
812
+ else if (analysis.action === failure_policy_1.RecoveryAction.SEND_GIT_GUIDANCE) {
813
+ // Send guidance message to agent for git issues
814
+ const interventionPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'intervention.txt');
815
+ // Determine which guidance to send based on the failure type
816
+ let guidance;
817
+ if (analysis.type === failure_policy_1.FailureType.GIT_PUSH_REJECTED) {
818
+ guidance = (0, auto_recovery_1.getGitPushFailureGuidance)();
819
+ }
820
+ else if (analysis.type === failure_policy_1.FailureType.MERGE_CONFLICT) {
821
+ guidance = (0, auto_recovery_1.getMergeConflictGuidance)();
822
+ }
823
+ else {
824
+ guidance = (0, auto_recovery_1.getGitErrorGuidance)(analysis.message);
825
+ }
826
+ try {
827
+ fs.writeFileSync(interventionPath, guidance);
828
+ info.lastActivity = now;
829
+ logger.info(`[${laneName}] Sent git issue guidance to agent`);
830
+ }
831
+ catch (e) {
832
+ logger.error(`[${laneName}] Failed to send guidance: ${e.message}`);
833
+ }
834
+ }
835
+ }
836
+ }
837
+ continue;
838
+ }
839
+ const finished = result;
840
+ const info = running.get(finished.name);
525
841
  running.delete(finished.name);
526
842
  exitCodes[finished.name] = finished.code;
843
+ // Unregister from auto-recovery manager
844
+ autoRecoveryManager.unregisterLane(finished.name);
527
845
  if (finished.code === 0) {
528
846
  completedLanes.add(finished.name);
529
847
  events_1.events.emit('lane.completed', {
@@ -533,7 +851,7 @@ async function orchestrate(tasksDir, options = {}) {
533
851
  }
534
852
  else if (finished.code === 2) {
535
853
  // Blocked by dependency
536
- const statePath = path.join(laneRunDirs[finished.name], 'state.json');
854
+ const statePath = (0, path_1.safeJoin)(laneRunDirs[finished.name], 'state.json');
537
855
  const state = (0, state_1.loadState)(statePath);
538
856
  if (state && state.dependencyRequest) {
539
857
  blockedLanes.set(finished.name, state.dependencyRequest);
@@ -553,11 +871,27 @@ async function orchestrate(tasksDir, options = {}) {
553
871
  }
554
872
  }
555
873
  else {
874
+ // Check if it was a restart request
875
+ if (info.stallPhase === 2) {
876
+ logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
877
+ // Update startIndex from current state to resume from the same task
878
+ const statePath = (0, path_1.safeJoin)(laneRunDirs[finished.name], 'state.json');
879
+ const state = (0, state_1.loadState)(statePath);
880
+ if (state) {
881
+ const lane = lanes.find(l => l.name === finished.name);
882
+ if (lane) {
883
+ lane.startIndex = state.currentTaskIndex;
884
+ }
885
+ }
886
+ // Note: we don't add to failedLanes or completedLanes,
887
+ // so it will be eligible to start again in the next iteration.
888
+ continue;
889
+ }
556
890
  failedLanes.add(finished.name);
557
891
  events_1.events.emit('lane.failed', {
558
892
  laneName: finished.name,
559
893
  exitCode: finished.code,
560
- error: 'Process exited with non-zero code',
894
+ error: info.stallPhase === 3 ? 'Stopped due to repeated stall' : 'Process exited with non-zero code',
561
895
  });
562
896
  }
563
897
  printLaneStatus(lanes, laneRunDirs);