@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +26 -7
  3. package/commands/cursorflow-run.md +2 -0
  4. package/commands/cursorflow-triggers.md +250 -0
  5. package/dist/cli/clean.js +8 -7
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +5 -1
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +20 -14
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +64 -47
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.js +27 -17
  14. package/dist/cli/monitor.js.map +1 -1
  15. package/dist/cli/prepare.js +73 -33
  16. package/dist/cli/prepare.js.map +1 -1
  17. package/dist/cli/resume.js +193 -40
  18. package/dist/cli/resume.js.map +1 -1
  19. package/dist/cli/run.js +3 -2
  20. package/dist/cli/run.js.map +1 -1
  21. package/dist/cli/signal.js +7 -7
  22. package/dist/cli/signal.js.map +1 -1
  23. package/dist/core/orchestrator.d.ts +2 -1
  24. package/dist/core/orchestrator.js +54 -93
  25. package/dist/core/orchestrator.js.map +1 -1
  26. package/dist/core/reviewer.d.ts +6 -4
  27. package/dist/core/reviewer.js +7 -5
  28. package/dist/core/reviewer.js.map +1 -1
  29. package/dist/core/runner.d.ts +8 -0
  30. package/dist/core/runner.js +219 -32
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +20 -10
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/doctor.js +35 -7
  35. package/dist/utils/doctor.js.map +1 -1
  36. package/dist/utils/enhanced-logger.d.ts +2 -2
  37. package/dist/utils/enhanced-logger.js +114 -43
  38. package/dist/utils/enhanced-logger.js.map +1 -1
  39. package/dist/utils/git.js +163 -10
  40. package/dist/utils/git.js.map +1 -1
  41. package/dist/utils/log-formatter.d.ts +16 -0
  42. package/dist/utils/log-formatter.js +194 -0
  43. package/dist/utils/log-formatter.js.map +1 -0
  44. package/dist/utils/path.d.ts +19 -0
  45. package/dist/utils/path.js +77 -0
  46. package/dist/utils/path.js.map +1 -0
  47. package/dist/utils/repro-thinking-logs.d.ts +1 -0
  48. package/dist/utils/repro-thinking-logs.js +80 -0
  49. package/dist/utils/repro-thinking-logs.js.map +1 -0
  50. package/dist/utils/state.d.ts +4 -1
  51. package/dist/utils/state.js +11 -8
  52. package/dist/utils/state.js.map +1 -1
  53. package/dist/utils/template.d.ts +14 -0
  54. package/dist/utils/template.js +122 -0
  55. package/dist/utils/template.js.map +1 -0
  56. package/dist/utils/types.d.ts +13 -0
  57. package/dist/utils/webhook.js +3 -0
  58. package/dist/utils/webhook.js.map +1 -1
  59. package/package.json +4 -2
  60. package/scripts/ai-security-check.js +3 -0
  61. package/scripts/local-security-gate.sh +9 -1
  62. package/scripts/verify-and-fix.sh +37 -0
  63. package/src/cli/clean.ts +8 -7
  64. package/src/cli/index.ts +5 -1
  65. package/src/cli/init.ts +19 -15
  66. package/src/cli/logs.ts +67 -47
  67. package/src/cli/monitor.ts +28 -18
  68. package/src/cli/prepare.ts +75 -35
  69. package/src/cli/resume.ts +810 -626
  70. package/src/cli/run.ts +3 -2
  71. package/src/cli/signal.ts +7 -6
  72. package/src/core/orchestrator.ts +68 -93
  73. package/src/core/reviewer.ts +14 -9
  74. package/src/core/runner.ts +229 -33
  75. package/src/utils/config.ts +19 -11
  76. package/src/utils/doctor.ts +38 -7
  77. package/src/utils/enhanced-logger.ts +117 -49
  78. package/src/utils/git.ts +145 -11
  79. package/src/utils/log-formatter.ts +162 -0
  80. package/src/utils/path.ts +45 -0
  81. package/src/utils/repro-thinking-logs.ts +54 -0
  82. package/src/utils/state.ts +16 -8
  83. package/src/utils/template.ts +92 -0
  84. package/src/utils/types.ts +13 -0
  85. package/src/utils/webhook.ts +3 -0
  86. package/templates/basic.json +21 -0
  87. package/scripts/simple-logging-test.sh +0 -97
  88. package/scripts/test-real-cursor-lifecycle.sh +0 -289
  89. package/scripts/test-real-logging.sh +0 -289
  90. 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
- : path.join(config.projectRoot, options.tasksDir));
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 = path.join(logsDir, 'runs');
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 ? path.join(runsDir, runs[0]!) : null;
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 = path.join(runDir, 'lanes', options.lane);
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 = path.join(laneDir, 'timeout.txt');
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 = path.join(laneDir, 'intervention.txt');
105
- const convoPath = path.join(laneDir, 'conversation.jsonl');
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}"`);
@@ -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
- // 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
- }
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 = path.join(laneRunDir, 'terminal.log');
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 = path.join(tasksDir, f);
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 = path.join(dir, 'state.json');
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 = path.join(laneRunDirs[firstLaneName]!, 'state.json');
301
+ const statePath = safeJoin(laneRunDirs[firstLaneName]!, 'state.json');
366
302
  const state = loadState<LaneState>(statePath);
367
- const worktreeDir = state?.worktreeDir || path.join(runRoot, 'resolution-worktree');
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>(path.join(laneDir, 'state.json'));
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 taskBranch = `${pipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
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
- : path.join(logsDir, 'runs', runId);
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 pipelineBranch = `cursorflow/run-${Date.now().toString(36)}`;
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] = path.join(runRoot, 'lanes', lane.name);
500
- fs.mkdirSync(laneRunDirs[lane.name], { recursive: true });
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 = path.join(laneRunDirs[finished.name]!, 'state.json');
581
+ const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
607
582
  const state = loadState<LaneState>(statePath);
608
583
 
609
584
  if (state && state.dependencyRequest) {
@@ -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: config.reviewModel || 'sonnet-4.5-thinking',
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: config.reviewModel,
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', {