@litmers/cursorflow-orchestrator 0.1.14 ā 0.1.15
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 +3 -0
- package/dist/cli/logs.js +42 -29
- package/dist/cli/logs.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 +147 -8
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +4 -2
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +160 -55
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +4 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +3 -2
- package/dist/utils/enhanced-logger.js +87 -20
- 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/types.d.ts +10 -0
- package/package.json +2 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/test-real-cursor-lifecycle.sh +289 -0
- package/src/cli/logs.ts +43 -28
- package/src/cli/run.ts +8 -0
- package/src/core/orchestrator.ts +164 -8
- package/src/core/reviewer.ts +18 -4
- package/src/core/runner.ts +174 -57
- package/src/utils/config.ts +5 -1
- package/src/utils/enhanced-logger.ts +90 -21
- package/src/utils/git.ts +15 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/types.ts +10 -0
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,87 @@ 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
|
+
}
|
|
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
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
logManager = createLogManager(laneRunDir, laneName, logConfig, onParsedMessage);
|
|
83
176
|
logPath = logManager.getLogPaths().clean;
|
|
84
177
|
|
|
85
178
|
// Spawn with pipe for enhanced logging
|
|
@@ -89,20 +182,55 @@ export function spawnLane({
|
|
|
89
182
|
detached: false,
|
|
90
183
|
});
|
|
91
184
|
|
|
185
|
+
// Buffer for non-JSON lines
|
|
186
|
+
let lineBuffer = '';
|
|
187
|
+
|
|
92
188
|
// Pipe stdout and stderr through enhanced logger
|
|
93
189
|
if (child.stdout) {
|
|
94
190
|
child.stdout.on('data', (data: Buffer) => {
|
|
95
191
|
logManager!.writeStdout(data);
|
|
96
|
-
|
|
97
|
-
|
|
192
|
+
|
|
193
|
+
// Filter out JSON lines from console output to keep it clean
|
|
194
|
+
const str = data.toString();
|
|
195
|
+
lineBuffer += str;
|
|
196
|
+
const lines = lineBuffer.split('\n');
|
|
197
|
+
lineBuffer = lines.pop() || '';
|
|
198
|
+
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
const trimmed = line.trim();
|
|
201
|
+
// Only print if NOT a noisy line
|
|
202
|
+
if (trimmed &&
|
|
203
|
+
!trimmed.startsWith('{') &&
|
|
204
|
+
!trimmed.startsWith('[') &&
|
|
205
|
+
!trimmed.includes('{"type"') &&
|
|
206
|
+
!trimmed.includes('Heartbeat:')) {
|
|
207
|
+
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
|
+
}
|
|
209
|
+
}
|
|
98
210
|
});
|
|
99
211
|
}
|
|
100
212
|
|
|
101
213
|
if (child.stderr) {
|
|
102
214
|
child.stderr.on('data', (data: Buffer) => {
|
|
103
215
|
logManager!.writeStderr(data);
|
|
104
|
-
|
|
105
|
-
|
|
216
|
+
const str = data.toString();
|
|
217
|
+
const lines = str.split('\n');
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const trimmed = line.trim();
|
|
220
|
+
if (trimmed) {
|
|
221
|
+
// Check if it's a real error or just git/status output on stderr
|
|
222
|
+
const isStatus = trimmed.startsWith('Preparing worktree') ||
|
|
223
|
+
trimmed.startsWith('Switched to a new branch') ||
|
|
224
|
+
trimmed.startsWith('HEAD is now at') ||
|
|
225
|
+
trimmed.includes('actual output');
|
|
226
|
+
|
|
227
|
+
if (isStatus) {
|
|
228
|
+
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`);
|
|
229
|
+
} else {
|
|
230
|
+
process.stderr.write(`${logger.COLORS.red}[${laneName}] ERROR: ${trimmed}${logger.COLORS.reset}\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
106
234
|
});
|
|
107
235
|
}
|
|
108
236
|
|
|
@@ -321,6 +449,7 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
321
449
|
webhooks?: WebhookConfig[];
|
|
322
450
|
autoResolveDependencies?: boolean;
|
|
323
451
|
enhancedLogging?: Partial<EnhancedLogConfig>;
|
|
452
|
+
noGit?: boolean;
|
|
324
453
|
} = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
|
|
325
454
|
const lanes = listLaneFiles(tasksDir);
|
|
326
455
|
|
|
@@ -328,8 +457,14 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
328
457
|
throw new Error(`No lane task files found in ${tasksDir}`);
|
|
329
458
|
}
|
|
330
459
|
|
|
460
|
+
const config = loadConfig();
|
|
461
|
+
const logsDir = getLogsDir(config);
|
|
331
462
|
const runId = `run-${Date.now()}`;
|
|
332
|
-
|
|
463
|
+
// Use absolute path for runRoot to avoid issues with subfolders
|
|
464
|
+
const runRoot = options.runDir
|
|
465
|
+
? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir))
|
|
466
|
+
: path.join(logsDir, 'runs', runId);
|
|
467
|
+
|
|
333
468
|
fs.mkdirSync(runRoot, { recursive: true });
|
|
334
469
|
|
|
335
470
|
const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}`;
|
|
@@ -370,7 +505,27 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
370
505
|
logger.info(`Run directory: ${runRoot}`);
|
|
371
506
|
logger.info(`Lanes: ${lanes.length}`);
|
|
372
507
|
|
|
373
|
-
|
|
508
|
+
// Display dependency graph
|
|
509
|
+
logger.info('\nš Dependency Graph:');
|
|
510
|
+
for (const lane of lanes) {
|
|
511
|
+
const deps = lane.dependsOn.length > 0 ? ` [depends on: ${lane.dependsOn.join(', ')}]` : '';
|
|
512
|
+
console.log(` ${logger.COLORS.cyan}${lane.name}${logger.COLORS.reset}${deps}`);
|
|
513
|
+
|
|
514
|
+
// Simple tree-like visualization for deep dependencies
|
|
515
|
+
if (lane.dependsOn.length > 0) {
|
|
516
|
+
for (const dep of lane.dependsOn) {
|
|
517
|
+
console.log(` āā ${dep}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
console.log('');
|
|
522
|
+
|
|
523
|
+
// Disable auto-resolve when noGit mode is enabled
|
|
524
|
+
const autoResolve = !options.noGit && options.autoResolveDependencies !== false;
|
|
525
|
+
|
|
526
|
+
if (options.noGit) {
|
|
527
|
+
logger.info('š« Git operations disabled (--no-git mode)');
|
|
528
|
+
}
|
|
374
529
|
|
|
375
530
|
// Monitor lanes
|
|
376
531
|
const monitorInterval = setInterval(() => {
|
|
@@ -417,6 +572,7 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
417
572
|
startIndex: lane.startIndex,
|
|
418
573
|
pipelineBranch,
|
|
419
574
|
enhancedLogConfig: options.enhancedLogging,
|
|
575
|
+
noGit: options.noGit,
|
|
420
576
|
});
|
|
421
577
|
|
|
422
578
|
running.set(lane.name, spawnResult);
|
package/src/core/reviewer.ts
CHANGED
|
@@ -149,7 +149,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
149
149
|
worktreeDir: string;
|
|
150
150
|
runDir: string;
|
|
151
151
|
config: RunnerConfig;
|
|
152
|
-
cursorAgentSend: (options: {
|
|
152
|
+
cursorAgentSend: (options: {
|
|
153
|
+
workspaceDir: string;
|
|
154
|
+
chatId: string;
|
|
155
|
+
prompt: string;
|
|
156
|
+
model?: string;
|
|
157
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
158
|
+
}) => AgentSendResult;
|
|
153
159
|
cursorAgentCreateChat: () => string;
|
|
154
160
|
}): Promise<ReviewResult> {
|
|
155
161
|
const reviewPrompt = buildReviewPrompt({
|
|
@@ -166,11 +172,12 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
166
172
|
});
|
|
167
173
|
|
|
168
174
|
const reviewChatId = cursorAgentCreateChat();
|
|
169
|
-
const reviewResult = cursorAgentSend({
|
|
175
|
+
const reviewResult = await cursorAgentSend({
|
|
170
176
|
workspaceDir: worktreeDir,
|
|
171
177
|
chatId: reviewChatId,
|
|
172
178
|
prompt: reviewPrompt,
|
|
173
179
|
model: config.reviewModel || 'sonnet-4.5-thinking',
|
|
180
|
+
outputFormat: config.agentOutputFormat,
|
|
174
181
|
});
|
|
175
182
|
|
|
176
183
|
const review = parseReviewResult(reviewResult.resultText || '');
|
|
@@ -203,7 +210,13 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
203
210
|
runDir: string;
|
|
204
211
|
config: RunnerConfig;
|
|
205
212
|
workChatId: string;
|
|
206
|
-
cursorAgentSend: (options: {
|
|
213
|
+
cursorAgentSend: (options: {
|
|
214
|
+
workspaceDir: string;
|
|
215
|
+
chatId: string;
|
|
216
|
+
prompt: string;
|
|
217
|
+
model?: string;
|
|
218
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
219
|
+
}) => AgentSendResult;
|
|
207
220
|
cursorAgentCreateChat: () => string;
|
|
208
221
|
}): Promise<{ approved: boolean; review: ReviewResult; iterations: number; error?: string }> {
|
|
209
222
|
const maxIterations = config.maxReviewIterations || 3;
|
|
@@ -245,11 +258,12 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
245
258
|
logger.info(`Sending feedback (iteration ${iteration}/${maxIterations})`);
|
|
246
259
|
const feedbackPrompt = buildFeedbackPrompt(currentReview);
|
|
247
260
|
|
|
248
|
-
const fixResult = cursorAgentSend({
|
|
261
|
+
const fixResult = await cursorAgentSend({
|
|
249
262
|
workspaceDir: worktreeDir,
|
|
250
263
|
chatId: workChatId,
|
|
251
264
|
prompt: feedbackPrompt,
|
|
252
265
|
model: config.model,
|
|
266
|
+
outputFormat: config.agentOutputFormat,
|
|
253
267
|
});
|
|
254
268
|
|
|
255
269
|
if (!fixResult.ok) {
|