@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +83 -2
  3. package/commands/cursorflow-clean.md +20 -6
  4. package/commands/cursorflow-prepare.md +1 -1
  5. package/commands/cursorflow-resume.md +127 -6
  6. package/commands/cursorflow-run.md +2 -2
  7. package/commands/cursorflow-signal.md +11 -4
  8. package/dist/cli/clean.js +164 -12
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +6 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/logs.d.ts +8 -0
  14. package/dist/cli/logs.js +746 -0
  15. package/dist/cli/logs.js.map +1 -0
  16. package/dist/cli/monitor.js +113 -30
  17. package/dist/cli/monitor.js.map +1 -1
  18. package/dist/cli/prepare.js +1 -1
  19. package/dist/cli/resume.js +367 -18
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +2 -0
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/signal.js +34 -20
  24. package/dist/cli/signal.js.map +1 -1
  25. package/dist/core/orchestrator.d.ts +11 -1
  26. package/dist/core/orchestrator.js +257 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.js +20 -0
  29. package/dist/core/reviewer.js.map +1 -1
  30. package/dist/core/runner.js +113 -13
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +34 -0
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/enhanced-logger.d.ts +209 -0
  35. package/dist/utils/enhanced-logger.js +963 -0
  36. package/dist/utils/enhanced-logger.js.map +1 -0
  37. package/dist/utils/events.d.ts +59 -0
  38. package/dist/utils/events.js +37 -0
  39. package/dist/utils/events.js.map +1 -0
  40. package/dist/utils/git.d.ts +5 -0
  41. package/dist/utils/git.js +25 -0
  42. package/dist/utils/git.js.map +1 -1
  43. package/dist/utils/types.d.ts +122 -1
  44. package/dist/utils/webhook.d.ts +5 -0
  45. package/dist/utils/webhook.js +109 -0
  46. package/dist/utils/webhook.js.map +1 -0
  47. package/examples/README.md +1 -1
  48. package/package.json +1 -1
  49. package/scripts/simple-logging-test.sh +97 -0
  50. package/scripts/test-real-logging.sh +289 -0
  51. package/scripts/test-streaming-multi-task.sh +247 -0
  52. package/src/cli/clean.ts +170 -13
  53. package/src/cli/index.ts +4 -1
  54. package/src/cli/logs.ts +848 -0
  55. package/src/cli/monitor.ts +123 -30
  56. package/src/cli/prepare.ts +1 -1
  57. package/src/cli/resume.ts +463 -22
  58. package/src/cli/run.ts +2 -0
  59. package/src/cli/signal.ts +43 -27
  60. package/src/core/orchestrator.ts +303 -37
  61. package/src/core/reviewer.ts +22 -0
  62. package/src/core/runner.ts +128 -12
  63. package/src/utils/config.ts +36 -0
  64. package/src/utils/enhanced-logger.ts +1097 -0
  65. package/src/utils/events.ts +117 -0
  66. package/src/utils/git.ts +25 -0
  67. package/src/utils/types.ts +150 -1
  68. package/src/utils/webhook.ts +85 -0
@@ -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: 5 minutes */
111
- const DEFAULT_TIMEOUT_MS = 300000;
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" signal file if any
260
+ // Watch for "intervention.txt" or "timeout.txt" signal files
255
261
  const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
256
- let interventionWatcher: fs.FSWatcher | null = null;
262
+ const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
263
+ let signalWatcher: fs.FSWatcher | null = null;
257
264
 
258
- if (interventionPath && fs.existsSync(path.dirname(interventionPath))) {
259
- interventionWatcher = fs.watch(path.dirname(interventionPath), (event, filename) => {
260
- if (filename === 'intervention.txt' && fs.existsSync(interventionPath)) {
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
- const timeoutHandle = setTimeout(() => {
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 (interventionWatcher) interventionWatcher.close();
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
- // const executorIdx = args.indexOf('--executor');
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
- // const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
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);
@@ -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