@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/runner.ts
CHANGED
|
@@ -12,6 +12,10 @@ import * as git from '../utils/git';
|
|
|
12
12
|
import * as logger from '../utils/logger';
|
|
13
13
|
import { ensureCursorAgent, checkCursorAuth, printAuthHelp } from '../utils/cursor-agent';
|
|
14
14
|
import { saveState, appendLog, createConversationEntry } from '../utils/state';
|
|
15
|
+
import { events } from '../utils/events';
|
|
16
|
+
import { loadConfig } from '../utils/config';
|
|
17
|
+
import { registerWebhooks } from '../utils/webhook';
|
|
18
|
+
import { stripAnsi } from '../utils/enhanced-logger';
|
|
15
19
|
import {
|
|
16
20
|
RunnerConfig,
|
|
17
21
|
Task,
|
|
@@ -107,8 +111,8 @@ function parseJsonFromStdout(stdout: string): any {
|
|
|
107
111
|
return null;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
/** Default timeout:
|
|
111
|
-
const DEFAULT_TIMEOUT_MS =
|
|
114
|
+
/** Default timeout: 10 minutes */
|
|
115
|
+
const DEFAULT_TIMEOUT_MS = 600000;
|
|
112
116
|
|
|
113
117
|
/** Heartbeat interval: 30 seconds */
|
|
114
118
|
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
@@ -181,9 +185,10 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
181
185
|
/** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
|
|
182
186
|
enableIntervention?: boolean;
|
|
183
187
|
}): Promise<AgentSendResult> {
|
|
188
|
+
// Use stream-json format for structured output with tool calls and results
|
|
184
189
|
const args = [
|
|
185
190
|
'--print',
|
|
186
|
-
'--output-format', 'json',
|
|
191
|
+
'--output-format', 'stream-json',
|
|
187
192
|
'--workspace', workspaceDir,
|
|
188
193
|
...(model ? ['--model', model] : []),
|
|
189
194
|
'--resume', chatId,
|
|
@@ -240,6 +245,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
240
245
|
|
|
241
246
|
let fullStdout = '';
|
|
242
247
|
let fullStderr = '';
|
|
248
|
+
let timeoutHandle: NodeJS.Timeout;
|
|
243
249
|
|
|
244
250
|
// Heartbeat logging to show progress
|
|
245
251
|
let lastHeartbeat = Date.now();
|
|
@@ -251,13 +257,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
251
257
|
}, HEARTBEAT_INTERVAL_MS);
|
|
252
258
|
const startTime = Date.now();
|
|
253
259
|
|
|
254
|
-
// Watch for "intervention.txt"
|
|
260
|
+
// Watch for "intervention.txt" or "timeout.txt" signal files
|
|
255
261
|
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
256
|
-
|
|
262
|
+
const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
|
|
263
|
+
let signalWatcher: fs.FSWatcher | null = null;
|
|
257
264
|
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
if (signalDir && fs.existsSync(signalDir)) {
|
|
266
|
+
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
267
|
+
// Handle intervention
|
|
268
|
+
if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
|
|
261
269
|
try {
|
|
262
270
|
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
263
271
|
if (message) {
|
|
@@ -274,6 +282,40 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
274
282
|
logger.warn('Failed to read intervention file');
|
|
275
283
|
}
|
|
276
284
|
}
|
|
285
|
+
|
|
286
|
+
// Handle dynamic timeout update
|
|
287
|
+
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
288
|
+
try {
|
|
289
|
+
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
290
|
+
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
291
|
+
|
|
292
|
+
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
293
|
+
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
294
|
+
|
|
295
|
+
// Clear old timeout
|
|
296
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
297
|
+
|
|
298
|
+
// Set new timeout based on total elapsed time
|
|
299
|
+
const elapsed = Date.now() - startTime;
|
|
300
|
+
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
301
|
+
|
|
302
|
+
timeoutHandle = setTimeout(() => {
|
|
303
|
+
clearInterval(heartbeatInterval);
|
|
304
|
+
child.kill();
|
|
305
|
+
const totalSec = Math.round(newTimeoutMs / 1000);
|
|
306
|
+
resolve({
|
|
307
|
+
ok: false,
|
|
308
|
+
exitCode: -1,
|
|
309
|
+
error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
|
|
310
|
+
});
|
|
311
|
+
}, remaining);
|
|
312
|
+
|
|
313
|
+
fs.unlinkSync(timeoutPath); // Clear it
|
|
314
|
+
}
|
|
315
|
+
} catch (e) {
|
|
316
|
+
logger.warn('Failed to read timeout update file');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
277
319
|
});
|
|
278
320
|
}
|
|
279
321
|
|
|
@@ -295,7 +337,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
295
337
|
});
|
|
296
338
|
}
|
|
297
339
|
|
|
298
|
-
|
|
340
|
+
timeoutHandle = setTimeout(() => {
|
|
299
341
|
clearInterval(heartbeatInterval);
|
|
300
342
|
child.kill();
|
|
301
343
|
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
@@ -309,7 +351,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
309
351
|
child.on('close', (code) => {
|
|
310
352
|
clearTimeout(timeoutHandle);
|
|
311
353
|
clearInterval(heartbeatInterval);
|
|
312
|
-
if (
|
|
354
|
+
if (signalWatcher) signalWatcher.close();
|
|
313
355
|
|
|
314
356
|
const json = parseJsonFromStdout(fullStdout);
|
|
315
357
|
|
|
@@ -469,6 +511,12 @@ export async function runTask({
|
|
|
469
511
|
logger.info(`Model: ${model}`);
|
|
470
512
|
logger.info(`Branch: ${taskBranch}`);
|
|
471
513
|
|
|
514
|
+
events.emit('task.started', {
|
|
515
|
+
taskName: task.name,
|
|
516
|
+
taskBranch,
|
|
517
|
+
index,
|
|
518
|
+
});
|
|
519
|
+
|
|
472
520
|
// Checkout task branch
|
|
473
521
|
git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
|
|
474
522
|
|
|
@@ -484,6 +532,13 @@ export async function runTask({
|
|
|
484
532
|
}));
|
|
485
533
|
|
|
486
534
|
logger.info('Sending prompt to agent...');
|
|
535
|
+
const startTime = Date.now();
|
|
536
|
+
events.emit('agent.prompt_sent', {
|
|
537
|
+
taskName: task.name,
|
|
538
|
+
model,
|
|
539
|
+
promptLength: prompt1.length,
|
|
540
|
+
});
|
|
541
|
+
|
|
487
542
|
const r1 = await cursorAgentSend({
|
|
488
543
|
workspaceDir: worktreeDir,
|
|
489
544
|
chatId,
|
|
@@ -494,12 +549,26 @@ export async function runTask({
|
|
|
494
549
|
enableIntervention: config.enableIntervention,
|
|
495
550
|
});
|
|
496
551
|
|
|
552
|
+
const duration = Date.now() - startTime;
|
|
553
|
+
events.emit('agent.response_received', {
|
|
554
|
+
taskName: task.name,
|
|
555
|
+
ok: r1.ok,
|
|
556
|
+
duration,
|
|
557
|
+
responseLength: r1.resultText?.length || 0,
|
|
558
|
+
error: r1.error,
|
|
559
|
+
});
|
|
560
|
+
|
|
497
561
|
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
|
|
498
562
|
task: task.name,
|
|
499
563
|
model,
|
|
500
564
|
}));
|
|
501
565
|
|
|
502
566
|
if (!r1.ok) {
|
|
567
|
+
events.emit('task.failed', {
|
|
568
|
+
taskName: task.name,
|
|
569
|
+
taskBranch,
|
|
570
|
+
error: r1.error,
|
|
571
|
+
});
|
|
503
572
|
return {
|
|
504
573
|
taskName: task.name,
|
|
505
574
|
taskBranch,
|
|
@@ -522,6 +591,12 @@ export async function runTask({
|
|
|
522
591
|
// Push task branch
|
|
523
592
|
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
524
593
|
|
|
594
|
+
events.emit('task.completed', {
|
|
595
|
+
taskName: task.name,
|
|
596
|
+
taskBranch,
|
|
597
|
+
status: 'FINISHED',
|
|
598
|
+
});
|
|
599
|
+
|
|
525
600
|
return {
|
|
526
601
|
taskName: task.name,
|
|
527
602
|
taskBranch,
|
|
@@ -667,6 +742,12 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
667
742
|
noFf: true,
|
|
668
743
|
message: `chore: merge dependency ${depName} (${depState.pipelineBranch})`
|
|
669
744
|
});
|
|
745
|
+
|
|
746
|
+
// Log changed files
|
|
747
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
748
|
+
if (stats) {
|
|
749
|
+
logger.info('Changed files:\n' + stats);
|
|
750
|
+
}
|
|
670
751
|
}
|
|
671
752
|
} catch (e) {
|
|
672
753
|
logger.error(`Failed to merge dependency ${depName}: ${e}`);
|
|
@@ -706,6 +787,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
706
787
|
state.status = 'failed';
|
|
707
788
|
state.dependencyRequest = result.dependencyRequest || null;
|
|
708
789
|
saveState(statePath, state);
|
|
790
|
+
|
|
791
|
+
if (result.dependencyRequest) {
|
|
792
|
+
events.emit('lane.dependency_requested', {
|
|
793
|
+
laneName: state.label,
|
|
794
|
+
dependencyRequest: result.dependencyRequest,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
709
798
|
logger.warn('Task blocked on dependency change');
|
|
710
799
|
process.exit(2);
|
|
711
800
|
}
|
|
@@ -721,6 +810,13 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
721
810
|
// Merge into pipeline
|
|
722
811
|
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
723
812
|
git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
|
|
813
|
+
|
|
814
|
+
// Log changed files
|
|
815
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
816
|
+
if (stats) {
|
|
817
|
+
logger.info('Changed files:\n' + stats);
|
|
818
|
+
}
|
|
819
|
+
|
|
724
820
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
725
821
|
}
|
|
726
822
|
|
|
@@ -747,11 +843,28 @@ if (require.main === module) {
|
|
|
747
843
|
const tasksFile = args[0]!;
|
|
748
844
|
const runDirIdx = args.indexOf('--run-dir');
|
|
749
845
|
const startIdxIdx = args.indexOf('--start-index');
|
|
750
|
-
|
|
846
|
+
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
751
847
|
|
|
752
848
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
753
849
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
754
|
-
|
|
850
|
+
const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
|
|
851
|
+
|
|
852
|
+
// Extract runId from runDir (format: .../runs/run-123/lanes/lane-name)
|
|
853
|
+
const parts = runDir.split(path.sep);
|
|
854
|
+
const runsIdx = parts.lastIndexOf('runs');
|
|
855
|
+
const runId = runsIdx >= 0 && parts[runsIdx + 1] ? parts[runsIdx + 1]! : `run-${Date.now()}`;
|
|
856
|
+
|
|
857
|
+
events.setRunId(runId);
|
|
858
|
+
|
|
859
|
+
// Load global config to register webhooks in this process
|
|
860
|
+
try {
|
|
861
|
+
const globalConfig = loadConfig();
|
|
862
|
+
if (globalConfig.webhooks) {
|
|
863
|
+
registerWebhooks(globalConfig.webhooks);
|
|
864
|
+
}
|
|
865
|
+
} catch (e) {
|
|
866
|
+
// Non-blocking
|
|
867
|
+
}
|
|
755
868
|
|
|
756
869
|
if (!fs.existsSync(tasksFile)) {
|
|
757
870
|
console.error(`Tasks file not found: ${tasksFile}`);
|
|
@@ -762,6 +875,9 @@ if (require.main === module) {
|
|
|
762
875
|
let config: RunnerConfig;
|
|
763
876
|
try {
|
|
764
877
|
config = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
|
|
878
|
+
if (forcedPipelineBranch) {
|
|
879
|
+
config.pipelineBranch = forcedPipelineBranch;
|
|
880
|
+
}
|
|
765
881
|
} catch (error: any) {
|
|
766
882
|
console.error(`Failed to load tasks file: ${error.message}`);
|
|
767
883
|
process.exit(1);
|
package/src/utils/config.ts
CHANGED
|
@@ -73,6 +73,21 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
|
|
|
73
73
|
worktreePrefix: 'cursorflow-',
|
|
74
74
|
maxConcurrentLanes: 10,
|
|
75
75
|
|
|
76
|
+
// Webhooks
|
|
77
|
+
webhooks: [],
|
|
78
|
+
|
|
79
|
+
// Enhanced logging
|
|
80
|
+
enhancedLogging: {
|
|
81
|
+
enabled: true,
|
|
82
|
+
stripAnsi: true,
|
|
83
|
+
addTimestamps: true,
|
|
84
|
+
maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
85
|
+
maxFiles: 5,
|
|
86
|
+
keepRawLogs: true,
|
|
87
|
+
writeJsonLog: true,
|
|
88
|
+
timestampFormat: 'iso',
|
|
89
|
+
},
|
|
90
|
+
|
|
76
91
|
// Internal
|
|
77
92
|
projectRoot,
|
|
78
93
|
};
|
|
@@ -179,6 +194,27 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
179
194
|
// Advanced
|
|
180
195
|
worktreePrefix: 'cursorflow-',
|
|
181
196
|
maxConcurrentLanes: 10,
|
|
197
|
+
|
|
198
|
+
// Webhook configuration
|
|
199
|
+
// webhooks: [
|
|
200
|
+
// {
|
|
201
|
+
// enabled: true,
|
|
202
|
+
// url: 'https://api.example.com/events',
|
|
203
|
+
// events: ['*'],
|
|
204
|
+
// }
|
|
205
|
+
// ],
|
|
206
|
+
|
|
207
|
+
// Enhanced logging configuration
|
|
208
|
+
enhancedLogging: {
|
|
209
|
+
enabled: true, // Enable enhanced logging features
|
|
210
|
+
stripAnsi: true, // Strip ANSI codes for clean logs
|
|
211
|
+
addTimestamps: true, // Add timestamps to each line
|
|
212
|
+
maxFileSize: 52428800, // 50MB max file size before rotation
|
|
213
|
+
maxFiles: 5, // Number of rotated files to keep
|
|
214
|
+
keepRawLogs: true, // Keep raw logs with ANSI codes
|
|
215
|
+
writeJsonLog: true, // Write structured JSON logs
|
|
216
|
+
timestampFormat: 'iso', // 'iso' | 'relative' | 'short'
|
|
217
|
+
},
|
|
182
218
|
};
|
|
183
219
|
`;
|
|
184
220
|
|