@litmers/cursorflow-orchestrator 0.1.13 → 0.1.14
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 +34 -0
- package/README.md +83 -2
- package/commands/cursorflow-clean.md +20 -6
- package/commands/cursorflow-prepare.md +1 -1
- package/commands/cursorflow-resume.md +127 -6
- package/commands/cursorflow-run.md +2 -2
- package/commands/cursorflow-signal.md +11 -4
- package/dist/cli/clean.js +164 -12
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +8 -0
- package/dist/cli/logs.js +746 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/monitor.js +113 -30
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +1 -1
- package/dist/cli/resume.js +367 -18
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +2 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +34 -20
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +11 -1
- package/dist/core/orchestrator.js +257 -35
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.js +20 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.js +113 -13
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +34 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +209 -0
- package/dist/utils/enhanced-logger.js +963 -0
- package/dist/utils/enhanced-logger.js.map +1 -0
- package/dist/utils/events.d.ts +59 -0
- package/dist/utils/events.js +37 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.js +25 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/types.d.ts +122 -1
- package/dist/utils/webhook.d.ts +5 -0
- package/dist/utils/webhook.js +109 -0
- package/dist/utils/webhook.js.map +1 -0
- package/examples/README.md +1 -1
- package/package.json +1 -1
- package/scripts/simple-logging-test.sh +97 -0
- package/scripts/test-real-logging.sh +289 -0
- package/scripts/test-streaming-multi-task.sh +247 -0
- package/src/cli/clean.ts +170 -13
- package/src/cli/index.ts +4 -1
- package/src/cli/logs.ts +848 -0
- package/src/cli/monitor.ts +123 -30
- package/src/cli/prepare.ts +1 -1
- package/src/cli/resume.ts +463 -22
- package/src/cli/run.ts +2 -0
- package/src/cli/signal.ts +43 -27
- package/src/core/orchestrator.ts +303 -37
- package/src/core/reviewer.ts +22 -0
- package/src/core/runner.ts +128 -12
- package/src/utils/config.ts +36 -0
- package/src/utils/enhanced-logger.ts +1097 -0
- package/src/utils/events.ts +117 -0
- package/src/utils/git.ts +25 -0
- package/src/utils/types.ts +150 -1
- package/src/utils/webhook.ts +85 -0
package/src/core/orchestrator.ts
CHANGED
|
@@ -10,31 +10,47 @@ import { spawn, ChildProcess } from 'child_process';
|
|
|
10
10
|
|
|
11
11
|
import * as logger from '../utils/logger';
|
|
12
12
|
import { loadState } from '../utils/state';
|
|
13
|
-
import { LaneState, RunnerConfig } from '../utils/types';
|
|
13
|
+
import { LaneState, RunnerConfig, WebhookConfig, DependencyRequestPlan, EnhancedLogConfig } from '../utils/types';
|
|
14
|
+
import { events } from '../utils/events';
|
|
15
|
+
import { registerWebhooks } from '../utils/webhook';
|
|
16
|
+
import * as git from '../utils/git';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import { EnhancedLogManager, createLogManager, DEFAULT_LOG_CONFIG } from '../utils/enhanced-logger';
|
|
14
19
|
|
|
15
20
|
export interface LaneInfo {
|
|
16
21
|
name: string;
|
|
17
22
|
path: string;
|
|
18
23
|
dependsOn: string[];
|
|
24
|
+
startIndex?: number; // Current task index to resume from
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export interface SpawnLaneResult {
|
|
22
28
|
child: ChildProcess;
|
|
23
29
|
logPath: string;
|
|
30
|
+
logManager?: EnhancedLogManager;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
27
34
|
* Spawn a lane process
|
|
28
35
|
*/
|
|
29
|
-
export function spawnLane({
|
|
36
|
+
export function spawnLane({
|
|
37
|
+
laneName,
|
|
38
|
+
tasksFile,
|
|
39
|
+
laneRunDir,
|
|
40
|
+
executor,
|
|
41
|
+
startIndex = 0,
|
|
42
|
+
pipelineBranch,
|
|
43
|
+
enhancedLogConfig,
|
|
44
|
+
}: {
|
|
30
45
|
laneName: string;
|
|
31
46
|
tasksFile: string;
|
|
32
47
|
laneRunDir: string;
|
|
33
48
|
executor: string;
|
|
49
|
+
startIndex?: number;
|
|
50
|
+
pipelineBranch?: string;
|
|
51
|
+
enhancedLogConfig?: Partial<EnhancedLogConfig>;
|
|
34
52
|
}): SpawnLaneResult {
|
|
35
53
|
fs.mkdirSync(laneRunDir, { recursive: true});
|
|
36
|
-
const logPath = path.join(laneRunDir, 'terminal.log');
|
|
37
|
-
const logFd = fs.openSync(logPath, 'a');
|
|
38
54
|
|
|
39
55
|
// Use extension-less resolve to handle both .ts (dev) and .js (dist)
|
|
40
56
|
const runnerPath = require.resolve('./runner');
|
|
@@ -44,21 +60,75 @@ export function spawnLane({ tasksFile, laneRunDir, executor }: {
|
|
|
44
60
|
tasksFile,
|
|
45
61
|
'--run-dir', laneRunDir,
|
|
46
62
|
'--executor', executor,
|
|
63
|
+
'--start-index', startIndex.toString(),
|
|
47
64
|
];
|
|
65
|
+
|
|
66
|
+
if (pipelineBranch) {
|
|
67
|
+
args.push('--pipeline-branch', pipelineBranch);
|
|
68
|
+
}
|
|
48
69
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
// Create enhanced log manager if enabled
|
|
71
|
+
const logConfig = { ...DEFAULT_LOG_CONFIG, ...enhancedLogConfig };
|
|
72
|
+
let logManager: EnhancedLogManager | undefined;
|
|
73
|
+
let logPath: string;
|
|
74
|
+
let child: ChildProcess;
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
// Build environment for child process
|
|
77
|
+
const childEnv = {
|
|
78
|
+
...process.env,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (logConfig.enabled) {
|
|
82
|
+
logManager = createLogManager(laneRunDir, laneName, logConfig);
|
|
83
|
+
logPath = logManager.getLogPaths().clean;
|
|
84
|
+
|
|
85
|
+
// Spawn with pipe for enhanced logging
|
|
86
|
+
child = spawn('node', args, {
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
|
+
env: childEnv,
|
|
89
|
+
detached: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Pipe stdout and stderr through enhanced logger
|
|
93
|
+
if (child.stdout) {
|
|
94
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
95
|
+
logManager!.writeStdout(data);
|
|
96
|
+
// Also write to process stdout for real-time visibility
|
|
97
|
+
process.stdout.write(data);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (child.stderr) {
|
|
102
|
+
child.stderr.on('data', (data: Buffer) => {
|
|
103
|
+
logManager!.writeStderr(data);
|
|
104
|
+
// Also write to process stderr for real-time visibility
|
|
105
|
+
process.stderr.write(data);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Close log manager when process exits
|
|
110
|
+
child.on('exit', () => {
|
|
111
|
+
logManager?.close();
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
// Fallback to simple file logging
|
|
115
|
+
logPath = path.join(laneRunDir, 'terminal.log');
|
|
116
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
117
|
+
|
|
118
|
+
child = spawn('node', args, {
|
|
119
|
+
stdio: ['ignore', logFd, logFd],
|
|
120
|
+
env: childEnv,
|
|
121
|
+
detached: false,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
fs.closeSync(logFd);
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore
|
|
128
|
+
}
|
|
59
129
|
}
|
|
60
130
|
|
|
61
|
-
return { child, logPath };
|
|
131
|
+
return { child, logPath, logManager };
|
|
62
132
|
}
|
|
63
133
|
|
|
64
134
|
/**
|
|
@@ -138,6 +208,108 @@ export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, s
|
|
|
138
208
|
}
|
|
139
209
|
}
|
|
140
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Resolve dependencies for all blocked lanes and sync with all active lanes
|
|
213
|
+
*/
|
|
214
|
+
async function resolveAllDependencies(
|
|
215
|
+
blockedLanes: Map<string, DependencyRequestPlan>,
|
|
216
|
+
allLanes: LaneInfo[],
|
|
217
|
+
laneRunDirs: Record<string, string>,
|
|
218
|
+
pipelineBranch: string,
|
|
219
|
+
runRoot: string
|
|
220
|
+
) {
|
|
221
|
+
// 1. Collect all unique changes and commands from blocked lanes
|
|
222
|
+
const allChanges: string[] = [];
|
|
223
|
+
const allCommands: string[] = [];
|
|
224
|
+
|
|
225
|
+
for (const [, plan] of blockedLanes) {
|
|
226
|
+
if (plan.changes) allChanges.push(...plan.changes);
|
|
227
|
+
if (plan.commands) allCommands.push(...plan.commands);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const uniqueChanges = Array.from(new Set(allChanges));
|
|
231
|
+
const uniqueCommands = Array.from(new Set(allCommands));
|
|
232
|
+
|
|
233
|
+
if (uniqueCommands.length === 0) return;
|
|
234
|
+
|
|
235
|
+
// 2. Setup a temporary worktree for resolution if needed, or use the first available one
|
|
236
|
+
const firstLaneName = Array.from(blockedLanes.keys())[0]!;
|
|
237
|
+
const statePath = path.join(laneRunDirs[firstLaneName]!, 'state.json');
|
|
238
|
+
const state = loadState<LaneState>(statePath);
|
|
239
|
+
const worktreeDir = state?.worktreeDir || path.join(runRoot, 'resolution-worktree');
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(worktreeDir)) {
|
|
242
|
+
logger.info(`Creating resolution worktree at ${worktreeDir}`);
|
|
243
|
+
git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: 'main' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 3. Resolve on pipeline branch
|
|
247
|
+
logger.info(`Resolving dependencies on ${pipelineBranch}`);
|
|
248
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
249
|
+
|
|
250
|
+
for (const cmd of uniqueCommands) {
|
|
251
|
+
logger.info(`Running: ${cmd}`);
|
|
252
|
+
try {
|
|
253
|
+
execSync(cmd, { cwd: worktreeDir, stdio: 'inherit' });
|
|
254
|
+
} catch (e: any) {
|
|
255
|
+
throw new Error(`Command failed: ${cmd}. ${e.message}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
git.runGit(['add', '.'], { cwd: worktreeDir });
|
|
261
|
+
git.runGit(['commit', '-m', `chore: auto-resolve dependencies\n\n${uniqueChanges.join('\n')}`], { cwd: worktreeDir });
|
|
262
|
+
|
|
263
|
+
// Log changed files
|
|
264
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
265
|
+
if (stats) {
|
|
266
|
+
logger.info('Changed files:\n' + stats);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
270
|
+
} catch (e) { /* ignore if nothing to commit */ }
|
|
271
|
+
|
|
272
|
+
// 4. Sync ALL active lanes (blocked + pending + running)
|
|
273
|
+
// Since we only call this when running.size === 0, "active" means not completed/failed
|
|
274
|
+
for (const lane of allLanes) {
|
|
275
|
+
const laneDir = laneRunDirs[lane.name];
|
|
276
|
+
if (!laneDir) continue;
|
|
277
|
+
|
|
278
|
+
const laneState = loadState<LaneState>(path.join(laneDir, 'state.json'));
|
|
279
|
+
if (!laneState || laneState.status === 'completed' || laneState.status === 'failed') continue;
|
|
280
|
+
|
|
281
|
+
// Merge pipelineBranch into the lane's current task branch
|
|
282
|
+
const currentIdx = laneState.currentTaskIndex;
|
|
283
|
+
const taskConfig = JSON.parse(fs.readFileSync(lane.path, 'utf8')) as RunnerConfig;
|
|
284
|
+
const task = taskConfig.tasks[currentIdx];
|
|
285
|
+
|
|
286
|
+
if (task) {
|
|
287
|
+
const taskBranch = `${pipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
|
|
288
|
+
logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// If task branch doesn't exist yet, it will be created from pipelineBranch when the lane starts
|
|
292
|
+
if (git.branchExists(taskBranch, { cwd: worktreeDir })) {
|
|
293
|
+
git.runGit(['checkout', taskBranch], { cwd: worktreeDir });
|
|
294
|
+
git.runGit(['merge', pipelineBranch, '--no-edit'], { cwd: worktreeDir });
|
|
295
|
+
|
|
296
|
+
// Log changed files
|
|
297
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
298
|
+
if (stats) {
|
|
299
|
+
logger.info(`Sync results for ${lane.name}:\n` + stats);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
git.push(taskBranch, { cwd: worktreeDir });
|
|
303
|
+
}
|
|
304
|
+
} catch (e: any) {
|
|
305
|
+
logger.warn(`Failed to sync branch ${taskBranch}: ${e.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
311
|
+
}
|
|
312
|
+
|
|
141
313
|
/**
|
|
142
314
|
* Run orchestration with dependency management
|
|
143
315
|
*/
|
|
@@ -146,6 +318,9 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
146
318
|
executor?: string;
|
|
147
319
|
pollInterval?: number;
|
|
148
320
|
maxConcurrentLanes?: number;
|
|
321
|
+
webhooks?: WebhookConfig[];
|
|
322
|
+
autoResolveDependencies?: boolean;
|
|
323
|
+
enhancedLogging?: Partial<EnhancedLogConfig>;
|
|
149
324
|
} = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
|
|
150
325
|
const lanes = listLaneFiles(tasksDir);
|
|
151
326
|
|
|
@@ -153,8 +328,36 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
153
328
|
throw new Error(`No lane task files found in ${tasksDir}`);
|
|
154
329
|
}
|
|
155
330
|
|
|
156
|
-
const
|
|
331
|
+
const runId = `run-${Date.now()}`;
|
|
332
|
+
const runRoot = options.runDir || `_cursorflow/logs/runs/${runId}`;
|
|
157
333
|
fs.mkdirSync(runRoot, { recursive: true });
|
|
334
|
+
|
|
335
|
+
const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}`;
|
|
336
|
+
|
|
337
|
+
// Initialize event system
|
|
338
|
+
events.setRunId(runId);
|
|
339
|
+
if (options.webhooks) {
|
|
340
|
+
registerWebhooks(options.webhooks);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
events.emit('orchestration.started', {
|
|
344
|
+
runId,
|
|
345
|
+
tasksDir,
|
|
346
|
+
laneCount: lanes.length,
|
|
347
|
+
runRoot,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const maxConcurrent = options.maxConcurrentLanes || 10;
|
|
351
|
+
const running: Map<string, { child: ChildProcess; logPath: string }> = new Map();
|
|
352
|
+
const exitCodes: Record<string, number> = {};
|
|
353
|
+
const completedLanes = new Set<string>();
|
|
354
|
+
const failedLanes = new Set<string>();
|
|
355
|
+
const blockedLanes: Map<string, DependencyRequestPlan> = new Map();
|
|
356
|
+
|
|
357
|
+
// Track start index for each lane (initially 0)
|
|
358
|
+
for (const lane of lanes) {
|
|
359
|
+
lane.startIndex = 0;
|
|
360
|
+
}
|
|
158
361
|
|
|
159
362
|
const laneRunDirs: Record<string, string> = {};
|
|
160
363
|
for (const lane of lanes) {
|
|
@@ -166,35 +369,34 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
166
369
|
logger.info(`Tasks directory: ${tasksDir}`);
|
|
167
370
|
logger.info(`Run directory: ${runRoot}`);
|
|
168
371
|
logger.info(`Lanes: ${lanes.length}`);
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const running: Map<string, { child: ChildProcess; logPath: string }> = new Map();
|
|
172
|
-
const exitCodes: Record<string, number> = {};
|
|
173
|
-
const completedLanes = new Set<string>();
|
|
174
|
-
const failedLanes = new Set<string>();
|
|
372
|
+
|
|
373
|
+
const autoResolve = options.autoResolveDependencies !== false;
|
|
175
374
|
|
|
176
375
|
// Monitor lanes
|
|
177
376
|
const monitorInterval = setInterval(() => {
|
|
178
377
|
printLaneStatus(lanes, laneRunDirs);
|
|
179
378
|
}, options.pollInterval || 60000);
|
|
180
379
|
|
|
181
|
-
while (completedLanes.size + failedLanes.size < lanes.length) {
|
|
380
|
+
while (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length || (blockedLanes.size > 0 && running.size === 0)) {
|
|
182
381
|
// 1. Identify lanes ready to start
|
|
183
382
|
const readyToStart = lanes.filter(lane => {
|
|
184
|
-
// Not already running or completed
|
|
185
|
-
if (running.has(lane.name) || completedLanes.has(lane.name) || failedLanes.has(lane.name)) {
|
|
383
|
+
// Not already running or completed or failed or blocked
|
|
384
|
+
if (running.has(lane.name) || completedLanes.has(lane.name) || failedLanes.has(lane.name) || blockedLanes.has(lane.name)) {
|
|
186
385
|
return false;
|
|
187
386
|
}
|
|
188
387
|
|
|
189
388
|
// Check dependencies
|
|
190
389
|
for (const dep of lane.dependsOn) {
|
|
191
390
|
if (failedLanes.has(dep)) {
|
|
192
|
-
|
|
193
|
-
logger.error(`Lane ${lane.name} failed because dependency ${dep} failed`);
|
|
391
|
+
logger.error(`Lane ${lane.name} will not start because dependency ${dep} failed`);
|
|
194
392
|
failedLanes.add(lane.name);
|
|
195
393
|
exitCodes[lane.name] = 1;
|
|
196
394
|
return false;
|
|
197
395
|
}
|
|
396
|
+
if (blockedLanes.has(dep)) {
|
|
397
|
+
// If a dependency is blocked, wait
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
198
400
|
if (!completedLanes.has(dep)) {
|
|
199
401
|
return false;
|
|
200
402
|
}
|
|
@@ -206,20 +408,27 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
206
408
|
for (const lane of readyToStart) {
|
|
207
409
|
if (running.size >= maxConcurrent) break;
|
|
208
410
|
|
|
209
|
-
logger.info(`Lane started: ${lane.name}`);
|
|
411
|
+
logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
|
|
210
412
|
const spawnResult = spawnLane({
|
|
211
413
|
laneName: lane.name,
|
|
212
414
|
tasksFile: lane.path,
|
|
213
415
|
laneRunDir: laneRunDirs[lane.name]!,
|
|
214
416
|
executor: options.executor || 'cursor-agent',
|
|
417
|
+
startIndex: lane.startIndex,
|
|
418
|
+
pipelineBranch,
|
|
419
|
+
enhancedLogConfig: options.enhancedLogging,
|
|
215
420
|
});
|
|
216
421
|
|
|
217
422
|
running.set(lane.name, spawnResult);
|
|
423
|
+
events.emit('lane.started', {
|
|
424
|
+
laneName: lane.name,
|
|
425
|
+
pid: spawnResult.child.pid,
|
|
426
|
+
logPath: spawnResult.logPath,
|
|
427
|
+
});
|
|
218
428
|
}
|
|
219
429
|
|
|
220
430
|
// 3. Wait for any running lane to finish
|
|
221
431
|
if (running.size > 0) {
|
|
222
|
-
// We need to wait for at least one to finish
|
|
223
432
|
const promises = Array.from(running.entries()).map(async ([name, { child }]) => {
|
|
224
433
|
const code = await waitChild(child);
|
|
225
434
|
return { name, code };
|
|
@@ -230,23 +439,72 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
230
439
|
running.delete(finished.name);
|
|
231
440
|
exitCodes[finished.name] = finished.code;
|
|
232
441
|
|
|
233
|
-
if (finished.code === 0
|
|
442
|
+
if (finished.code === 0) {
|
|
234
443
|
completedLanes.add(finished.name);
|
|
444
|
+
events.emit('lane.completed', {
|
|
445
|
+
laneName: finished.name,
|
|
446
|
+
exitCode: finished.code,
|
|
447
|
+
});
|
|
448
|
+
} else if (finished.code === 2) {
|
|
449
|
+
// Blocked by dependency
|
|
450
|
+
const statePath = path.join(laneRunDirs[finished.name]!, 'state.json');
|
|
451
|
+
const state = loadState<LaneState>(statePath);
|
|
452
|
+
|
|
453
|
+
if (state && state.dependencyRequest) {
|
|
454
|
+
blockedLanes.set(finished.name, state.dependencyRequest);
|
|
455
|
+
const lane = lanes.find(l => l.name === finished.name);
|
|
456
|
+
if (lane) {
|
|
457
|
+
lane.startIndex = Math.max(0, state.currentTaskIndex - 1); // Task was blocked, retry it
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
events.emit('lane.blocked', {
|
|
461
|
+
laneName: finished.name,
|
|
462
|
+
dependencyRequest: state.dependencyRequest,
|
|
463
|
+
});
|
|
464
|
+
logger.warn(`Lane ${finished.name} is blocked on dependency change request`);
|
|
465
|
+
} else {
|
|
466
|
+
failedLanes.add(finished.name);
|
|
467
|
+
logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
|
|
468
|
+
}
|
|
235
469
|
} else {
|
|
236
470
|
failedLanes.add(finished.name);
|
|
471
|
+
events.emit('lane.failed', {
|
|
472
|
+
laneName: finished.name,
|
|
473
|
+
exitCode: finished.code,
|
|
474
|
+
error: 'Process exited with non-zero code',
|
|
475
|
+
});
|
|
237
476
|
}
|
|
238
477
|
|
|
239
478
|
printLaneStatus(lanes, laneRunDirs);
|
|
240
479
|
} else {
|
|
241
|
-
// Nothing running
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
480
|
+
// Nothing running. Are we blocked?
|
|
481
|
+
if (blockedLanes.size > 0 && autoResolve) {
|
|
482
|
+
logger.section('🛠 Auto-Resolving Dependencies');
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await resolveAllDependencies(blockedLanes, lanes, laneRunDirs, pipelineBranch, runRoot);
|
|
486
|
+
|
|
487
|
+
// Clear blocked status
|
|
488
|
+
blockedLanes.clear();
|
|
489
|
+
logger.success('Dependencies resolved and synced across all active lanes. Resuming...');
|
|
490
|
+
} catch (error: any) {
|
|
491
|
+
logger.error(`Auto-resolution failed: ${error.message}`);
|
|
492
|
+
// Move blocked to failed
|
|
493
|
+
for (const name of blockedLanes.keys()) {
|
|
494
|
+
failedLanes.add(name);
|
|
495
|
+
}
|
|
496
|
+
blockedLanes.clear();
|
|
497
|
+
}
|
|
498
|
+
} else if (readyToStart.length === 0 && completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length) {
|
|
499
|
+
const remaining = lanes.filter(l => !completedLanes.has(l.name) && !failedLanes.has(l.name) && !blockedLanes.has(l.name));
|
|
245
500
|
logger.error(`Deadlock detected! Remaining lanes cannot start: ${remaining.map(l => l.name).join(', ')}`);
|
|
246
501
|
for (const l of remaining) {
|
|
247
502
|
failedLanes.add(l.name);
|
|
248
503
|
exitCodes[l.name] = 1;
|
|
249
504
|
}
|
|
505
|
+
} else {
|
|
506
|
+
// All finished
|
|
507
|
+
break;
|
|
250
508
|
}
|
|
251
509
|
}
|
|
252
510
|
}
|
|
@@ -262,17 +520,25 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
262
520
|
process.exit(1);
|
|
263
521
|
}
|
|
264
522
|
|
|
265
|
-
// Check for blocked lanes
|
|
266
|
-
const blocked =
|
|
267
|
-
.filter(([, code]) => code === 2)
|
|
268
|
-
.map(([lane]) => lane);
|
|
523
|
+
// Check for blocked lanes (if autoResolve was false)
|
|
524
|
+
const blocked = Array.from(blockedLanes.keys());
|
|
269
525
|
|
|
270
526
|
if (blocked.length > 0) {
|
|
271
527
|
logger.warn(`Lanes blocked on dependency: ${blocked.join(', ')}`);
|
|
272
528
|
logger.info('Handle dependency changes manually and resume lanes');
|
|
529
|
+
events.emit('orchestration.failed', {
|
|
530
|
+
error: 'Some lanes blocked on dependency change requests',
|
|
531
|
+
blockedLanes: blocked,
|
|
532
|
+
});
|
|
273
533
|
process.exit(2);
|
|
274
534
|
}
|
|
275
535
|
|
|
276
536
|
logger.success('All lanes completed successfully!');
|
|
537
|
+
events.emit('orchestration.completed', {
|
|
538
|
+
runId,
|
|
539
|
+
laneCount: lanes.length,
|
|
540
|
+
completedCount: completedLanes.size,
|
|
541
|
+
failedCount: failedLanes.size,
|
|
542
|
+
});
|
|
277
543
|
return { lanes, exitCodes, runRoot };
|
|
278
544
|
}
|
package/src/core/reviewer.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as logger from '../utils/logger';
|
|
|
8
8
|
import { appendLog, createConversationEntry } from '../utils/state';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { ReviewResult, ReviewIssue, TaskResult, RunnerConfig, AgentSendResult } from '../utils/types';
|
|
11
|
+
import { events } from '../utils/events';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Build review prompt
|
|
@@ -159,6 +160,11 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
159
160
|
|
|
160
161
|
logger.info(`Reviewing: ${taskResult.taskName}`);
|
|
161
162
|
|
|
163
|
+
events.emit('review.started', {
|
|
164
|
+
taskName: taskResult.taskName,
|
|
165
|
+
taskBranch: taskResult.taskBranch,
|
|
166
|
+
});
|
|
167
|
+
|
|
162
168
|
const reviewChatId = cursorAgentCreateChat();
|
|
163
169
|
const reviewResult = cursorAgentSend({
|
|
164
170
|
workspaceDir: worktreeDir,
|
|
@@ -178,6 +184,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
178
184
|
|
|
179
185
|
logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
|
|
180
186
|
|
|
187
|
+
events.emit('review.completed', {
|
|
188
|
+
taskName: taskResult.taskName,
|
|
189
|
+
status: review.status,
|
|
190
|
+
issueCount: review.issues?.length || 0,
|
|
191
|
+
summary: review.summary,
|
|
192
|
+
});
|
|
193
|
+
|
|
181
194
|
return review;
|
|
182
195
|
}
|
|
183
196
|
|
|
@@ -209,6 +222,10 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
209
222
|
|
|
210
223
|
if (currentReview.status === 'approved') {
|
|
211
224
|
logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
|
|
225
|
+
events.emit('review.approved', {
|
|
226
|
+
taskName: taskResult.taskName,
|
|
227
|
+
iterations: iteration + 1,
|
|
228
|
+
});
|
|
212
229
|
return { approved: true, review: currentReview, iterations: iteration + 1 };
|
|
213
230
|
}
|
|
214
231
|
|
|
@@ -216,6 +233,11 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
216
233
|
|
|
217
234
|
if (iteration >= maxIterations) {
|
|
218
235
|
logger.warn(`Max review iterations (${maxIterations}) reached: ${taskResult.taskName}`);
|
|
236
|
+
events.emit('review.rejected', {
|
|
237
|
+
taskName: taskResult.taskName,
|
|
238
|
+
reason: 'Max iterations reached',
|
|
239
|
+
iterations: iteration,
|
|
240
|
+
});
|
|
219
241
|
break;
|
|
220
242
|
}
|
|
221
243
|
|