@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -0
  3. package/commands/cursorflow-run.md +2 -0
  4. package/commands/cursorflow-triggers.md +250 -0
  5. package/dist/cli/clean.js +1 -1
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/init.js +13 -8
  8. package/dist/cli/init.js.map +1 -1
  9. package/dist/cli/logs.js +66 -44
  10. package/dist/cli/logs.js.map +1 -1
  11. package/dist/cli/monitor.js +12 -3
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/prepare.js +36 -13
  14. package/dist/cli/prepare.js.map +1 -1
  15. package/dist/cli/resume.js.map +1 -1
  16. package/dist/cli/run.js +7 -0
  17. package/dist/cli/run.js.map +1 -1
  18. package/dist/core/orchestrator.d.ts +3 -1
  19. package/dist/core/orchestrator.js +154 -11
  20. package/dist/core/orchestrator.js.map +1 -1
  21. package/dist/core/reviewer.d.ts +8 -4
  22. package/dist/core/reviewer.js +11 -7
  23. package/dist/core/reviewer.js.map +1 -1
  24. package/dist/core/runner.d.ts +17 -3
  25. package/dist/core/runner.js +326 -69
  26. package/dist/core/runner.js.map +1 -1
  27. package/dist/utils/config.js +17 -5
  28. package/dist/utils/config.js.map +1 -1
  29. package/dist/utils/doctor.js +28 -1
  30. package/dist/utils/doctor.js.map +1 -1
  31. package/dist/utils/enhanced-logger.d.ts +5 -4
  32. package/dist/utils/enhanced-logger.js +178 -43
  33. package/dist/utils/enhanced-logger.js.map +1 -1
  34. package/dist/utils/git.d.ts +6 -0
  35. package/dist/utils/git.js +15 -0
  36. package/dist/utils/git.js.map +1 -1
  37. package/dist/utils/logger.d.ts +2 -0
  38. package/dist/utils/logger.js +4 -1
  39. package/dist/utils/logger.js.map +1 -1
  40. package/dist/utils/repro-thinking-logs.d.ts +1 -0
  41. package/dist/utils/repro-thinking-logs.js +80 -0
  42. package/dist/utils/repro-thinking-logs.js.map +1 -0
  43. package/dist/utils/types.d.ts +22 -0
  44. package/dist/utils/webhook.js +3 -0
  45. package/dist/utils/webhook.js.map +1 -1
  46. package/package.json +4 -1
  47. package/scripts/ai-security-check.js +3 -0
  48. package/scripts/local-security-gate.sh +9 -1
  49. package/scripts/patches/test-cursor-agent.js +1 -1
  50. package/scripts/verify-and-fix.sh +37 -0
  51. package/src/cli/clean.ts +1 -1
  52. package/src/cli/init.ts +12 -9
  53. package/src/cli/logs.ts +68 -43
  54. package/src/cli/monitor.ts +13 -4
  55. package/src/cli/prepare.ts +36 -15
  56. package/src/cli/resume.ts +1 -1
  57. package/src/cli/run.ts +8 -0
  58. package/src/core/orchestrator.ts +171 -11
  59. package/src/core/reviewer.ts +30 -11
  60. package/src/core/runner.ts +346 -71
  61. package/src/utils/config.ts +17 -6
  62. package/src/utils/doctor.ts +31 -1
  63. package/src/utils/enhanced-logger.ts +182 -48
  64. package/src/utils/git.ts +15 -0
  65. package/src/utils/logger.ts +4 -1
  66. package/src/utils/repro-thinking-logs.ts +54 -0
  67. package/src/utils/types.ts +22 -0
  68. package/src/utils/webhook.ts +3 -0
  69. package/scripts/simple-logging-test.sh +0 -97
  70. package/scripts/test-real-logging.sh +0 -289
  71. package/scripts/test-streaming-multi-task.sh +0 -247
@@ -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[|criteria1,criteria2]"`);
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
- fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', 'utf8');
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
- const existingConfig = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
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, listLanesInRun } from '../utils/state';
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
@@ -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,90 @@ 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
+ 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
- // Also write to process stdout for real-time visibility
97
- process.stdout.write(data);
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
- // Also write to process stderr for real-time visibility
105
- process.stderr.write(data);
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 taskBranch = `${pipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
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
- const runRoot = options.runDir || `_cursorflow/logs/runs/${runId}`;
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 pipelineBranch = `cursorflow/run-${Date.now().toString(36)}`;
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
- const autoResolve = options.autoResolveDependencies !== false;
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);
@@ -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
- cursorAgentSend: (options: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
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 reviewResult = cursorAgentSend({
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: config.reviewModel || 'sonnet-4.5-thinking',
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: config.reviewModel,
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
- cursorAgentSend: (options: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
207
- cursorAgentCreateChat: () => string;
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) {