@litmers/cursorflow-orchestrator 0.1.14 → 0.1.18
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 +9 -0
- package/README.md +1 -0
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +1 -1
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/init.js +13 -8
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +66 -44
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +12 -3
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +36 -13
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +7 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +3 -1
- package/dist/core/orchestrator.js +154 -11
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +8 -4
- package/dist/core/reviewer.js +11 -7
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +17 -3
- package/dist/core/runner.js +326 -69
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +17 -5
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +28 -1
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -4
- package/dist/utils/enhanced-logger.js +178 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +6 -0
- package/dist/utils/git.js +15 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -1
- package/dist/utils/logger.js.map +1 -1
- 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/types.d.ts +22 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -1
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +1 -1
- package/src/cli/init.ts +12 -9
- package/src/cli/logs.ts +68 -43
- package/src/cli/monitor.ts +13 -4
- package/src/cli/prepare.ts +36 -15
- package/src/cli/resume.ts +1 -1
- package/src/cli/run.ts +8 -0
- package/src/core/orchestrator.ts +171 -11
- package/src/core/reviewer.ts +30 -11
- package/src/core/runner.ts +346 -71
- package/src/utils/config.ts +17 -6
- package/src/utils/doctor.ts +31 -1
- package/src/utils/enhanced-logger.ts +182 -48
- package/src/utils/git.ts +15 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/types.ts +22 -0
- package/src/utils/webhook.ts +3 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
package/src/cli/prepare.ts
CHANGED
|
@@ -97,12 +97,16 @@ Prepare task files for a new feature - Terminal-first workflow.
|
|
|
97
97
|
--prompt <text> Task prompt (uses preset or single task)
|
|
98
98
|
--criteria <list> Comma-separated acceptance criteria
|
|
99
99
|
--model <model> Model to use (default: sonnet-4.5)
|
|
100
|
-
--task <spec> Full task spec: "name|model|prompt|criteria" (repeatable)
|
|
100
|
+
--task <spec> Full task spec: "name|model|prompt|criteria|dependsOn|timeout" (repeatable)
|
|
101
101
|
|
|
102
102
|
Dependencies:
|
|
103
103
|
--sequential Chain lanes: 1 → 2 → 3
|
|
104
104
|
--deps <spec> Custom dependencies: "2:1;3:1,2"
|
|
105
105
|
--depends-on <lanes> Dependencies for --add-lane: "01-lane-1,02-lane-2"
|
|
106
|
+
Task-level deps: In --task, add "lane:task" at the end.
|
|
107
|
+
Example: "test|sonnet-4.5|Run tests|All pass|01-lane-1:setup"
|
|
108
|
+
Task-level timeout: In --task, add milliseconds at the end.
|
|
109
|
+
Example: "heavy|sonnet-4.5|Big task|Done||1200000"
|
|
106
110
|
|
|
107
111
|
Incremental (add to existing):
|
|
108
112
|
--add-lane <dir> Add a new lane to existing task directory
|
|
@@ -203,23 +207,31 @@ function parseArgs(args: string[]): PrepareOptions {
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
function parseTaskSpec(spec: string): Task {
|
|
206
|
-
// Format: "name|model|prompt|criteria1,criteria2"
|
|
210
|
+
// Format: "name|model|prompt|criteria1,criteria2|lane:task1,lane:task2|timeoutMs"
|
|
207
211
|
const parts = spec.split('|');
|
|
208
212
|
|
|
209
213
|
if (parts.length < 3) {
|
|
210
|
-
throw new Error(`Invalid task spec: "${spec}". Expected format: "name|model|prompt[|
|
|
214
|
+
throw new Error(`Invalid task spec: "${spec}". Expected format: "name|model|prompt[|criteria[|dependsOn[|timeout]]]"`);
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
const [name, model, prompt, criteriaStr] = parts;
|
|
217
|
+
const [name, model, prompt, criteriaStr, depsStr, timeoutStr] = parts;
|
|
214
218
|
const acceptanceCriteria = criteriaStr
|
|
215
219
|
? criteriaStr.split(',').map(c => c.trim()).filter(c => c)
|
|
216
220
|
: undefined;
|
|
217
221
|
|
|
222
|
+
const dependsOn = depsStr
|
|
223
|
+
? depsStr.split(',').map(d => d.trim()).filter(d => d)
|
|
224
|
+
: undefined;
|
|
225
|
+
|
|
226
|
+
const timeout = timeoutStr ? parseInt(timeoutStr) : undefined;
|
|
227
|
+
|
|
218
228
|
return {
|
|
219
229
|
name: name.trim(),
|
|
220
230
|
model: model.trim() || 'sonnet-4.5',
|
|
221
231
|
prompt: prompt.trim(),
|
|
222
232
|
...(acceptanceCriteria && acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {}),
|
|
233
|
+
...(dependsOn && dependsOn.length > 0 ? { dependsOn } : {}),
|
|
234
|
+
...(timeout ? { timeout } : {}),
|
|
223
235
|
};
|
|
224
236
|
}
|
|
225
237
|
|
|
@@ -612,10 +624,6 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
|
612
624
|
const fileName = `${laneNumber.toString().padStart(2, '0')}-${laneName}.json`;
|
|
613
625
|
const filePath = path.join(taskDir, fileName);
|
|
614
626
|
|
|
615
|
-
if (fs.existsSync(filePath) && !options.force) {
|
|
616
|
-
throw new Error(`Lane file already exists: ${filePath}. Use --force to overwrite.`);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
627
|
const hasDependencies = options.dependsOnLanes.length > 0;
|
|
620
628
|
|
|
621
629
|
// Build tasks from options (auto-detects merge preset if has dependencies)
|
|
@@ -628,7 +636,16 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
|
628
636
|
...(hasDependencies ? { dependsOn: options.dependsOnLanes } : {}),
|
|
629
637
|
};
|
|
630
638
|
|
|
631
|
-
|
|
639
|
+
// Use atomic write with wx flag to avoid TOCTOU race condition (unless force is set)
|
|
640
|
+
try {
|
|
641
|
+
const writeFlag = options.force ? 'w' : 'wx';
|
|
642
|
+
fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', { encoding: 'utf8', flag: writeFlag });
|
|
643
|
+
} catch (err: any) {
|
|
644
|
+
if (err.code === 'EEXIST') {
|
|
645
|
+
throw new Error(`Lane file already exists: ${filePath}. Use --force to overwrite.`);
|
|
646
|
+
}
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
632
649
|
|
|
633
650
|
const taskSummary = tasks.map(t => t.name).join(' → ');
|
|
634
651
|
const depsInfo = hasDependencies ? ` (depends: ${options.dependsOnLanes.join(', ')})` : '';
|
|
@@ -645,16 +662,20 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
|
645
662
|
async function addTaskToLane(options: PrepareOptions): Promise<void> {
|
|
646
663
|
const laneFile = path.resolve(process.cwd(), options.addTask!);
|
|
647
664
|
|
|
648
|
-
if (!fs.existsSync(laneFile)) {
|
|
649
|
-
throw new Error(`Lane file not found: ${laneFile}`);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
665
|
if (options.taskSpecs.length === 0) {
|
|
653
666
|
throw new Error('No task specified. Use --task "name|model|prompt|criteria" to define a task.');
|
|
654
667
|
}
|
|
655
668
|
|
|
656
|
-
// Read existing config
|
|
657
|
-
|
|
669
|
+
// Read existing config - let the error propagate if file doesn't exist (avoids TOCTOU)
|
|
670
|
+
let existingConfig: any;
|
|
671
|
+
try {
|
|
672
|
+
existingConfig = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
|
|
673
|
+
} catch (err: any) {
|
|
674
|
+
if (err.code === 'ENOENT') {
|
|
675
|
+
throw new Error(`Lane file not found: ${laneFile}`);
|
|
676
|
+
}
|
|
677
|
+
throw err;
|
|
678
|
+
}
|
|
658
679
|
|
|
659
680
|
if (!existingConfig.tasks || !Array.isArray(existingConfig.tasks)) {
|
|
660
681
|
existingConfig.tasks = [];
|
package/src/cli/resume.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import { spawn, ChildProcess } from 'child_process';
|
|
8
8
|
import * as logger from '../utils/logger';
|
|
9
9
|
import { loadConfig, getLogsDir } from '../utils/config';
|
|
10
|
-
import { loadState
|
|
10
|
+
import { loadState } from '../utils/state';
|
|
11
11
|
import { LaneState } from '../utils/types';
|
|
12
12
|
import { runDoctor } from '../utils/doctor';
|
|
13
13
|
|
package/src/cli/run.ts
CHANGED
|
@@ -16,6 +16,7 @@ interface RunOptions {
|
|
|
16
16
|
executor: string | null;
|
|
17
17
|
maxConcurrent: number | null;
|
|
18
18
|
skipDoctor: boolean;
|
|
19
|
+
noGit: boolean;
|
|
19
20
|
help: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -30,8 +31,13 @@ Options:
|
|
|
30
31
|
--max-concurrent <num> Limit parallel agents (overrides config)
|
|
31
32
|
--executor <type> cursor-agent | cloud
|
|
32
33
|
--skip-doctor Skip environment checks (not recommended)
|
|
34
|
+
--no-git Disable Git operations (worktree, push, commit)
|
|
33
35
|
--dry-run Show execution plan without starting agents
|
|
34
36
|
--help, -h Show help
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
cursorflow run _cursorflow/tasks
|
|
40
|
+
cursorflow run _cursorflow/tasks --no-git --skip-doctor
|
|
35
41
|
`);
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -46,6 +52,7 @@ function parseArgs(args: string[]): RunOptions {
|
|
|
46
52
|
executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
|
|
47
53
|
maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '0') || null : null,
|
|
48
54
|
skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
|
|
55
|
+
noGit: args.includes('--no-git'),
|
|
49
56
|
help: args.includes('--help') || args.includes('-h'),
|
|
50
57
|
};
|
|
51
58
|
}
|
|
@@ -136,6 +143,7 @@ async function run(args: string[]): Promise<void> {
|
|
|
136
143
|
maxConcurrentLanes: options.maxConcurrent || config.maxConcurrentLanes,
|
|
137
144
|
webhooks: config.webhooks || [],
|
|
138
145
|
enhancedLogging: config.enhancedLogging,
|
|
146
|
+
noGit: options.noGit,
|
|
139
147
|
});
|
|
140
148
|
} catch (error: any) {
|
|
141
149
|
// Re-throw to be handled by the main entry point
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -13,9 +13,16 @@ import { loadState } 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
|
+
import { loadConfig, getLogsDir } from '../utils/config';
|
|
16
17
|
import * as git from '../utils/git';
|
|
17
18
|
import { execSync } from 'child_process';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
EnhancedLogManager,
|
|
21
|
+
createLogManager,
|
|
22
|
+
DEFAULT_LOG_CONFIG,
|
|
23
|
+
ParsedMessage,
|
|
24
|
+
stripAnsi
|
|
25
|
+
} from '../utils/enhanced-logger';
|
|
19
26
|
|
|
20
27
|
export interface LaneInfo {
|
|
21
28
|
name: string;
|
|
@@ -41,6 +48,7 @@ export function spawnLane({
|
|
|
41
48
|
startIndex = 0,
|
|
42
49
|
pipelineBranch,
|
|
43
50
|
enhancedLogConfig,
|
|
51
|
+
noGit = false,
|
|
44
52
|
}: {
|
|
45
53
|
laneName: string;
|
|
46
54
|
tasksFile: string;
|
|
@@ -49,6 +57,7 @@ export function spawnLane({
|
|
|
49
57
|
startIndex?: number;
|
|
50
58
|
pipelineBranch?: string;
|
|
51
59
|
enhancedLogConfig?: Partial<EnhancedLogConfig>;
|
|
60
|
+
noGit?: boolean;
|
|
52
61
|
}): SpawnLaneResult {
|
|
53
62
|
fs.mkdirSync(laneRunDir, { recursive: true});
|
|
54
63
|
|
|
@@ -67,6 +76,10 @@ export function spawnLane({
|
|
|
67
76
|
args.push('--pipeline-branch', pipelineBranch);
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
if (noGit) {
|
|
80
|
+
args.push('--no-git');
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
// Create enhanced log manager if enabled
|
|
71
84
|
const logConfig = { ...DEFAULT_LOG_CONFIG, ...enhancedLogConfig };
|
|
72
85
|
let logManager: EnhancedLogManager | undefined;
|
|
@@ -79,7 +92,90 @@ export function spawnLane({
|
|
|
79
92
|
};
|
|
80
93
|
|
|
81
94
|
if (logConfig.enabled) {
|
|
82
|
-
|
|
95
|
+
// Create callback for clean console output
|
|
96
|
+
const onParsedMessage = (msg: ParsedMessage) => {
|
|
97
|
+
// Print a clean, colored version of the message to the console
|
|
98
|
+
const ts = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false });
|
|
99
|
+
const laneLabel = `[${laneName}]`.padEnd(12);
|
|
100
|
+
|
|
101
|
+
let prefix = '';
|
|
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
|
+
case 'thinking':
|
|
157
|
+
prefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (prefix) {
|
|
162
|
+
const lines = content.split('\n');
|
|
163
|
+
const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
|
|
164
|
+
|
|
165
|
+
if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result' || msg.type === 'thinking') {
|
|
166
|
+
const header = `${prefix} ┌${'─'.repeat(60)}`;
|
|
167
|
+
process.stdout.write(`${tsPrefix} ${header}\n`);
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} │ ${line}\n`);
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} └${'─'.repeat(60)}\n`);
|
|
172
|
+
} else {
|
|
173
|
+
process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
logManager = createLogManager(laneRunDir, laneName, logConfig, onParsedMessage);
|
|
83
179
|
logPath = logManager.getLogPaths().clean;
|
|
84
180
|
|
|
85
181
|
// Spawn with pipe for enhanced logging
|
|
@@ -89,20 +185,54 @@ export function spawnLane({
|
|
|
89
185
|
detached: false,
|
|
90
186
|
});
|
|
91
187
|
|
|
188
|
+
// Buffer for non-JSON lines
|
|
189
|
+
let lineBuffer = '';
|
|
190
|
+
|
|
92
191
|
// Pipe stdout and stderr through enhanced logger
|
|
93
192
|
if (child.stdout) {
|
|
94
193
|
child.stdout.on('data', (data: Buffer) => {
|
|
95
194
|
logManager!.writeStdout(data);
|
|
96
|
-
|
|
97
|
-
|
|
195
|
+
|
|
196
|
+
// Filter out JSON lines from console output to keep it clean
|
|
197
|
+
const str = data.toString();
|
|
198
|
+
lineBuffer += str;
|
|
199
|
+
const lines = lineBuffer.split('\n');
|
|
200
|
+
lineBuffer = lines.pop() || '';
|
|
201
|
+
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
// Only print if NOT a noisy line
|
|
205
|
+
if (trimmed &&
|
|
206
|
+
!trimmed.startsWith('{') &&
|
|
207
|
+
!trimmed.startsWith('[') &&
|
|
208
|
+
!trimmed.includes('{"type"')) {
|
|
209
|
+
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`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
98
212
|
});
|
|
99
213
|
}
|
|
100
214
|
|
|
101
215
|
if (child.stderr) {
|
|
102
216
|
child.stderr.on('data', (data: Buffer) => {
|
|
103
217
|
logManager!.writeStderr(data);
|
|
104
|
-
|
|
105
|
-
|
|
218
|
+
const str = data.toString();
|
|
219
|
+
const lines = str.split('\n');
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
const trimmed = line.trim();
|
|
222
|
+
if (trimmed) {
|
|
223
|
+
// Check if it's a real error or just git/status output on stderr
|
|
224
|
+
const isStatus = trimmed.startsWith('Preparing worktree') ||
|
|
225
|
+
trimmed.startsWith('Switched to a new branch') ||
|
|
226
|
+
trimmed.startsWith('HEAD is now at') ||
|
|
227
|
+
trimmed.includes('actual output');
|
|
228
|
+
|
|
229
|
+
if (isStatus) {
|
|
230
|
+
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} ${trimmed}\n`);
|
|
231
|
+
} else {
|
|
232
|
+
process.stderr.write(`${logger.COLORS.red}[${laneName}] ERROR: ${trimmed}${logger.COLORS.reset}\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
106
236
|
});
|
|
107
237
|
}
|
|
108
238
|
|
|
@@ -284,7 +414,8 @@ async function resolveAllDependencies(
|
|
|
284
414
|
const task = taskConfig.tasks[currentIdx];
|
|
285
415
|
|
|
286
416
|
if (task) {
|
|
287
|
-
const
|
|
417
|
+
const lanePipelineBranch = `${pipelineBranch}/${lane.name}`;
|
|
418
|
+
const taskBranch = `${lanePipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
|
|
288
419
|
logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`);
|
|
289
420
|
|
|
290
421
|
try {
|
|
@@ -321,6 +452,7 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
321
452
|
webhooks?: WebhookConfig[];
|
|
322
453
|
autoResolveDependencies?: boolean;
|
|
323
454
|
enhancedLogging?: Partial<EnhancedLogConfig>;
|
|
455
|
+
noGit?: boolean;
|
|
324
456
|
} = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
|
|
325
457
|
const lanes = listLaneFiles(tasksDir);
|
|
326
458
|
|
|
@@ -328,11 +460,18 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
328
460
|
throw new Error(`No lane task files found in ${tasksDir}`);
|
|
329
461
|
}
|
|
330
462
|
|
|
463
|
+
const config = loadConfig();
|
|
464
|
+
const logsDir = getLogsDir(config);
|
|
331
465
|
const runId = `run-${Date.now()}`;
|
|
332
|
-
|
|
466
|
+
// Use absolute path for runRoot to avoid issues with subfolders
|
|
467
|
+
const runRoot = options.runDir
|
|
468
|
+
? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir))
|
|
469
|
+
: path.join(logsDir, 'runs', runId);
|
|
470
|
+
|
|
333
471
|
fs.mkdirSync(runRoot, { recursive: true });
|
|
334
472
|
|
|
335
|
-
const
|
|
473
|
+
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
474
|
+
const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}-${randomSuffix}`;
|
|
336
475
|
|
|
337
476
|
// Initialize event system
|
|
338
477
|
events.setRunId(runId);
|
|
@@ -370,7 +509,27 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
370
509
|
logger.info(`Run directory: ${runRoot}`);
|
|
371
510
|
logger.info(`Lanes: ${lanes.length}`);
|
|
372
511
|
|
|
373
|
-
|
|
512
|
+
// Display dependency graph
|
|
513
|
+
logger.info('\n📊 Dependency Graph:');
|
|
514
|
+
for (const lane of lanes) {
|
|
515
|
+
const deps = lane.dependsOn.length > 0 ? ` [depends on: ${lane.dependsOn.join(', ')}]` : '';
|
|
516
|
+
console.log(` ${logger.COLORS.cyan}${lane.name}${logger.COLORS.reset}${deps}`);
|
|
517
|
+
|
|
518
|
+
// Simple tree-like visualization for deep dependencies
|
|
519
|
+
if (lane.dependsOn.length > 0) {
|
|
520
|
+
for (const dep of lane.dependsOn) {
|
|
521
|
+
console.log(` └─ ${dep}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
console.log('');
|
|
526
|
+
|
|
527
|
+
// Disable auto-resolve when noGit mode is enabled
|
|
528
|
+
const autoResolve = !options.noGit && options.autoResolveDependencies !== false;
|
|
529
|
+
|
|
530
|
+
if (options.noGit) {
|
|
531
|
+
logger.info('🚫 Git operations disabled (--no-git mode)');
|
|
532
|
+
}
|
|
374
533
|
|
|
375
534
|
// Monitor lanes
|
|
376
535
|
const monitorInterval = setInterval(() => {
|
|
@@ -415,8 +574,9 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
415
574
|
laneRunDir: laneRunDirs[lane.name]!,
|
|
416
575
|
executor: options.executor || 'cursor-agent',
|
|
417
576
|
startIndex: lane.startIndex,
|
|
418
|
-
pipelineBranch
|
|
577
|
+
pipelineBranch: `${pipelineBranch}/${lane.name}`,
|
|
419
578
|
enhancedLogConfig: options.enhancedLogging,
|
|
579
|
+
noGit: options.noGit,
|
|
420
580
|
});
|
|
421
581
|
|
|
422
582
|
running.set(lane.name, spawnResult);
|
package/src/core/reviewer.ts
CHANGED
|
@@ -144,18 +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
|
-
|
|
152
|
+
model?: string;
|
|
153
|
+
cursorAgentSend: (options: {
|
|
154
|
+
workspaceDir: string;
|
|
155
|
+
chatId: string;
|
|
156
|
+
prompt: string;
|
|
157
|
+
model?: string;
|
|
158
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
159
|
+
}) => Promise<AgentSendResult>;
|
|
153
160
|
cursorAgentCreateChat: () => string;
|
|
154
161
|
}): Promise<ReviewResult> {
|
|
155
162
|
const reviewPrompt = buildReviewPrompt({
|
|
156
163
|
taskName: taskResult.taskName,
|
|
157
164
|
taskBranch: taskResult.taskBranch,
|
|
158
|
-
acceptanceCriteria: config.acceptanceCriteria || [],
|
|
165
|
+
acceptanceCriteria: taskResult.acceptanceCriteria || config.acceptanceCriteria || [],
|
|
159
166
|
});
|
|
160
167
|
|
|
161
168
|
logger.info(`Reviewing: ${taskResult.taskName}`);
|
|
@@ -166,11 +173,14 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
166
173
|
});
|
|
167
174
|
|
|
168
175
|
const reviewChatId = cursorAgentCreateChat();
|
|
169
|
-
const
|
|
176
|
+
const reviewModel = model || config.reviewModel || config.model || 'sonnet-4.5';
|
|
177
|
+
|
|
178
|
+
const reviewResult = await cursorAgentSend({
|
|
170
179
|
workspaceDir: worktreeDir,
|
|
171
180
|
chatId: reviewChatId,
|
|
172
181
|
prompt: reviewPrompt,
|
|
173
|
-
model:
|
|
182
|
+
model: reviewModel,
|
|
183
|
+
outputFormat: config.agentOutputFormat,
|
|
174
184
|
});
|
|
175
185
|
|
|
176
186
|
const review = parseReviewResult(reviewResult.resultText || '');
|
|
@@ -179,7 +189,7 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
179
189
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
180
190
|
appendLog(convoPath, createConversationEntry('reviewer', reviewResult.resultText || 'No result', {
|
|
181
191
|
task: taskResult.taskName,
|
|
182
|
-
model:
|
|
192
|
+
model: reviewModel,
|
|
183
193
|
}));
|
|
184
194
|
|
|
185
195
|
logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
|
|
@@ -197,14 +207,21 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
197
207
|
/**
|
|
198
208
|
* Review loop with feedback
|
|
199
209
|
*/
|
|
200
|
-
export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, cursorAgentSend, cursorAgentCreateChat }: {
|
|
210
|
+
export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, model, cursorAgentSend, cursorAgentCreateChat }: {
|
|
201
211
|
taskResult: TaskResult;
|
|
202
212
|
worktreeDir: string;
|
|
203
213
|
runDir: string;
|
|
204
214
|
config: RunnerConfig;
|
|
205
215
|
workChatId: string;
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
model?: string;
|
|
217
|
+
cursorAgentSend: (options: {
|
|
218
|
+
workspaceDir: string;
|
|
219
|
+
chatId: string;
|
|
220
|
+
prompt: string;
|
|
221
|
+
model?: string;
|
|
222
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
223
|
+
}) => Promise<AgentSendResult>;
|
|
224
|
+
cursorAgentCreateChat: () => string;
|
|
208
225
|
}): Promise<{ approved: boolean; review: ReviewResult; iterations: number; error?: string }> {
|
|
209
226
|
const maxIterations = config.maxReviewIterations || 3;
|
|
210
227
|
let iteration = 0;
|
|
@@ -216,10 +233,11 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
216
233
|
worktreeDir,
|
|
217
234
|
runDir,
|
|
218
235
|
config,
|
|
236
|
+
model,
|
|
219
237
|
cursorAgentSend,
|
|
220
238
|
cursorAgentCreateChat,
|
|
221
239
|
});
|
|
222
|
-
|
|
240
|
+
|
|
223
241
|
if (currentReview.status === 'approved') {
|
|
224
242
|
logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
|
|
225
243
|
events.emit('review.approved', {
|
|
@@ -245,11 +263,12 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
245
263
|
logger.info(`Sending feedback (iteration ${iteration}/${maxIterations})`);
|
|
246
264
|
const feedbackPrompt = buildFeedbackPrompt(currentReview);
|
|
247
265
|
|
|
248
|
-
const fixResult = cursorAgentSend({
|
|
266
|
+
const fixResult = await cursorAgentSend({
|
|
249
267
|
workspaceDir: worktreeDir,
|
|
250
268
|
chatId: workChatId,
|
|
251
269
|
prompt: feedbackPrompt,
|
|
252
270
|
model: config.model,
|
|
271
|
+
outputFormat: config.agentOutputFormat,
|
|
253
272
|
});
|
|
254
273
|
|
|
255
274
|
if (!fixResult.ok) {
|