@litmers/cursorflow-orchestrator 0.1.15 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/README.md +26 -7
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +8 -7
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +20 -14
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +64 -47
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +27 -17
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +73 -33
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +193 -40
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +3 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +7 -7
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +2 -1
- package/dist/core/orchestrator.js +54 -93
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +6 -4
- package/dist/core/reviewer.js +7 -5
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +8 -0
- package/dist/core/runner.js +219 -32
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +20 -10
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +35 -7
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +2 -2
- package/dist/utils/enhanced-logger.js +114 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.js +163 -10
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +16 -0
- package/dist/utils/log-formatter.js +194 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/repro-thinking-logs.d.ts +1 -0
- package/dist/utils/repro-thinking-logs.js +80 -0
- package/dist/utils/repro-thinking-logs.js.map +1 -0
- package/dist/utils/state.d.ts +4 -1
- package/dist/utils/state.js +11 -8
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -2
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +8 -7
- package/src/cli/index.ts +5 -1
- package/src/cli/init.ts +19 -15
- package/src/cli/logs.ts +67 -47
- package/src/cli/monitor.ts +28 -18
- package/src/cli/prepare.ts +75 -35
- package/src/cli/resume.ts +810 -626
- package/src/cli/run.ts +3 -2
- package/src/cli/signal.ts +7 -6
- package/src/core/orchestrator.ts +68 -93
- package/src/core/reviewer.ts +14 -9
- package/src/core/runner.ts +229 -33
- package/src/utils/config.ts +19 -11
- package/src/utils/doctor.ts +38 -7
- package/src/utils/enhanced-logger.ts +117 -49
- package/src/utils/git.ts +145 -11
- package/src/utils/log-formatter.ts +162 -0
- package/src/utils/path.ts +45 -0
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/state.ts +16 -8
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +13 -0
- package/src/utils/webhook.ts +3 -0
- package/templates/basic.json +21 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-cursor-lifecycle.sh +0 -289
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
package/src/cli/run.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { orchestrate } from '../core/orchestrator';
|
|
|
9
9
|
import { getLogsDir, loadConfig } from '../utils/config';
|
|
10
10
|
import { runDoctor, getDoctorStatus } from '../utils/doctor';
|
|
11
11
|
import { areCommandsInstalled, setupCommands } from './setup-commands';
|
|
12
|
+
import { safeJoin } from '../utils/path';
|
|
12
13
|
|
|
13
14
|
interface RunOptions {
|
|
14
15
|
tasksDir?: string;
|
|
@@ -90,8 +91,8 @@ async function run(args: string[]): Promise<void> {
|
|
|
90
91
|
path.isAbsolute(options.tasksDir)
|
|
91
92
|
? options.tasksDir
|
|
92
93
|
: (fs.existsSync(options.tasksDir)
|
|
93
|
-
? path.resolve(process.cwd(), options.tasksDir)
|
|
94
|
-
:
|
|
94
|
+
? path.resolve(process.cwd(), options.tasksDir) // nosemgrep
|
|
95
|
+
: safeJoin(config.projectRoot, options.tasksDir));
|
|
95
96
|
|
|
96
97
|
if (!fs.existsSync(tasksDir)) {
|
|
97
98
|
throw new Error(`Tasks directory not found: ${tasksDir}`);
|
package/src/cli/signal.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as logger from '../utils/logger';
|
|
10
10
|
import { loadConfig, getLogsDir } from '../utils/config';
|
|
11
11
|
import { appendLog, createConversationEntry } from '../utils/state';
|
|
12
|
+
import { safeJoin } from '../utils/path';
|
|
12
13
|
|
|
13
14
|
interface SignalOptions {
|
|
14
15
|
lane: string | null;
|
|
@@ -51,7 +52,7 @@ function parseArgs(args: string[]): SignalOptions {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function findLatestRunDir(logsDir: string): string | null {
|
|
54
|
-
const runsDir =
|
|
55
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
55
56
|
if (!fs.existsSync(runsDir)) return null;
|
|
56
57
|
|
|
57
58
|
const runs = fs.readdirSync(runsDir)
|
|
@@ -59,7 +60,7 @@ function findLatestRunDir(logsDir: string): string | null {
|
|
|
59
60
|
.sort()
|
|
60
61
|
.reverse();
|
|
61
62
|
|
|
62
|
-
return runs.length > 0 ?
|
|
63
|
+
return runs.length > 0 ? safeJoin(runsDir, runs[0]!) : null;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
async function signal(args: string[]): Promise<void> {
|
|
@@ -86,14 +87,14 @@ async function signal(args: string[]): Promise<void> {
|
|
|
86
87
|
throw new Error(`Run directory not found: ${runDir || 'latest'}`);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
const laneDir =
|
|
90
|
+
const laneDir = safeJoin(runDir, 'lanes', options.lane);
|
|
90
91
|
if (!fs.existsSync(laneDir)) {
|
|
91
92
|
throw new Error(`Lane directory not found: ${laneDir}`);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// Case 1: Timeout update
|
|
95
96
|
if (options.timeout !== null) {
|
|
96
|
-
const timeoutPath =
|
|
97
|
+
const timeoutPath = safeJoin(laneDir, 'timeout.txt');
|
|
97
98
|
fs.writeFileSync(timeoutPath, String(options.timeout));
|
|
98
99
|
logger.success(`Timeout update signal sent to ${options.lane}: ${options.timeout}ms`);
|
|
99
100
|
return;
|
|
@@ -101,8 +102,8 @@ async function signal(args: string[]): Promise<void> {
|
|
|
101
102
|
|
|
102
103
|
// Case 2: Intervention message
|
|
103
104
|
if (options.message) {
|
|
104
|
-
const interventionPath =
|
|
105
|
-
const convoPath =
|
|
105
|
+
const interventionPath = safeJoin(laneDir, 'intervention.txt');
|
|
106
|
+
const convoPath = safeJoin(laneDir, 'conversation.jsonl');
|
|
106
107
|
|
|
107
108
|
logger.info(`Sending signal to lane: ${options.lane}`);
|
|
108
109
|
logger.info(`Message: "${options.message}"`);
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -9,13 +9,14 @@ import * as path from 'path';
|
|
|
9
9
|
import { spawn, ChildProcess } from 'child_process';
|
|
10
10
|
|
|
11
11
|
import * as logger from '../utils/logger';
|
|
12
|
-
import { loadState } from '../utils/state';
|
|
12
|
+
import { loadState, saveState, createLaneState } from '../utils/state';
|
|
13
13
|
import { LaneState, RunnerConfig, WebhookConfig, DependencyRequestPlan, EnhancedLogConfig } from '../utils/types';
|
|
14
14
|
import { events } from '../utils/events';
|
|
15
15
|
import { registerWebhooks } from '../utils/webhook';
|
|
16
16
|
import { loadConfig, getLogsDir } from '../utils/config';
|
|
17
17
|
import * as git from '../utils/git';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
|
+
import { safeJoin } from '../utils/path';
|
|
19
20
|
import {
|
|
20
21
|
EnhancedLogManager,
|
|
21
22
|
createLogManager,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
ParsedMessage,
|
|
24
25
|
stripAnsi
|
|
25
26
|
} from '../utils/enhanced-logger';
|
|
27
|
+
import { formatMessageForConsole } from '../utils/log-formatter';
|
|
26
28
|
|
|
27
29
|
export interface LaneInfo {
|
|
28
30
|
name: string;
|
|
@@ -47,6 +49,7 @@ export function spawnLane({
|
|
|
47
49
|
executor,
|
|
48
50
|
startIndex = 0,
|
|
49
51
|
pipelineBranch,
|
|
52
|
+
worktreeDir,
|
|
50
53
|
enhancedLogConfig,
|
|
51
54
|
noGit = false,
|
|
52
55
|
}: {
|
|
@@ -56,6 +59,7 @@ export function spawnLane({
|
|
|
56
59
|
executor: string;
|
|
57
60
|
startIndex?: number;
|
|
58
61
|
pipelineBranch?: string;
|
|
62
|
+
worktreeDir?: string;
|
|
59
63
|
enhancedLogConfig?: Partial<EnhancedLogConfig>;
|
|
60
64
|
noGit?: boolean;
|
|
61
65
|
}): SpawnLaneResult {
|
|
@@ -75,6 +79,10 @@ export function spawnLane({
|
|
|
75
79
|
if (pipelineBranch) {
|
|
76
80
|
args.push('--pipeline-branch', pipelineBranch);
|
|
77
81
|
}
|
|
82
|
+
|
|
83
|
+
if (worktreeDir) {
|
|
84
|
+
args.push('--worktree-dir', worktreeDir);
|
|
85
|
+
}
|
|
78
86
|
|
|
79
87
|
if (noGit) {
|
|
80
88
|
args.push('--no-git');
|
|
@@ -94,82 +102,11 @@ export function spawnLane({
|
|
|
94
102
|
if (logConfig.enabled) {
|
|
95
103
|
// Create callback for clean console output
|
|
96
104
|
const onParsedMessage = (msg: ParsedMessage) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let content = msg.content;
|
|
103
|
-
|
|
104
|
-
switch (msg.type) {
|
|
105
|
-
case 'user':
|
|
106
|
-
prefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
|
|
107
|
-
// No truncation for user prompt to ensure full command visibility
|
|
108
|
-
content = content.replace(/\n/g, ' ');
|
|
109
|
-
break;
|
|
110
|
-
case 'assistant':
|
|
111
|
-
prefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
|
|
112
|
-
break;
|
|
113
|
-
case 'tool':
|
|
114
|
-
prefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
|
|
115
|
-
// Simplify tool call: [Tool: read_file] {"target_file":"..."} -> read_file(target_file: ...)
|
|
116
|
-
const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
|
|
117
|
-
if (toolMatch) {
|
|
118
|
-
const [, name, args] = toolMatch;
|
|
119
|
-
try {
|
|
120
|
-
const parsedArgs = JSON.parse(args!);
|
|
121
|
-
let argStr = '';
|
|
122
|
-
|
|
123
|
-
if (name === 'read_file' && parsedArgs.target_file) {
|
|
124
|
-
argStr = parsedArgs.target_file;
|
|
125
|
-
} else if (name === 'run_terminal_cmd' && parsedArgs.command) {
|
|
126
|
-
argStr = parsedArgs.command;
|
|
127
|
-
} else if (name === 'write' && parsedArgs.file_path) {
|
|
128
|
-
argStr = parsedArgs.file_path;
|
|
129
|
-
} else if (name === 'search_replace' && parsedArgs.file_path) {
|
|
130
|
-
argStr = parsedArgs.file_path;
|
|
131
|
-
} else {
|
|
132
|
-
// Generic summary for other tools
|
|
133
|
-
const keys = Object.keys(parsedArgs);
|
|
134
|
-
if (keys.length > 0) {
|
|
135
|
-
argStr = String(parsedArgs[keys[0]]).substring(0, 50);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
|
|
139
|
-
} catch {
|
|
140
|
-
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
case 'tool_result':
|
|
145
|
-
prefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
|
|
146
|
-
// Simplify tool result: [Tool Result: read_file] ... -> read_file OK
|
|
147
|
-
const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
|
|
148
|
-
content = resMatch ? `${resMatch[1]} OK` : 'result';
|
|
149
|
-
break;
|
|
150
|
-
case 'result':
|
|
151
|
-
prefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
|
|
152
|
-
break;
|
|
153
|
-
case 'system':
|
|
154
|
-
prefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (prefix) {
|
|
159
|
-
const lines = content.split('\n');
|
|
160
|
-
const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
|
|
161
|
-
|
|
162
|
-
if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result') {
|
|
163
|
-
const header = `${prefix} ┌${'─'.repeat(60)}`;
|
|
164
|
-
process.stdout.write(`${tsPrefix} ${header}\n`);
|
|
165
|
-
for (const line of lines) {
|
|
166
|
-
process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} │ ${line}\n`);
|
|
167
|
-
}
|
|
168
|
-
process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} └${'─'.repeat(60)}\n`);
|
|
169
|
-
} else {
|
|
170
|
-
process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
105
|
+
const formatted = formatMessageForConsole(msg, {
|
|
106
|
+
laneLabel: `[${laneName}]`,
|
|
107
|
+
includeTimestamp: true
|
|
108
|
+
});
|
|
109
|
+
process.stdout.write(formatted + '\n');
|
|
173
110
|
};
|
|
174
111
|
|
|
175
112
|
logManager = createLogManager(laneRunDir, laneName, logConfig, onParsedMessage);
|
|
@@ -202,8 +139,7 @@ export function spawnLane({
|
|
|
202
139
|
if (trimmed &&
|
|
203
140
|
!trimmed.startsWith('{') &&
|
|
204
141
|
!trimmed.startsWith('[') &&
|
|
205
|
-
!trimmed.includes('{"type"')
|
|
206
|
-
!trimmed.includes('Heartbeat:')) {
|
|
142
|
+
!trimmed.includes('{"type"')) {
|
|
207
143
|
process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${line}\n`);
|
|
208
144
|
}
|
|
209
145
|
}
|
|
@@ -240,7 +176,7 @@ export function spawnLane({
|
|
|
240
176
|
});
|
|
241
177
|
} else {
|
|
242
178
|
// Fallback to simple file logging
|
|
243
|
-
logPath =
|
|
179
|
+
logPath = safeJoin(laneRunDir, 'terminal.log');
|
|
244
180
|
const logFd = fs.openSync(logPath, 'a');
|
|
245
181
|
|
|
246
182
|
child = spawn('node', args, {
|
|
@@ -287,7 +223,7 @@ export function listLaneFiles(tasksDir: string): LaneInfo[] {
|
|
|
287
223
|
.filter(f => f.endsWith('.json'))
|
|
288
224
|
.sort()
|
|
289
225
|
.map(f => {
|
|
290
|
-
const filePath =
|
|
226
|
+
const filePath = safeJoin(tasksDir, f);
|
|
291
227
|
const name = path.basename(f, '.json');
|
|
292
228
|
let dependsOn: string[] = [];
|
|
293
229
|
|
|
@@ -314,7 +250,7 @@ export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, s
|
|
|
314
250
|
const dir = laneRunDirs[lane.name];
|
|
315
251
|
if (!dir) return { lane: lane.name, status: '(unknown)', task: '-' };
|
|
316
252
|
|
|
317
|
-
const statePath =
|
|
253
|
+
const statePath = safeJoin(dir, 'state.json');
|
|
318
254
|
const state = loadState<LaneState>(statePath);
|
|
319
255
|
|
|
320
256
|
if (!state) {
|
|
@@ -362,9 +298,9 @@ async function resolveAllDependencies(
|
|
|
362
298
|
|
|
363
299
|
// 2. Setup a temporary worktree for resolution if needed, or use the first available one
|
|
364
300
|
const firstLaneName = Array.from(blockedLanes.keys())[0]!;
|
|
365
|
-
const statePath =
|
|
301
|
+
const statePath = safeJoin(laneRunDirs[firstLaneName]!, 'state.json');
|
|
366
302
|
const state = loadState<LaneState>(statePath);
|
|
367
|
-
const worktreeDir = state?.worktreeDir ||
|
|
303
|
+
const worktreeDir = state?.worktreeDir || safeJoin(runRoot, 'resolution-worktree');
|
|
368
304
|
|
|
369
305
|
if (!fs.existsSync(worktreeDir)) {
|
|
370
306
|
logger.info(`Creating resolution worktree at ${worktreeDir}`);
|
|
@@ -403,7 +339,7 @@ async function resolveAllDependencies(
|
|
|
403
339
|
const laneDir = laneRunDirs[lane.name];
|
|
404
340
|
if (!laneDir) continue;
|
|
405
341
|
|
|
406
|
-
const laneState = loadState<LaneState>(
|
|
342
|
+
const laneState = loadState<LaneState>(safeJoin(laneDir, 'state.json'));
|
|
407
343
|
if (!laneState || laneState.status === 'completed' || laneState.status === 'failed') continue;
|
|
408
344
|
|
|
409
345
|
// Merge pipelineBranch into the lane's current task branch
|
|
@@ -412,7 +348,8 @@ async function resolveAllDependencies(
|
|
|
412
348
|
const task = taskConfig.tasks[currentIdx];
|
|
413
349
|
|
|
414
350
|
if (task) {
|
|
415
|
-
const
|
|
351
|
+
const lanePipelineBranch = `${pipelineBranch}/${lane.name}`;
|
|
352
|
+
const taskBranch = `${lanePipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
|
|
416
353
|
logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`);
|
|
417
354
|
|
|
418
355
|
try {
|
|
@@ -462,12 +399,13 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
462
399
|
const runId = `run-${Date.now()}`;
|
|
463
400
|
// Use absolute path for runRoot to avoid issues with subfolders
|
|
464
401
|
const runRoot = options.runDir
|
|
465
|
-
? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir))
|
|
466
|
-
:
|
|
402
|
+
? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir)) // nosemgrep
|
|
403
|
+
: safeJoin(logsDir, 'runs', runId);
|
|
467
404
|
|
|
468
405
|
fs.mkdirSync(runRoot, { recursive: true });
|
|
469
406
|
|
|
470
|
-
const
|
|
407
|
+
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
408
|
+
const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}-${randomSuffix}`;
|
|
471
409
|
|
|
472
410
|
// Initialize event system
|
|
473
411
|
events.setRunId(runId);
|
|
@@ -495,9 +433,45 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
495
433
|
}
|
|
496
434
|
|
|
497
435
|
const laneRunDirs: Record<string, string> = {};
|
|
436
|
+
const laneWorktreeDirs: Record<string, string> = {};
|
|
437
|
+
const repoRoot = git.getRepoRoot();
|
|
438
|
+
|
|
498
439
|
for (const lane of lanes) {
|
|
499
|
-
laneRunDirs[lane.name] =
|
|
500
|
-
fs.mkdirSync(laneRunDirs[lane.name]
|
|
440
|
+
laneRunDirs[lane.name] = safeJoin(runRoot, 'lanes', lane.name);
|
|
441
|
+
fs.mkdirSync(laneRunDirs[lane.name]!, { recursive: true });
|
|
442
|
+
|
|
443
|
+
// Create initial state for ALL lanes so resume can find them even if they didn't start
|
|
444
|
+
try {
|
|
445
|
+
const taskConfig = JSON.parse(fs.readFileSync(lane.path, 'utf8')) as RunnerConfig;
|
|
446
|
+
|
|
447
|
+
// Calculate unique branch and worktree for this lane
|
|
448
|
+
const lanePipelineBranch = `${pipelineBranch}/${lane.name}`;
|
|
449
|
+
|
|
450
|
+
// Use a flat worktree directory name to avoid race conditions in parent directory creation
|
|
451
|
+
// repoRoot/_cursorflow/worktrees/cursorflow-run-xxx-lane-name
|
|
452
|
+
const laneWorktreeDir = safeJoin(
|
|
453
|
+
repoRoot,
|
|
454
|
+
taskConfig.worktreeRoot || '_cursorflow/worktrees',
|
|
455
|
+
lanePipelineBranch.replace(/\//g, '-')
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Ensure the parent directory exists before spawning the runner
|
|
459
|
+
// to avoid race conditions in git worktree add or fs operations
|
|
460
|
+
const worktreeParent = path.dirname(laneWorktreeDir);
|
|
461
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
462
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
laneWorktreeDirs[lane.name] = laneWorktreeDir;
|
|
466
|
+
|
|
467
|
+
const initialState = createLaneState(lane.name, taskConfig, lane.path, {
|
|
468
|
+
pipelineBranch: lanePipelineBranch,
|
|
469
|
+
worktreeDir: laneWorktreeDir
|
|
470
|
+
});
|
|
471
|
+
saveState(safeJoin(laneRunDirs[lane.name]!, 'state.json'), initialState);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
logger.warn(`Failed to create initial state for lane ${lane.name}: ${e}`);
|
|
474
|
+
}
|
|
501
475
|
}
|
|
502
476
|
|
|
503
477
|
logger.section('🧭 Starting Orchestration');
|
|
@@ -570,7 +544,8 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
570
544
|
laneRunDir: laneRunDirs[lane.name]!,
|
|
571
545
|
executor: options.executor || 'cursor-agent',
|
|
572
546
|
startIndex: lane.startIndex,
|
|
573
|
-
pipelineBranch
|
|
547
|
+
pipelineBranch: `${pipelineBranch}/${lane.name}`,
|
|
548
|
+
worktreeDir: laneWorktreeDirs[lane.name],
|
|
574
549
|
enhancedLogConfig: options.enhancedLogging,
|
|
575
550
|
noGit: options.noGit,
|
|
576
551
|
});
|
|
@@ -603,7 +578,7 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
603
578
|
});
|
|
604
579
|
} else if (finished.code === 2) {
|
|
605
580
|
// Blocked by dependency
|
|
606
|
-
const statePath =
|
|
581
|
+
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
607
582
|
const state = loadState<LaneState>(statePath);
|
|
608
583
|
|
|
609
584
|
if (state && state.dependencyRequest) {
|
package/src/core/reviewer.ts
CHANGED
|
@@ -144,24 +144,25 @@ export function buildFeedbackPrompt(review: ReviewResult): string {
|
|
|
144
144
|
/**
|
|
145
145
|
* Review task
|
|
146
146
|
*/
|
|
147
|
-
export async function reviewTask({ taskResult, worktreeDir, runDir, config, cursorAgentSend, cursorAgentCreateChat }: {
|
|
147
|
+
export async function reviewTask({ taskResult, worktreeDir, runDir, config, model, cursorAgentSend, cursorAgentCreateChat }: {
|
|
148
148
|
taskResult: TaskResult;
|
|
149
149
|
worktreeDir: string;
|
|
150
150
|
runDir: string;
|
|
151
151
|
config: RunnerConfig;
|
|
152
|
+
model?: string;
|
|
152
153
|
cursorAgentSend: (options: {
|
|
153
154
|
workspaceDir: string;
|
|
154
155
|
chatId: string;
|
|
155
156
|
prompt: string;
|
|
156
157
|
model?: string;
|
|
157
158
|
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
158
|
-
}) => AgentSendResult
|
|
159
|
+
}) => Promise<AgentSendResult>;
|
|
159
160
|
cursorAgentCreateChat: () => string;
|
|
160
161
|
}): Promise<ReviewResult> {
|
|
161
162
|
const reviewPrompt = buildReviewPrompt({
|
|
162
163
|
taskName: taskResult.taskName,
|
|
163
164
|
taskBranch: taskResult.taskBranch,
|
|
164
|
-
acceptanceCriteria: config.acceptanceCriteria || [],
|
|
165
|
+
acceptanceCriteria: taskResult.acceptanceCriteria || config.acceptanceCriteria || [],
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
logger.info(`Reviewing: ${taskResult.taskName}`);
|
|
@@ -172,11 +173,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
const reviewChatId = cursorAgentCreateChat();
|
|
176
|
+
const reviewModel = model || config.reviewModel || config.model || 'sonnet-4.5';
|
|
177
|
+
|
|
175
178
|
const reviewResult = await cursorAgentSend({
|
|
176
179
|
workspaceDir: worktreeDir,
|
|
177
180
|
chatId: reviewChatId,
|
|
178
181
|
prompt: reviewPrompt,
|
|
179
|
-
model:
|
|
182
|
+
model: reviewModel,
|
|
180
183
|
outputFormat: config.agentOutputFormat,
|
|
181
184
|
});
|
|
182
185
|
|
|
@@ -186,7 +189,7 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
186
189
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
187
190
|
appendLog(convoPath, createConversationEntry('reviewer', reviewResult.resultText || 'No result', {
|
|
188
191
|
task: taskResult.taskName,
|
|
189
|
-
model:
|
|
192
|
+
model: reviewModel,
|
|
190
193
|
}));
|
|
191
194
|
|
|
192
195
|
logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
|
|
@@ -204,20 +207,21 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
204
207
|
/**
|
|
205
208
|
* Review loop with feedback
|
|
206
209
|
*/
|
|
207
|
-
export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, cursorAgentSend, cursorAgentCreateChat }: {
|
|
210
|
+
export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, model, cursorAgentSend, cursorAgentCreateChat }: {
|
|
208
211
|
taskResult: TaskResult;
|
|
209
212
|
worktreeDir: string;
|
|
210
213
|
runDir: string;
|
|
211
214
|
config: RunnerConfig;
|
|
212
215
|
workChatId: string;
|
|
216
|
+
model?: string;
|
|
213
217
|
cursorAgentSend: (options: {
|
|
214
218
|
workspaceDir: string;
|
|
215
219
|
chatId: string;
|
|
216
220
|
prompt: string;
|
|
217
221
|
model?: string;
|
|
218
222
|
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
219
|
-
}) => AgentSendResult
|
|
220
|
-
cursorAgentCreateChat: () => string;
|
|
223
|
+
}) => Promise<AgentSendResult>;
|
|
224
|
+
cursorAgentCreateChat: () => string;
|
|
221
225
|
}): Promise<{ approved: boolean; review: ReviewResult; iterations: number; error?: string }> {
|
|
222
226
|
const maxIterations = config.maxReviewIterations || 3;
|
|
223
227
|
let iteration = 0;
|
|
@@ -229,10 +233,11 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
229
233
|
worktreeDir,
|
|
230
234
|
runDir,
|
|
231
235
|
config,
|
|
236
|
+
model,
|
|
232
237
|
cursorAgentSend,
|
|
233
238
|
cursorAgentCreateChat,
|
|
234
239
|
});
|
|
235
|
-
|
|
240
|
+
|
|
236
241
|
if (currentReview.status === 'approved') {
|
|
237
242
|
logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
|
|
238
243
|
events.emit('review.approved', {
|