@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
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Dependency management utilities for CursorFlow
3
+ *
4
+ * Features:
5
+ * - Cyclic dependency detection
6
+ * - Dependency wait with timeout
7
+ * - Topological sorting
8
+ */
9
+
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { safeJoin } from './path';
13
+ import { loadState } from './state';
14
+ import { LaneState } from './types';
15
+ import * as logger from './logger';
16
+
17
+ export interface DependencyInfo {
18
+ name: string;
19
+ dependsOn: string[];
20
+ }
21
+
22
+ export interface CycleDetectionResult {
23
+ hasCycle: boolean;
24
+ cycle: string[] | null;
25
+ sortedOrder: string[] | null;
26
+ }
27
+
28
+ export interface DependencyWaitOptions {
29
+ /** Maximum time to wait in milliseconds */
30
+ timeoutMs?: number;
31
+ /** Polling interval in milliseconds */
32
+ pollIntervalMs?: number;
33
+ /** Callback when timeout is reached */
34
+ onTimeout?: 'fail' | 'warn' | 'continue';
35
+ /** Callback for progress updates */
36
+ onProgress?: (pending: string[], completed: string[]) => void;
37
+ }
38
+
39
+ export const DEFAULT_DEPENDENCY_WAIT_OPTIONS: Required<Omit<DependencyWaitOptions, 'onProgress'>> = {
40
+ timeoutMs: 30 * 60 * 1000, // 30 minutes
41
+ pollIntervalMs: 5000, // 5 seconds
42
+ onTimeout: 'fail',
43
+ };
44
+
45
+ export interface DependencyWaitResult {
46
+ success: boolean;
47
+ timedOut: boolean;
48
+ failedDependencies: string[];
49
+ completedDependencies: string[];
50
+ elapsedMs: number;
51
+ }
52
+
53
+ /**
54
+ * Detect cyclic dependencies in a list of lanes
55
+ */
56
+ export function detectCyclicDependencies(lanes: DependencyInfo[]): CycleDetectionResult {
57
+ // Build adjacency graph
58
+ const graph = new Map<string, Set<string>>();
59
+ const allNodes = new Set<string>();
60
+
61
+ for (const lane of lanes) {
62
+ allNodes.add(lane.name);
63
+ graph.set(lane.name, new Set(lane.dependsOn));
64
+
65
+ // Add dependency nodes even if they're not in the list
66
+ for (const dep of lane.dependsOn) {
67
+ allNodes.add(dep);
68
+ if (!graph.has(dep)) {
69
+ graph.set(dep, new Set());
70
+ }
71
+ }
72
+ }
73
+
74
+ // Kahn's algorithm for topological sort with cycle detection
75
+ const inDegree = new Map<string, number>();
76
+
77
+ // Initialize in-degrees
78
+ for (const node of allNodes) {
79
+ inDegree.set(node, 0);
80
+ }
81
+
82
+ for (const [, deps] of graph) {
83
+ for (const dep of deps) {
84
+ inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
85
+ }
86
+ }
87
+
88
+ // Queue of nodes with no incoming edges
89
+ const queue: string[] = [];
90
+ for (const [node, degree] of inDegree) {
91
+ if (degree === 0) {
92
+ queue.push(node);
93
+ }
94
+ }
95
+
96
+ const sorted: string[] = [];
97
+
98
+ while (queue.length > 0) {
99
+ const node = queue.shift()!;
100
+ sorted.push(node);
101
+
102
+ const deps = graph.get(node) || new Set();
103
+ for (const dep of deps) {
104
+ const newDegree = (inDegree.get(dep) || 0) - 1;
105
+ inDegree.set(dep, newDegree);
106
+
107
+ if (newDegree === 0) {
108
+ queue.push(dep);
109
+ }
110
+ }
111
+ }
112
+
113
+ // If not all nodes are in sorted order, there's a cycle
114
+ if (sorted.length !== allNodes.size) {
115
+ // Find the cycle using DFS
116
+ const cycle = findCycle(graph, allNodes);
117
+ return {
118
+ hasCycle: true,
119
+ cycle,
120
+ sortedOrder: null,
121
+ };
122
+ }
123
+
124
+ return {
125
+ hasCycle: false,
126
+ cycle: null,
127
+ sortedOrder: sorted,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Find a cycle in the graph using DFS
133
+ */
134
+ function findCycle(graph: Map<string, Set<string>>, allNodes: Set<string>): string[] | null {
135
+ const visited = new Set<string>();
136
+ const recursionStack = new Set<string>();
137
+ const parent = new Map<string, string>();
138
+
139
+ function dfs(node: string): string | null {
140
+ visited.add(node);
141
+ recursionStack.add(node);
142
+
143
+ const deps = graph.get(node) || new Set();
144
+ for (const dep of deps) {
145
+ if (!visited.has(dep)) {
146
+ parent.set(dep, node);
147
+ const cycleNode = dfs(dep);
148
+ if (cycleNode) return cycleNode;
149
+ } else if (recursionStack.has(dep)) {
150
+ // Found a cycle
151
+ parent.set(dep, node);
152
+ return dep;
153
+ }
154
+ }
155
+
156
+ recursionStack.delete(node);
157
+ return null;
158
+ }
159
+
160
+ for (const node of allNodes) {
161
+ if (!visited.has(node)) {
162
+ const cycleNode = dfs(node);
163
+ if (cycleNode) {
164
+ // Reconstruct the cycle
165
+ const cycle: string[] = [cycleNode];
166
+ let current = parent.get(cycleNode);
167
+ while (current && current !== cycleNode) {
168
+ cycle.push(current);
169
+ current = parent.get(current);
170
+ }
171
+ cycle.push(cycleNode);
172
+ return cycle.reverse();
173
+ }
174
+ }
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Get topologically sorted order for lanes
182
+ */
183
+ export function getExecutionOrder(lanes: DependencyInfo[]): string[] | null {
184
+ const result = detectCyclicDependencies(lanes);
185
+
186
+ if (result.hasCycle) {
187
+ return null;
188
+ }
189
+
190
+ // Reverse the sorted order (we want dependencies first)
191
+ return result.sortedOrder!.reverse();
192
+ }
193
+
194
+ /**
195
+ * Wait for task-level dependencies with timeout and progress tracking
196
+ */
197
+ export async function waitForTaskDependencies(
198
+ deps: string[],
199
+ lanesRoot: string,
200
+ options: DependencyWaitOptions = {}
201
+ ): Promise<DependencyWaitResult> {
202
+ const opts = { ...DEFAULT_DEPENDENCY_WAIT_OPTIONS, ...options };
203
+ const startTime = Date.now();
204
+ const pendingDeps = new Set(deps);
205
+ const completedDeps: string[] = [];
206
+ const failedDeps: string[] = [];
207
+
208
+ if (deps.length === 0) {
209
+ return {
210
+ success: true,
211
+ timedOut: false,
212
+ failedDependencies: [],
213
+ completedDependencies: [],
214
+ elapsedMs: 0,
215
+ };
216
+ }
217
+
218
+ logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
219
+
220
+ while (pendingDeps.size > 0) {
221
+ // Check timeout
222
+ const elapsed = Date.now() - startTime;
223
+ if (elapsed > opts.timeoutMs) {
224
+ logger.warn(`Dependency wait timeout after ${Math.round(elapsed / 1000)}s`);
225
+
226
+ if (opts.onTimeout === 'fail') {
227
+ return {
228
+ success: false,
229
+ timedOut: true,
230
+ failedDependencies: Array.from(pendingDeps),
231
+ completedDependencies: completedDeps,
232
+ elapsedMs: elapsed,
233
+ };
234
+ } else if (opts.onTimeout === 'warn') {
235
+ logger.warn('Continuing despite timeout');
236
+ return {
237
+ success: true,
238
+ timedOut: true,
239
+ failedDependencies: [],
240
+ completedDependencies: completedDeps,
241
+ elapsedMs: elapsed,
242
+ };
243
+ }
244
+ // 'continue' - just return success
245
+ return {
246
+ success: true,
247
+ timedOut: true,
248
+ failedDependencies: [],
249
+ completedDependencies: completedDeps,
250
+ elapsedMs: elapsed,
251
+ };
252
+ }
253
+
254
+ for (const dep of pendingDeps) {
255
+ const [laneName, taskName] = dep.split(':');
256
+
257
+ if (!laneName || !taskName) {
258
+ logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
259
+ pendingDeps.delete(dep);
260
+ failedDeps.push(dep);
261
+ continue;
262
+ }
263
+
264
+ const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
265
+
266
+ if (!fs.existsSync(depStatePath)) {
267
+ // Lane hasn't started yet - continue waiting
268
+ continue;
269
+ }
270
+
271
+ try {
272
+ const state = loadState<LaneState>(depStatePath);
273
+
274
+ if (!state) {
275
+ continue;
276
+ }
277
+
278
+ // Check if task is completed
279
+ if (state.completedTasks && state.completedTasks.includes(taskName)) {
280
+ logger.info(`✓ Dependency met: ${dep}`);
281
+ pendingDeps.delete(dep);
282
+ completedDeps.push(dep);
283
+ } else if (state.status === 'failed') {
284
+ // Dependency lane failed
285
+ logger.error(`✗ Dependency failed: ${dep} (Lane ${laneName} failed)`);
286
+ pendingDeps.delete(dep);
287
+ failedDeps.push(dep);
288
+ }
289
+ } catch (e: any) {
290
+ // Ignore parse errors, file might be being written
291
+ logger.warn(`Error reading dependency state: ${e.message}`);
292
+ }
293
+ }
294
+
295
+ // Report progress
296
+ if (options.onProgress) {
297
+ options.onProgress(Array.from(pendingDeps), completedDeps);
298
+ }
299
+
300
+ // Check for failed dependencies
301
+ if (failedDeps.length > 0) {
302
+ return {
303
+ success: false,
304
+ timedOut: false,
305
+ failedDependencies: failedDeps,
306
+ completedDependencies: completedDeps,
307
+ elapsedMs: Date.now() - startTime,
308
+ };
309
+ }
310
+
311
+ if (pendingDeps.size > 0) {
312
+ await new Promise(resolve => setTimeout(resolve, opts.pollIntervalMs));
313
+ }
314
+ }
315
+
316
+ return {
317
+ success: true,
318
+ timedOut: false,
319
+ failedDependencies: [],
320
+ completedDependencies: completedDeps,
321
+ elapsedMs: Date.now() - startTime,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Check if a lane can start based on its dependencies
327
+ */
328
+ export function canLaneStart(
329
+ laneName: string,
330
+ lanes: DependencyInfo[],
331
+ completedLanes: Set<string>,
332
+ failedLanes: Set<string>
333
+ ): { canStart: boolean; reason?: string } {
334
+ const lane = lanes.find(l => l.name === laneName);
335
+
336
+ if (!lane) {
337
+ return { canStart: false, reason: `Lane ${laneName} not found` };
338
+ }
339
+
340
+ for (const dep of lane.dependsOn) {
341
+ if (failedLanes.has(dep)) {
342
+ return {
343
+ canStart: false,
344
+ reason: `Dependency ${dep} has failed`
345
+ };
346
+ }
347
+
348
+ if (!completedLanes.has(dep)) {
349
+ return {
350
+ canStart: false,
351
+ reason: `Waiting for dependency ${dep}`
352
+ };
353
+ }
354
+ }
355
+
356
+ return { canStart: true };
357
+ }
358
+
359
+ /**
360
+ * Get all transitive dependencies for a lane
361
+ */
362
+ export function getTransitiveDependencies(
363
+ laneName: string,
364
+ lanes: DependencyInfo[]
365
+ ): string[] {
366
+ const laneMap = new Map(lanes.map(l => [l.name, l]));
367
+ const visited = new Set<string>();
368
+ const result: string[] = [];
369
+
370
+ function visit(name: string): void {
371
+ if (visited.has(name)) return;
372
+ visited.add(name);
373
+
374
+ const lane = laneMap.get(name);
375
+ if (!lane) return;
376
+
377
+ for (const dep of lane.dependsOn) {
378
+ visit(dep);
379
+ if (!result.includes(dep)) {
380
+ result.push(dep);
381
+ }
382
+ }
383
+ }
384
+
385
+ visit(laneName);
386
+ return result;
387
+ }
388
+
389
+ /**
390
+ * Get lanes that depend on a given lane
391
+ */
392
+ export function getDependentLanes(
393
+ laneName: string,
394
+ lanes: DependencyInfo[]
395
+ ): string[] {
396
+ return lanes
397
+ .filter(l => l.dependsOn.includes(laneName))
398
+ .map(l => l.name);
399
+ }
400
+
401
+ /**
402
+ * Validate dependency configuration
403
+ */
404
+ export function validateDependencies(lanes: DependencyInfo[]): {
405
+ valid: boolean;
406
+ errors: string[];
407
+ warnings: string[];
408
+ } {
409
+ const errors: string[] = [];
410
+ const warnings: string[] = [];
411
+ const laneNames = new Set(lanes.map(l => l.name));
412
+
413
+ // Check for missing dependencies
414
+ for (const lane of lanes) {
415
+ for (const dep of lane.dependsOn) {
416
+ // Check if it's a task-level dependency
417
+ if (dep.includes(':')) {
418
+ const [depLane] = dep.split(':');
419
+ if (!laneNames.has(depLane!)) {
420
+ errors.push(`Lane "${lane.name}" depends on unknown lane "${depLane}"`);
421
+ }
422
+ } else if (!laneNames.has(dep)) {
423
+ errors.push(`Lane "${lane.name}" depends on unknown lane "${dep}"`);
424
+ }
425
+ }
426
+ }
427
+
428
+ // Check for cycles
429
+ const cycleResult = detectCyclicDependencies(lanes);
430
+ if (cycleResult.hasCycle && cycleResult.cycle) {
431
+ errors.push(`Cyclic dependency detected: ${cycleResult.cycle.join(' -> ')}`);
432
+ }
433
+
434
+ // Warning for deeply nested dependencies
435
+ for (const lane of lanes) {
436
+ const transitive = getTransitiveDependencies(lane.name, lanes);
437
+ if (transitive.length > 5) {
438
+ warnings.push(`Lane "${lane.name}" has ${transitive.length} transitive dependencies`);
439
+ }
440
+ }
441
+
442
+ return {
443
+ valid: errors.length === 0,
444
+ errors,
445
+ warnings,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Print dependency graph to console
451
+ */
452
+ export function printDependencyGraph(lanes: DependencyInfo[]): void {
453
+ const cycleResult = detectCyclicDependencies(lanes);
454
+
455
+ logger.section('📊 Dependency Graph');
456
+
457
+ if (cycleResult.hasCycle) {
458
+ logger.error(`⚠️ Cyclic dependency detected: ${cycleResult.cycle?.join(' -> ')}`);
459
+ console.log('');
460
+ }
461
+
462
+ for (const lane of lanes) {
463
+ const deps = lane.dependsOn.length > 0
464
+ ? ` [depends on: ${lane.dependsOn.join(', ')}]`
465
+ : '';
466
+ console.log(` ${logger.COLORS.cyan}${lane.name}${logger.COLORS.reset}${deps}`);
467
+
468
+ for (let i = 0; i < lane.dependsOn.length; i++) {
469
+ const isLast = i === lane.dependsOn.length - 1;
470
+ const prefix = isLast ? '└─' : '├─';
471
+ console.log(` ${prefix} ${lane.dependsOn[i]}`);
472
+ }
473
+ }
474
+
475
+ if (cycleResult.sortedOrder) {
476
+ console.log('');
477
+ console.log(` Execution order: ${cycleResult.sortedOrder.reverse().join(' → ')}`);
478
+ }
479
+
480
+ console.log('');
481
+ }
482
+
@@ -18,6 +18,7 @@ import * as path from 'path';
18
18
  import * as git from './git';
19
19
  import { checkCursorAgentInstalled, checkCursorAuth } from './cursor-agent';
20
20
  import { areCommandsInstalled } from '../cli/setup-commands';
21
+ import { safeJoin } from './path';
21
22
 
22
23
  export type DoctorSeverity = 'error' | 'warn';
23
24
 
@@ -73,9 +74,11 @@ function addIssue(issues: DoctorIssue[], issue: DoctorIssue): void {
73
74
  }
74
75
 
75
76
  function resolveRepoRoot(cwd: string): string | null {
76
- const res = git.runGitResult(['rev-parse', '--show-toplevel'], { cwd });
77
- if (!res.success || !res.stdout) return null;
78
- return res.stdout;
77
+ try {
78
+ return git.getMainRepoRoot(cwd);
79
+ } catch {
80
+ return null;
81
+ }
79
82
  }
80
83
 
81
84
  function isInsideGitWorktree(cwd: string): boolean {
@@ -149,7 +152,7 @@ function readLaneJsonFiles(tasksDir: string): { path: string; json: any; fileNam
149
152
  .readdirSync(tasksDir)
150
153
  .filter(f => f.endsWith('.json'))
151
154
  .sort()
152
- .map(f => path.join(tasksDir, f));
155
+ .map(f => safeJoin(tasksDir, f));
153
156
 
154
157
  return files.map(p => {
155
158
  const raw = fs.readFileSync(p, 'utf8');
@@ -160,8 +163,12 @@ function readLaneJsonFiles(tasksDir: string): { path: string; json: any; fileNam
160
163
 
161
164
  function collectBaseBranchesFromLanes(lanes: { path: string; json: any }[], defaultBaseBranch: string): string[] {
162
165
  const set = new Set<string>();
166
+ // CursorFlow now defaults to the current branch to maintain dependency structure
167
+ const currentBranch = git.getCurrentBranch();
168
+ set.add(currentBranch);
169
+
163
170
  for (const lane of lanes) {
164
- const baseBranch = String(lane.json?.baseBranch || defaultBaseBranch || 'main').trim();
171
+ const baseBranch = String(lane.json?.baseBranch || defaultBaseBranch || currentBranch).trim();
165
172
  if (baseBranch) set.add(baseBranch);
166
173
  }
167
174
  return Array.from(set);
@@ -428,7 +435,7 @@ function checkDiskSpace(dir: string): { ok: boolean; freeBytes?: number; error?:
428
435
  const { spawnSync } = require('child_process');
429
436
  try {
430
437
  // Validate and normalize the directory path to prevent command injection
431
- const safePath = path.resolve(dir);
438
+ const safePath = path.resolve(dir); // nosemgrep
432
439
 
433
440
  // Use spawnSync instead of execSync to avoid shell interpolation vulnerabilities
434
441
  // df -B1 returns bytes. We look for the line corresponding to our directory.
@@ -612,7 +619,7 @@ function validateBranchNames(
612
619
  const DOCTOR_STATUS_FILE = '.cursorflow/doctor-status.json';
613
620
 
614
621
  export function saveDoctorStatus(repoRoot: string, report: DoctorReport): void {
615
- const statusPath = path.join(repoRoot, DOCTOR_STATUS_FILE);
622
+ const statusPath = safeJoin(repoRoot, DOCTOR_STATUS_FILE);
616
623
  const statusDir = path.dirname(statusPath);
617
624
 
618
625
  if (!fs.existsSync(statusDir)) {
@@ -630,7 +637,7 @@ export function saveDoctorStatus(repoRoot: string, report: DoctorReport): void {
630
637
  }
631
638
 
632
639
  export function getDoctorStatus(repoRoot: string): { lastRun: number; ok: boolean; issueCount: number } | null {
633
- const statusPath = path.join(repoRoot, DOCTOR_STATUS_FILE);
640
+ const statusPath = safeJoin(repoRoot, DOCTOR_STATUS_FILE);
634
641
  if (!fs.existsSync(statusPath)) return null;
635
642
 
636
643
  try {
@@ -830,7 +837,7 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
830
837
  });
831
838
  } else {
832
839
  // Advanced check: .gitignore check for worktrees
833
- const gitignorePath = path.join(gitCwd, '.gitignore');
840
+ const gitignorePath = safeJoin(gitCwd, '.gitignore');
834
841
  const worktreeDirName = '_cursorflow'; // Default directory name
835
842
  if (fs.existsSync(gitignorePath)) {
836
843
  const content = fs.readFileSync(gitignorePath, 'utf8');
@@ -853,7 +860,7 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
853
860
  if (options.tasksDir) {
854
861
  const tasksDirAbs = path.isAbsolute(options.tasksDir)
855
862
  ? options.tasksDir
856
- : path.resolve(cwd, options.tasksDir);
863
+ : safeJoin(cwd, options.tasksDir);
857
864
  context.tasksDir = tasksDirAbs;
858
865
 
859
866
  if (!fs.existsSync(tasksDirAbs)) {
@@ -893,7 +900,7 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
893
900
  });
894
901
  } else {
895
902
  // Validate base branches
896
- const baseBranches = collectBaseBranchesFromLanes(lanes, 'main');
903
+ const baseBranches = collectBaseBranchesFromLanes(lanes, git.getCurrentBranch());
897
904
  for (const baseBranch of baseBranches) {
898
905
  if (!branchExists(gitCwd, baseBranch)) {
899
906
  addIssue(issues, {