@litmers/cursorflow-orchestrator 0.1.31 → 0.1.36

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 (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +182 -59
  3. package/commands/cursorflow-add.md +159 -0
  4. package/commands/cursorflow-doctor.md +45 -23
  5. package/commands/cursorflow-monitor.md +23 -2
  6. package/commands/cursorflow-new.md +87 -0
  7. package/commands/cursorflow-run.md +60 -111
  8. package/dist/cli/add.d.ts +7 -0
  9. package/dist/cli/add.js +377 -0
  10. package/dist/cli/add.js.map +1 -0
  11. package/dist/cli/clean.js +1 -0
  12. package/dist/cli/clean.js.map +1 -1
  13. package/dist/cli/config.d.ts +7 -0
  14. package/dist/cli/config.js +181 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/doctor.js +47 -4
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +34 -30
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/logs.js +17 -34
  21. package/dist/cli/logs.js.map +1 -1
  22. package/dist/cli/monitor.js +62 -65
  23. package/dist/cli/monitor.js.map +1 -1
  24. package/dist/cli/new.d.ts +7 -0
  25. package/dist/cli/new.js +232 -0
  26. package/dist/cli/new.js.map +1 -0
  27. package/dist/cli/prepare.js +95 -193
  28. package/dist/cli/prepare.js.map +1 -1
  29. package/dist/cli/resume.js +57 -68
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +60 -30
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/stop.js +6 -0
  34. package/dist/cli/stop.js.map +1 -1
  35. package/dist/cli/tasks.d.ts +5 -3
  36. package/dist/cli/tasks.js +181 -29
  37. package/dist/cli/tasks.js.map +1 -1
  38. package/dist/core/failure-policy.d.ts +9 -0
  39. package/dist/core/failure-policy.js +9 -0
  40. package/dist/core/failure-policy.js.map +1 -1
  41. package/dist/core/orchestrator.d.ts +20 -6
  42. package/dist/core/orchestrator.js +215 -334
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +27 -0
  45. package/dist/core/runner/agent.js +294 -0
  46. package/dist/core/runner/agent.js.map +1 -0
  47. package/dist/core/runner/index.d.ts +5 -0
  48. package/dist/core/runner/index.js +22 -0
  49. package/dist/core/runner/index.js.map +1 -0
  50. package/dist/core/runner/pipeline.d.ts +9 -0
  51. package/dist/core/runner/pipeline.js +539 -0
  52. package/dist/core/runner/pipeline.js.map +1 -0
  53. package/dist/core/runner/prompt.d.ts +25 -0
  54. package/dist/core/runner/prompt.js +175 -0
  55. package/dist/core/runner/prompt.js.map +1 -0
  56. package/dist/core/runner/task.d.ts +26 -0
  57. package/dist/core/runner/task.js +283 -0
  58. package/dist/core/runner/task.js.map +1 -0
  59. package/dist/core/runner/utils.d.ts +37 -0
  60. package/dist/core/runner/utils.js +161 -0
  61. package/dist/core/runner/utils.js.map +1 -0
  62. package/dist/core/runner.d.ts +2 -96
  63. package/dist/core/runner.js +11 -1136
  64. package/dist/core/runner.js.map +1 -1
  65. package/dist/core/stall-detection.d.ts +326 -0
  66. package/dist/core/stall-detection.js +781 -0
  67. package/dist/core/stall-detection.js.map +1 -0
  68. package/dist/services/logging/console.js +2 -1
  69. package/dist/services/logging/console.js.map +1 -1
  70. package/dist/types/config.d.ts +6 -6
  71. package/dist/types/flow.d.ts +84 -0
  72. package/dist/types/flow.js +10 -0
  73. package/dist/types/flow.js.map +1 -0
  74. package/dist/types/index.d.ts +1 -0
  75. package/dist/types/index.js +3 -3
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/types/lane.d.ts +0 -2
  78. package/dist/types/logging.d.ts +5 -1
  79. package/dist/types/task.d.ts +7 -11
  80. package/dist/utils/config.d.ts +5 -1
  81. package/dist/utils/config.js +15 -16
  82. package/dist/utils/config.js.map +1 -1
  83. package/dist/utils/dependency.d.ts +36 -1
  84. package/dist/utils/dependency.js +256 -1
  85. package/dist/utils/dependency.js.map +1 -1
  86. package/dist/utils/doctor.js +40 -8
  87. package/dist/utils/doctor.js.map +1 -1
  88. package/dist/utils/enhanced-logger.d.ts +45 -82
  89. package/dist/utils/enhanced-logger.js +239 -844
  90. package/dist/utils/enhanced-logger.js.map +1 -1
  91. package/dist/utils/flow.d.ts +9 -0
  92. package/dist/utils/flow.js +73 -0
  93. package/dist/utils/flow.js.map +1 -0
  94. package/dist/utils/git.d.ts +29 -0
  95. package/dist/utils/git.js +115 -5
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/state.js +0 -2
  98. package/dist/utils/state.js.map +1 -1
  99. package/dist/utils/task-service.d.ts +2 -2
  100. package/dist/utils/task-service.js +40 -31
  101. package/dist/utils/task-service.js.map +1 -1
  102. package/package.json +4 -3
  103. package/src/cli/add.ts +397 -0
  104. package/src/cli/clean.ts +1 -0
  105. package/src/cli/config.ts +177 -0
  106. package/src/cli/doctor.ts +48 -4
  107. package/src/cli/index.ts +36 -32
  108. package/src/cli/logs.ts +20 -33
  109. package/src/cli/monitor.ts +70 -75
  110. package/src/cli/new.ts +235 -0
  111. package/src/cli/prepare.ts +98 -205
  112. package/src/cli/resume.ts +61 -76
  113. package/src/cli/run.ts +333 -306
  114. package/src/cli/stop.ts +8 -0
  115. package/src/cli/tasks.ts +200 -21
  116. package/src/core/failure-policy.ts +9 -0
  117. package/src/core/orchestrator.ts +279 -379
  118. package/src/core/runner/agent.ts +314 -0
  119. package/src/core/runner/index.ts +6 -0
  120. package/src/core/runner/pipeline.ts +567 -0
  121. package/src/core/runner/prompt.ts +174 -0
  122. package/src/core/runner/task.ts +320 -0
  123. package/src/core/runner/utils.ts +142 -0
  124. package/src/core/runner.ts +8 -1347
  125. package/src/core/stall-detection.ts +936 -0
  126. package/src/services/logging/console.ts +2 -1
  127. package/src/types/config.ts +6 -6
  128. package/src/types/flow.ts +91 -0
  129. package/src/types/index.ts +15 -3
  130. package/src/types/lane.ts +0 -2
  131. package/src/types/logging.ts +5 -1
  132. package/src/types/task.ts +7 -11
  133. package/src/utils/config.ts +16 -17
  134. package/src/utils/dependency.ts +311 -2
  135. package/src/utils/doctor.ts +36 -8
  136. package/src/utils/enhanced-logger.ts +264 -927
  137. package/src/utils/flow.ts +42 -0
  138. package/src/utils/git.ts +145 -5
  139. package/src/utils/state.ts +0 -2
  140. package/src/utils/task-service.ts +48 -40
  141. package/commands/cursorflow-review.md +0 -56
  142. package/commands/cursorflow-runs.md +0 -59
  143. package/dist/cli/runs.d.ts +0 -5
  144. package/dist/cli/runs.js +0 -214
  145. package/dist/cli/runs.js.map +0 -1
  146. package/dist/core/reviewer.d.ts +0 -66
  147. package/dist/core/reviewer.js +0 -265
  148. package/dist/core/reviewer.js.map +0 -1
  149. package/src/cli/runs.ts +0 -212
  150. package/src/core/reviewer.ts +0 -285
@@ -143,7 +143,8 @@ export function withContext(context: string) {
143
143
  */
144
144
  export function laneOutput(laneName: string, message: string, isError = false): void {
145
145
  const timestamp = `${COLORS.gray}[${formatTimestamp()}]${COLORS.reset}`;
146
- const laneLabel = `${COLORS.magenta}${laneName.padEnd(10)}${COLORS.reset}`;
146
+ const shortName = laneName.substring(0, 10).padEnd(10);
147
+ const laneLabel = `${COLORS.magenta}${shortName}${COLORS.reset}`;
147
148
  const output = isError ? `${COLORS.red}${message}${COLORS.reset}` : message;
148
149
 
149
150
  if (isError) {
@@ -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
  };
@@ -114,12 +112,19 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
114
112
  }
115
113
 
116
114
  /**
117
- * Get absolute path for tasks directory
115
+ * Get absolute path for tasks directory (legacy)
118
116
  */
119
117
  export function getTasksDir(config: CursorFlowConfig): string {
120
118
  return safeJoin(config.projectRoot, config.tasksDir);
121
119
  }
122
120
 
121
+ /**
122
+ * Get absolute path for flows directory (new architecture)
123
+ */
124
+ export function getFlowsDir(config: CursorFlowConfig): string {
125
+ return safeJoin(config.projectRoot, config.flowsDir);
126
+ }
127
+
123
128
  /**
124
129
  * Get absolute path for logs directory
125
130
  */
@@ -188,12 +193,6 @@ export function createDefaultConfig(projectRoot: string, force = false): string
188
193
  allowDependencyChange: false,
189
194
  lockfileReadOnly: true,
190
195
 
191
- // Review configuration
192
- enableReview: false,
193
- reviewModel: 'sonnet-4.5-thinking',
194
- reviewAllTasks: false,
195
- maxReviewIterations: 3,
196
-
197
196
  // Lane configuration
198
197
  defaultLaneConfig: {
199
198
  devPort: 3001, // 3000 + laneNumber
@@ -202,12 +201,12 @@ export function createDefaultConfig(projectRoot: string, force = false): string
202
201
 
203
202
  // Logging
204
203
  logLevel: 'info', // 'error' | 'warn' | 'info' | 'debug'
205
- verboseGit: false,
204
+ verboseGit: true,
206
205
 
207
206
  // Advanced
208
207
  worktreePrefix: 'cursorflow-',
209
208
  maxConcurrentLanes: 10,
210
- agentOutputFormat: 'stream-json', // 'stream-json' | 'json' | 'plain'
209
+ agentOutputFormat: 'json', // 'json' | 'plain'
211
210
 
212
211
  // Webhook configuration
213
212
  // 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
+