@litmers/cursorflow-orchestrator 0.1.9 → 0.1.12

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 (60) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +90 -72
  3. package/commands/cursorflow-clean.md +24 -135
  4. package/commands/cursorflow-doctor.md +66 -38
  5. package/commands/cursorflow-init.md +33 -50
  6. package/commands/cursorflow-models.md +51 -0
  7. package/commands/cursorflow-monitor.md +52 -72
  8. package/commands/cursorflow-prepare.md +426 -147
  9. package/commands/cursorflow-resume.md +51 -159
  10. package/commands/cursorflow-review.md +38 -202
  11. package/commands/cursorflow-run.md +197 -84
  12. package/commands/cursorflow-signal.md +27 -72
  13. package/dist/cli/clean.js +23 -0
  14. package/dist/cli/clean.js.map +1 -1
  15. package/dist/cli/doctor.js +14 -1
  16. package/dist/cli/doctor.js.map +1 -1
  17. package/dist/cli/index.js +14 -3
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/init.js +5 -4
  20. package/dist/cli/init.js.map +1 -1
  21. package/dist/cli/models.d.ts +7 -0
  22. package/dist/cli/models.js +104 -0
  23. package/dist/cli/models.js.map +1 -0
  24. package/dist/cli/monitor.js +17 -0
  25. package/dist/cli/monitor.js.map +1 -1
  26. package/dist/cli/prepare.d.ts +7 -0
  27. package/dist/cli/prepare.js +748 -0
  28. package/dist/cli/prepare.js.map +1 -0
  29. package/dist/cli/resume.js +56 -0
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +30 -1
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/signal.js +18 -0
  34. package/dist/cli/signal.js.map +1 -1
  35. package/dist/utils/cursor-agent.d.ts +4 -0
  36. package/dist/utils/cursor-agent.js +58 -10
  37. package/dist/utils/cursor-agent.js.map +1 -1
  38. package/dist/utils/doctor.d.ts +10 -0
  39. package/dist/utils/doctor.js +581 -1
  40. package/dist/utils/doctor.js.map +1 -1
  41. package/dist/utils/types.d.ts +2 -0
  42. package/examples/README.md +114 -59
  43. package/examples/demo-project/README.md +61 -79
  44. package/examples/demo-project/_cursorflow/tasks/demo-test/01-create-utils.json +17 -6
  45. package/examples/demo-project/_cursorflow/tasks/demo-test/02-add-tests.json +17 -6
  46. package/examples/demo-project/_cursorflow/tasks/demo-test/README.md +66 -25
  47. package/package.json +1 -1
  48. package/src/cli/clean.ts +27 -0
  49. package/src/cli/doctor.ts +18 -2
  50. package/src/cli/index.ts +15 -3
  51. package/src/cli/init.ts +6 -4
  52. package/src/cli/models.ts +83 -0
  53. package/src/cli/monitor.ts +20 -0
  54. package/src/cli/prepare.ts +844 -0
  55. package/src/cli/resume.ts +66 -0
  56. package/src/cli/run.ts +36 -2
  57. package/src/cli/signal.ts +22 -0
  58. package/src/utils/cursor-agent.ts +62 -10
  59. package/src/utils/doctor.ts +633 -5
  60. package/src/utils/types.ts +2 -0
@@ -0,0 +1,844 @@
1
+ /**
2
+ * CursorFlow prepare command
3
+ *
4
+ * Prepare task files for a new feature - Terminal-first approach
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as logger from '../utils/logger';
10
+ import { loadConfig, getTasksDir } from '../utils/config';
11
+ import { Task, RunnerConfig } from '../utils/types';
12
+
13
+ // Preset template types
14
+ type PresetType = 'complex' | 'simple' | 'merge';
15
+ type EffectivePresetType = PresetType | 'custom';
16
+
17
+ interface PrepareOptions {
18
+ featureName: string;
19
+ lanes: number;
20
+ template: string | null;
21
+ preset: PresetType | null; // --preset complex|simple|merge
22
+ sequential: boolean;
23
+ deps: string | null;
24
+ // Terminal-first options
25
+ prompt: string | null;
26
+ criteria: string[];
27
+ model: string | null;
28
+ taskSpecs: string[]; // Multiple --task "name|model|prompt|criteria"
29
+ // Incremental options
30
+ addLane: string | null; // Add lane to existing task dir
31
+ addTask: string | null; // Add task to existing lane file
32
+ dependsOnLanes: string[]; // --depends-on for new lane
33
+ force: boolean;
34
+ help: boolean;
35
+ }
36
+
37
+ function printHelp(): void {
38
+ console.log(`
39
+ Usage: cursorflow prepare <feature-name> [options]
40
+
41
+ Prepare task files for a new feature - Terminal-first workflow.
42
+
43
+ ═══════════════════════════════════════════════════════════════════════════════
44
+ WORKFLOW: Requirements → Lanes → Tasks → Validate → Run
45
+ ═══════════════════════════════════════════════════════════════════════════════
46
+
47
+ ## Step 1: Create Initial Lanes (with Presets)
48
+
49
+ # Complex implementation: plan → implement → test
50
+ cursorflow prepare FeatureName --preset complex --prompt "Implement user auth"
51
+
52
+ # Simple implementation: implement → test
53
+ cursorflow prepare BugFix --preset simple --prompt "Fix login bug"
54
+
55
+ # Merge lane: merge → test (for lanes with dependencies)
56
+ cursorflow prepare Integration --preset merge --depends-on "01-lane-1,02-lane-2"
57
+
58
+ # Multiple sequential lanes (auto-detects merge preset for dependent lanes)
59
+ cursorflow prepare FullStack --lanes 3 --sequential --prompt "Build your layer"
60
+
61
+ ## Step 2: Add More Lanes (Incremental)
62
+
63
+ # Add a merge lane to existing task directory
64
+ cursorflow prepare --add-lane _cursorflow/tasks/2412211530_FullStack \\
65
+ --preset merge --depends-on "01-lane-1,02-lane-2"
66
+
67
+ ## Step 3: Add More Tasks to a Lane
68
+
69
+ # Append a task to an existing lane
70
+ cursorflow prepare --add-task _cursorflow/tasks/2412211530_FullStack/01-lane-1.json \\
71
+ --task "verify|sonnet-4.5|Double-check all requirements|All criteria met"
72
+
73
+ ## Step 4: Validate Configuration
74
+
75
+ cursorflow doctor --tasks-dir _cursorflow/tasks/2412211530_FullStack
76
+
77
+ ## Step 5: Run
78
+
79
+ cursorflow run _cursorflow/tasks/2412211530_FullStack
80
+
81
+ ═══════════════════════════════════════════════════════════════════════════════
82
+
83
+ ## Preset Templates
84
+
85
+ --preset complex plan → implement → test (for complex features)
86
+ --preset simple implement → test (for simple changes)
87
+ --preset merge merge → test (auto-applied when --depends-on is set)
88
+
89
+ ## Options
90
+
91
+ Core:
92
+ <feature-name> Name of the feature (for new task directories)
93
+ --lanes <num> Number of lanes to create (default: 1)
94
+ --preset <type> Use preset template: complex | simple | merge
95
+
96
+ Task Definition:
97
+ --prompt <text> Task prompt (uses preset or single task)
98
+ --criteria <list> Comma-separated acceptance criteria
99
+ --model <model> Model to use (default: sonnet-4.5)
100
+ --task <spec> Full task spec: "name|model|prompt|criteria" (repeatable)
101
+
102
+ Dependencies:
103
+ --sequential Chain lanes: 1 → 2 → 3
104
+ --deps <spec> Custom dependencies: "2:1;3:1,2"
105
+ --depends-on <lanes> Dependencies for --add-lane: "01-lane-1,02-lane-2"
106
+
107
+ Incremental (add to existing):
108
+ --add-lane <dir> Add a new lane to existing task directory
109
+ --add-task <file> Append task(s) to existing lane JSON file
110
+
111
+ Advanced:
112
+ --template <path> Custom template JSON file
113
+ --force Overwrite existing files
114
+
115
+ ═══════════════════════════════════════════════════════════════════════════════
116
+
117
+ ## Examples
118
+
119
+ # 1. Complex feature with multiple lanes
120
+ cursorflow prepare AuthSystem --lanes 3 --sequential --preset complex \\
121
+ --prompt "Implement authentication for your layer"
122
+
123
+ # 2. Simple bug fix
124
+ cursorflow prepare FixLoginBug --preset simple \\
125
+ --prompt "Fix the login validation bug in auth.ts"
126
+
127
+ # 3. Add a merge/integration lane
128
+ cursorflow prepare --add-lane _cursorflow/tasks/2412211530_AuthSystem \\
129
+ --preset merge --depends-on "01-lane-1,02-lane-2"
130
+
131
+ # 4. Custom multi-task lane (overrides preset)
132
+ cursorflow prepare ComplexFeature \\
133
+ --task "plan|sonnet-4.5-thinking|Create implementation plan|Plan documented" \\
134
+ --task "implement|sonnet-4.5|Build the feature|Code complete" \\
135
+ --task "test|sonnet-4.5|Write comprehensive tests|Tests pass"
136
+ `);
137
+ }
138
+
139
+ function parseArgs(args: string[]): PrepareOptions {
140
+ const result: PrepareOptions = {
141
+ featureName: '',
142
+ lanes: 1,
143
+ template: null,
144
+ preset: null,
145
+ sequential: false,
146
+ deps: null,
147
+ prompt: null,
148
+ criteria: [],
149
+ model: null,
150
+ taskSpecs: [],
151
+ addLane: null,
152
+ addTask: null,
153
+ dependsOnLanes: [],
154
+ force: false,
155
+ help: false,
156
+ };
157
+
158
+ let i = 0;
159
+ while (i < args.length) {
160
+ const arg = args[i];
161
+
162
+ if (arg === '--help' || arg === '-h') {
163
+ result.help = true;
164
+ } else if (arg === '--force') {
165
+ result.force = true;
166
+ } else if (arg === '--sequential') {
167
+ result.sequential = true;
168
+ } else if (arg === '--lanes' && args[i + 1]) {
169
+ result.lanes = parseInt(args[++i]) || 1;
170
+ } else if (arg === '--template' && args[i + 1]) {
171
+ result.template = args[++i];
172
+ } else if (arg === '--preset' && args[i + 1]) {
173
+ const presetValue = args[++i].toLowerCase();
174
+ if (presetValue === 'complex' || presetValue === 'simple' || presetValue === 'merge') {
175
+ result.preset = presetValue;
176
+ } else {
177
+ throw new Error(`Invalid preset: "${presetValue}". Must be one of: complex, simple, merge`);
178
+ }
179
+ } else if (arg === '--deps' && args[i + 1]) {
180
+ result.deps = args[++i];
181
+ } else if (arg === '--prompt' && args[i + 1]) {
182
+ result.prompt = args[++i];
183
+ } else if (arg === '--criteria' && args[i + 1]) {
184
+ result.criteria = args[++i].split(',').map(c => c.trim()).filter(c => c);
185
+ } else if (arg === '--model' && args[i + 1]) {
186
+ result.model = args[++i];
187
+ } else if (arg === '--task' && args[i + 1]) {
188
+ result.taskSpecs.push(args[++i]);
189
+ } else if (arg === '--add-lane' && args[i + 1]) {
190
+ result.addLane = args[++i];
191
+ } else if (arg === '--add-task' && args[i + 1]) {
192
+ result.addTask = args[++i];
193
+ } else if (arg === '--depends-on' && args[i + 1]) {
194
+ result.dependsOnLanes = args[++i].split(',').map(d => d.trim()).filter(d => d);
195
+ } else if (!arg.startsWith('--') && !result.featureName) {
196
+ result.featureName = arg;
197
+ }
198
+
199
+ i++;
200
+ }
201
+
202
+ return result;
203
+ }
204
+
205
+ function parseTaskSpec(spec: string): Task {
206
+ // Format: "name|model|prompt|criteria1,criteria2"
207
+ const parts = spec.split('|');
208
+
209
+ if (parts.length < 3) {
210
+ throw new Error(`Invalid task spec: "${spec}". Expected format: "name|model|prompt[|criteria1,criteria2]"`);
211
+ }
212
+
213
+ const [name, model, prompt, criteriaStr] = parts;
214
+ const acceptanceCriteria = criteriaStr
215
+ ? criteriaStr.split(',').map(c => c.trim()).filter(c => c)
216
+ : undefined;
217
+
218
+ return {
219
+ name: name.trim(),
220
+ model: model.trim() || 'sonnet-4.5',
221
+ prompt: prompt.trim(),
222
+ ...(acceptanceCriteria && acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {}),
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Generate tasks based on preset template
228
+ */
229
+ function buildTasksFromPreset(
230
+ preset: PresetType,
231
+ featureName: string,
232
+ laneNumber: number,
233
+ basePrompt: string,
234
+ criteria: string[],
235
+ hasDependencies: boolean
236
+ ): Task[] {
237
+ const tasks: Task[] = [];
238
+
239
+ // Plan document path - stored in the worktree root
240
+ const planDocPath = `_cursorflow/PLAN_lane-${laneNumber}.md`;
241
+
242
+ // If lane has dependencies, auto-apply merge preset logic
243
+ const effectivePreset = hasDependencies && preset !== 'merge' ? preset : preset;
244
+
245
+ switch (effectivePreset) {
246
+ case 'complex':
247
+ // plan → implement → test
248
+ tasks.push(
249
+ {
250
+ name: 'plan',
251
+ model: 'sonnet-4.5-thinking',
252
+ prompt: `# Planning: ${featureName} (Lane ${laneNumber})
253
+
254
+ ## Goal
255
+ Analyze the requirements and create a detailed implementation plan.
256
+
257
+ ## Context
258
+ ${basePrompt}
259
+
260
+ ## Instructions
261
+ 1. Understand the scope and requirements.
262
+ 2. List all files that need to be created or modified.
263
+ 3. Define data structures and interfaces.
264
+ 4. Outline step-by-step implementation plan.
265
+
266
+ ## Output
267
+ **IMPORTANT: Save the plan document to \`${planDocPath}\`**
268
+
269
+ The plan document should include:
270
+ - Overview of the implementation approach
271
+ - List of files to create/modify
272
+ - Data structures and interfaces
273
+ - Step-by-step implementation tasks
274
+ - Potential risks and edge cases`,
275
+ acceptanceCriteria: [
276
+ `Plan document saved to ${planDocPath}`,
277
+ 'All required files are identified',
278
+ 'Approach is clearly defined',
279
+ ],
280
+ },
281
+ {
282
+ name: 'implement',
283
+ model: 'sonnet-4.5',
284
+ prompt: `# Implementation: ${featureName} (Lane ${laneNumber})
285
+
286
+ ## Goal
287
+ Implement the planned changes.
288
+
289
+ ## Context
290
+ ${basePrompt}
291
+
292
+ ## Plan Document
293
+ **Read the plan from \`${planDocPath}\` before starting implementation.**
294
+
295
+ ## Instructions
296
+ 1. Read and understand the plan document at \`${planDocPath}\`.
297
+ 2. Follow the plan step by step.
298
+ 3. Implement all code changes.
299
+ 4. Ensure no build errors.
300
+ 5. Write necessary code comments.
301
+ 6. Double-check all requirements before finishing.
302
+
303
+ ## Important
304
+ - Refer back to the plan document if unsure about any step.
305
+ - Verify all edge cases from the plan are handled.
306
+ - Ensure code follows project conventions.`,
307
+ acceptanceCriteria: criteria.length > 0 ? criteria : [
308
+ 'Code implemented according to plan',
309
+ 'No build errors',
310
+ 'All edge cases handled',
311
+ ],
312
+ },
313
+ {
314
+ name: 'test',
315
+ model: 'sonnet-4.5',
316
+ prompt: `# Testing: ${featureName} (Lane ${laneNumber})
317
+
318
+ ## Goal
319
+ Write comprehensive tests for the implementation.
320
+
321
+ ## Plan Document
322
+ **Refer to \`${planDocPath}\` for the list of features and edge cases to test.**
323
+
324
+ ## Instructions
325
+ 1. Review the plan document for test requirements.
326
+ 2. Write unit tests for new functions/classes.
327
+ 3. Write integration tests if applicable.
328
+ 4. Ensure all tests pass.
329
+ 5. Verify edge cases from the plan are covered.
330
+ 6. Double-check that nothing is missing.
331
+
332
+ ## Important
333
+ - All tests must pass before completing.
334
+ - Cover happy path and error cases from the plan.`,
335
+ acceptanceCriteria: [
336
+ 'Unit tests written',
337
+ 'All tests pass',
338
+ 'Edge cases covered',
339
+ ],
340
+ }
341
+ );
342
+ break;
343
+
344
+ case 'simple':
345
+ // implement → test
346
+ tasks.push(
347
+ {
348
+ name: 'implement',
349
+ model: 'sonnet-4.5',
350
+ prompt: `# Implementation: ${featureName} (Lane ${laneNumber})
351
+
352
+ ## Goal
353
+ ${basePrompt}
354
+
355
+ ## Instructions
356
+ 1. Implement the required changes.
357
+ 2. Ensure no build errors.
358
+ 3. Handle edge cases appropriately.
359
+ 4. Double-check all requirements before finishing.
360
+
361
+ ## Important
362
+ - Keep changes focused and minimal.
363
+ - Follow existing code conventions.`,
364
+ acceptanceCriteria: criteria.length > 0 ? criteria : [
365
+ 'Implementation complete',
366
+ 'No build errors',
367
+ 'Code follows conventions',
368
+ ],
369
+ },
370
+ {
371
+ name: 'test',
372
+ model: 'sonnet-4.5',
373
+ prompt: `# Testing: ${featureName} (Lane ${laneNumber})
374
+
375
+ ## Goal
376
+ Test the implementation thoroughly.
377
+
378
+ ## Instructions
379
+ 1. Write or update tests for the changes.
380
+ 2. Run all related tests.
381
+ 3. Ensure all tests pass.
382
+ 4. Double-check edge cases.
383
+
384
+ ## Important
385
+ - All tests must pass before completing.`,
386
+ acceptanceCriteria: [
387
+ 'Tests written/updated',
388
+ 'All tests pass',
389
+ ],
390
+ }
391
+ );
392
+ break;
393
+
394
+ case 'merge':
395
+ // merge → test (for dependent lanes)
396
+ tasks.push(
397
+ {
398
+ name: 'merge',
399
+ model: 'sonnet-4.5',
400
+ prompt: `# Merge & Integrate: ${featureName} (Lane ${laneNumber})
401
+
402
+ ## Goal
403
+ Merge dependent branches and resolve any conflicts.
404
+
405
+ ## Instructions
406
+ 1. The dependent branches have been automatically merged.
407
+ 2. Check for any merge conflicts and resolve them.
408
+ 3. Ensure all imports and dependencies are correct.
409
+ 4. Verify the integrated code compiles without errors.
410
+ 5. Fix any integration issues.
411
+
412
+ ## Important
413
+ - Resolve all conflicts cleanly.
414
+ - Ensure code from all merged branches works together.
415
+ - Check that no functionality was broken by the merge.`,
416
+ acceptanceCriteria: [
417
+ 'All conflicts resolved',
418
+ 'No build errors',
419
+ 'Integration verified',
420
+ ],
421
+ },
422
+ {
423
+ name: 'test',
424
+ model: 'sonnet-4.5',
425
+ prompt: `# Integration Testing: ${featureName} (Lane ${laneNumber})
426
+
427
+ ## Goal
428
+ Run comprehensive tests after the merge.
429
+
430
+ ## Instructions
431
+ 1. Run all unit tests.
432
+ 2. Run integration tests.
433
+ 3. Test that features from merged branches work together.
434
+ 4. Verify no regressions were introduced.
435
+ 5. Fix any failing tests.
436
+
437
+ ## Important
438
+ - All tests must pass.
439
+ - Test the interaction between merged features.`,
440
+ acceptanceCriteria: criteria.length > 0 ? criteria : [
441
+ 'All unit tests pass',
442
+ 'Integration tests pass',
443
+ 'No regressions',
444
+ ],
445
+ }
446
+ );
447
+ break;
448
+ }
449
+
450
+ return tasks;
451
+ }
452
+
453
+ function buildTasksFromOptions(
454
+ options: PrepareOptions,
455
+ laneNumber: number,
456
+ featureName: string,
457
+ hasDependencies: boolean = false
458
+ ): Task[] {
459
+ // Priority: --task > --preset/dependencies > --prompt alone > default
460
+
461
+ // 1. Explicit --task specifications (highest priority)
462
+ if (options.taskSpecs.length > 0) {
463
+ const tasks: Task[] = [];
464
+ for (const spec of options.taskSpecs) {
465
+ tasks.push(parseTaskSpec(spec));
466
+ }
467
+ return tasks;
468
+ }
469
+
470
+ // 2. Preset template (use when --preset specified OR lane has dependencies)
471
+ // --prompt serves as context when used with preset
472
+ if (options.preset || hasDependencies) {
473
+ // Auto-apply merge preset if lane has dependencies and no explicit preset
474
+ const preset = options.preset || (hasDependencies ? 'merge' : 'complex');
475
+ return buildTasksFromPreset(
476
+ preset,
477
+ featureName,
478
+ laneNumber,
479
+ options.prompt || `Implement ${featureName}`,
480
+ options.criteria,
481
+ hasDependencies
482
+ );
483
+ }
484
+
485
+ // 3. Single task from --prompt (only when no preset specified)
486
+ if (options.prompt) {
487
+ const task: Task = {
488
+ name: 'implement',
489
+ model: options.model || 'sonnet-4.5',
490
+ prompt: options.prompt,
491
+ };
492
+
493
+ if (options.criteria.length > 0) {
494
+ task.acceptanceCriteria = options.criteria;
495
+ }
496
+
497
+ return [task];
498
+ }
499
+
500
+ // 4. Default: complex preset
501
+ return buildTasksFromPreset(
502
+ 'complex',
503
+ featureName,
504
+ laneNumber,
505
+ `Implement ${featureName}`,
506
+ options.criteria,
507
+ hasDependencies
508
+ );
509
+ }
510
+
511
+ function getDefaultConfig(laneNumber: number, featureName: string, tasks: Task[]) {
512
+ return {
513
+ // Git Configuration
514
+ baseBranch: 'main',
515
+ branchPrefix: `${featureName.toLowerCase()}/lane-${laneNumber}-`,
516
+
517
+ // Execution Settings
518
+ timeout: 300000,
519
+ enableIntervention: false,
520
+
521
+ // Dependency Policy
522
+ dependencyPolicy: {
523
+ allowDependencyChange: false,
524
+ lockfileReadOnly: true,
525
+ },
526
+
527
+ // Review Settings
528
+ enableReview: true,
529
+ reviewModel: 'sonnet-4.5-thinking',
530
+ maxReviewIterations: 3,
531
+
532
+ // Lane Metadata
533
+ laneNumber: laneNumber,
534
+ devPort: 3000 + laneNumber,
535
+
536
+ // Tasks
537
+ tasks: tasks,
538
+ };
539
+ }
540
+
541
+ function parseDeps(depsStr: string): Map<number, number[]> {
542
+ const map = new Map<number, number[]>();
543
+ // Format: "2:1;3:1,2"
544
+ const lanes = depsStr.split(';');
545
+ for (const lane of lanes) {
546
+ const [targetStr, depsPart] = lane.split(':');
547
+ if (!targetStr || !depsPart) continue;
548
+
549
+ const target = parseInt(targetStr);
550
+ const deps = depsPart.split(',').map(d => parseInt(d)).filter(d => !isNaN(d));
551
+
552
+ if (!isNaN(target) && deps.length > 0) {
553
+ map.set(target, deps);
554
+ }
555
+ }
556
+ return map;
557
+ }
558
+
559
+ function replacePlaceholders(obj: any, context: { featureName: string; laneNumber: number; devPort: number }): any {
560
+ if (typeof obj === 'string') {
561
+ return obj
562
+ .replace(/\{\{featureName\}\}/g, context.featureName)
563
+ .replace(/\{\{laneNumber\}\}/g, String(context.laneNumber))
564
+ .replace(/\{\{devPort\}\}/g, String(context.devPort));
565
+ }
566
+
567
+ if (Array.isArray(obj)) {
568
+ return obj.map(item => replacePlaceholders(item, context));
569
+ }
570
+
571
+ if (obj !== null && typeof obj === 'object') {
572
+ const result: any = {};
573
+ for (const key in obj) {
574
+ result[key] = replacePlaceholders(obj[key], context);
575
+ }
576
+ return result;
577
+ }
578
+
579
+ return obj;
580
+ }
581
+
582
+ function getNextLaneNumber(taskDir: string): number {
583
+ const files = fs.readdirSync(taskDir).filter(f => f.endsWith('.json'));
584
+ let maxNum = 0;
585
+ for (const file of files) {
586
+ const match = file.match(/^(\d+)-/);
587
+ if (match) {
588
+ const num = parseInt(match[1]);
589
+ if (num > maxNum) maxNum = num;
590
+ }
591
+ }
592
+ return maxNum + 1;
593
+ }
594
+
595
+ function getFeatureNameFromDir(taskDir: string): string {
596
+ const dirName = path.basename(taskDir);
597
+ // Format: YYMMDDHHMM_FeatureName
598
+ const match = dirName.match(/^\d+_(.+)$/);
599
+ return match ? match[1] : dirName;
600
+ }
601
+
602
+ async function addLaneToDir(options: PrepareOptions): Promise<void> {
603
+ const taskDir = path.resolve(process.cwd(), options.addLane!);
604
+
605
+ if (!fs.existsSync(taskDir)) {
606
+ throw new Error(`Task directory not found: ${taskDir}`);
607
+ }
608
+
609
+ const featureName = getFeatureNameFromDir(taskDir);
610
+ const laneNumber = getNextLaneNumber(taskDir);
611
+ const laneName = `lane-${laneNumber}`;
612
+ const fileName = `${laneNumber.toString().padStart(2, '0')}-${laneName}.json`;
613
+ const filePath = path.join(taskDir, fileName);
614
+
615
+ if (fs.existsSync(filePath) && !options.force) {
616
+ throw new Error(`Lane file already exists: ${filePath}. Use --force to overwrite.`);
617
+ }
618
+
619
+ const hasDependencies = options.dependsOnLanes.length > 0;
620
+
621
+ // Build tasks from options (auto-detects merge preset if has dependencies)
622
+ const tasks = buildTasksFromOptions(options, laneNumber, featureName, hasDependencies);
623
+ const config = getDefaultConfig(laneNumber, featureName, tasks);
624
+
625
+ // Add dependencies if specified
626
+ const finalConfig = {
627
+ ...config,
628
+ ...(hasDependencies ? { dependsOn: options.dependsOnLanes } : {}),
629
+ };
630
+
631
+ fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', 'utf8');
632
+
633
+ const taskSummary = tasks.map(t => t.name).join(' → ');
634
+ const depsInfo = hasDependencies ? ` (depends: ${options.dependsOnLanes.join(', ')})` : '';
635
+ const presetInfo = options.preset ? ` [${options.preset}]` : (hasDependencies ? ' [merge]' : '');
636
+
637
+ logger.success(`Added lane: ${fileName} [${taskSummary}]${presetInfo}${depsInfo}`);
638
+ logger.info(`Directory: ${taskDir}`);
639
+
640
+ console.log(`\nNext steps:`);
641
+ console.log(` 1. Validate: cursorflow doctor --tasks-dir ${taskDir}`);
642
+ console.log(` 2. Run: cursorflow run ${taskDir}`);
643
+ }
644
+
645
+ async function addTaskToLane(options: PrepareOptions): Promise<void> {
646
+ const laneFile = path.resolve(process.cwd(), options.addTask!);
647
+
648
+ if (!fs.existsSync(laneFile)) {
649
+ throw new Error(`Lane file not found: ${laneFile}`);
650
+ }
651
+
652
+ if (options.taskSpecs.length === 0) {
653
+ throw new Error('No task specified. Use --task "name|model|prompt|criteria" to define a task.');
654
+ }
655
+
656
+ // Read existing config
657
+ const existingConfig = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
658
+
659
+ if (!existingConfig.tasks || !Array.isArray(existingConfig.tasks)) {
660
+ existingConfig.tasks = [];
661
+ }
662
+
663
+ // Add new tasks
664
+ const newTasks: Task[] = [];
665
+ for (const spec of options.taskSpecs) {
666
+ const task = parseTaskSpec(spec);
667
+ existingConfig.tasks.push(task);
668
+ newTasks.push(task);
669
+ }
670
+
671
+ // Write back
672
+ fs.writeFileSync(laneFile, JSON.stringify(existingConfig, null, 2) + '\n', 'utf8');
673
+
674
+ const taskNames = newTasks.map(t => t.name).join(', ');
675
+ logger.success(`Added task(s): ${taskNames}`);
676
+ logger.info(`Updated: ${laneFile}`);
677
+
678
+ const taskSummary = existingConfig.tasks.map((t: Task) => t.name).join(' → ');
679
+ logger.info(`Lane now has: ${taskSummary}`);
680
+ }
681
+
682
+ async function createNewFeature(options: PrepareOptions): Promise<void> {
683
+ const config = loadConfig();
684
+ const tasksBaseDir = getTasksDir(config);
685
+
686
+ // Timestamp-based folder name (YYMMDDHHMM)
687
+ const now = new Date();
688
+ const timestamp = now.toISOString().replace(/[-T:]/g, '').substring(2, 12);
689
+ const taskDirName = `${timestamp}_${options.featureName}`;
690
+ const taskDir = path.join(tasksBaseDir, taskDirName);
691
+
692
+ if (fs.existsSync(taskDir) && !options.force) {
693
+ throw new Error(`Task directory already exists: ${taskDir}. Use --force to overwrite.`);
694
+ }
695
+
696
+ if (!fs.existsSync(taskDir)) {
697
+ fs.mkdirSync(taskDir, { recursive: true });
698
+ }
699
+
700
+ logger.info(`Creating tasks in: ${path.relative(config.projectRoot, taskDir)}`);
701
+
702
+ // Load template if provided (overrides --prompt/--task/--preset)
703
+ let template = null;
704
+ if (options.template) {
705
+ const templatePath = path.resolve(process.cwd(), options.template);
706
+ if (!fs.existsSync(templatePath)) {
707
+ throw new Error(`Template file not found: ${templatePath}`);
708
+ }
709
+ template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
710
+ logger.info(`Using template: ${options.template}`);
711
+ }
712
+
713
+ // Calculate dependencies
714
+ const dependencyMap = options.sequential
715
+ ? new Map(Array.from({ length: options.lanes - 1 }, (_, i) => [i + 2, [i + 1]]))
716
+ : (options.deps ? parseDeps(options.deps) : new Map<number, number[]>());
717
+
718
+ const laneInfoList: { name: string; fileName: string; dependsOn: string[]; preset: string }[] = [];
719
+
720
+ for (let i = 1; i <= options.lanes; i++) {
721
+ const laneName = `lane-${i}`;
722
+ const fileName = `${i.toString().padStart(2, '0')}-${laneName}.json`;
723
+ const filePath = path.join(taskDir, fileName);
724
+
725
+ const depNums = dependencyMap.get(i) || [];
726
+ const dependsOn = depNums.map(n => {
727
+ const depLaneName = `lane-${n}`;
728
+ return `${n.toString().padStart(2, '0')}-${depLaneName}`;
729
+ });
730
+
731
+ const hasDependencies = dependsOn.length > 0;
732
+ const devPort = 3000 + i;
733
+
734
+ let taskConfig;
735
+ let effectivePreset: EffectivePresetType = options.preset || (hasDependencies ? 'merge' : 'complex');
736
+
737
+ if (template) {
738
+ // Use template
739
+ taskConfig = { ...template, laneNumber: i, devPort };
740
+ effectivePreset = 'custom';
741
+ } else {
742
+ // Build from CLI options
743
+ const tasks = buildTasksFromOptions(options, i, options.featureName, hasDependencies);
744
+ taskConfig = getDefaultConfig(i, options.featureName, tasks);
745
+ }
746
+
747
+ // Replace placeholders
748
+ const processedConfig = replacePlaceholders(taskConfig, {
749
+ featureName: options.featureName,
750
+ laneNumber: i,
751
+ devPort: devPort,
752
+ });
753
+
754
+ // Add dependencies if any
755
+ const finalConfig = {
756
+ ...processedConfig,
757
+ ...(dependsOn.length > 0 ? { dependsOn } : {}),
758
+ };
759
+
760
+ fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', 'utf8');
761
+
762
+ const taskSummary = finalConfig.tasks?.map((t: any) => t.name).join(' → ') || 'default';
763
+ const presetLabel = effectivePreset !== 'custom' ? ` [${effectivePreset}]` : '';
764
+ logger.success(`Created: ${fileName} [${taskSummary}]${presetLabel}${dependsOn.length > 0 ? ` (depends: ${dependsOn.join(', ')})` : ''}`);
765
+
766
+ laneInfoList.push({ name: laneName, fileName, dependsOn, preset: effectivePreset });
767
+ }
768
+
769
+ // Create README
770
+ const readmePath = path.join(taskDir, 'README.md');
771
+ const readme = `# Task: ${options.featureName}
772
+
773
+ Prepared at: ${now.toISOString()}
774
+ Lanes: ${options.lanes}
775
+
776
+ ## How to Run
777
+
778
+ \`\`\`bash
779
+ # 1. Validate configuration
780
+ cursorflow doctor --tasks-dir ${path.relative(config.projectRoot, taskDir)}
781
+
782
+ # 2. Run
783
+ cursorflow run ${path.relative(config.projectRoot, taskDir)}
784
+ \`\`\`
785
+
786
+ ## Lanes
787
+
788
+ ${laneInfoList.map(l => `- **${l.fileName.replace('.json', '')}** [${l.preset}]${l.dependsOn.length > 0 ? ` (depends: ${l.dependsOn.join(', ')})` : ''}`).join('\n')}
789
+
790
+ ## Modifying Tasks
791
+
792
+ \`\`\`bash
793
+ # Add a new lane (with merge preset for dependent lanes)
794
+ cursorflow prepare --add-lane ${path.relative(config.projectRoot, taskDir)} \\
795
+ --preset merge --depends-on "01-lane-1"
796
+
797
+ # Add task to existing lane
798
+ cursorflow prepare --add-task ${path.relative(config.projectRoot, taskDir)}/01-lane-1.json \\
799
+ --task "verify|sonnet-4.5|Verify requirements|All met"
800
+ \`\`\`
801
+ `;
802
+
803
+ fs.writeFileSync(readmePath, readme, 'utf8');
804
+ logger.success('Created README.md');
805
+
806
+ logger.section('✅ Preparation complete!');
807
+ console.log(`\nNext steps:`);
808
+ console.log(` 1. (Optional) Add more lanes/tasks`);
809
+ console.log(` 2. Validate: cursorflow doctor --tasks-dir ${path.relative(config.projectRoot, taskDir)}`);
810
+ console.log(` 3. Run: cursorflow run ${path.relative(config.projectRoot, taskDir)}`);
811
+ console.log('');
812
+ }
813
+
814
+ async function prepare(args: string[]): Promise<void> {
815
+ const options = parseArgs(args);
816
+
817
+ if (options.help) {
818
+ printHelp();
819
+ return;
820
+ }
821
+
822
+ // Mode 1: Add task to existing lane
823
+ if (options.addTask) {
824
+ await addTaskToLane(options);
825
+ return;
826
+ }
827
+
828
+ // Mode 2: Add lane to existing directory
829
+ if (options.addLane) {
830
+ await addLaneToDir(options);
831
+ return;
832
+ }
833
+
834
+ // Mode 3: Create new feature (requires featureName)
835
+ if (!options.featureName) {
836
+ printHelp();
837
+ process.exit(1);
838
+ return;
839
+ }
840
+
841
+ await createNewFeature(options);
842
+ }
843
+
844
+ export = prepare;