@litmers/cursorflow-orchestrator 0.1.34 → 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 (48) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +38 -7
  3. package/commands/cursorflow-doctor.md +45 -23
  4. package/commands/cursorflow-run.md +60 -111
  5. package/dist/cli/doctor.js +47 -4
  6. package/dist/cli/doctor.js.map +1 -1
  7. package/dist/cli/logs.js +10 -1
  8. package/dist/cli/logs.js.map +1 -1
  9. package/dist/cli/monitor.js +12 -4
  10. package/dist/cli/monitor.js.map +1 -1
  11. package/dist/cli/resume.js +46 -21
  12. package/dist/cli/resume.js.map +1 -1
  13. package/dist/cli/run.js +33 -8
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/cli/stop.js +6 -0
  16. package/dist/cli/stop.js.map +1 -1
  17. package/dist/cli/tasks.d.ts +5 -3
  18. package/dist/cli/tasks.js +180 -27
  19. package/dist/cli/tasks.js.map +1 -1
  20. package/dist/core/orchestrator.js +6 -5
  21. package/dist/core/orchestrator.js.map +1 -1
  22. package/dist/services/logging/console.js +2 -1
  23. package/dist/services/logging/console.js.map +1 -1
  24. package/dist/utils/config.d.ts +5 -1
  25. package/dist/utils/config.js +8 -1
  26. package/dist/utils/config.js.map +1 -1
  27. package/dist/utils/doctor.js +40 -8
  28. package/dist/utils/doctor.js.map +1 -1
  29. package/dist/utils/enhanced-logger.d.ts +1 -1
  30. package/dist/utils/enhanced-logger.js +3 -2
  31. package/dist/utils/enhanced-logger.js.map +1 -1
  32. package/dist/utils/flow.d.ts +9 -0
  33. package/dist/utils/flow.js +73 -0
  34. package/dist/utils/flow.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/cli/doctor.ts +48 -4
  37. package/src/cli/logs.ts +13 -2
  38. package/src/cli/monitor.ts +16 -5
  39. package/src/cli/resume.ts +48 -20
  40. package/src/cli/run.ts +31 -9
  41. package/src/cli/stop.ts +8 -0
  42. package/src/cli/tasks.ts +199 -19
  43. package/src/core/orchestrator.ts +6 -5
  44. package/src/services/logging/console.ts +2 -1
  45. package/src/utils/config.ts +8 -1
  46. package/src/utils/doctor.ts +36 -8
  47. package/src/utils/enhanced-logger.ts +3 -2
  48. package/src/utils/flow.ts +42 -0
package/src/cli/tasks.ts CHANGED
@@ -2,36 +2,55 @@
2
2
  * CursorFlow tasks command
3
3
  *
4
4
  * Usage:
5
- * cursorflow tasks # List all tasks
6
- * cursorflow tasks --validate # List all tasks with validation
7
- * cursorflow tasks <name> # Show detailed task info
5
+ * cursorflow tasks # List all flows (new) and tasks (legacy)
6
+ * cursorflow tasks --flows # List only flows
7
+ * cursorflow tasks --legacy # List only legacy tasks
8
+ * cursorflow tasks --validate # List with validation
9
+ * cursorflow tasks <name> # Show detailed info
8
10
  */
9
11
 
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
10
14
  import * as logger from '../utils/logger';
11
15
  import { TaskService, TaskDirInfo, ValidationStatus } from '../utils/task-service';
12
- import { findProjectRoot, loadConfig, getTasksDir } from '../utils/config';
16
+ import { findProjectRoot, loadConfig, getTasksDir, getFlowsDir } from '../utils/config';
17
+ import { safeJoin } from '../utils/path';
13
18
 
14
19
  const COLORS = logger.COLORS;
15
20
 
21
+ interface FlowInfo {
22
+ id: string;
23
+ name: string;
24
+ path: string;
25
+ timestamp: Date;
26
+ lanes: string[];
27
+ status: string;
28
+ }
29
+
16
30
  interface TasksCliOptions {
17
31
  validate: boolean;
18
32
  taskName: string | null;
33
+ flowsOnly: boolean;
34
+ legacyOnly: boolean;
19
35
  }
20
36
 
21
37
  function printHelp(): void {
22
38
  console.log(`
23
- Usage: cursorflow tasks [options] [task-name]
39
+ Usage: cursorflow tasks [options] [name]
24
40
 
25
- Manage and view prepared tasks in _cursorflow/tasks/.
41
+ List and view flows (new) and prepared tasks (legacy).
26
42
 
27
43
  Options:
28
- --validate Run validation on all tasks before listing
44
+ --flows Show only flows (from _cursorflow/flows/)
45
+ --legacy Show only legacy tasks (from _cursorflow/tasks/)
46
+ --validate Run validation before listing
29
47
  --help, -h Show help
30
48
 
31
49
  Examples:
32
- cursorflow tasks
33
- cursorflow tasks --validate
34
- cursorflow tasks 2412221530_AuthSystem
50
+ cursorflow tasks # List all flows and tasks
51
+ cursorflow tasks --flows # List only flows
52
+ cursorflow tasks TestFeature # Show flow or task details
53
+ cursorflow tasks --validate # Validate all entries
35
54
  `);
36
55
  }
37
56
 
@@ -39,6 +58,8 @@ function parseArgs(args: string[]): TasksCliOptions {
39
58
  const options: TasksCliOptions = {
40
59
  validate: args.includes('--validate'),
41
60
  taskName: null,
61
+ flowsOnly: args.includes('--flows'),
62
+ legacyOnly: args.includes('--legacy'),
42
63
  };
43
64
 
44
65
  const nameArg = args.find(arg => !arg.startsWith('-'));
@@ -54,6 +75,136 @@ function parseArgs(args: string[]): TasksCliOptions {
54
75
  return options;
55
76
  }
56
77
 
78
+ /**
79
+ * List flows from _cursorflow/flows/
80
+ */
81
+ function listFlows(flowsDir: string): FlowInfo[] {
82
+ if (!fs.existsSync(flowsDir)) {
83
+ return [];
84
+ }
85
+
86
+ const dirs = fs.readdirSync(flowsDir)
87
+ .filter(name => {
88
+ const dirPath = safeJoin(flowsDir, name);
89
+ return fs.statSync(dirPath).isDirectory() && !name.startsWith('.');
90
+ })
91
+ .sort((a, b) => b.localeCompare(a)); // Most recent first
92
+
93
+ return dirs.map(name => {
94
+ const flowPath = safeJoin(flowsDir, name);
95
+ const metaPath = safeJoin(flowPath, 'flow.meta.json');
96
+
97
+ let meta: any = {};
98
+ try {
99
+ if (fs.existsSync(metaPath)) {
100
+ meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
101
+ }
102
+ } catch {}
103
+
104
+ // Parse flow name from directory (e.g., "001_TestFeature" -> "TestFeature")
105
+ const match = name.match(/^(\d+)_(.+)$/);
106
+ const id = match ? match[1] : name;
107
+ const flowName = match ? match[2] : name;
108
+
109
+ // Get lane files
110
+ const laneFiles = fs.readdirSync(flowPath)
111
+ .filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
112
+ .map(f => {
113
+ const laneMatch = f.match(/^\d+-([^.]+)\.json$/);
114
+ return laneMatch ? laneMatch[1] : f.replace('.json', '');
115
+ });
116
+
117
+ return {
118
+ id,
119
+ name: flowName,
120
+ path: flowPath,
121
+ timestamp: meta.createdAt ? new Date(meta.createdAt) : new Date(),
122
+ lanes: laneFiles,
123
+ status: meta.status || 'pending',
124
+ };
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Get flow info by name
130
+ */
131
+ function getFlowInfo(flowsDir: string, flowName: string): FlowInfo | null {
132
+ const flows = listFlows(flowsDir);
133
+ return flows.find(f => f.name === flowName || `${f.id}_${f.name}` === flowName) || null;
134
+ }
135
+
136
+ /**
137
+ * Print flows list
138
+ */
139
+ function printFlowsList(flows: FlowInfo[]): void {
140
+ if (flows.length === 0) {
141
+ logger.info('No flows found in _cursorflow/flows/');
142
+ return;
143
+ }
144
+
145
+ console.log(`${COLORS.bold}Flows:${COLORS.reset}`);
146
+
147
+ for (let i = 0; i < flows.length; i++) {
148
+ const flow = flows[i]!;
149
+ const prefix = i === 0 ? ' ▶' : ' ';
150
+ const name = `${flow.id}_${flow.name}`.padEnd(30);
151
+ const lanes = `${flow.lanes.length} lane${flow.lanes.length !== 1 ? 's' : ''}`.padEnd(10);
152
+ const status = flow.status.padEnd(10);
153
+ const date = formatDate(flow.timestamp);
154
+
155
+ let color = COLORS.reset;
156
+ if (flow.status === 'completed') color = COLORS.green;
157
+ else if (flow.status === 'running') color = COLORS.cyan;
158
+ else if (flow.status === 'failed') color = COLORS.red;
159
+
160
+ console.log(`${color}${prefix} ${name} ${lanes} ${status} ${date}${COLORS.reset}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Print flow details
166
+ */
167
+ function printFlowDetail(flow: FlowInfo, flowsDir: string): void {
168
+ console.log(`${COLORS.bold}Flow: ${flow.name}${COLORS.reset}`);
169
+ console.log(`${COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
170
+ console.log(` ID: ${flow.id}`);
171
+ console.log(` Status: ${flow.status}`);
172
+ console.log(` Path: ${flow.path}`);
173
+ console.log('');
174
+
175
+ if (flow.lanes.length === 0) {
176
+ console.log('No lanes defined.');
177
+ } else {
178
+ console.log(`${COLORS.bold}Lanes:${COLORS.reset}`);
179
+ for (const laneName of flow.lanes) {
180
+ // Read lane file for task info
181
+ const laneFiles = fs.readdirSync(flow.path)
182
+ .filter(f => f.endsWith('.json') && f !== 'flow.meta.json');
183
+
184
+ const laneFile = laneFiles.find(f => {
185
+ const match = f.match(/^\d+-([^.]+)\.json$/);
186
+ return match && match[1] === laneName;
187
+ });
188
+
189
+ if (laneFile) {
190
+ try {
191
+ const laneData = JSON.parse(fs.readFileSync(safeJoin(flow.path, laneFile), 'utf-8'));
192
+ const tasks = laneData.tasks || [];
193
+ const taskFlow = tasks.map((t: any) => t.name).join(' → ');
194
+ console.log(` ${laneName.padEnd(18)} ${COLORS.blue}[${tasks.length} tasks]${COLORS.reset} ${taskFlow}`);
195
+ } catch {
196
+ console.log(` ${laneName.padEnd(18)} ${COLORS.yellow}[error reading]${COLORS.reset}`);
197
+ }
198
+ } else {
199
+ console.log(` ${laneName}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ console.log('');
205
+ console.log(`${COLORS.gray}Run: cursorflow run ${flow.name}${COLORS.reset}`);
206
+ }
207
+
57
208
  function formatDate(date: Date): string {
58
209
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
59
210
  return `${months[date.getMonth()]} ${date.getDate()}`;
@@ -118,25 +269,37 @@ async function tasks(args: string[]): Promise<void> {
118
269
  const options = parseArgs(args);
119
270
  const projectRoot = findProjectRoot();
120
271
  const config = loadConfig(projectRoot);
272
+ const flowsDir = getFlowsDir(config);
121
273
  const tasksDir = getTasksDir(config);
122
274
  const taskService = new TaskService(tasksDir);
123
275
 
276
+ // Check for specific flow/task by name
124
277
  if (options.taskName) {
125
- const info = taskService.getTaskDirInfo(options.taskName);
126
- if (!info) {
127
- logger.error(`Task not found: ${options.taskName}`);
128
- process.exit(1);
278
+ // First try to find as a flow
279
+ const flowInfo = getFlowInfo(flowsDir, options.taskName);
280
+ if (flowInfo) {
281
+ printFlowDetail(flowInfo, flowsDir);
282
+ return;
129
283
  }
130
284
 
131
- // Always validate for detail view to have report
285
+ // Then try as a legacy task
286
+ const taskInfo = taskService.getTaskDirInfo(options.taskName);
287
+ if (taskInfo) {
132
288
  taskService.validateTaskDir(options.taskName);
133
289
  const updatedInfo = taskService.getTaskDirInfo(options.taskName)!;
134
-
135
290
  printTaskDetail(updatedInfo);
136
- } else {
137
- let taskList = taskService.listTaskDirs();
291
+ return;
292
+ }
293
+
294
+ logger.error(`Flow or task not found: ${options.taskName}`);
295
+ process.exit(1);
296
+ }
297
+
298
+ // List flows and/or tasks
299
+ const flows = options.legacyOnly ? [] : listFlows(flowsDir);
300
+ let taskList = options.flowsOnly ? [] : taskService.listTaskDirs();
138
301
 
139
- if (options.validate) {
302
+ if (options.validate && taskList.length > 0) {
140
303
  const spinner = logger.createSpinner('Validating tasks...');
141
304
  spinner.start();
142
305
  for (const task of taskList) {
@@ -146,8 +309,25 @@ async function tasks(args: string[]): Promise<void> {
146
309
  taskList = taskService.listTaskDirs();
147
310
  }
148
311
 
312
+ // Print results
313
+ if (flows.length > 0) {
314
+ printFlowsList(flows);
315
+ if (taskList.length > 0) {
316
+ console.log('');
317
+ }
318
+ }
319
+
320
+ if (taskList.length > 0) {
321
+ console.log(`${COLORS.gray}Legacy Tasks:${COLORS.reset}`);
149
322
  printTasksList(taskList);
150
323
  }
324
+
325
+ if (flows.length === 0 && taskList.length === 0) {
326
+ logger.info('No flows or tasks found.');
327
+ console.log('');
328
+ console.log('Create a new flow:');
329
+ console.log(' cursorflow new <FlowName> --lanes "lane1,lane2"');
330
+ }
151
331
  }
152
332
 
153
333
  export = tasks;
@@ -278,11 +278,12 @@ export function spawnLane({
278
278
  };
279
279
 
280
280
  if (logConfig.enabled) {
281
- // Helper to get dynamic lane label like [L01-T01-laneName]
281
+ // Helper to get dynamic lane label like [L1-T1-lanename10]
282
282
  const getDynamicLabel = () => {
283
- const laneNum = `L${(laneIndex + 1).toString().padStart(2, '0')}`;
284
- const taskPart = info.currentTaskIndex ? `-T${info.currentTaskIndex.toString().padStart(2, '0')}` : '';
285
- return `[${laneNum}${taskPart}-${laneName}]`;
283
+ const laneNum = `L${laneIndex + 1}`;
284
+ const taskPart = info.currentTaskIndex ? `-T${info.currentTaskIndex}` : '';
285
+ const shortLaneName = laneName.substring(0, 10);
286
+ return `[${laneNum}${taskPart}-${shortLaneName}]`;
286
287
  };
287
288
 
288
289
  // Create callback for clean console output
@@ -474,7 +475,7 @@ export function listLaneFiles(tasksDir: string): LaneInfo[] {
474
475
 
475
476
  const files = fs.readdirSync(tasksDir);
476
477
  return files
477
- .filter(f => f.endsWith('.json'))
478
+ .filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
478
479
  .sort()
479
480
  .map(f => {
480
481
  const filePath = safeJoin(tasksDir, f);
@@ -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) {
@@ -112,12 +112,19 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
112
112
  }
113
113
 
114
114
  /**
115
- * Get absolute path for tasks directory
115
+ * Get absolute path for tasks directory (legacy)
116
116
  */
117
117
  export function getTasksDir(config: CursorFlowConfig): string {
118
118
  return safeJoin(config.projectRoot, config.tasksDir);
119
119
  }
120
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
+
121
128
  /**
122
129
  * Get absolute path for logs directory
123
130
  */
@@ -19,6 +19,8 @@ import * as git from './git';
19
19
  import { checkCursorAgentInstalled, checkCursorAuth } from './cursor-agent';
20
20
  import { areCommandsInstalled } from '../cli/setup-commands';
21
21
  import { safeJoin } from './path';
22
+ import { findFlowDir } from './flow';
23
+ import { loadConfig, getFlowsDir } from './config';
22
24
 
23
25
  export type DoctorSeverity = 'error' | 'warn';
24
26
 
@@ -150,7 +152,7 @@ function branchExists(repoRoot: string, branchName: string): boolean {
150
152
  function readLaneJsonFiles(tasksDir: string): { path: string; json: any; fileName: string }[] {
151
153
  const files = fs
152
154
  .readdirSync(tasksDir)
153
- .filter(f => f.endsWith('.json'))
155
+ .filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
154
156
  .sort()
155
157
  .map(f => safeJoin(tasksDir, f));
156
158
 
@@ -858,20 +860,46 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
858
860
 
859
861
  // 2) Tasks-dir checks (optional; used by `cursorflow run` preflight)
860
862
  if (options.tasksDir) {
861
- const tasksDirAbs = path.isAbsolute(options.tasksDir)
862
- ? options.tasksDir
863
- : safeJoin(cwd, options.tasksDir);
863
+ // Resolve tasks dir with flow name support:
864
+ // 1. Absolute paths are used as-is
865
+ // 2. Relative paths that exist are used as-is
866
+ // 3. Try finding as a flow name in flowsDir
867
+ // 4. Fall back to relative path from cwd
868
+ let tasksDirAbs = '';
869
+ if (path.isAbsolute(options.tasksDir)) {
870
+ tasksDirAbs = options.tasksDir;
871
+ } else {
872
+ const relPath = safeJoin(cwd, options.tasksDir);
873
+ if (fs.existsSync(relPath)) {
874
+ tasksDirAbs = relPath;
875
+ } else {
876
+ // Try finding as a flow name
877
+ try {
878
+ const config = loadConfig(repoRoot || cwd);
879
+ const flowsDir = getFlowsDir(config);
880
+ const foundFlow = findFlowDir(flowsDir, options.tasksDir);
881
+ if (foundFlow) {
882
+ tasksDirAbs = foundFlow;
883
+ } else {
884
+ tasksDirAbs = relPath;
885
+ }
886
+ } catch {
887
+ tasksDirAbs = relPath;
888
+ }
889
+ }
890
+ }
864
891
  context.tasksDir = tasksDirAbs;
865
892
 
866
893
  if (!fs.existsSync(tasksDirAbs)) {
867
894
  addIssue(issues, {
868
895
  id: 'tasks.missing_dir',
869
896
  severity: 'error',
870
- title: 'Tasks directory not found',
871
- message: `Tasks directory does not exist: ${tasksDirAbs}`,
897
+ title: 'Tasks or Flow directory not found',
898
+ message: `Tasks/Flow directory does not exist: ${options.tasksDir} (resolved to: ${tasksDirAbs})`,
872
899
  fixes: [
873
- 'Double-check the path you passed to `cursorflow run`',
874
- 'If needed, run: cursorflow init --example',
900
+ 'Double-check the path or flow name you passed',
901
+ 'Use: cursorflow new <FlowName> --lanes "lane1,lane2" to create a new flow',
902
+ 'Or run: cursorflow init --example for legacy tasks',
875
903
  ],
876
904
  });
877
905
  } else {
@@ -131,12 +131,13 @@ export class EnhancedLogManager {
131
131
  }
132
132
 
133
133
  /**
134
- * Get lane-task label like [L01-T02]
134
+ * Get lane-task label like [L1-T2-lanename10]
135
135
  */
136
136
  private getLaneTaskLabel(): string {
137
137
  const laneNum = (this.session.laneIndex ?? 0) + 1;
138
138
  const taskNum = (this.session.taskIndex ?? 0) + 1;
139
- return `L${laneNum.toString().padStart(2, '0')}-T${taskNum.toString().padStart(2, '0')}`;
139
+ const shortLaneName = this.session.laneName.substring(0, 10);
140
+ return `L${laneNum}-T${taskNum}-${shortLaneName}`;
140
141
  }
141
142
 
142
143
  /**
@@ -0,0 +1,42 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { safeJoin } from './path';
4
+
5
+ /**
6
+ * Find flow directory by name in the flows directory.
7
+ * Matches by exact name or by suffix (ignoring ID prefix like '001_').
8
+ *
9
+ * @param flowsDir The base flows directory (e.g., _cursorflow/flows)
10
+ * @param flowName The name of the flow to find
11
+ * @returns The absolute path to the flow directory, or null if not found
12
+ */
13
+ export function findFlowDir(flowsDir: string, flowName: string): string | null {
14
+ if (!fs.existsSync(flowsDir)) {
15
+ return null;
16
+ }
17
+
18
+ const dirs = fs.readdirSync(flowsDir)
19
+ .filter(name => {
20
+ const dirPath = safeJoin(flowsDir, name);
21
+ try {
22
+ return fs.statSync(dirPath).isDirectory();
23
+ } catch {
24
+ return false;
25
+ }
26
+ })
27
+ .filter(name => {
28
+ // Match by exact name or by suffix (ignoring ID prefix)
29
+ const match = name.match(/^\d+_(.+)$/);
30
+ return match ? match[1] === flowName : name === flowName;
31
+ });
32
+
33
+ if (dirs.length === 0) {
34
+ return null;
35
+ }
36
+
37
+ // Return the most recent one (highest ID / alphabetical)
38
+ dirs.sort((a, b) => b.localeCompare(a));
39
+ return safeJoin(flowsDir, dirs[0]!);
40
+ }
41
+
42
+