@litmers/cursorflow-orchestrator 0.1.6 → 0.1.8
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/dist/cli/monitor.d.ts +1 -1
- package/dist/cli/monitor.js +640 -145
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/run.js +1 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +4 -2
- package/dist/core/orchestrator.js +92 -23
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner.d.ts +6 -2
- package/dist/core/runner.js +128 -58
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/cli/monitor.ts +693 -185
- package/src/cli/run.ts +1 -0
- package/src/core/orchestrator.ts +102 -27
- package/src/core/runner.ts +147 -70
- package/src/utils/types.ts +2 -0
package/src/cli/run.ts
CHANGED
|
@@ -99,6 +99,7 @@ async function run(args: string[]): Promise<void> {
|
|
|
99
99
|
executor: options.executor || config.executor,
|
|
100
100
|
pollInterval: config.pollInterval * 1000,
|
|
101
101
|
runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
|
|
102
|
+
maxConcurrentLanes: config.maxConcurrentLanes,
|
|
102
103
|
});
|
|
103
104
|
} catch (error: any) {
|
|
104
105
|
// Re-throw to be handled by the main entry point
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -10,11 +10,12 @@ 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 } from '../utils/types';
|
|
13
|
+
import { LaneState, RunnerConfig } from '../utils/types';
|
|
14
14
|
|
|
15
15
|
export interface LaneInfo {
|
|
16
16
|
name: string;
|
|
17
17
|
path: string;
|
|
18
|
+
dependsOn: string[];
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface SpawnLaneResult {
|
|
@@ -76,7 +77,7 @@ export function waitChild(proc: ChildProcess): Promise<number> {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* List lane task files in directory
|
|
80
|
+
* List lane task files in directory and load their configs for dependencies
|
|
80
81
|
*/
|
|
81
82
|
export function listLaneFiles(tasksDir: string): LaneInfo[] {
|
|
82
83
|
if (!fs.existsSync(tasksDir)) {
|
|
@@ -87,10 +88,24 @@ export function listLaneFiles(tasksDir: string): LaneInfo[] {
|
|
|
87
88
|
return files
|
|
88
89
|
.filter(f => f.endsWith('.json'))
|
|
89
90
|
.sort()
|
|
90
|
-
.map(f =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
.map(f => {
|
|
92
|
+
const filePath = path.join(tasksDir, f);
|
|
93
|
+
const name = path.basename(f, '.json');
|
|
94
|
+
let dependsOn: string[] = [];
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const config = JSON.parse(fs.readFileSync(filePath, 'utf8')) as RunnerConfig;
|
|
98
|
+
dependsOn = config.dependsOn || [];
|
|
99
|
+
} catch (e) {
|
|
100
|
+
logger.warn(`Failed to parse config for lane ${name}: ${e}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
path: filePath,
|
|
106
|
+
dependsOn,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
/**
|
|
@@ -105,7 +120,8 @@ export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, s
|
|
|
105
120
|
const state = loadState<LaneState>(statePath);
|
|
106
121
|
|
|
107
122
|
if (!state) {
|
|
108
|
-
|
|
123
|
+
const isWaiting = lane.dependsOn.length > 0;
|
|
124
|
+
return { lane: lane.name, status: isWaiting ? 'waiting' : 'pending', task: '-' };
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
const idx = (state.currentTaskIndex || 0) + 1;
|
|
@@ -123,12 +139,13 @@ export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, s
|
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
/**
|
|
126
|
-
* Run orchestration
|
|
142
|
+
* Run orchestration with dependency management
|
|
127
143
|
*/
|
|
128
144
|
export async function orchestrate(tasksDir: string, options: {
|
|
129
145
|
runDir?: string;
|
|
130
146
|
executor?: string;
|
|
131
147
|
pollInterval?: number;
|
|
148
|
+
maxConcurrentLanes?: number;
|
|
132
149
|
} = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
|
|
133
150
|
const lanes = listLaneFiles(tasksDir);
|
|
134
151
|
|
|
@@ -142,6 +159,7 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
142
159
|
const laneRunDirs: Record<string, string> = {};
|
|
143
160
|
for (const lane of lanes) {
|
|
144
161
|
laneRunDirs[lane.name] = path.join(runRoot, 'lanes', lane.name);
|
|
162
|
+
fs.mkdirSync(laneRunDirs[lane.name], { recursive: true });
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
logger.section('🧭 Starting Orchestration');
|
|
@@ -149,31 +167,88 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
149
167
|
logger.info(`Run directory: ${runRoot}`);
|
|
150
168
|
logger.info(`Lanes: ${lanes.length}`);
|
|
151
169
|
|
|
152
|
-
|
|
153
|
-
const running: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
laneName: lane.name,
|
|
158
|
-
tasksFile: lane.path,
|
|
159
|
-
laneRunDir: laneRunDirs[lane.name]!,
|
|
160
|
-
executor: options.executor || 'cursor-agent',
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
running.push({ lane: lane.name, child, logPath });
|
|
164
|
-
logger.info(`Lane started: ${lane.name}`);
|
|
165
|
-
}
|
|
170
|
+
const maxConcurrent = options.maxConcurrentLanes || 10;
|
|
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>();
|
|
166
175
|
|
|
167
176
|
// Monitor lanes
|
|
168
177
|
const monitorInterval = setInterval(() => {
|
|
169
178
|
printLaneStatus(lanes, laneRunDirs);
|
|
170
179
|
}, options.pollInterval || 60000);
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
while (completedLanes.size + failedLanes.size < lanes.length) {
|
|
182
|
+
// 1. Identify lanes ready to start
|
|
183
|
+
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)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check dependencies
|
|
190
|
+
for (const dep of lane.dependsOn) {
|
|
191
|
+
if (failedLanes.has(dep)) {
|
|
192
|
+
// If a dependency failed, this lane fails too
|
|
193
|
+
logger.error(`Lane ${lane.name} failed because dependency ${dep} failed`);
|
|
194
|
+
failedLanes.add(lane.name);
|
|
195
|
+
exitCodes[lane.name] = 1;
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (!completedLanes.has(dep)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// 2. Spawn ready lanes up to maxConcurrent
|
|
206
|
+
for (const lane of readyToStart) {
|
|
207
|
+
if (running.size >= maxConcurrent) break;
|
|
208
|
+
|
|
209
|
+
logger.info(`Lane started: ${lane.name}`);
|
|
210
|
+
const spawnResult = spawnLane({
|
|
211
|
+
laneName: lane.name,
|
|
212
|
+
tasksFile: lane.path,
|
|
213
|
+
laneRunDir: laneRunDirs[lane.name]!,
|
|
214
|
+
executor: options.executor || 'cursor-agent',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
running.set(lane.name, spawnResult);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 3. Wait for any running lane to finish
|
|
221
|
+
if (running.size > 0) {
|
|
222
|
+
// We need to wait for at least one to finish
|
|
223
|
+
const promises = Array.from(running.entries()).map(async ([name, { child }]) => {
|
|
224
|
+
const code = await waitChild(child);
|
|
225
|
+
return { name, code };
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const finished = await Promise.race(promises);
|
|
229
|
+
|
|
230
|
+
running.delete(finished.name);
|
|
231
|
+
exitCodes[finished.name] = finished.code;
|
|
232
|
+
|
|
233
|
+
if (finished.code === 0 || finished.code === 2) {
|
|
234
|
+
completedLanes.add(finished.name);
|
|
235
|
+
} else {
|
|
236
|
+
failedLanes.add(finished.name);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
240
|
+
} else {
|
|
241
|
+
// Nothing running and nothing ready (but not all finished)
|
|
242
|
+
// This could happen if there's a circular dependency or some logic error
|
|
243
|
+
if (readyToStart.length === 0 && completedLanes.size + failedLanes.size < lanes.length) {
|
|
244
|
+
const remaining = lanes.filter(l => !completedLanes.has(l.name) && !failedLanes.has(l.name));
|
|
245
|
+
logger.error(`Deadlock detected! Remaining lanes cannot start: ${remaining.map(l => l.name).join(', ')}`);
|
|
246
|
+
for (const l of remaining) {
|
|
247
|
+
failedLanes.add(l.name);
|
|
248
|
+
exitCodes[l.name] = 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
177
252
|
}
|
|
178
253
|
|
|
179
254
|
clearInterval(monitorInterval);
|
package/src/core/runner.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import { execSync, spawnSync } from 'child_process';
|
|
9
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
10
10
|
|
|
11
11
|
import * as git from '../utils/git';
|
|
12
12
|
import * as logger from '../utils/logger';
|
|
@@ -107,12 +107,16 @@ function parseJsonFromStdout(stdout: string): any {
|
|
|
107
107
|
return null;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Execute cursor-agent command with streaming and better error handling
|
|
112
|
+
*/
|
|
113
|
+
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir }: {
|
|
111
114
|
workspaceDir: string;
|
|
112
115
|
chatId: string;
|
|
113
116
|
prompt: string;
|
|
114
117
|
model?: string;
|
|
115
|
-
|
|
118
|
+
signalDir?: string;
|
|
119
|
+
}): Promise<AgentSendResult> {
|
|
116
120
|
const args = [
|
|
117
121
|
'--print',
|
|
118
122
|
'--output-format', 'json',
|
|
@@ -124,74 +128,100 @@ export function cursorAgentSend({ workspaceDir, chatId, prompt, model }: {
|
|
|
124
128
|
|
|
125
129
|
logger.info('Executing cursor-agent...');
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const child = spawn('cursor-agent', args, {
|
|
133
|
+
stdio: ['pipe', 'pipe', 'pipe'], // Enable stdin piping
|
|
134
|
+
env: process.env,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
let fullStdout = '';
|
|
138
|
+
let fullStderr = '';
|
|
139
|
+
|
|
140
|
+
// Watch for "intervention.txt" signal file if any
|
|
141
|
+
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
142
|
+
let interventionWatcher: fs.FSWatcher | null = null;
|
|
143
|
+
|
|
144
|
+
if (interventionPath && fs.existsSync(path.dirname(interventionPath))) {
|
|
145
|
+
interventionWatcher = fs.watch(path.dirname(interventionPath), (event, filename) => {
|
|
146
|
+
if (filename === 'intervention.txt' && fs.existsSync(interventionPath)) {
|
|
147
|
+
try {
|
|
148
|
+
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
149
|
+
if (message) {
|
|
150
|
+
logger.info(`Injecting intervention: ${message}`);
|
|
151
|
+
child.stdin.write(message + '\n');
|
|
152
|
+
fs.unlinkSync(interventionPath); // Clear it
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
logger.warn('Failed to read intervention file');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
child.stdout.on('data', (data) => {
|
|
162
|
+
const str = data.toString();
|
|
163
|
+
fullStdout += str;
|
|
164
|
+
// Also pipe to our own stdout so it goes to terminal.log
|
|
165
|
+
process.stdout.write(data);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.stderr.on('data', (data) => {
|
|
169
|
+
fullStderr += data.toString();
|
|
170
|
+
// Pipe to our own stderr so it goes to terminal.log
|
|
171
|
+
process.stderr.write(data);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const timeout = setTimeout(() => {
|
|
175
|
+
child.kill();
|
|
176
|
+
resolve({
|
|
137
177
|
ok: false,
|
|
138
178
|
exitCode: -1,
|
|
139
179
|
error: 'cursor-agent timed out after 5 minutes. The LLM request may be taking too long or there may be network issues.',
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
errorMsg.includes('
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
error: errorMsg,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
ok: !json.is_error,
|
|
191
|
-
exitCode: res.status ?? 0,
|
|
192
|
-
sessionId: json.session_id || chatId,
|
|
193
|
-
resultText: json.result || '',
|
|
194
|
-
};
|
|
180
|
+
});
|
|
181
|
+
}, 300000);
|
|
182
|
+
|
|
183
|
+
child.on('close', (code) => {
|
|
184
|
+
clearTimeout(timeout);
|
|
185
|
+
if (interventionWatcher) interventionWatcher.close();
|
|
186
|
+
|
|
187
|
+
const json = parseJsonFromStdout(fullStdout);
|
|
188
|
+
|
|
189
|
+
if (code !== 0 || !json || json.type !== 'result') {
|
|
190
|
+
let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
|
|
191
|
+
|
|
192
|
+
// Check for common errors
|
|
193
|
+
if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
|
|
194
|
+
errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
|
|
195
|
+
} else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
|
|
196
|
+
errorMsg = 'API rate limit or quota exceeded.';
|
|
197
|
+
} else if (errorMsg.includes('model')) {
|
|
198
|
+
errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
resolve({
|
|
202
|
+
ok: false,
|
|
203
|
+
exitCode: code ?? -1,
|
|
204
|
+
error: errorMsg,
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
resolve({
|
|
208
|
+
ok: !json.is_error,
|
|
209
|
+
exitCode: code ?? 0,
|
|
210
|
+
sessionId: json.session_id || chatId,
|
|
211
|
+
resultText: json.result || '',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
child.on('error', (err) => {
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
resolve({
|
|
219
|
+
ok: false,
|
|
220
|
+
exitCode: -1,
|
|
221
|
+
error: `Failed to start cursor-agent: ${err.message}`,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
195
225
|
}
|
|
196
226
|
|
|
197
227
|
/**
|
|
@@ -326,11 +356,12 @@ export async function runTask({
|
|
|
326
356
|
}));
|
|
327
357
|
|
|
328
358
|
logger.info('Sending prompt to agent...');
|
|
329
|
-
const r1 = cursorAgentSend({
|
|
359
|
+
const r1 = await cursorAgentSend({
|
|
330
360
|
workspaceDir: worktreeDir,
|
|
331
361
|
chatId,
|
|
332
362
|
prompt: prompt1,
|
|
333
363
|
model,
|
|
364
|
+
signalDir: runDir
|
|
334
365
|
});
|
|
335
366
|
|
|
336
367
|
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
|
|
@@ -426,7 +457,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
426
457
|
|
|
427
458
|
// Create worktree only if starting fresh
|
|
428
459
|
if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
|
|
429
|
-
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
460
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
430
461
|
baseBranch: config.baseBranch || 'main',
|
|
431
462
|
cwd: repoRoot,
|
|
432
463
|
});
|
|
@@ -450,15 +481,61 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
450
481
|
error: null,
|
|
451
482
|
dependencyRequest: null,
|
|
452
483
|
tasksFile, // Store tasks file for resume
|
|
484
|
+
dependsOn: config.dependsOn || [],
|
|
453
485
|
};
|
|
454
486
|
} else {
|
|
455
487
|
state.status = 'running';
|
|
456
488
|
state.error = null;
|
|
457
489
|
state.dependencyRequest = null;
|
|
490
|
+
state.dependsOn = config.dependsOn || [];
|
|
458
491
|
}
|
|
459
492
|
|
|
460
493
|
saveState(statePath, state);
|
|
461
494
|
|
|
495
|
+
// Merge dependencies if any
|
|
496
|
+
if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
497
|
+
logger.section('🔗 Merging Dependencies');
|
|
498
|
+
|
|
499
|
+
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
500
|
+
const lanesRoot = path.dirname(runDir);
|
|
501
|
+
|
|
502
|
+
for (const depName of config.dependsOn) {
|
|
503
|
+
const depRunDir = path.join(lanesRoot, depName);
|
|
504
|
+
const depStatePath = path.join(depRunDir, 'state.json');
|
|
505
|
+
|
|
506
|
+
if (!fs.existsSync(depStatePath)) {
|
|
507
|
+
logger.warn(`Dependency state not found for ${depName} at ${depStatePath}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
513
|
+
if (depState.status !== 'completed') {
|
|
514
|
+
logger.warn(`Dependency ${depName} is in status ${depState.status}, merge might be incomplete`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (depState.pipelineBranch) {
|
|
518
|
+
logger.info(`Merging dependency branch: ${depState.pipelineBranch} (${depName})`);
|
|
519
|
+
|
|
520
|
+
// Fetch first to ensure we have the branch
|
|
521
|
+
git.runGit(['fetch', 'origin', depState.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
522
|
+
|
|
523
|
+
// Merge
|
|
524
|
+
git.merge(depState.pipelineBranch, {
|
|
525
|
+
cwd: worktreeDir,
|
|
526
|
+
noFf: true,
|
|
527
|
+
message: `chore: merge dependency ${depName} (${depState.pipelineBranch})`
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
} catch (e) {
|
|
531
|
+
logger.error(`Failed to merge dependency ${depName}: ${e}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Push the merged state
|
|
536
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
537
|
+
}
|
|
538
|
+
|
|
462
539
|
// Run tasks
|
|
463
540
|
const results: TaskExecutionResult[] = [];
|
|
464
541
|
|
package/src/utils/types.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface Task {
|
|
|
40
40
|
|
|
41
41
|
export interface RunnerConfig {
|
|
42
42
|
tasks: Task[];
|
|
43
|
+
dependsOn?: string[];
|
|
43
44
|
pipelineBranch?: string;
|
|
44
45
|
branchPrefix?: string;
|
|
45
46
|
worktreeRoot?: string;
|
|
@@ -109,6 +110,7 @@ export interface LaneState {
|
|
|
109
110
|
dependencyRequest: DependencyRequestPlan | null;
|
|
110
111
|
updatedAt?: number;
|
|
111
112
|
tasksFile?: string; // Original tasks file path
|
|
113
|
+
dependsOn?: string[];
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
export interface ConversationEntry {
|