@litmers/cursorflow-orchestrator 0.1.5 → 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/CHANGELOG.md +15 -6
- package/README.md +33 -2
- package/commands/cursorflow-doctor.md +24 -0
- package/commands/cursorflow-signal.md +19 -0
- package/dist/cli/doctor.d.ts +15 -0
- package/dist/cli/doctor.js +139 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- 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/resume.d.ts +1 -1
- package/dist/cli/resume.js +80 -10
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +60 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/setup-commands.d.ts +4 -0
- package/dist/cli/setup-commands.js +16 -0
- package/dist/cli/setup-commands.js.map +1 -1
- package/dist/cli/signal.d.ts +7 -0
- package/dist/cli/signal.js +99 -0
- package/dist/cli/signal.js.map +1 -0
- 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 +9 -3
- package/dist/core/runner.js +182 -88
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/doctor.d.ts +63 -0
- package/dist/utils/doctor.js +280 -0
- package/dist/utils/doctor.js.map +1 -0
- package/dist/utils/types.d.ts +3 -0
- package/package.json +1 -1
- package/src/cli/doctor.ts +127 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/monitor.ts +693 -185
- package/src/cli/resume.ts +94 -12
- package/src/cli/run.ts +63 -7
- package/src/cli/setup-commands.ts +19 -0
- package/src/cli/signal.ts +89 -0
- package/src/core/orchestrator.ts +102 -27
- package/src/core/runner.ts +203 -99
- package/src/utils/doctor.ts +312 -0
- package/src/utils/types.ts +3 -0
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', {
|
|
@@ -371,7 +402,9 @@ export async function runTask({
|
|
|
371
402
|
/**
|
|
372
403
|
* Run all tasks in sequence
|
|
373
404
|
*/
|
|
374
|
-
export async function runTasks(config: RunnerConfig, runDir: string): Promise<TaskExecutionResult[]> {
|
|
405
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
|
|
406
|
+
const startIndex = options.startIndex || 0;
|
|
407
|
+
|
|
375
408
|
// Ensure cursor-agent is installed
|
|
376
409
|
ensureCursorAgent();
|
|
377
410
|
|
|
@@ -400,44 +433,113 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
400
433
|
logger.success('✓ Cursor authentication OK');
|
|
401
434
|
|
|
402
435
|
const repoRoot = git.getRepoRoot();
|
|
403
|
-
const pipelineBranch = config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
404
|
-
const worktreeDir = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
405
436
|
|
|
406
|
-
|
|
437
|
+
// Load existing state if resuming
|
|
438
|
+
const statePath = path.join(runDir, 'state.json');
|
|
439
|
+
let state: LaneState | null = null;
|
|
440
|
+
|
|
441
|
+
if (startIndex > 0 && fs.existsSync(statePath)) {
|
|
442
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
446
|
+
const worktreeDir = state?.worktreeDir || path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
447
|
+
|
|
448
|
+
if (startIndex === 0) {
|
|
449
|
+
logger.section('🚀 Starting Pipeline');
|
|
450
|
+
} else {
|
|
451
|
+
logger.section(`🔁 Resuming Pipeline from task ${startIndex + 1}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
407
454
|
logger.info(`Pipeline Branch: ${pipelineBranch}`);
|
|
408
455
|
logger.info(`Worktree: ${worktreeDir}`);
|
|
409
456
|
logger.info(`Tasks: ${config.tasks.length}`);
|
|
410
457
|
|
|
411
|
-
// Create worktree
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
458
|
+
// Create worktree only if starting fresh
|
|
459
|
+
if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
|
|
460
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
461
|
+
baseBranch: config.baseBranch || 'main',
|
|
462
|
+
cwd: repoRoot,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
416
465
|
|
|
417
466
|
// Create chat
|
|
418
467
|
logger.info('Creating chat session...');
|
|
419
468
|
const chatId = cursorAgentCreateChat();
|
|
420
469
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
470
|
+
// Initialize state if not loaded
|
|
471
|
+
if (!state) {
|
|
472
|
+
state = {
|
|
473
|
+
status: 'running',
|
|
474
|
+
pipelineBranch,
|
|
475
|
+
worktreeDir,
|
|
476
|
+
totalTasks: config.tasks.length,
|
|
477
|
+
currentTaskIndex: 0,
|
|
478
|
+
label: pipelineBranch,
|
|
479
|
+
startTime: Date.now(),
|
|
480
|
+
endTime: null,
|
|
481
|
+
error: null,
|
|
482
|
+
dependencyRequest: null,
|
|
483
|
+
tasksFile, // Store tasks file for resume
|
|
484
|
+
dependsOn: config.dependsOn || [],
|
|
485
|
+
};
|
|
486
|
+
} else {
|
|
487
|
+
state.status = 'running';
|
|
488
|
+
state.error = null;
|
|
489
|
+
state.dependencyRequest = null;
|
|
490
|
+
state.dependsOn = config.dependsOn || [];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
saveState(statePath, state);
|
|
434
494
|
|
|
435
|
-
|
|
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
|
+
}
|
|
436
538
|
|
|
437
539
|
// Run tasks
|
|
438
540
|
const results: TaskExecutionResult[] = [];
|
|
439
541
|
|
|
440
|
-
for (let i =
|
|
542
|
+
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
441
543
|
const task = config.tasks[i]!;
|
|
442
544
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
443
545
|
|
|
@@ -456,13 +558,13 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
456
558
|
|
|
457
559
|
// Update state
|
|
458
560
|
state.currentTaskIndex = i + 1;
|
|
459
|
-
saveState(
|
|
561
|
+
saveState(statePath, state);
|
|
460
562
|
|
|
461
563
|
// Handle blocked or error
|
|
462
564
|
if (result.status === 'BLOCKED_DEPENDENCY') {
|
|
463
|
-
state.status = 'failed';
|
|
565
|
+
state.status = 'failed';
|
|
464
566
|
state.dependencyRequest = result.dependencyRequest || null;
|
|
465
|
-
saveState(
|
|
567
|
+
saveState(statePath, state);
|
|
466
568
|
logger.warn('Task blocked on dependency change');
|
|
467
569
|
process.exit(2);
|
|
468
570
|
}
|
|
@@ -470,7 +572,7 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
470
572
|
if (result.status !== 'FINISHED') {
|
|
471
573
|
state.status = 'failed';
|
|
472
574
|
state.error = result.error || 'Unknown error';
|
|
473
|
-
saveState(
|
|
575
|
+
saveState(statePath, state);
|
|
474
576
|
logger.error(`Task failed: ${result.error}`);
|
|
475
577
|
process.exit(1);
|
|
476
578
|
}
|
|
@@ -484,7 +586,7 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
484
586
|
// Complete
|
|
485
587
|
state.status = 'completed';
|
|
486
588
|
state.endTime = Date.now();
|
|
487
|
-
saveState(
|
|
589
|
+
saveState(statePath, state);
|
|
488
590
|
|
|
489
591
|
logger.success('All tasks completed!');
|
|
490
592
|
return results;
|
|
@@ -503,9 +605,11 @@ if (require.main === module) {
|
|
|
503
605
|
|
|
504
606
|
const tasksFile = args[0]!;
|
|
505
607
|
const runDirIdx = args.indexOf('--run-dir');
|
|
608
|
+
const startIdxIdx = args.indexOf('--start-index');
|
|
506
609
|
// const executorIdx = args.indexOf('--executor');
|
|
507
610
|
|
|
508
611
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
612
|
+
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
509
613
|
// const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
|
|
510
614
|
|
|
511
615
|
if (!fs.existsSync(tasksFile)) {
|
|
@@ -529,7 +633,7 @@ if (require.main === module) {
|
|
|
529
633
|
};
|
|
530
634
|
|
|
531
635
|
// Run tasks
|
|
532
|
-
runTasks(config, runDir)
|
|
636
|
+
runTasks(tasksFile, config, runDir, { startIndex })
|
|
533
637
|
.then(() => {
|
|
534
638
|
process.exit(0);
|
|
535
639
|
})
|