@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.
- package/CHANGELOG.md +27 -0
- package/README.md +182 -59
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-doctor.md +45 -23
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/commands/cursorflow-run.md +60 -111
- 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/doctor.js +47 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +17 -34
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +62 -65
- 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 +57 -68
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +60 -30
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stop.js +6 -0
- package/dist/cli/stop.js.map +1 -1
- package/dist/cli/tasks.d.ts +5 -3
- package/dist/cli/tasks.js +181 -29
- 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 +215 -334
- 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/services/logging/console.js +2 -1
- package/dist/services/logging/console.js.map +1 -1
- 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.d.ts +5 -1
- package/dist/utils/config.js +15 -16
- 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/doctor.js +40 -8
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +239 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/flow.d.ts +9 -0
- package/dist/utils/flow.js +73 -0
- package/dist/utils/flow.js.map +1 -0
- 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/doctor.ts +48 -4
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +20 -33
- package/src/cli/monitor.ts +70 -75
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +61 -76
- package/src/cli/run.ts +333 -306
- package/src/cli/stop.ts +8 -0
- package/src/cli/tasks.ts +200 -21
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +279 -379
- 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/services/logging/console.ts +2 -1
- 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 +16 -17
- package/src/utils/dependency.ts +311 -2
- package/src/utils/doctor.ts +36 -8
- package/src/utils/enhanced-logger.ts +264 -927
- package/src/utils/flow.ts +42 -0
- 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
|
@@ -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
|
|
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) {
|
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
|
};
|
|
@@ -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:
|
|
204
|
+
verboseGit: true,
|
|
206
205
|
|
|
207
206
|
// Advanced
|
|
208
207
|
worktreePrefix: 'cursorflow-',
|
|
209
208
|
maxConcurrentLanes: 10,
|
|
210
|
-
agentOutputFormat: '
|
|
209
|
+
agentOutputFormat: 'json', // 'json' | 'plain'
|
|
211
210
|
|
|
212
211
|
// Webhook configuration
|
|
213
212
|
// 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
|
+
|