@litmers/cursorflow-orchestrator 0.1.37 → 0.1.40

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 (102) hide show
  1. package/README.md +13 -13
  2. package/commands/cursorflow-init.md +113 -32
  3. package/commands/cursorflow-prepare.md +146 -339
  4. package/commands/cursorflow-run.md +148 -131
  5. package/dist/cli/add.js +8 -4
  6. package/dist/cli/add.js.map +1 -1
  7. package/dist/cli/index.js +2 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/new.js +3 -5
  10. package/dist/cli/new.js.map +1 -1
  11. package/dist/cli/prepare.js +0 -1
  12. package/dist/cli/prepare.js.map +1 -1
  13. package/dist/cli/resume.js +24 -15
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +1 -6
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/setup-commands.d.ts +1 -0
  18. package/dist/cli/setup-commands.js +1 -0
  19. package/dist/cli/setup-commands.js.map +1 -1
  20. package/dist/core/orchestrator.js +13 -5
  21. package/dist/core/orchestrator.js.map +1 -1
  22. package/dist/core/runner/agent.d.ts +5 -1
  23. package/dist/core/runner/agent.js +31 -1
  24. package/dist/core/runner/agent.js.map +1 -1
  25. package/dist/core/runner/pipeline.d.ts +0 -1
  26. package/dist/core/runner/pipeline.js +136 -173
  27. package/dist/core/runner/pipeline.js.map +1 -1
  28. package/dist/core/runner/prompt.d.ts +0 -1
  29. package/dist/core/runner/prompt.js +11 -16
  30. package/dist/core/runner/prompt.js.map +1 -1
  31. package/dist/core/runner/task.d.ts +1 -2
  32. package/dist/core/runner/task.js +31 -40
  33. package/dist/core/runner/task.js.map +1 -1
  34. package/dist/core/runner.js +15 -2
  35. package/dist/core/runner.js.map +1 -1
  36. package/dist/core/stall-detection.d.ts +32 -4
  37. package/dist/core/stall-detection.js +151 -149
  38. package/dist/core/stall-detection.js.map +1 -1
  39. package/dist/services/logging/console.d.ts +7 -1
  40. package/dist/services/logging/console.js +13 -3
  41. package/dist/services/logging/console.js.map +1 -1
  42. package/dist/services/logging/formatter.d.ts +1 -0
  43. package/dist/services/logging/formatter.js +6 -3
  44. package/dist/services/logging/formatter.js.map +1 -1
  45. package/dist/types/config.d.ts +3 -1
  46. package/dist/types/logging.d.ts +1 -1
  47. package/dist/types/task.d.ts +3 -8
  48. package/dist/utils/config.js +5 -0
  49. package/dist/utils/config.js.map +1 -1
  50. package/dist/utils/doctor.js +4 -4
  51. package/dist/utils/doctor.js.map +1 -1
  52. package/dist/utils/enhanced-logger.d.ts +1 -1
  53. package/dist/utils/enhanced-logger.js +3 -3
  54. package/dist/utils/enhanced-logger.js.map +1 -1
  55. package/dist/utils/git.d.ts +12 -1
  56. package/dist/utils/git.js +56 -1
  57. package/dist/utils/git.js.map +1 -1
  58. package/dist/utils/health.js +13 -13
  59. package/dist/utils/health.js.map +1 -1
  60. package/dist/utils/log-formatter.d.ts +1 -1
  61. package/dist/utils/log-formatter.js +45 -8
  62. package/dist/utils/log-formatter.js.map +1 -1
  63. package/dist/utils/logger.js +2 -2
  64. package/dist/utils/logger.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/cli/add.ts +9 -4
  67. package/src/cli/index.ts +3 -0
  68. package/src/cli/new.ts +3 -5
  69. package/src/cli/prepare.ts +0 -1
  70. package/src/cli/resume.ts +28 -19
  71. package/src/cli/run.ts +1 -6
  72. package/src/cli/setup-commands.ts +1 -1
  73. package/src/core/orchestrator.ts +14 -5
  74. package/src/core/runner/agent.ts +36 -4
  75. package/src/core/runner/pipeline.ts +149 -182
  76. package/src/core/runner/prompt.ts +11 -18
  77. package/src/core/runner/task.ts +32 -41
  78. package/src/core/runner.ts +17 -2
  79. package/src/core/stall-detection.ts +263 -147
  80. package/src/services/logging/console.ts +13 -3
  81. package/src/services/logging/formatter.ts +6 -3
  82. package/src/types/config.ts +3 -1
  83. package/src/types/logging.ts +4 -2
  84. package/src/types/task.ts +3 -8
  85. package/src/utils/config.ts +6 -0
  86. package/src/utils/doctor.ts +5 -5
  87. package/src/utils/enhanced-logger.ts +3 -3
  88. package/src/utils/flow.ts +1 -0
  89. package/src/utils/git.ts +61 -1
  90. package/src/utils/health.ts +15 -15
  91. package/src/utils/log-formatter.ts +51 -8
  92. package/src/utils/logger.ts +2 -2
  93. package/commands/cursorflow-add.md +0 -159
  94. package/commands/cursorflow-clean.md +0 -84
  95. package/commands/cursorflow-doctor.md +0 -102
  96. package/commands/cursorflow-models.md +0 -51
  97. package/commands/cursorflow-monitor.md +0 -90
  98. package/commands/cursorflow-new.md +0 -87
  99. package/commands/cursorflow-resume.md +0 -205
  100. package/commands/cursorflow-signal.md +0 -52
  101. package/commands/cursorflow-stop.md +0 -55
  102. package/commands/cursorflow-triggers.md +0 -250
package/src/cli/resume.ts CHANGED
@@ -29,7 +29,6 @@ interface ResumeOptions {
29
29
  status: boolean;
30
30
  maxConcurrent: number;
31
31
  help: boolean;
32
- noGit: boolean;
33
32
  executor: string | null;
34
33
  }
35
34
 
@@ -48,7 +47,6 @@ Options:
48
47
  --clean Clean up existing worktree before resuming
49
48
  --restart Restart from the first task (index 0)
50
49
  --skip-doctor Skip environment/branch checks (not recommended)
51
- --no-git Disable Git operations (must match original run)
52
50
  --executor <type> Override executor (default: cursor-agent)
53
51
  --help, -h Show help
54
52
 
@@ -56,7 +54,7 @@ Examples:
56
54
  cursorflow resume --status # Check status of all lanes
57
55
  cursorflow resume --all # Resume all incomplete lanes
58
56
  cursorflow resume lane-1 # Resume single lane
59
- cursorflow resume _cursorflow/tasks/feat1 # Resume all lanes in directory
57
+ cursorflow resume _cursorflow/flows/MyFeature # Resume all lanes in flow
60
58
  cursorflow resume --all --restart # Restart all incomplete lanes from task 0
61
59
  `);
62
60
  }
@@ -76,7 +74,6 @@ function parseArgs(args: string[]): ResumeOptions {
76
74
  status: args.includes('--status'),
77
75
  maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '3') : 3,
78
76
  help: args.includes('--help') || args.includes('-h'),
79
- noGit: args.includes('--no-git'),
80
77
  executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
81
78
  };
82
79
  }
@@ -397,10 +394,10 @@ function spawnLaneResume(
397
394
  state: LaneState,
398
395
  options: {
399
396
  restart: boolean;
400
- noGit?: boolean;
401
397
  pipelineBranch?: string;
402
398
  executor?: string | null;
403
399
  enhancedLogConfig?: any;
400
+ laneIndex?: number;
404
401
  }
405
402
  ): { child: ChildProcess; logManager: EnhancedLogManager } {
406
403
  const runnerPath = require.resolve('../core/runner');
@@ -417,10 +414,6 @@ function spawnLaneResume(
417
414
  runnerArgs.push('--worktree-dir', state.worktreeDir);
418
415
  }
419
416
 
420
- if (options.noGit) {
421
- runnerArgs.push('--no-git');
422
- }
423
-
424
417
  // Explicitly pass pipeline branch if available (either from state or override)
425
418
  const branch = options.pipelineBranch || state.pipelineBranch;
426
419
  if (branch) {
@@ -432,16 +425,26 @@ function spawnLaneResume(
432
425
  runnerArgs.push('--executor', options.executor);
433
426
  }
434
427
 
435
- const shortLaneName = laneName.substring(0, 10);
428
+ // Generate lane label: [laneIdx-taskIdx-laneName] format, padded to 18 chars
429
+ // Note: taskName will be updated dynamically via logManager.setTask()
430
+ let currentTaskName = '';
431
+ let currentTaskIdx = (state.currentTaskIndex ?? 0) + 1;
432
+ const getLaneLabel = () => {
433
+ const laneIdx = options.laneIndex ?? 1;
434
+ const combined = `${laneIdx}-${currentTaskIdx}-${laneName}`;
435
+ const label = combined.substring(0, 18).padEnd(18);
436
+ return `[${label}]`;
437
+ };
438
+
436
439
  const logManager = createLogManager(laneDir, laneName, options.enhancedLogConfig || {}, (msg) => {
437
440
  const formatted = formatMessageForConsole(msg, {
438
- laneLabel: `[${shortLaneName}]`,
441
+ laneLabel: getLaneLabel(),
439
442
  includeTimestamp: true
440
443
  });
441
444
  process.stdout.write(formatted + '\n');
442
- });
445
+ }, options.laneIndex);
443
446
 
444
- const child = spawn('node', runnerArgs, {
447
+ const child = spawn(process.execPath, runnerArgs, {
445
448
  stdio: ['ignore', 'pipe', 'pipe'],
446
449
  env: process.env,
447
450
  });
@@ -489,7 +492,6 @@ async function resumeLanes(
489
492
  restart: boolean;
490
493
  maxConcurrent: number;
491
494
  skipDoctor: boolean;
492
- noGit: boolean;
493
495
  executor: string | null;
494
496
  enhancedLogConfig?: any;
495
497
  }
@@ -547,6 +549,8 @@ async function resumeLanes(
547
549
  const pending = new Set<string>(resolvableLanes.map(l => l.name));
548
550
  const active: Map<string, ChildProcess> = new Map();
549
551
  const laneMap = new Map<string, LaneInfo>(resolvableLanes.map(l => [l.name, l]));
552
+ // Track lane indices for consistent logging
553
+ const laneIndexMap = new Map<string, number>(allLanes.map((l, i) => [l.name, i + 1]));
550
554
 
551
555
  const findReadyLane = (): LaneInfo | null => {
552
556
  for (const laneName of pending) {
@@ -558,7 +562,7 @@ async function resumeLanes(
558
562
  return null;
559
563
  };
560
564
 
561
- const processNext = (): void => {
565
+ const processNext = async (): Promise<void> => {
562
566
  while (active.size < options.maxConcurrent) {
563
567
  const lane = findReadyLane();
564
568
  if (!lane) {
@@ -572,13 +576,19 @@ async function resumeLanes(
572
576
  }
573
577
 
574
578
  pending.delete(lane.name);
579
+
580
+ // Add a small delay between starting lanes to reduce initial resource contention
581
+ if (active.size > 0) {
582
+ await new Promise(resolve => setTimeout(resolve, 500));
583
+ }
584
+
575
585
  logger.info(`Starting: ${lane.name} (task ${lane.state!.currentTaskIndex}/${lane.state!.totalTasks})`);
576
586
 
577
587
  const { child } = spawnLaneResume(lane.name, lane.dir, lane.state!, {
578
588
  restart: options.restart,
579
- noGit: options.noGit,
580
589
  executor: options.executor,
581
590
  enhancedLogConfig: options.enhancedLogConfig,
591
+ laneIndex: laneIndexMap.get(lane.name) ?? 1,
582
592
  });
583
593
 
584
594
  active.set(lane.name, child);
@@ -606,12 +616,12 @@ async function resumeLanes(
606
616
  }
607
617
  };
608
618
 
609
- processNext();
619
+ await processNext();
610
620
 
611
621
  while (active.size > 0 || pending.size > 0) {
612
622
  await new Promise(resolve => setTimeout(resolve, 1000));
613
623
  if (active.size < options.maxConcurrent && pending.size > 0) {
614
- processNext();
624
+ await processNext();
615
625
  }
616
626
  }
617
627
 
@@ -725,7 +735,6 @@ async function resume(args: string[]): Promise<void> {
725
735
  restart: options.restart,
726
736
  maxConcurrent: options.maxConcurrent,
727
737
  skipDoctor: options.skipDoctor,
728
- noGit: options.noGit,
729
738
  executor: options.executor,
730
739
  enhancedLogConfig: config.enhancedLogging,
731
740
  });
package/src/cli/run.ts CHANGED
@@ -118,7 +118,6 @@ interface RunOptions {
118
118
  maxConcurrent: number | null;
119
119
  skipDoctor: boolean;
120
120
  skipPreflight: boolean;
121
- noGit: boolean;
122
121
  raw: boolean;
123
122
  help: boolean;
124
123
  }
@@ -138,14 +137,13 @@ Options:
138
137
  --executor <type> cursor-agent | cloud
139
138
  --skip-doctor Skip environment checks (not recommended)
140
139
  --skip-preflight Skip preflight checks (Git remote, etc.)
141
- --no-git Disable Git operations (worktree, push, commit)
142
140
  --raw Save raw logs (absolute raw, no processing)
143
141
  --dry-run Show execution plan without starting agents
144
142
  --help, -h Show help
145
143
 
146
144
  Examples:
147
145
  cursorflow run _cursorflow/tasks
148
- cursorflow run _cursorflow/tasks --no-git --skip-doctor
146
+ cursorflow run _cursorflow/flows/MyFeature
149
147
  `);
150
148
  }
151
149
 
@@ -161,7 +159,6 @@ function parseArgs(args: string[]): RunOptions {
161
159
  maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '0') || null : null,
162
160
  skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
163
161
  skipPreflight: args.includes('--skip-preflight'),
164
- noGit: args.includes('--no-git'),
165
162
  raw: args.includes('--raw'),
166
163
  help: args.includes('--help') || args.includes('-h'),
167
164
  };
@@ -260,7 +257,6 @@ async function run(args: string[]): Promise<void> {
260
257
 
261
258
  if (options.skipDoctor) resumeArgs.push('--skip-doctor');
262
259
  if (options.skipPreflight) resumeArgs.push('--skip-preflight');
263
- if (options.noGit) resumeArgs.push('--no-git');
264
260
  if (options.executor) {
265
261
  resumeArgs.push('--executor', options.executor);
266
262
  }
@@ -321,7 +317,6 @@ async function run(args: string[]): Promise<void> {
321
317
  ...config.enhancedLogging,
322
318
  ...(options.raw ? { raw: true } : {}),
323
319
  },
324
- noGit: options.noGit,
325
320
  skipPreflight: options.skipPreflight,
326
321
  });
327
322
  } catch (error: any) {
@@ -202,7 +202,7 @@ export function areCommandsInstalled(): boolean {
202
202
  return sourceFiles.every(f => targetFiles.includes(f));
203
203
  }
204
204
 
205
- async function main(args: string[]): Promise<any> {
205
+ export async function main(args: string[]): Promise<any> {
206
206
  const options = parseArgs(args);
207
207
 
208
208
  try {
@@ -278,12 +278,13 @@ export function spawnLane({
278
278
  };
279
279
 
280
280
  if (logConfig.enabled) {
281
- // Helper to get dynamic lane label like [L1-T1-lanename10]
281
+ // Helper to get dynamic lane label like [1-1-lanename10]
282
282
  const getDynamicLabel = () => {
283
- const laneNum = `L${laneIndex + 1}`;
284
- const taskPart = info.currentTaskIndex ? `-T${info.currentTaskIndex}` : '';
283
+ const laneNum = `${laneIndex + 1}`;
284
+ const taskPart = `-${info.currentTaskIndex || 1}`;
285
285
  const shortLaneName = laneName.substring(0, 10);
286
- return `[${laneNum}${taskPart}-${shortLaneName}]`;
286
+ const combined = `${laneNum}${taskPart}-${shortLaneName}`.substring(0, 18).padEnd(18);
287
+ return `[${combined}]`;
287
288
  };
288
289
 
289
290
  // Create callback for clean console output
@@ -838,8 +839,10 @@ export async function orchestrate(tasksDir: string, options: {
838
839
  const now = Date.now();
839
840
 
840
841
  // Register lane with unified stall detection service FIRST
842
+ // Pass intervention capability so stall service knows if continue signals will work
841
843
  stallService.registerLane(lane.name, {
842
844
  laneRunDir: laneRunDirs[lane.name]!,
845
+ interventionEnabled: config.enableIntervention ?? true,
843
846
  });
844
847
 
845
848
  const laneIdx = lanes.findIndex(l => l.name === lane.name);
@@ -931,13 +934,19 @@ export async function orchestrate(tasksDir: string, options: {
931
934
  for (const [laneName, info] of running.entries()) {
932
935
  const lane = lanes.find(l => l.name === laneName)!;
933
936
 
934
- // Check state file for progress updates
937
+ // Check state file for progress updates and sync lane status
935
938
  try {
936
939
  const stateStat = fs.statSync(info.statePath);
937
940
  const stallState = stallService.getState(laneName);
938
941
  if (stallState && stateStat.mtimeMs > stallState.lastStateUpdateTime) {
939
942
  stallService.recordStateUpdate(laneName);
940
943
  }
944
+
945
+ // Sync lane status to stall service (skips stall detection when 'waiting' for dependencies)
946
+ const laneState = loadState<LaneState>(info.statePath);
947
+ if (laneState && stallState && laneState.status !== stallState.laneStatus) {
948
+ stallService.setLaneStatus(laneName, laneState.status || 'running');
949
+ }
941
950
  } catch {
942
951
  // State file might not exist yet
943
952
  }
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from 'child_process';
1
+ import { spawn, spawnSync, ChildProcess } from 'child_process';
2
2
  import * as logger from '../../utils/logger';
3
3
  import { AgentSendResult, DependencyRequestPlan } from '../../types';
4
4
  import { withRetry } from '../failure-policy';
@@ -6,6 +6,30 @@ import * as path from 'path';
6
6
  import * as fs from 'fs';
7
7
  import { appendLog, createConversationEntry } from '../../utils/state';
8
8
 
9
+ /**
10
+ * Track active child processes for cleanup
11
+ */
12
+ const activeChildren = new Set<ChildProcess>();
13
+
14
+ /**
15
+ * Cleanup all active children
16
+ */
17
+ export function cleanupAgentChildren(): void {
18
+ if (activeChildren.size > 0) {
19
+ logger.warn(`Cleaning up ${activeChildren.size} active agent child processes...`);
20
+ for (const child of activeChildren) {
21
+ if (!child.killed) {
22
+ try {
23
+ child.kill('SIGKILL');
24
+ } catch {
25
+ // Ignore
26
+ }
27
+ }
28
+ }
29
+ activeChildren.clear();
30
+ }
31
+ }
32
+
9
33
  /**
10
34
  * Execute cursor-agent command with timeout and better error handling
11
35
  */
@@ -115,7 +139,7 @@ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalD
115
139
  signalDir?: string;
116
140
  timeout?: number;
117
141
  enableIntervention?: boolean;
118
- outputFormat?: 'json' | 'plain';
142
+ outputFormat?: 'json' | 'plain' | 'stream-json';
119
143
  taskName?: string;
120
144
  }): Promise<AgentSendResult> {
121
145
  const timeoutMs = timeout || 10 * 60 * 1000; // 10 minutes default
@@ -125,7 +149,7 @@ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalD
125
149
  args.push('--model', model);
126
150
  }
127
151
 
128
- if (outputFormat === 'json') {
152
+ if (outputFormat === 'json' || outputFormat === 'stream-json') {
129
153
  args.push('--print', '--output-format', 'json');
130
154
  }
131
155
 
@@ -145,6 +169,12 @@ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalD
145
169
  stdio: enableIntervention ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
146
170
  });
147
171
 
172
+ activeChildren.add(child);
173
+
174
+ if (!enableIntervention) {
175
+ logger.info('ℹ️ Intervention is disabled. Stall recovery using "continue" will not be available for this agent session.');
176
+ }
177
+
148
178
  let fullStdout = '';
149
179
  let fullStderr = '';
150
180
  let timeoutHandle: NodeJS.Timeout;
@@ -235,6 +265,7 @@ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalD
235
265
  }, timeoutMs);
236
266
 
237
267
  child.on('close', (code) => {
268
+ activeChildren.delete(child);
238
269
  clearTimeout(timeoutHandle);
239
270
  clearInterval(heartbeatInterval);
240
271
  if (signalWatcher) signalWatcher.close();
@@ -255,6 +286,7 @@ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalD
255
286
  });
256
287
 
257
288
  child.on('error', (err) => {
289
+ activeChildren.delete(child);
258
290
  clearTimeout(timeoutHandle);
259
291
  if (signalWatcher) signalWatcher.close();
260
292
  resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
@@ -273,7 +305,7 @@ export async function cursorAgentSend(options: {
273
305
  signalDir?: string;
274
306
  timeout?: number;
275
307
  enableIntervention?: boolean;
276
- outputFormat?: 'json' | 'plain';
308
+ outputFormat?: 'json' | 'plain' | 'stream-json';
277
309
  taskName?: string;
278
310
  }): Promise<AgentSendResult> {
279
311
  const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';