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