@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.
@@ -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 { EnhancedLogManager, createLogManager, DEFAULT_LOG_CONFIG } from '../utils/enhanced-logger';
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
- logManager = createLogManager(laneRunDir, laneName, logConfig);
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
- // Also write to process stdout for real-time visibility
97
- process.stdout.write(data);
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
- // Also write to process stderr for real-time visibility
105
- process.stderr.write(data);
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
- const runRoot = options.runDir || `_cursorflow/logs/runs/${runId}`;
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
- const autoResolve = options.autoResolveDependencies !== false;
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);
@@ -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: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
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: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
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) {