@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.
- package/README.md +144 -52
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/dist/cli/add.d.ts +7 -0
- package/dist/cli/add.js +377 -0
- package/dist/cli/add.js.map +1 -0
- package/dist/cli/clean.js +1 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/config.d.ts +7 -0
- package/dist/cli/config.js +181 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +7 -33
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +51 -62
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/new.d.ts +7 -0
- package/dist/cli/new.js +232 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/prepare.js +95 -193
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +11 -47
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +27 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tasks.js +1 -2
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/failure-policy.d.ts +9 -0
- package/dist/core/failure-policy.js +9 -0
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.d.ts +20 -6
- package/dist/core/orchestrator.js +217 -331
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +27 -0
- package/dist/core/runner/agent.js +294 -0
- package/dist/core/runner/agent.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +22 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/pipeline.d.ts +9 -0
- package/dist/core/runner/pipeline.js +539 -0
- package/dist/core/runner/pipeline.js.map +1 -0
- package/dist/core/runner/prompt.d.ts +25 -0
- package/dist/core/runner/prompt.js +175 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/task.d.ts +26 -0
- package/dist/core/runner/task.js +283 -0
- package/dist/core/runner/task.js.map +1 -0
- package/dist/core/runner/utils.d.ts +37 -0
- package/dist/core/runner/utils.js +161 -0
- package/dist/core/runner/utils.js.map +1 -0
- package/dist/core/runner.d.ts +2 -96
- package/dist/core/runner.js +11 -1136
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +326 -0
- package/dist/core/stall-detection.js +781 -0
- package/dist/core/stall-detection.js.map +1 -0
- package/dist/types/config.d.ts +6 -6
- package/dist/types/flow.d.ts +84 -0
- package/dist/types/flow.js +10 -0
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +0 -2
- package/dist/types/logging.d.ts +5 -1
- package/dist/types/task.d.ts +7 -11
- package/dist/utils/config.js +7 -15
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/dependency.d.ts +36 -1
- package/dist/utils/dependency.js +256 -1
- package/dist/utils/dependency.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +238 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +115 -5
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/state.js +0 -2
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +2 -2
- package/dist/utils/task-service.js +40 -31
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/add.ts +397 -0
- package/src/cli/clean.ts +1 -0
- package/src/cli/config.ts +177 -0
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +7 -31
- package/src/cli/monitor.ts +55 -71
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +13 -56
- package/src/cli/run.ts +311 -306
- package/src/cli/tasks.ts +1 -2
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +281 -375
- package/src/core/runner/agent.ts +314 -0
- package/src/core/runner/index.ts +6 -0
- package/src/core/runner/pipeline.ts +567 -0
- package/src/core/runner/prompt.ts +174 -0
- package/src/core/runner/task.ts +320 -0
- package/src/core/runner/utils.ts +142 -0
- package/src/core/runner.ts +8 -1347
- package/src/core/stall-detection.ts +936 -0
- package/src/types/config.ts +6 -6
- package/src/types/flow.ts +91 -0
- package/src/types/index.ts +15 -3
- package/src/types/lane.ts +0 -2
- package/src/types/logging.ts +5 -1
- package/src/types/task.ts +7 -11
- package/src/utils/config.ts +8 -16
- package/src/utils/dependency.ts +311 -2
- package/src/utils/enhanced-logger.ts +263 -927
- package/src/utils/git.ts +145 -5
- package/src/utils/state.ts +0 -2
- package/src/utils/task-service.ts +48 -40
- package/commands/cursorflow-review.md +0 -56
- package/commands/cursorflow-runs.md +0 -59
- package/dist/cli/runs.d.ts +0 -5
- package/dist/cli/runs.js +0 -214
- package/dist/cli/runs.js.map +0 -1
- package/dist/core/reviewer.d.ts +0 -66
- package/dist/core/reviewer.js +0 -265
- package/dist/core/reviewer.js.map +0 -1
- package/src/cli/runs.ts +0 -212
- package/src/core/reviewer.ts +0 -285
package/src/types/config.ts
CHANGED
|
@@ -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: '
|
|
74
|
-
agentOutputFormat: '
|
|
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
|
+
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
|
package/src/types/logging.ts
CHANGED
|
@@ -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' | '
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
package/src/utils/config.ts
CHANGED
|
@@ -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:
|
|
71
|
+
verboseGit: true,
|
|
77
72
|
|
|
78
73
|
// Advanced
|
|
79
74
|
worktreePrefix: 'cursorflow-',
|
|
80
75
|
maxConcurrentLanes: 10,
|
|
81
|
-
agentOutputFormat: '
|
|
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:
|
|
197
|
+
verboseGit: true,
|
|
206
198
|
|
|
207
199
|
// Advanced
|
|
208
200
|
worktreePrefix: 'cursorflow-',
|
|
209
201
|
maxConcurrentLanes: 10,
|
|
210
|
-
agentOutputFormat: '
|
|
202
|
+
agentOutputFormat: 'json', // 'json' | 'plain'
|
|
211
203
|
|
|
212
204
|
// Webhook configuration
|
|
213
205
|
// webhooks: [
|
package/src/utils/dependency.ts
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
* Dependency management utilities for CursorFlow
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* -
|
|
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
|
+
|