@litmers/cursorflow-orchestrator 0.1.15 → 0.1.20

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 (90) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +26 -7
  3. package/commands/cursorflow-run.md +2 -0
  4. package/commands/cursorflow-triggers.md +250 -0
  5. package/dist/cli/clean.js +8 -7
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +5 -1
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +20 -14
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +64 -47
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.js +27 -17
  14. package/dist/cli/monitor.js.map +1 -1
  15. package/dist/cli/prepare.js +73 -33
  16. package/dist/cli/prepare.js.map +1 -1
  17. package/dist/cli/resume.js +193 -40
  18. package/dist/cli/resume.js.map +1 -1
  19. package/dist/cli/run.js +3 -2
  20. package/dist/cli/run.js.map +1 -1
  21. package/dist/cli/signal.js +7 -7
  22. package/dist/cli/signal.js.map +1 -1
  23. package/dist/core/orchestrator.d.ts +2 -1
  24. package/dist/core/orchestrator.js +54 -93
  25. package/dist/core/orchestrator.js.map +1 -1
  26. package/dist/core/reviewer.d.ts +6 -4
  27. package/dist/core/reviewer.js +7 -5
  28. package/dist/core/reviewer.js.map +1 -1
  29. package/dist/core/runner.d.ts +8 -0
  30. package/dist/core/runner.js +219 -32
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +20 -10
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/doctor.js +35 -7
  35. package/dist/utils/doctor.js.map +1 -1
  36. package/dist/utils/enhanced-logger.d.ts +2 -2
  37. package/dist/utils/enhanced-logger.js +114 -43
  38. package/dist/utils/enhanced-logger.js.map +1 -1
  39. package/dist/utils/git.js +163 -10
  40. package/dist/utils/git.js.map +1 -1
  41. package/dist/utils/log-formatter.d.ts +16 -0
  42. package/dist/utils/log-formatter.js +194 -0
  43. package/dist/utils/log-formatter.js.map +1 -0
  44. package/dist/utils/path.d.ts +19 -0
  45. package/dist/utils/path.js +77 -0
  46. package/dist/utils/path.js.map +1 -0
  47. package/dist/utils/repro-thinking-logs.d.ts +1 -0
  48. package/dist/utils/repro-thinking-logs.js +80 -0
  49. package/dist/utils/repro-thinking-logs.js.map +1 -0
  50. package/dist/utils/state.d.ts +4 -1
  51. package/dist/utils/state.js +11 -8
  52. package/dist/utils/state.js.map +1 -1
  53. package/dist/utils/template.d.ts +14 -0
  54. package/dist/utils/template.js +122 -0
  55. package/dist/utils/template.js.map +1 -0
  56. package/dist/utils/types.d.ts +13 -0
  57. package/dist/utils/webhook.js +3 -0
  58. package/dist/utils/webhook.js.map +1 -1
  59. package/package.json +4 -2
  60. package/scripts/ai-security-check.js +3 -0
  61. package/scripts/local-security-gate.sh +9 -1
  62. package/scripts/verify-and-fix.sh +37 -0
  63. package/src/cli/clean.ts +8 -7
  64. package/src/cli/index.ts +5 -1
  65. package/src/cli/init.ts +19 -15
  66. package/src/cli/logs.ts +67 -47
  67. package/src/cli/monitor.ts +28 -18
  68. package/src/cli/prepare.ts +75 -35
  69. package/src/cli/resume.ts +810 -626
  70. package/src/cli/run.ts +3 -2
  71. package/src/cli/signal.ts +7 -6
  72. package/src/core/orchestrator.ts +68 -93
  73. package/src/core/reviewer.ts +14 -9
  74. package/src/core/runner.ts +229 -33
  75. package/src/utils/config.ts +19 -11
  76. package/src/utils/doctor.ts +38 -7
  77. package/src/utils/enhanced-logger.ts +117 -49
  78. package/src/utils/git.ts +145 -11
  79. package/src/utils/log-formatter.ts +162 -0
  80. package/src/utils/path.ts +45 -0
  81. package/src/utils/repro-thinking-logs.ts +54 -0
  82. package/src/utils/state.ts +16 -8
  83. package/src/utils/template.ts +92 -0
  84. package/src/utils/types.ts +13 -0
  85. package/src/utils/webhook.ts +3 -0
  86. package/templates/basic.json +21 -0
  87. package/scripts/simple-logging-test.sh +0 -97
  88. package/scripts/test-real-cursor-lifecycle.sh +0 -289
  89. package/scripts/test-real-logging.sh +0 -289
  90. package/scripts/test-streaming-multi-task.sh +0 -247
@@ -9,6 +9,8 @@ import * as path from 'path';
9
9
  import * as logger from '../utils/logger';
10
10
  import { loadConfig, getTasksDir } from '../utils/config';
11
11
  import { Task, RunnerConfig } from '../utils/types';
12
+ import { safeJoin } from '../utils/path';
13
+ import { resolveTemplate } from '../utils/template';
12
14
 
13
15
  // Preset template types
14
16
  type PresetType = 'complex' | 'simple' | 'merge';
@@ -97,20 +99,24 @@ Prepare task files for a new feature - Terminal-first workflow.
97
99
  --prompt <text> Task prompt (uses preset or single task)
98
100
  --criteria <list> Comma-separated acceptance criteria
99
101
  --model <model> Model to use (default: sonnet-4.5)
100
- --task <spec> Full task spec: "name|model|prompt|criteria" (repeatable)
102
+ --task <spec> Full task spec: "name|model|prompt|criteria|dependsOn|timeout" (repeatable)
101
103
 
102
104
  Dependencies:
103
105
  --sequential Chain lanes: 1 → 2 → 3
104
106
  --deps <spec> Custom dependencies: "2:1;3:1,2"
105
107
  --depends-on <lanes> Dependencies for --add-lane: "01-lane-1,02-lane-2"
108
+ Task-level deps: In --task, add "lane:task" at the end.
109
+ Example: "test|sonnet-4.5|Run tests|All pass|01-lane-1:setup"
110
+ Task-level timeout: In --task, add milliseconds at the end.
111
+ Example: "heavy|sonnet-4.5|Big task|Done||1200000"
106
112
 
107
113
  Incremental (add to existing):
108
114
  --add-lane <dir> Add a new lane to existing task directory
109
115
  --add-task <file> Append task(s) to existing lane JSON file
110
116
 
111
117
  Advanced:
112
- --template <path> Custom template JSON file
113
- --force Overwrite existing files
118
+ --template <path|url|name> External template JSON file, URL, or built-in name
119
+ --force Overwrite existing files
114
120
 
115
121
  ═══════════════════════════════════════════════════════════════════════════════
116
122
 
@@ -203,23 +209,31 @@ function parseArgs(args: string[]): PrepareOptions {
203
209
  }
204
210
 
205
211
  function parseTaskSpec(spec: string): Task {
206
- // Format: "name|model|prompt|criteria1,criteria2"
212
+ // Format: "name|model|prompt|criteria1,criteria2|lane:task1,lane:task2|timeoutMs"
207
213
  const parts = spec.split('|');
208
214
 
209
215
  if (parts.length < 3) {
210
- throw new Error(`Invalid task spec: "${spec}". Expected format: "name|model|prompt[|criteria1,criteria2]"`);
216
+ throw new Error(`Invalid task spec: "${spec}". Expected format: "name|model|prompt[|criteria[|dependsOn[|timeout]]]"`);
211
217
  }
212
218
 
213
- const [name, model, prompt, criteriaStr] = parts;
219
+ const [name, model, prompt, criteriaStr, depsStr, timeoutStr] = parts;
214
220
  const acceptanceCriteria = criteriaStr
215
221
  ? criteriaStr.split(',').map(c => c.trim()).filter(c => c)
216
222
  : undefined;
217
223
 
224
+ const dependsOn = depsStr
225
+ ? depsStr.split(',').map(d => d.trim()).filter(d => d)
226
+ : undefined;
227
+
228
+ const timeout = timeoutStr ? parseInt(timeoutStr) : undefined;
229
+
218
230
  return {
219
231
  name: name.trim(),
220
232
  model: model.trim() || 'sonnet-4.5',
221
233
  prompt: prompt.trim(),
222
234
  ...(acceptanceCriteria && acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {}),
235
+ ...(dependsOn && dependsOn.length > 0 ? { dependsOn } : {}),
236
+ ...(timeout ? { timeout } : {}),
223
237
  };
224
238
  }
225
239
 
@@ -600,7 +614,7 @@ function getFeatureNameFromDir(taskDir: string): string {
600
614
  }
601
615
 
602
616
  async function addLaneToDir(options: PrepareOptions): Promise<void> {
603
- const taskDir = path.resolve(process.cwd(), options.addLane!);
617
+ const taskDir = path.resolve(process.cwd(), options.addLane!); // nosemgrep
604
618
 
605
619
  if (!fs.existsSync(taskDir)) {
606
620
  throw new Error(`Task directory not found: ${taskDir}`);
@@ -610,29 +624,56 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
610
624
  const laneNumber = getNextLaneNumber(taskDir);
611
625
  const laneName = `lane-${laneNumber}`;
612
626
  const fileName = `${laneNumber.toString().padStart(2, '0')}-${laneName}.json`;
613
- const filePath = path.join(taskDir, fileName);
614
-
615
- if (fs.existsSync(filePath) && !options.force) {
616
- throw new Error(`Lane file already exists: ${filePath}. Use --force to overwrite.`);
617
- }
627
+ const filePath = safeJoin(taskDir, fileName);
618
628
 
619
629
  const hasDependencies = options.dependsOnLanes.length > 0;
620
630
 
621
- // Build tasks from options (auto-detects merge preset if has dependencies)
622
- const tasks = buildTasksFromOptions(options, laneNumber, featureName, hasDependencies);
623
- const config = getDefaultConfig(laneNumber, featureName, tasks);
631
+ // Load template if provided
632
+ let template = null;
633
+ if (options.template) {
634
+ template = await resolveTemplate(options.template);
635
+ }
636
+
637
+ let taskConfig;
638
+ let effectivePreset: EffectivePresetType = options.preset || (hasDependencies ? 'merge' : 'complex');
639
+
640
+ if (template) {
641
+ taskConfig = { ...template, laneNumber, devPort: 3000 + laneNumber };
642
+ effectivePreset = 'custom';
643
+ } else {
644
+ // Build tasks from options (auto-detects merge preset if has dependencies)
645
+ const tasks = buildTasksFromOptions(options, laneNumber, featureName, hasDependencies);
646
+ taskConfig = getDefaultConfig(laneNumber, featureName, tasks);
647
+ }
648
+
649
+ // Replace placeholders
650
+ const processedConfig = replacePlaceholders(taskConfig, {
651
+ featureName,
652
+ laneNumber,
653
+ devPort: 3000 + laneNumber,
654
+ });
624
655
 
625
656
  // Add dependencies if specified
626
657
  const finalConfig = {
627
- ...config,
658
+ ...processedConfig,
628
659
  ...(hasDependencies ? { dependsOn: options.dependsOnLanes } : {}),
629
660
  };
630
661
 
631
- fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', 'utf8');
662
+ // Use atomic write with wx flag to avoid TOCTOU race condition (unless force is set)
663
+ try {
664
+ const writeFlag = options.force ? 'w' : 'wx';
665
+ fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', { encoding: 'utf8', flag: writeFlag });
666
+ } catch (err: any) {
667
+ if (err.code === 'EEXIST') {
668
+ throw new Error(`Lane file already exists: ${filePath}. Use --force to overwrite.`);
669
+ }
670
+ throw err;
671
+ }
632
672
 
633
- const taskSummary = tasks.map(t => t.name).join(' → ');
673
+ const tasksList = finalConfig.tasks || [];
674
+ const taskSummary = tasksList.map((t: any) => t.name).join(' → ');
634
675
  const depsInfo = hasDependencies ? ` (depends: ${options.dependsOnLanes.join(', ')})` : '';
635
- const presetInfo = options.preset ? ` [${options.preset}]` : (hasDependencies ? ' [merge]' : '');
676
+ const presetInfo = options.preset ? ` [${options.preset}]` : (hasDependencies ? ' [merge]' : (template ? ' [template]' : ''));
636
677
 
637
678
  logger.success(`Added lane: ${fileName} [${taskSummary}]${presetInfo}${depsInfo}`);
638
679
  logger.info(`Directory: ${taskDir}`);
@@ -643,18 +684,22 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
643
684
  }
644
685
 
645
686
  async function addTaskToLane(options: PrepareOptions): Promise<void> {
646
- const laneFile = path.resolve(process.cwd(), options.addTask!);
647
-
648
- if (!fs.existsSync(laneFile)) {
649
- throw new Error(`Lane file not found: ${laneFile}`);
650
- }
687
+ const laneFile = path.resolve(process.cwd(), options.addTask!); // nosemgrep
651
688
 
652
689
  if (options.taskSpecs.length === 0) {
653
690
  throw new Error('No task specified. Use --task "name|model|prompt|criteria" to define a task.');
654
691
  }
655
692
 
656
- // Read existing config
657
- const existingConfig = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
693
+ // Read existing config - let the error propagate if file doesn't exist (avoids TOCTOU)
694
+ let existingConfig: any;
695
+ try {
696
+ existingConfig = JSON.parse(fs.readFileSync(laneFile, 'utf8'));
697
+ } catch (err: any) {
698
+ if (err.code === 'ENOENT') {
699
+ throw new Error(`Lane file not found: ${laneFile}`);
700
+ }
701
+ throw err;
702
+ }
658
703
 
659
704
  if (!existingConfig.tasks || !Array.isArray(existingConfig.tasks)) {
660
705
  existingConfig.tasks = [];
@@ -687,7 +732,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
687
732
  const now = new Date();
688
733
  const timestamp = now.toISOString().replace(/[-T:]/g, '').substring(2, 12);
689
734
  const taskDirName = `${timestamp}_${options.featureName}`;
690
- const taskDir = path.join(tasksBaseDir, taskDirName);
735
+ const taskDir = safeJoin(tasksBaseDir, taskDirName);
691
736
 
692
737
  if (fs.existsSync(taskDir) && !options.force) {
693
738
  throw new Error(`Task directory already exists: ${taskDir}. Use --force to overwrite.`);
@@ -702,12 +747,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
702
747
  // Load template if provided (overrides --prompt/--task/--preset)
703
748
  let template = null;
704
749
  if (options.template) {
705
- const templatePath = path.resolve(process.cwd(), options.template);
706
- if (!fs.existsSync(templatePath)) {
707
- throw new Error(`Template file not found: ${templatePath}`);
708
- }
709
- template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
710
- logger.info(`Using template: ${options.template}`);
750
+ template = await resolveTemplate(options.template);
711
751
  }
712
752
 
713
753
  // Calculate dependencies
@@ -720,7 +760,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
720
760
  for (let i = 1; i <= options.lanes; i++) {
721
761
  const laneName = `lane-${i}`;
722
762
  const fileName = `${i.toString().padStart(2, '0')}-${laneName}.json`;
723
- const filePath = path.join(taskDir, fileName);
763
+ const filePath = safeJoin(taskDir, fileName);
724
764
 
725
765
  const depNums = dependencyMap.get(i) || [];
726
766
  const dependsOn = depNums.map(n => {
@@ -767,7 +807,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
767
807
  }
768
808
 
769
809
  // Create README
770
- const readmePath = path.join(taskDir, 'README.md');
810
+ const readmePath = safeJoin(taskDir, 'README.md');
771
811
  const readme = `# Task: ${options.featureName}
772
812
 
773
813
  Prepared at: ${now.toISOString()}