@litmers/cursorflow-orchestrator 0.1.30 → 0.1.34

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 (129) hide show
  1. package/README.md +144 -52
  2. package/commands/cursorflow-add.md +159 -0
  3. package/commands/cursorflow-monitor.md +23 -2
  4. package/commands/cursorflow-new.md +87 -0
  5. package/dist/cli/add.d.ts +7 -0
  6. package/dist/cli/add.js +377 -0
  7. package/dist/cli/add.js.map +1 -0
  8. package/dist/cli/clean.js +1 -0
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/config.d.ts +7 -0
  11. package/dist/cli/config.js +181 -0
  12. package/dist/cli/config.js.map +1 -0
  13. package/dist/cli/index.js +34 -30
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/logs.js +7 -33
  16. package/dist/cli/logs.js.map +1 -1
  17. package/dist/cli/monitor.js +51 -62
  18. package/dist/cli/monitor.js.map +1 -1
  19. package/dist/cli/new.d.ts +7 -0
  20. package/dist/cli/new.js +232 -0
  21. package/dist/cli/new.js.map +1 -0
  22. package/dist/cli/prepare.js +95 -193
  23. package/dist/cli/prepare.js.map +1 -1
  24. package/dist/cli/resume.js +11 -47
  25. package/dist/cli/resume.js.map +1 -1
  26. package/dist/cli/run.js +27 -22
  27. package/dist/cli/run.js.map +1 -1
  28. package/dist/cli/tasks.js +1 -2
  29. package/dist/cli/tasks.js.map +1 -1
  30. package/dist/core/failure-policy.d.ts +9 -0
  31. package/dist/core/failure-policy.js +9 -0
  32. package/dist/core/failure-policy.js.map +1 -1
  33. package/dist/core/orchestrator.d.ts +20 -6
  34. package/dist/core/orchestrator.js +217 -331
  35. package/dist/core/orchestrator.js.map +1 -1
  36. package/dist/core/runner/agent.d.ts +27 -0
  37. package/dist/core/runner/agent.js +294 -0
  38. package/dist/core/runner/agent.js.map +1 -0
  39. package/dist/core/runner/index.d.ts +5 -0
  40. package/dist/core/runner/index.js +22 -0
  41. package/dist/core/runner/index.js.map +1 -0
  42. package/dist/core/runner/pipeline.d.ts +9 -0
  43. package/dist/core/runner/pipeline.js +539 -0
  44. package/dist/core/runner/pipeline.js.map +1 -0
  45. package/dist/core/runner/prompt.d.ts +25 -0
  46. package/dist/core/runner/prompt.js +175 -0
  47. package/dist/core/runner/prompt.js.map +1 -0
  48. package/dist/core/runner/task.d.ts +26 -0
  49. package/dist/core/runner/task.js +283 -0
  50. package/dist/core/runner/task.js.map +1 -0
  51. package/dist/core/runner/utils.d.ts +37 -0
  52. package/dist/core/runner/utils.js +161 -0
  53. package/dist/core/runner/utils.js.map +1 -0
  54. package/dist/core/runner.d.ts +2 -96
  55. package/dist/core/runner.js +11 -1136
  56. package/dist/core/runner.js.map +1 -1
  57. package/dist/core/stall-detection.d.ts +326 -0
  58. package/dist/core/stall-detection.js +781 -0
  59. package/dist/core/stall-detection.js.map +1 -0
  60. package/dist/types/config.d.ts +6 -6
  61. package/dist/types/flow.d.ts +84 -0
  62. package/dist/types/flow.js +10 -0
  63. package/dist/types/flow.js.map +1 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/index.js +3 -3
  66. package/dist/types/index.js.map +1 -1
  67. package/dist/types/lane.d.ts +0 -2
  68. package/dist/types/logging.d.ts +5 -1
  69. package/dist/types/task.d.ts +7 -11
  70. package/dist/utils/config.js +7 -15
  71. package/dist/utils/config.js.map +1 -1
  72. package/dist/utils/dependency.d.ts +36 -1
  73. package/dist/utils/dependency.js +256 -1
  74. package/dist/utils/dependency.js.map +1 -1
  75. package/dist/utils/enhanced-logger.d.ts +45 -82
  76. package/dist/utils/enhanced-logger.js +238 -844
  77. package/dist/utils/enhanced-logger.js.map +1 -1
  78. package/dist/utils/git.d.ts +29 -0
  79. package/dist/utils/git.js +115 -5
  80. package/dist/utils/git.js.map +1 -1
  81. package/dist/utils/state.js +0 -2
  82. package/dist/utils/state.js.map +1 -1
  83. package/dist/utils/task-service.d.ts +2 -2
  84. package/dist/utils/task-service.js +40 -31
  85. package/dist/utils/task-service.js.map +1 -1
  86. package/package.json +4 -3
  87. package/src/cli/add.ts +397 -0
  88. package/src/cli/clean.ts +1 -0
  89. package/src/cli/config.ts +177 -0
  90. package/src/cli/index.ts +36 -32
  91. package/src/cli/logs.ts +7 -31
  92. package/src/cli/monitor.ts +55 -71
  93. package/src/cli/new.ts +235 -0
  94. package/src/cli/prepare.ts +98 -205
  95. package/src/cli/resume.ts +13 -56
  96. package/src/cli/run.ts +311 -306
  97. package/src/cli/tasks.ts +1 -2
  98. package/src/core/failure-policy.ts +9 -0
  99. package/src/core/orchestrator.ts +281 -375
  100. package/src/core/runner/agent.ts +314 -0
  101. package/src/core/runner/index.ts +6 -0
  102. package/src/core/runner/pipeline.ts +567 -0
  103. package/src/core/runner/prompt.ts +174 -0
  104. package/src/core/runner/task.ts +320 -0
  105. package/src/core/runner/utils.ts +142 -0
  106. package/src/core/runner.ts +8 -1347
  107. package/src/core/stall-detection.ts +936 -0
  108. package/src/types/config.ts +6 -6
  109. package/src/types/flow.ts +91 -0
  110. package/src/types/index.ts +15 -3
  111. package/src/types/lane.ts +0 -2
  112. package/src/types/logging.ts +5 -1
  113. package/src/types/task.ts +7 -11
  114. package/src/utils/config.ts +8 -16
  115. package/src/utils/dependency.ts +311 -2
  116. package/src/utils/enhanced-logger.ts +263 -927
  117. package/src/utils/git.ts +145 -5
  118. package/src/utils/state.ts +0 -2
  119. package/src/utils/task-service.ts +48 -40
  120. package/commands/cursorflow-review.md +0 -56
  121. package/commands/cursorflow-runs.md +0 -59
  122. package/dist/cli/runs.d.ts +0 -5
  123. package/dist/cli/runs.js +0 -214
  124. package/dist/cli/runs.js.map +0 -1
  125. package/dist/core/reviewer.d.ts +0 -66
  126. package/dist/core/reviewer.js +0 -265
  127. package/dist/core/reviewer.js.map +0 -1
  128. package/src/cli/runs.ts +0 -212
  129. package/src/core/reviewer.ts +0 -285
@@ -51,6 +51,8 @@ export interface EnhancedLogConfig {
51
51
 
52
52
  export interface CursorFlowConfig {
53
53
  tasksDir: string;
54
+ /** New flows directory (replaces tasksDir in new architecture) */
55
+ flowsDir: string;
54
56
  logsDir: string;
55
57
  pofDir: string;
56
58
  /** Base branch (optional, auto-detected from current branch if not specified) */
@@ -60,20 +62,18 @@ export interface CursorFlowConfig {
60
62
  pollInterval: number;
61
63
  allowDependencyChange: boolean;
62
64
  lockfileReadOnly: boolean;
63
- enableReview: boolean;
64
- reviewModel: string;
65
- reviewAllTasks?: boolean;
66
- maxReviewIterations: number;
67
65
  defaultLaneConfig: LaneConfig;
68
66
  logLevel: string;
69
67
  verboseGit: boolean;
70
68
  worktreePrefix: string;
71
69
  maxConcurrentLanes: number;
72
70
  projectRoot: string;
73
- /** Output format for cursor-agent (default: 'stream-json') */
74
- agentOutputFormat: 'stream-json' | 'json' | 'plain';
71
+ /** Output format for cursor-agent (default: 'json') */
72
+ agentOutputFormat: 'json' | 'plain';
75
73
  webhooks?: WebhookConfig[];
76
74
  /** Enhanced logging configuration */
77
75
  enhancedLogging?: Partial<EnhancedLogConfig>;
76
+ /** Default AI model for tasks (default: 'gemini-3-flash') */
77
+ defaultModel: string;
78
78
  }
79
79
 
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Flow type definitions for CursorFlow
3
+ *
4
+ * Flow: A collection of Lanes working together on a feature
5
+ * Lane: A parallel execution path with its own worktree
6
+ * Task: A unit of work executed by an AI agent
7
+ */
8
+
9
+ /**
10
+ * Flow metadata stored in flow.meta.json
11
+ */
12
+ export interface FlowMeta {
13
+ /** Unique ID (sequential number) */
14
+ id: string;
15
+ /** Human-readable flow name */
16
+ name: string;
17
+ /** Creation timestamp (ISO 8601) */
18
+ createdAt: string;
19
+ /** Creator identifier */
20
+ createdBy: string;
21
+ /** Git branch this flow started from */
22
+ baseBranch: string;
23
+ /** Current flow status */
24
+ status: FlowStatus;
25
+ /** List of lane names in this flow */
26
+ lanes: string[];
27
+ }
28
+
29
+ /**
30
+ * Flow execution status
31
+ */
32
+ export type FlowStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
33
+
34
+ /**
35
+ * Lane configuration stored in {NN}-{laneName}.json
36
+ */
37
+ export interface LaneConfig {
38
+ /** Lane identifier (matches filename without prefix/extension) */
39
+ laneName: string;
40
+ /** Branch prefix for this lane */
41
+ branchPrefix?: string;
42
+ /** Tasks to execute in this lane */
43
+ tasks: FlowTask[];
44
+ }
45
+
46
+ /**
47
+ * Task definition within a lane
48
+ */
49
+ export interface FlowTask {
50
+ /** Task identifier (unique within lane) */
51
+ name: string;
52
+ /** AI model to use */
53
+ model: string;
54
+ /** Task prompt/instructions */
55
+ prompt: string;
56
+ /** Acceptance criteria (optional) */
57
+ acceptanceCriteria?: string[];
58
+ /** Dependencies: wait for these tasks to complete first */
59
+ dependsOn?: string[];
60
+ /** Task timeout in milliseconds (optional) */
61
+ timeout?: number;
62
+ }
63
+
64
+ /**
65
+ * Parsed task spec from CLI --task option
66
+ */
67
+ export interface ParsedTaskSpec {
68
+ name: string;
69
+ model: string;
70
+ prompt: string;
71
+ acceptanceCriteria?: string[];
72
+ dependsOn?: string[];
73
+ timeout?: number;
74
+ }
75
+
76
+ /**
77
+ * Flow directory info for listing
78
+ */
79
+ export interface FlowInfo {
80
+ /** Flow ID */
81
+ id: string;
82
+ /** Flow name */
83
+ name: string;
84
+ /** Full directory path */
85
+ path: string;
86
+ /** Flow metadata */
87
+ meta: FlowMeta;
88
+ /** Lane count */
89
+ laneCount: number;
90
+ }
91
+
@@ -3,15 +3,27 @@
3
3
  * Re-exports all types from separate modules
4
4
  */
5
5
 
6
- // Config
6
+ // Config (LaneConfig defined here)
7
7
  export * from './config';
8
8
 
9
- // Lane
9
+ // Lane (LaneInfo, LaneState, etc.)
10
10
  export * from './lane';
11
11
 
12
12
  // Task
13
13
  export * from './task';
14
14
 
15
+ // Flow (new architecture) - explicit exports to avoid conflicts
16
+ export {
17
+ FlowMeta,
18
+ FlowStatus,
19
+ FlowTask,
20
+ ParsedTaskSpec,
21
+ // Note: LaneConfig and FlowInfo are also defined in config.ts and run.ts
22
+ // Use aliases if you need the flow-specific versions:
23
+ LaneConfig as FlowLaneConfig,
24
+ FlowInfo as FlowDirInfo,
25
+ } from './flow';
26
+
15
27
  // Agent
16
28
  export * from './agent';
17
29
 
@@ -24,6 +36,6 @@ export * from './events';
24
36
  // Logging
25
37
  export * from './logging';
26
38
 
27
- // Run
39
+ // Run (FlowInfo also defined here - using this version as default)
28
40
  export * from './run';
29
41
 
package/src/types/lane.ts CHANGED
@@ -35,7 +35,6 @@ export interface LaneState {
35
35
  dependencyRequest: DependencyRequestPlan | null;
36
36
  updatedAt?: number;
37
37
  tasksFile?: string; // Original tasks file path
38
- dependsOn?: string[];
39
38
  pid?: number;
40
39
  /** List of completed task names in this lane */
41
40
  completedTasks?: string[];
@@ -51,6 +50,5 @@ export interface LaneFileInfo {
51
50
  preset: string;
52
51
  taskCount: number;
53
52
  taskFlow: string;
54
- dependsOn: string[];
55
53
  }
56
54
 
@@ -71,11 +71,15 @@ export interface LogSession {
71
71
  model?: string;
72
72
  startTime: number;
73
73
  metadata?: Record<string, any>;
74
+ /** Lane index (0-based) for display as L01, L02, etc. */
75
+ laneIndex?: number;
76
+ /** Task index (0-based) for display as T01, T02, etc. */
77
+ taskIndex?: number;
74
78
  }
75
79
 
76
80
  export interface ConversationEntry {
77
81
  timestamp: string;
78
- role: 'user' | 'assistant' | 'reviewer' | 'system' | 'intervention';
82
+ role: 'user' | 'assistant' | 'system' | 'intervention';
79
83
  task: string | null;
80
84
  fullText: string;
81
85
  textLength: number;
package/src/types/task.ts CHANGED
@@ -9,8 +9,6 @@ export interface Task {
9
9
  name: string;
10
10
  prompt: string;
11
11
  model?: string;
12
- /** Acceptance criteria for the AI reviewer to validate */
13
- acceptanceCriteria?: string[];
14
12
  /** Task-level dependencies (format: "lane:task") */
15
13
  dependsOn?: string[];
16
14
  /** Task execution timeout in milliseconds. Overrides lane-level timeout. */
@@ -19,7 +17,6 @@ export interface Task {
19
17
 
20
18
  export interface RunnerConfig {
21
19
  tasks: Task[];
22
- dependsOn?: string[];
23
20
  pipelineBranch?: string;
24
21
  worktreeDir?: string;
25
22
  branchPrefix?: string;
@@ -27,13 +24,8 @@ export interface RunnerConfig {
27
24
  baseBranch?: string;
28
25
  model?: string;
29
26
  dependencyPolicy: DependencyPolicy;
30
- enableReview?: boolean;
31
- /** Output format for cursor-agent (default: 'stream-json') */
32
- agentOutputFormat?: 'stream-json' | 'json' | 'plain';
33
- reviewModel?: string;
34
- reviewAllTasks?: boolean;
35
- maxReviewIterations?: number;
36
- acceptanceCriteria?: string[];
27
+ /** Output format for cursor-agent (default: 'json') */
28
+ agentOutputFormat?: 'json' | 'plain';
37
29
  /** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
38
30
  timeout?: number;
39
31
  /**
@@ -48,6 +40,11 @@ export interface RunnerConfig {
48
40
  * Default: false
49
41
  */
50
42
  noGit?: boolean;
43
+ /**
44
+ * Enable verbose Git logging.
45
+ * Default: false
46
+ */
47
+ verboseGit?: boolean;
51
48
  }
52
49
 
53
50
  export interface TaskDirInfo {
@@ -71,7 +68,6 @@ export interface TaskExecutionResult {
71
68
  export interface TaskResult {
72
69
  taskName: string;
73
70
  taskBranch: string;
74
- acceptanceCriteria?: string[];
75
71
  [key: string]: any;
76
72
  }
77
73
 
@@ -44,6 +44,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
44
44
  const defaults: CursorFlowConfig = {
45
45
  // Directories
46
46
  tasksDir: '_cursorflow/tasks',
47
+ flowsDir: '_cursorflow/flows',
47
48
  logsDir: '_cursorflow/logs',
48
49
  pofDir: '_cursorflow/pof',
49
50
 
@@ -59,12 +60,6 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
59
60
  allowDependencyChange: false,
60
61
  lockfileReadOnly: true,
61
62
 
62
- // Review
63
- enableReview: false,
64
- reviewModel: 'sonnet-4.5-thinking',
65
- reviewAllTasks: false,
66
- maxReviewIterations: 3,
67
-
68
63
  // Lane defaults
69
64
  defaultLaneConfig: {
70
65
  devPort: 3001,
@@ -73,12 +68,12 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
73
68
 
74
69
  // Logging
75
70
  logLevel: 'info',
76
- verboseGit: false,
71
+ verboseGit: true,
77
72
 
78
73
  // Advanced
79
74
  worktreePrefix: 'cursorflow-',
80
75
  maxConcurrentLanes: 10,
81
- agentOutputFormat: 'stream-json',
76
+ agentOutputFormat: 'json',
82
77
 
83
78
  // Webhooks
84
79
  webhooks: [],
@@ -95,6 +90,9 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
95
90
  timestampFormat: 'iso',
96
91
  },
97
92
 
93
+ // Default AI model
94
+ defaultModel: 'gemini-3-flash',
95
+
98
96
  // Internal
99
97
  projectRoot,
100
98
  };
@@ -188,12 +186,6 @@ export function createDefaultConfig(projectRoot: string, force = false): string
188
186
  allowDependencyChange: false,
189
187
  lockfileReadOnly: true,
190
188
 
191
- // Review configuration
192
- enableReview: false,
193
- reviewModel: 'sonnet-4.5-thinking',
194
- reviewAllTasks: false,
195
- maxReviewIterations: 3,
196
-
197
189
  // Lane configuration
198
190
  defaultLaneConfig: {
199
191
  devPort: 3001, // 3000 + laneNumber
@@ -202,12 +194,12 @@ export function createDefaultConfig(projectRoot: string, force = false): string
202
194
 
203
195
  // Logging
204
196
  logLevel: 'info', // 'error' | 'warn' | 'info' | 'debug'
205
- verboseGit: false,
197
+ verboseGit: true,
206
198
 
207
199
  // Advanced
208
200
  worktreePrefix: 'cursorflow-',
209
201
  maxConcurrentLanes: 10,
210
- agentOutputFormat: 'stream-json', // 'stream-json' | 'json' | 'plain'
202
+ agentOutputFormat: 'json', // 'json' | 'plain'
211
203
 
212
204
  // Webhook configuration
213
205
  // webhooks: [
@@ -2,16 +2,19 @@
2
2
  * Dependency management utilities for CursorFlow
3
3
  *
4
4
  * Features:
5
- * - Cyclic dependency detection
5
+ * - Task-level cyclic dependency detection
6
6
  * - Dependency wait with timeout
7
7
  * - Topological sorting
8
+ *
9
+ * Note: Lane-level dependencies have been removed.
10
+ * Use task-level dependencies (format: "lane:task") for fine-grained control.
8
11
  */
9
12
 
10
13
  import * as fs from 'fs';
11
14
  import * as path from 'path';
12
15
  import { safeJoin } from './path';
13
16
  import { loadState } from './state';
14
- import { LaneState } from './types';
17
+ import { LaneState, Task, RunnerConfig } from './types';
15
18
  import * as logger from './logger';
16
19
 
17
20
  export interface DependencyInfo {
@@ -19,6 +22,18 @@ export interface DependencyInfo {
19
22
  dependsOn: string[];
20
23
  }
21
24
 
25
+ /** Task-level dependency info for cycle detection */
26
+ export interface TaskDependencyInfo {
27
+ /** Full identifier: "lane:task" */
28
+ id: string;
29
+ /** Lane name */
30
+ lane: string;
31
+ /** Task name */
32
+ task: string;
33
+ /** Dependencies in "lane:task" format */
34
+ dependsOn: string[];
35
+ }
36
+
22
37
  export interface CycleDetectionResult {
23
38
  hasCycle: boolean;
24
39
  cycle: string[] | null;
@@ -480,3 +495,297 @@ export function printDependencyGraph(lanes: DependencyInfo[]): void {
480
495
  console.log('');
481
496
  }
482
497
 
498
+ // ============================================================================
499
+ // Task-Level Dependency Detection (New)
500
+ // ============================================================================
501
+
502
+ /**
503
+ * Extract all task dependencies from lane configuration files
504
+ */
505
+ export function extractTaskDependencies(tasksDir: string): TaskDependencyInfo[] {
506
+ if (!fs.existsSync(tasksDir)) {
507
+ return [];
508
+ }
509
+
510
+ const tasks: TaskDependencyInfo[] = [];
511
+ const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
512
+
513
+ for (const file of files) {
514
+ const filePath = safeJoin(tasksDir, file);
515
+ const laneName = path.basename(file, '.json');
516
+
517
+ try {
518
+ const config = JSON.parse(fs.readFileSync(filePath, 'utf8')) as RunnerConfig;
519
+
520
+ for (const task of config.tasks || []) {
521
+ const taskId = `${laneName}:${task.name}`;
522
+ tasks.push({
523
+ id: taskId,
524
+ lane: laneName,
525
+ task: task.name,
526
+ dependsOn: task.dependsOn || [],
527
+ });
528
+ }
529
+ } catch (e) {
530
+ logger.warn(`Failed to parse task config from ${file}: ${e}`);
531
+ }
532
+ }
533
+
534
+ return tasks;
535
+ }
536
+
537
+ /**
538
+ * Detect cyclic dependencies in task-level dependencies
539
+ */
540
+ export function detectTaskCyclicDependencies(tasks: TaskDependencyInfo[]): CycleDetectionResult {
541
+ // Build adjacency graph using task IDs (lane:task format)
542
+ const graph = new Map<string, Set<string>>();
543
+ const allNodes = new Set<string>();
544
+
545
+ for (const task of tasks) {
546
+ allNodes.add(task.id);
547
+ graph.set(task.id, new Set(task.dependsOn));
548
+
549
+ // Add dependency nodes even if they're not in the list
550
+ for (const dep of task.dependsOn) {
551
+ allNodes.add(dep);
552
+ if (!graph.has(dep)) {
553
+ graph.set(dep, new Set());
554
+ }
555
+ }
556
+ }
557
+
558
+ // Kahn's algorithm for topological sort with cycle detection
559
+ const inDegree = new Map<string, number>();
560
+
561
+ // Initialize in-degrees
562
+ for (const node of allNodes) {
563
+ inDegree.set(node, 0);
564
+ }
565
+
566
+ for (const [, deps] of graph) {
567
+ for (const dep of deps) {
568
+ inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
569
+ }
570
+ }
571
+
572
+ // Queue of nodes with no incoming edges
573
+ const queue: string[] = [];
574
+ for (const [node, degree] of inDegree) {
575
+ if (degree === 0) {
576
+ queue.push(node);
577
+ }
578
+ }
579
+
580
+ const sorted: string[] = [];
581
+
582
+ while (queue.length > 0) {
583
+ const node = queue.shift()!;
584
+ sorted.push(node);
585
+
586
+ const deps = graph.get(node) || new Set();
587
+ for (const dep of deps) {
588
+ const newDegree = (inDegree.get(dep) || 0) - 1;
589
+ inDegree.set(dep, newDegree);
590
+
591
+ if (newDegree === 0) {
592
+ queue.push(dep);
593
+ }
594
+ }
595
+ }
596
+
597
+ // If not all nodes are in sorted order, there's a cycle
598
+ if (sorted.length !== allNodes.size) {
599
+ const cycle = findTaskCycle(graph, allNodes);
600
+ return {
601
+ hasCycle: true,
602
+ cycle,
603
+ sortedOrder: null,
604
+ };
605
+ }
606
+
607
+ return {
608
+ hasCycle: false,
609
+ cycle: null,
610
+ sortedOrder: sorted,
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Find a cycle in task dependency graph using DFS
616
+ */
617
+ function findTaskCycle(graph: Map<string, Set<string>>, allNodes: Set<string>): string[] | null {
618
+ const visited = new Set<string>();
619
+ const recursionStack = new Set<string>();
620
+ const parent = new Map<string, string>();
621
+
622
+ function dfs(node: string): string | null {
623
+ visited.add(node);
624
+ recursionStack.add(node);
625
+
626
+ const deps = graph.get(node) || new Set();
627
+ for (const dep of deps) {
628
+ if (!visited.has(dep)) {
629
+ parent.set(dep, node);
630
+ const cycleNode = dfs(dep);
631
+ if (cycleNode) return cycleNode;
632
+ } else if (recursionStack.has(dep)) {
633
+ // Found a cycle
634
+ parent.set(dep, node);
635
+ return dep;
636
+ }
637
+ }
638
+
639
+ recursionStack.delete(node);
640
+ return null;
641
+ }
642
+
643
+ for (const node of allNodes) {
644
+ if (!visited.has(node)) {
645
+ const cycleNode = dfs(node);
646
+ if (cycleNode) {
647
+ // Reconstruct the cycle
648
+ const cycle: string[] = [cycleNode];
649
+ let current = parent.get(cycleNode);
650
+ while (current && current !== cycleNode) {
651
+ cycle.push(current);
652
+ current = parent.get(current);
653
+ }
654
+ cycle.push(cycleNode);
655
+ return cycle.reverse();
656
+ }
657
+ }
658
+ }
659
+
660
+ return null;
661
+ }
662
+
663
+ /**
664
+ * Validate task-level dependencies across all lanes
665
+ */
666
+ export function validateTaskDependencies(tasksDir: string): {
667
+ valid: boolean;
668
+ errors: string[];
669
+ warnings: string[];
670
+ tasks: TaskDependencyInfo[];
671
+ } {
672
+ const errors: string[] = [];
673
+ const warnings: string[] = [];
674
+
675
+ const tasks = extractTaskDependencies(tasksDir);
676
+ const taskIds = new Set(tasks.map(t => t.id));
677
+ const laneNames = new Set(tasks.map(t => t.lane));
678
+
679
+ // Check for missing dependencies
680
+ for (const task of tasks) {
681
+ for (const dep of task.dependsOn) {
682
+ // Validate format
683
+ if (!dep.includes(':')) {
684
+ errors.push(`Task "${task.id}" has invalid dependency format "${dep}". Expected "lane:task".`);
685
+ continue;
686
+ }
687
+
688
+ const [depLane, depTask] = dep.split(':');
689
+
690
+ // Check if lane exists
691
+ if (!laneNames.has(depLane!)) {
692
+ errors.push(`Task "${task.id}" depends on unknown lane "${depLane}"`);
693
+ continue;
694
+ }
695
+
696
+ // Check if task exists (warning only, since task might be created later)
697
+ if (!taskIds.has(dep)) {
698
+ warnings.push(`Task "${task.id}" depends on "${dep}" which doesn't exist yet`);
699
+ }
700
+
701
+ // Check for self-dependency
702
+ if (dep === task.id) {
703
+ errors.push(`Task "${task.id}" depends on itself`);
704
+ }
705
+ }
706
+ }
707
+
708
+ // Check for cycles
709
+ const cycleResult = detectTaskCyclicDependencies(tasks);
710
+ if (cycleResult.hasCycle && cycleResult.cycle) {
711
+ errors.push(`Cyclic task dependency detected: ${cycleResult.cycle.join(' → ')}`);
712
+ }
713
+
714
+ // Warning for deeply nested dependencies
715
+ const dependencyCounts = new Map<string, number>();
716
+ for (const task of tasks) {
717
+ let count = 0;
718
+ const visited = new Set<string>();
719
+ const queue = [...task.dependsOn];
720
+
721
+ while (queue.length > 0) {
722
+ const dep = queue.shift()!;
723
+ if (visited.has(dep)) continue;
724
+ visited.add(dep);
725
+ count++;
726
+
727
+ const depTask = tasks.find(t => t.id === dep);
728
+ if (depTask) {
729
+ queue.push(...depTask.dependsOn);
730
+ }
731
+ }
732
+
733
+ dependencyCounts.set(task.id, count);
734
+ if (count > 10) {
735
+ warnings.push(`Task "${task.id}" has ${count} transitive dependencies`);
736
+ }
737
+ }
738
+
739
+ return {
740
+ valid: errors.length === 0,
741
+ errors,
742
+ warnings,
743
+ tasks,
744
+ };
745
+ }
746
+
747
+ /**
748
+ * Print task-level dependency graph to console
749
+ */
750
+ export function printTaskDependencyGraph(tasks: TaskDependencyInfo[]): void {
751
+ const cycleResult = detectTaskCyclicDependencies(tasks);
752
+
753
+ logger.section('📊 Task Dependency Graph');
754
+
755
+ if (cycleResult.hasCycle) {
756
+ logger.error(`⚠️ Cyclic dependency detected: ${cycleResult.cycle?.join(' → ')}`);
757
+ console.log('');
758
+ }
759
+
760
+ // Group tasks by lane
761
+ const byLane = new Map<string, TaskDependencyInfo[]>();
762
+ for (const task of tasks) {
763
+ const existing = byLane.get(task.lane) || [];
764
+ existing.push(task);
765
+ byLane.set(task.lane, existing);
766
+ }
767
+
768
+ for (const [lane, laneTasks] of byLane) {
769
+ console.log(` ${logger.COLORS.cyan}${lane}${logger.COLORS.reset}`);
770
+
771
+ for (const task of laneTasks) {
772
+ const deps = task.dependsOn.length > 0
773
+ ? ` → ${task.dependsOn.join(', ')}`
774
+ : '';
775
+ console.log(` • ${task.task}${logger.COLORS.gray}${deps}${logger.COLORS.reset}`);
776
+ }
777
+ }
778
+
779
+ if (cycleResult.sortedOrder) {
780
+ console.log('');
781
+ const order = cycleResult.sortedOrder.reverse();
782
+ if (order.length <= 10) {
783
+ console.log(` Execution order: ${order.join(' → ')}`);
784
+ } else {
785
+ console.log(` Execution order: ${order.slice(0, 5).join(' → ')} ... (${order.length} total)`);
786
+ }
787
+ }
788
+
789
+ console.log('');
790
+ }
791
+