@litmers/cursorflow-orchestrator 0.2.2 → 0.2.3

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.
@@ -1,777 +0,0 @@
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
- import { safeJoin } from '../utils/path';
13
- import { resolveTemplate } from '../utils/template';
14
-
15
- // Preset template types
16
- type PresetType = 'complex' | 'simple' | 'merge';
17
- type EffectivePresetType = PresetType | 'custom';
18
-
19
- interface PrepareOptions {
20
- featureName: string;
21
- lanes: number;
22
- template: string | null;
23
- preset: PresetType | null; // --preset complex|simple|merge
24
- // Terminal-first options
25
- prompt: string | null;
26
- criteria: string[];
27
- model: string | null;
28
- taskSpecs: string[]; // Multiple --task "name|model|prompt|criteria|dependsOn|timeout"
29
- // Incremental options
30
- addLane: string | null; // Add lane to existing task dir
31
- addTask: string | null; // Add task to existing lane file
32
- force: boolean;
33
- help: boolean;
34
- }
35
-
36
- function printHelp(): void {
37
- console.log(`
38
- cursorflow prepare - 태스크 파일 생성
39
-
40
- ═══════════════════════════════════════════════════════════════════════════════
41
- 시나리오: "쇼핑몰" 프로젝트에서 백엔드 API와 프론트엔드 동시 개발
42
- ═══════════════════════════════════════════════════════════════════════════════
43
-
44
- [Case 1] 가장 간단하게 - 버그 하나 고치기
45
- ─────────────────────────────────────────
46
- cursorflow prepare FixCartBug --prompt "장바구니 수량 버그 수정"
47
-
48
- 결과: _cursorflow/tasks/2412251030_FixCartBug/
49
- └── 01-FixCartBug.json (implement 태스크 1개)
50
-
51
-
52
- [Case 2] 프리셋 사용 - 계획부터 테스트까지
53
- ─────────────────────────────────────────
54
- cursorflow prepare PaymentAPI --preset complex --prompt "Stripe 결제 연동"
55
-
56
- 결과: 01-PaymentAPI.json에 plan → implement → test 태스크 생성
57
-
58
- 프리셋:
59
- --preset complex plan → implement → test
60
- --preset simple implement → test
61
- (없으면) implement만
62
-
63
-
64
- [Case 3] 병렬 레인 - 백엔드/프론트 동시 개발
65
- ─────────────────────────────────────────
66
- cursorflow prepare ShopFeature --lanes 2 --preset complex \\
67
- --prompt "상품 검색 기능"
68
-
69
- 결과: 01-lane-1.json (백엔드) ─┬─ 동시 실행
70
- 02-lane-2.json (프론트) ─┘
71
-
72
-
73
- [Case 4] 의존성 - 프론트가 백엔드 완료 후 시작
74
- ─────────────────────────────────────────
75
- cursorflow prepare --add-task ./02-lane-2.json \\
76
- --task "integrate|sonnet-4.5|API 연동|완료|01-lane-1:implement"
77
- └─ 이 태스크 완료 후 시작
78
-
79
- 실행 흐름:
80
- 01-lane-1: [plan] → [implement] → [test]
81
- ↓ 완료되면
82
- 02-lane-2: [plan] ───────┴─────→ [integrate]
83
-
84
-
85
- [Case 5] 커스텀 태스크 - 원하는 대로 구성
86
- ─────────────────────────────────────────
87
- cursorflow prepare CustomFlow \\
88
- --task "setup|sonnet-4.5|DB 스키마 생성|완료" \\
89
- --task "api|sonnet-4.5|REST API 구현|동작" \\
90
- --task "test|sonnet-4.5|테스트 작성|통과"
91
-
92
-
93
- [Case 6] 나중에 추가 - 레인이나 태스크 덧붙이기
94
- ─────────────────────────────────────────
95
- # 새 레인 추가
96
- cursorflow prepare --add-lane ./tasks/ShopFeature --preset simple
97
-
98
- # 기존 레인에 태스크 추가
99
- cursorflow prepare --add-task ./01-lane-1.json \\
100
- --task "docs|sonnet-4.5|API 문서화|완성"
101
-
102
- ═══════════════════════════════════════════════════════════════════════════════
103
-
104
- --task 형식: "이름|모델|프롬프트|완료조건|의존성|타임아웃"
105
-
106
- 예시:
107
- "build|sonnet-4.5|빌드하기|완료" 기본
108
- "deploy|sonnet-4.5|배포|성공|01-lane:build" 의존성
109
- "heavy|sonnet-4.5|대용량|완료||1200000" 타임아웃 20분
110
-
111
- ═══════════════════════════════════════════════════════════════════════════════
112
-
113
- 옵션 요약:
114
- --prompt <text> 작업 설명
115
- --preset <type> complex | simple | merge
116
- --lanes <num> 병렬 레인 수 (기본: 1)
117
- --task <spec> 커스텀 태스크 (반복 가능)
118
- --add-lane <dir> 기존 디렉토리에 레인 추가
119
- --add-task <file> 기존 레인에 태스크 추가
120
- --model <model> AI 모델 (기본: sonnet-4.5)
121
- --template <path> 외부 템플릿 파일
122
- --force 덮어쓰기
123
- `);
124
- }
125
-
126
- function parseArgs(args: string[]): PrepareOptions {
127
- const result: PrepareOptions = {
128
- featureName: '',
129
- lanes: 1,
130
- template: null,
131
- preset: null,
132
- prompt: null,
133
- criteria: [],
134
- model: null,
135
- taskSpecs: [],
136
- addLane: null,
137
- addTask: null,
138
- force: false,
139
- help: false,
140
- };
141
-
142
- let i = 0;
143
- while (i < args.length) {
144
- const arg = args[i];
145
-
146
- if (arg === '--help' || arg === '-h') {
147
- result.help = true;
148
- } else if (arg === '--force') {
149
- result.force = true;
150
- } else if (arg === '--lanes' && args[i + 1]) {
151
- result.lanes = parseInt(args[++i]) || 1;
152
- } else if (arg === '--template' && args[i + 1]) {
153
- result.template = args[++i];
154
- } else if (arg === '--preset' && args[i + 1]) {
155
- const presetValue = args[++i].toLowerCase();
156
- if (presetValue === 'complex' || presetValue === 'simple' || presetValue === 'merge') {
157
- result.preset = presetValue;
158
- } else {
159
- throw new Error(`Invalid preset: "${presetValue}". Must be one of: complex, simple, merge`);
160
- }
161
- } else if (arg === '--prompt' && args[i + 1]) {
162
- result.prompt = args[++i];
163
- } else if (arg === '--criteria' && args[i + 1]) {
164
- result.criteria = args[++i].split(',').map(c => c.trim()).filter(c => c);
165
- } else if (arg === '--model' && args[i + 1]) {
166
- result.model = args[++i];
167
- } else if (arg === '--task' && args[i + 1]) {
168
- result.taskSpecs.push(args[++i]);
169
- } else if (arg === '--add-lane' && args[i + 1]) {
170
- result.addLane = args[++i];
171
- } else if (arg === '--add-task' && args[i + 1]) {
172
- result.addTask = args[++i];
173
- } else if (!arg.startsWith('--') && !result.featureName) {
174
- result.featureName = arg;
175
- }
176
-
177
- i++;
178
- }
179
-
180
- return result;
181
- }
182
-
183
- function parseTaskSpec(spec: string): Task {
184
- // Format: "name|model|prompt|criteria1,criteria2|lane:task1,lane:task2|timeoutMs"
185
- const parts = spec.split('|');
186
-
187
- if (parts.length < 3) {
188
- throw new Error(`Invalid task spec: "${spec}". Expected format: "name|model|prompt[|criteria[|dependsOn[|timeout]]]"`);
189
- }
190
-
191
- const [name, model, prompt, _criteriaStr, depsStr, timeoutStr] = parts;
192
-
193
- const dependsOn = depsStr
194
- ? depsStr.split(',').map(d => d.trim()).filter(d => d)
195
- : undefined;
196
-
197
- const timeout = timeoutStr ? parseInt(timeoutStr) : undefined;
198
-
199
- return {
200
- name: name.trim(),
201
- model: model.trim() || 'sonnet-4.5',
202
- prompt: prompt.trim(),
203
- ...(dependsOn && dependsOn.length > 0 ? { dependsOn } : {}),
204
- ...(timeout ? { timeout } : {}),
205
- };
206
- }
207
-
208
- /**
209
- * Generate tasks based on preset template
210
- */
211
- function buildTasksFromPreset(
212
- preset: PresetType,
213
- featureName: string,
214
- laneNumber: number,
215
- basePrompt: string,
216
- criteria: string[],
217
- hasDependencies: boolean
218
- ): Task[] {
219
- const tasks: Task[] = [];
220
-
221
- // Plan document path - stored in the worktree root
222
- const planDocPath = `_cursorflow/PLAN_lane-${laneNumber}.md`;
223
-
224
- // If lane has dependencies, auto-apply merge preset logic
225
- const effectivePreset = hasDependencies && preset !== 'merge' ? preset : preset;
226
-
227
- switch (effectivePreset) {
228
- case 'complex':
229
- // plan → implement → test
230
- tasks.push(
231
- {
232
- name: 'plan',
233
- model: 'sonnet-4.5-thinking',
234
- prompt: `# Planning: ${featureName} (Lane ${laneNumber})
235
-
236
- ## Goal
237
- Analyze the requirements and create a detailed implementation plan.
238
-
239
- ## Context
240
- ${basePrompt}
241
-
242
- ## Instructions
243
- 1. Understand the scope and requirements.
244
- 2. List all files that need to be created or modified.
245
- 3. Define data structures and interfaces.
246
- 4. Outline step-by-step implementation plan.
247
-
248
- ## Output
249
- **IMPORTANT: Save the plan document to \`${planDocPath}\`**
250
-
251
- The plan document should include:
252
- - Overview of the implementation approach
253
- - List of files to create/modify
254
- - Data structures and interfaces
255
- - Step-by-step implementation tasks
256
- - Potential risks and edge cases`,
257
- },
258
- {
259
- name: 'implement',
260
- model: 'sonnet-4.5',
261
- prompt: `# Implementation: ${featureName} (Lane ${laneNumber})
262
-
263
- ## Goal
264
- Implement the planned changes.
265
-
266
- ## Context
267
- ${basePrompt}
268
-
269
- ## Plan Document
270
- **Read the plan from \`${planDocPath}\` before starting implementation.**
271
-
272
- ## Instructions
273
- 1. Read and understand the plan document at \`${planDocPath}\`.
274
- 2. Follow the plan step by step.
275
- 3. Implement all code changes.
276
- 4. Ensure no build errors.
277
- 5. Write necessary code comments.
278
- 6. Double-check all requirements before finishing.
279
-
280
- ## Important
281
- - Refer back to the plan document if unsure about any step.
282
- - Verify all edge cases from the plan are handled.
283
- - Ensure code follows project conventions.`,
284
- },
285
- {
286
- name: 'test',
287
- model: 'sonnet-4.5',
288
- prompt: `# Testing: ${featureName} (Lane ${laneNumber})
289
-
290
- ## Goal
291
- Write comprehensive tests for the implementation.
292
-
293
- ## Plan Document
294
- **Refer to \`${planDocPath}\` for the list of features and edge cases to test.**
295
-
296
- ## Instructions
297
- 1. Review the plan document for test requirements.
298
- 2. Write unit tests for new functions/classes.
299
- 3. Write integration tests if applicable.
300
- 4. Ensure all tests pass.
301
- 5. Verify edge cases from the plan are covered.
302
- 6. Double-check that nothing is missing.
303
-
304
- ## Important
305
- - All tests must pass before completing.
306
- - Cover happy path and error cases from the plan.`,
307
- }
308
- );
309
- break;
310
-
311
- case 'simple':
312
- // implement → test
313
- tasks.push(
314
- {
315
- name: 'implement',
316
- model: 'sonnet-4.5',
317
- prompt: `# Implementation: ${featureName} (Lane ${laneNumber})
318
-
319
- ## Goal
320
- ${basePrompt}
321
-
322
- ## Instructions
323
- 1. Implement the required changes.
324
- 2. Ensure no build errors.
325
- 3. Handle edge cases appropriately.
326
- 4. Double-check all requirements before finishing.
327
-
328
- ## Important
329
- - Keep changes focused and minimal.
330
- - Follow existing code conventions.`,
331
- },
332
- {
333
- name: 'test',
334
- model: 'sonnet-4.5',
335
- prompt: `# Testing: ${featureName} (Lane ${laneNumber})
336
-
337
- ## Goal
338
- Test the implementation thoroughly.
339
-
340
- ## Instructions
341
- 1. Write or update tests for the changes.
342
- 2. Run all related tests.
343
- 3. Ensure all tests pass.
344
- 4. Double-check edge cases.
345
-
346
- ## Important
347
- - All tests must pass before completing.`,
348
- }
349
- );
350
- break;
351
-
352
- case 'merge':
353
- // merge → test (for dependent lanes)
354
- tasks.push(
355
- {
356
- name: 'merge',
357
- model: 'sonnet-4.5',
358
- prompt: `# Merge & Integrate: ${featureName} (Lane ${laneNumber})
359
-
360
- ## Goal
361
- Merge dependent branches and resolve any conflicts.
362
-
363
- ## Instructions
364
- 1. The dependent branches have been automatically merged.
365
- 2. Check for any merge conflicts and resolve them.
366
- 3. Ensure all imports and dependencies are correct.
367
- 4. Verify the integrated code compiles without errors.
368
- 5. Fix any integration issues.
369
-
370
- ## Important
371
- - Resolve all conflicts cleanly.
372
- - Ensure code from all merged branches works together.
373
- - Check that no functionality was broken by the merge.`,
374
- },
375
- {
376
- name: 'test',
377
- model: 'sonnet-4.5',
378
- prompt: `# Integration Testing: ${featureName} (Lane ${laneNumber})
379
-
380
- ## Goal
381
- Run comprehensive tests after the merge.
382
-
383
- ## Instructions
384
- 1. Run all unit tests.
385
- 2. Run integration tests.
386
- 3. Test that features from merged branches work together.
387
- 4. Verify no regressions were introduced.
388
- 5. Fix any failing tests.
389
-
390
- ## Important
391
- - All tests must pass.
392
- - Test the interaction between merged features.`,
393
- }
394
- );
395
- break;
396
- }
397
-
398
- return tasks;
399
- }
400
-
401
- function buildTasksFromOptions(
402
- options: PrepareOptions,
403
- laneNumber: number,
404
- featureName: string,
405
- _hasDependencies: boolean = false
406
- ): Task[] {
407
- // Priority: --task > --preset > --prompt alone > default
408
-
409
- // 1. Explicit --task specifications (highest priority)
410
- if (options.taskSpecs.length > 0) {
411
- const tasks: Task[] = [];
412
- for (const spec of options.taskSpecs) {
413
- tasks.push(parseTaskSpec(spec));
414
- }
415
- return tasks;
416
- }
417
-
418
- // 2. Preset template (use when --preset specified)
419
- // --prompt serves as context when used with preset
420
- if (options.preset) {
421
- return buildTasksFromPreset(
422
- options.preset,
423
- featureName,
424
- laneNumber,
425
- options.prompt || `Implement ${featureName}`,
426
- options.criteria,
427
- false
428
- );
429
- }
430
-
431
- // 3. Single task from --prompt (only when no preset specified)
432
- if (options.prompt) {
433
- const task: Task = {
434
- name: 'implement',
435
- model: options.model || 'sonnet-4.5',
436
- prompt: options.prompt,
437
- };
438
-
439
- return [task];
440
- }
441
-
442
- // 4. Default: complex preset
443
- return buildTasksFromPreset(
444
- 'complex',
445
- featureName,
446
- laneNumber,
447
- `Implement ${featureName}`,
448
- options.criteria,
449
- false
450
- );
451
- }
452
-
453
- function getDefaultConfig(laneNumber: number, featureName: string, tasks: Task[]) {
454
- return {
455
- // Git Configuration
456
- // baseBranch is auto-detected from current branch at runtime
457
- branchPrefix: `${featureName.toLowerCase()}/lane-${laneNumber}-`,
458
-
459
- // Execution Settings
460
- timeout: 600000,
461
-
462
- // Dependency Policy
463
- dependencyPolicy: {
464
- allowDependencyChange: false,
465
- lockfileReadOnly: true,
466
- },
467
-
468
- // Lane Metadata
469
- laneNumber: laneNumber,
470
- devPort: 3000 + laneNumber,
471
-
472
- // Tasks
473
- tasks: tasks,
474
- };
475
- }
476
-
477
- function replacePlaceholders(obj: any, context: { featureName: string; laneNumber: number; devPort: number }): any {
478
- if (typeof obj === 'string') {
479
- return obj
480
- .replace(/\{\{featureName\}\}/g, context.featureName)
481
- .replace(/\{\{laneNumber\}\}/g, String(context.laneNumber))
482
- .replace(/\{\{devPort\}\}/g, String(context.devPort));
483
- }
484
-
485
- if (Array.isArray(obj)) {
486
- return obj.map(item => replacePlaceholders(item, context));
487
- }
488
-
489
- if (obj !== null && typeof obj === 'object') {
490
- const result: any = {};
491
- for (const key in obj) {
492
- result[key] = replacePlaceholders(obj[key], context);
493
- }
494
- return result;
495
- }
496
-
497
- return obj;
498
- }
499
-
500
- function getNextLaneNumber(taskDir: string): number {
501
- const files = fs.readdirSync(taskDir).filter(f => f.endsWith('.json'));
502
- let maxNum = 0;
503
- for (const file of files) {
504
- const match = file.match(/^(\d+)-/);
505
- if (match) {
506
- const num = parseInt(match[1]);
507
- if (num > maxNum) maxNum = num;
508
- }
509
- }
510
- return maxNum + 1;
511
- }
512
-
513
- function getFeatureNameFromDir(taskDir: string): string {
514
- const dirName = path.basename(taskDir);
515
- // Format: YYMMDDHHMM_FeatureName
516
- const match = dirName.match(/^\d+_(.+)$/);
517
- return match ? match[1] : dirName;
518
- }
519
-
520
- async function addLaneToDir(options: PrepareOptions): Promise<void> {
521
- const taskDir = path.resolve(process.cwd(), options.addLane!); // nosemgrep
522
-
523
- if (!fs.existsSync(taskDir)) {
524
- throw new Error(`Task directory not found: ${taskDir}`);
525
- }
526
-
527
- const featureName = getFeatureNameFromDir(taskDir);
528
- const laneNumber = getNextLaneNumber(taskDir);
529
- const laneName = `lane-${laneNumber}`;
530
- const fileName = `${laneNumber.toString().padStart(2, '0')}-${laneName}.json`;
531
- const filePath = safeJoin(taskDir, fileName);
532
-
533
- // Load template if provided
534
- let template = null;
535
- if (options.template) {
536
- template = await resolveTemplate(options.template);
537
- }
538
-
539
- let taskConfig;
540
-
541
- if (template) {
542
- taskConfig = { ...template, laneNumber, devPort: 3000 + laneNumber };
543
- } else {
544
- // Build tasks from options
545
- const tasks = buildTasksFromOptions(options, laneNumber, featureName, false);
546
- taskConfig = getDefaultConfig(laneNumber, featureName, tasks);
547
- }
548
-
549
- // Replace placeholders
550
- const finalConfig = replacePlaceholders(taskConfig, {
551
- featureName,
552
- laneNumber,
553
- devPort: 3000 + laneNumber,
554
- });
555
-
556
- // Use atomic write with wx flag to avoid TOCTOU race condition (unless force is set)
557
- // SECURITY NOTE: Writing user-defined task configuration to the file system.
558
- // The input is from CLI arguments and templates, used to generate CursorFlow lane files.
559
- try {
560
- const writeFlag = options.force ? 'w' : 'wx';
561
- fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', { encoding: 'utf8', flag: writeFlag });
562
- } catch (err: any) {
563
- if (err.code === 'EEXIST') {
564
- throw new Error(`Lane file already exists: ${filePath}. Use --force to overwrite.`);
565
- }
566
- throw err;
567
- }
568
-
569
- const tasksList = finalConfig.tasks || [];
570
- const taskSummary = tasksList.map((t: any) => t.name).join(' → ');
571
- const presetInfo = options.preset ? ` [${options.preset}]` : (template ? ' [template]' : '');
572
-
573
- logger.success(`Added lane: ${fileName} [${taskSummary}]${presetInfo}`);
574
- logger.info(`Directory: ${taskDir}`);
575
-
576
- console.log(`\nNext steps:`);
577
- console.log(` 1. Validate: cursorflow doctor --tasks-dir ${taskDir}`);
578
- console.log(` 2. Run: cursorflow run ${taskDir}`);
579
- }
580
-
581
- async function addTaskToLane(options: PrepareOptions): Promise<void> {
582
- const laneFile = path.resolve(process.cwd(), options.addTask!); // nosemgrep
583
-
584
- if (options.taskSpecs.length === 0) {
585
- throw new Error('No task specified. Use --task "name|model|prompt|criteria" to define a task.');
586
- }
587
-
588
- // Read existing config - let the error propagate if file doesn't exist (avoids TOCTOU)
589
- let existingConfig: any;
590
- try {
591
- existingConfig = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
592
- } catch (err: any) {
593
- if (err.code === 'ENOENT') {
594
- throw new Error(`Lane file not found: ${laneFile}`);
595
- }
596
- throw err;
597
- }
598
-
599
- if (!existingConfig.tasks || !Array.isArray(existingConfig.tasks)) {
600
- existingConfig.tasks = [];
601
- }
602
-
603
- // Add new tasks
604
- const newTasks: Task[] = [];
605
- for (const spec of options.taskSpecs) {
606
- const task = parseTaskSpec(spec);
607
- existingConfig.tasks.push(task);
608
- newTasks.push(task);
609
- }
610
-
611
- // Write back
612
- fs.writeFileSync(laneFile, JSON.stringify(existingConfig, null, 2) + '\n', 'utf8');
613
-
614
- const taskNames = newTasks.map(t => t.name).join(', ');
615
- logger.success(`Added task(s): ${taskNames}`);
616
- logger.info(`Updated: ${laneFile}`);
617
-
618
- const taskSummary = existingConfig.tasks.map((t: Task) => t.name).join(' → ');
619
- logger.info(`Lane now has: ${taskSummary}`);
620
- }
621
-
622
- async function createNewFeature(options: PrepareOptions): Promise<void> {
623
- const config = loadConfig();
624
- const tasksBaseDir = getTasksDir(config);
625
-
626
- // Timestamp-based folder name (YYMMDDHHMM)
627
- const now = new Date();
628
- const timestamp = now.toISOString().replace(/[-T:]/g, '').substring(2, 12);
629
- const taskDirName = `${timestamp}_${options.featureName}`;
630
- const taskDir = safeJoin(tasksBaseDir, taskDirName);
631
-
632
- if (fs.existsSync(taskDir) && !options.force) {
633
- throw new Error(`Task directory already exists: ${taskDir}. Use --force to overwrite.`);
634
- }
635
-
636
- if (!fs.existsSync(taskDir)) {
637
- fs.mkdirSync(taskDir, { recursive: true });
638
- }
639
-
640
- logger.info(`Creating tasks in: ${path.relative(config.projectRoot, taskDir)}`);
641
-
642
- // Load template if provided (overrides --prompt/--task/--preset)
643
- let template = null;
644
- if (options.template) {
645
- template = await resolveTemplate(options.template);
646
- }
647
-
648
- const laneInfoList: { name: string; fileName: string; preset: string }[] = [];
649
-
650
- for (let i = 1; i <= options.lanes; i++) {
651
- const laneName = `lane-${i}`;
652
- const fileName = `${i.toString().padStart(2, '0')}-${laneName}.json`;
653
- const filePath = safeJoin(taskDir, fileName);
654
-
655
- const devPort = 3000 + i;
656
-
657
- let taskConfig;
658
- let effectivePreset: EffectivePresetType = options.preset || 'complex';
659
-
660
- if (template) {
661
- // Use template
662
- taskConfig = { ...template, laneNumber: i, devPort };
663
- effectivePreset = 'custom';
664
- } else {
665
- // Build from CLI options
666
- const tasks = buildTasksFromOptions(options, i, options.featureName, false);
667
- taskConfig = getDefaultConfig(i, options.featureName, tasks);
668
- }
669
-
670
- // Replace placeholders
671
- const finalConfig = replacePlaceholders(taskConfig, {
672
- featureName: options.featureName,
673
- laneNumber: i,
674
- devPort: devPort,
675
- });
676
-
677
- // SECURITY NOTE: Writing generated lane configuration (containing user prompts) to file system.
678
- fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', 'utf8');
679
-
680
- const taskSummary = finalConfig.tasks?.map((t: any) => t.name).join(' → ') || 'default';
681
- const presetLabel = effectivePreset !== 'custom' ? ` [${effectivePreset}]` : '';
682
- logger.success(`Created: ${fileName} [${taskSummary}]${presetLabel}`);
683
-
684
- laneInfoList.push({ name: laneName, fileName, preset: effectivePreset });
685
- }
686
-
687
- // Create README
688
- const readmePath = safeJoin(taskDir, 'README.md');
689
- const readme = `# Task: ${options.featureName}
690
-
691
- Prepared at: ${now.toISOString()}
692
- Lanes: ${options.lanes}
693
-
694
- ## How to Run
695
-
696
- \`\`\`bash
697
- # 1. Validate configuration
698
- cursorflow doctor --tasks-dir ${path.relative(config.projectRoot, taskDir)}
699
-
700
- # 2. Run
701
- cursorflow run ${path.relative(config.projectRoot, taskDir)}
702
- \`\`\`
703
-
704
- ## Lanes
705
-
706
- ${laneInfoList.map(l => `- **${l.fileName.replace('.json', '')}** [${l.preset}]`).join('\n')}
707
-
708
- ## Task-Level Dependencies
709
-
710
- To make a task wait for another task to complete before starting, use the \`dependsOn\` field:
711
-
712
- \`\`\`json
713
- {
714
- "tasks": [
715
- {
716
- "name": "my-task",
717
- "prompt": "...",
718
- "dependsOn": ["other-lane:other-task"]
719
- }
720
- ]
721
- }
722
- \`\`\`
723
-
724
- ## Modifying Tasks
725
-
726
- \`\`\`bash
727
- # Add a new lane
728
- cursorflow prepare --add-lane ${path.relative(config.projectRoot, taskDir)} --preset complex
729
-
730
- # Add task to existing lane
731
- cursorflow prepare --add-task ${path.relative(config.projectRoot, taskDir)}/01-lane-1.json \\
732
- --task "verify|sonnet-4.5|Verify requirements|All met"
733
- \`\`\`
734
- `;
735
-
736
- fs.writeFileSync(readmePath, readme, 'utf8');
737
- logger.success('Created README.md');
738
-
739
- logger.section('✅ Preparation complete!');
740
- console.log(`\nNext steps:`);
741
- console.log(` 1. (Optional) Add more lanes/tasks`);
742
- console.log(` 2. Validate: cursorflow doctor --tasks-dir ${path.relative(config.projectRoot, taskDir)}`);
743
- console.log(` 3. Run: cursorflow run ${path.relative(config.projectRoot, taskDir)}`);
744
- console.log('');
745
- }
746
-
747
- async function prepare(args: string[]): Promise<void> {
748
- const options = parseArgs(args);
749
-
750
- if (options.help) {
751
- printHelp();
752
- return;
753
- }
754
-
755
- // Mode 1: Add task to existing lane
756
- if (options.addTask) {
757
- await addTaskToLane(options);
758
- return;
759
- }
760
-
761
- // Mode 2: Add lane to existing directory
762
- if (options.addLane) {
763
- await addLaneToDir(options);
764
- return;
765
- }
766
-
767
- // Mode 3: Create new feature (requires featureName)
768
- if (!options.featureName) {
769
- printHelp();
770
- process.exit(1);
771
- return;
772
- }
773
-
774
- await createNewFeature(options);
775
- }
776
-
777
- export = prepare;