@litmers/cursorflow-orchestrator 0.1.14 → 0.1.18

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 (71) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -0
  3. package/commands/cursorflow-run.md +2 -0
  4. package/commands/cursorflow-triggers.md +250 -0
  5. package/dist/cli/clean.js +1 -1
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/init.js +13 -8
  8. package/dist/cli/init.js.map +1 -1
  9. package/dist/cli/logs.js +66 -44
  10. package/dist/cli/logs.js.map +1 -1
  11. package/dist/cli/monitor.js +12 -3
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/prepare.js +36 -13
  14. package/dist/cli/prepare.js.map +1 -1
  15. package/dist/cli/resume.js.map +1 -1
  16. package/dist/cli/run.js +7 -0
  17. package/dist/cli/run.js.map +1 -1
  18. package/dist/core/orchestrator.d.ts +3 -1
  19. package/dist/core/orchestrator.js +154 -11
  20. package/dist/core/orchestrator.js.map +1 -1
  21. package/dist/core/reviewer.d.ts +8 -4
  22. package/dist/core/reviewer.js +11 -7
  23. package/dist/core/reviewer.js.map +1 -1
  24. package/dist/core/runner.d.ts +17 -3
  25. package/dist/core/runner.js +326 -69
  26. package/dist/core/runner.js.map +1 -1
  27. package/dist/utils/config.js +17 -5
  28. package/dist/utils/config.js.map +1 -1
  29. package/dist/utils/doctor.js +28 -1
  30. package/dist/utils/doctor.js.map +1 -1
  31. package/dist/utils/enhanced-logger.d.ts +5 -4
  32. package/dist/utils/enhanced-logger.js +178 -43
  33. package/dist/utils/enhanced-logger.js.map +1 -1
  34. package/dist/utils/git.d.ts +6 -0
  35. package/dist/utils/git.js +15 -0
  36. package/dist/utils/git.js.map +1 -1
  37. package/dist/utils/logger.d.ts +2 -0
  38. package/dist/utils/logger.js +4 -1
  39. package/dist/utils/logger.js.map +1 -1
  40. package/dist/utils/repro-thinking-logs.d.ts +1 -0
  41. package/dist/utils/repro-thinking-logs.js +80 -0
  42. package/dist/utils/repro-thinking-logs.js.map +1 -0
  43. package/dist/utils/types.d.ts +22 -0
  44. package/dist/utils/webhook.js +3 -0
  45. package/dist/utils/webhook.js.map +1 -1
  46. package/package.json +4 -1
  47. package/scripts/ai-security-check.js +3 -0
  48. package/scripts/local-security-gate.sh +9 -1
  49. package/scripts/patches/test-cursor-agent.js +1 -1
  50. package/scripts/verify-and-fix.sh +37 -0
  51. package/src/cli/clean.ts +1 -1
  52. package/src/cli/init.ts +12 -9
  53. package/src/cli/logs.ts +68 -43
  54. package/src/cli/monitor.ts +13 -4
  55. package/src/cli/prepare.ts +36 -15
  56. package/src/cli/resume.ts +1 -1
  57. package/src/cli/run.ts +8 -0
  58. package/src/core/orchestrator.ts +171 -11
  59. package/src/core/reviewer.ts +30 -11
  60. package/src/core/runner.ts +346 -71
  61. package/src/utils/config.ts +17 -6
  62. package/src/utils/doctor.ts +31 -1
  63. package/src/utils/enhanced-logger.ts +182 -48
  64. package/src/utils/git.ts +15 -0
  65. package/src/utils/logger.ts +4 -1
  66. package/src/utils/repro-thinking-logs.ts +54 -0
  67. package/src/utils/types.ts +22 -0
  68. package/src/utils/webhook.ts +3 -0
  69. package/scripts/simple-logging-test.sh +0 -97
  70. package/scripts/test-real-logging.sh +0 -289
  71. package/scripts/test-streaming-multi-task.sh +0 -247
@@ -44,6 +44,8 @@ exports.cursorAgentSend = cursorAgentSend;
44
44
  exports.extractDependencyRequest = extractDependencyRequest;
45
45
  exports.wrapPromptForDependencyPolicy = wrapPromptForDependencyPolicy;
46
46
  exports.applyDependencyFilePermissions = applyDependencyFilePermissions;
47
+ exports.waitForTaskDependencies = waitForTaskDependencies;
48
+ exports.mergeDependencyBranches = mergeDependencyBranches;
47
49
  exports.runTask = runTask;
48
50
  exports.runTasks = runTasks;
49
51
  const fs = __importStar(require("fs"));
@@ -56,6 +58,7 @@ const state_1 = require("../utils/state");
56
58
  const events_1 = require("../utils/events");
57
59
  const config_1 = require("../utils/config");
58
60
  const webhook_1 = require("../utils/webhook");
61
+ const reviewer_1 = require("./reviewer");
59
62
  /**
60
63
  * Execute cursor-agent command with timeout and better error handling
61
64
  */
@@ -173,11 +176,14 @@ function validateTaskConfig(config) {
173
176
  /**
174
177
  * Execute cursor-agent command with streaming and better error handling
175
178
  */
176
- async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }) {
179
+ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }) {
177
180
  // Use stream-json format for structured output with tool calls and results
181
+ const format = outputFormat || 'stream-json';
178
182
  const args = [
179
183
  '--print',
180
- '--output-format', 'stream-json',
184
+ '--force',
185
+ '--approve-mcps',
186
+ '--output-format', format,
181
187
  '--workspace', workspaceDir,
182
188
  ...(model ? ['--model', model] : []),
183
189
  '--resume', chatId,
@@ -210,18 +216,18 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
210
216
  stdio: [stdinMode, 'pipe', 'pipe'],
211
217
  env: childEnv,
212
218
  });
213
- // Save PID to state if possible
219
+ logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
220
+ // Save PID to state if possible (avoid TOCTOU by reading directly)
214
221
  if (child.pid && signalDir) {
215
222
  try {
216
223
  const statePath = path.join(signalDir, 'state.json');
217
- if (fs.existsSync(statePath)) {
218
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
219
- state.pid = child.pid;
220
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
221
- }
224
+ // Read directly without existence check to avoid race condition
225
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
226
+ state.pid = child.pid;
227
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
222
228
  }
223
- catch (e) {
224
- // Best effort
229
+ catch {
230
+ // Best effort - file may not exist yet
225
231
  }
226
232
  }
227
233
  let fullStdout = '';
@@ -392,32 +398,25 @@ function extractDependencyRequest(text) {
392
398
  /**
393
399
  * Wrap prompt with dependency policy
394
400
  */
395
- function wrapPromptForDependencyPolicy(prompt, policy) {
396
- if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
401
+ function wrapPromptForDependencyPolicy(prompt, policy, options = {}) {
402
+ const { noGit = false } = options;
403
+ if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
397
404
  return prompt;
398
405
  }
399
- return `# Dependency Policy (MUST FOLLOW)
400
-
401
- You are running in a restricted lane.
402
-
403
- - allowDependencyChange: ${policy.allowDependencyChange}
404
- - lockfileReadOnly: ${policy.lockfileReadOnly}
405
-
406
- Rules:
407
- - BEFORE making any code changes, decide whether dependency changes are required.
408
- - If dependency changes are required, DO NOT change any files. Instead reply with:
409
-
410
- DEPENDENCY_CHANGE_REQUIRED
411
- \`\`\`json
412
- { "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
413
- \`\`\`
414
-
415
- Then STOP.
416
- - If dependency changes are NOT required, proceed normally.
417
-
418
- ---
419
-
420
- ${prompt}`;
406
+ let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
407
+ rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
408
+ rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
409
+ if (noGit) {
410
+ rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
411
+ }
412
+ rules += '\nRules:\n';
413
+ rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
414
+ rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
415
+ rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
416
+ rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
417
+ rules += 'Then STOP.\n';
418
+ rules += '- If dependency changes are NOT required, proceed normally.\n';
419
+ return `${rules}\n---\n\n${prompt}`;
421
420
  }
422
421
  /**
423
422
  * Apply file permissions based on dependency policy
@@ -444,26 +443,105 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
444
443
  }
445
444
  }
446
445
  }
446
+ /**
447
+ * Wait for task-level dependencies to be completed by other lanes
448
+ */
449
+ async function waitForTaskDependencies(deps, runDir) {
450
+ if (!deps || deps.length === 0)
451
+ return;
452
+ const lanesRoot = path.dirname(runDir);
453
+ const pendingDeps = new Set(deps);
454
+ logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
455
+ while (pendingDeps.size > 0) {
456
+ for (const dep of pendingDeps) {
457
+ const [laneName, taskName] = dep.split(':');
458
+ if (!laneName || !taskName) {
459
+ logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
460
+ pendingDeps.delete(dep);
461
+ continue;
462
+ }
463
+ const depStatePath = path.join(lanesRoot, laneName, 'state.json');
464
+ if (fs.existsSync(depStatePath)) {
465
+ try {
466
+ const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
467
+ if (state.completedTasks && state.completedTasks.includes(taskName)) {
468
+ logger.info(`✓ Dependency met: ${dep}`);
469
+ pendingDeps.delete(dep);
470
+ }
471
+ else if (state.status === 'failed') {
472
+ throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
473
+ }
474
+ }
475
+ catch (e) {
476
+ if (e.message.includes('Dependency failed'))
477
+ throw e;
478
+ // Ignore parse errors, file might be being written
479
+ }
480
+ }
481
+ }
482
+ if (pendingDeps.size > 0) {
483
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
484
+ }
485
+ }
486
+ }
487
+ /**
488
+ * Merge branches from dependency lanes
489
+ */
490
+ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
491
+ if (!deps || deps.length === 0)
492
+ return;
493
+ const lanesRoot = path.dirname(runDir);
494
+ const lanesToMerge = new Set(deps.map(d => d.split(':')[0]));
495
+ for (const laneName of lanesToMerge) {
496
+ const depStatePath = path.join(lanesRoot, laneName, 'state.json');
497
+ if (!fs.existsSync(depStatePath))
498
+ continue;
499
+ try {
500
+ const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
501
+ if (state.pipelineBranch) {
502
+ logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
503
+ // Ensure we have the latest
504
+ git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
505
+ git.merge(state.pipelineBranch, {
506
+ cwd: worktreeDir,
507
+ noFf: true,
508
+ message: `chore: merge task dependency from ${laneName}`
509
+ });
510
+ }
511
+ }
512
+ catch (e) {
513
+ logger.error(`Failed to merge branch from ${laneName}: ${e}`);
514
+ }
515
+ }
516
+ }
447
517
  /**
448
518
  * Run a single task
449
519
  */
450
- async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, runDir, }) {
520
+ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskBranch, chatId, runDir, noGit = false, }) {
451
521
  const model = task.model || config.model || 'sonnet-4.5';
522
+ const timeout = task.timeout || config.timeout;
452
523
  const convoPath = path.join(runDir, 'conversation.jsonl');
453
524
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
454
525
  logger.info(`Model: ${model}`);
455
- logger.info(`Branch: ${taskBranch}`);
526
+ if (noGit) {
527
+ logger.info('🚫 noGit mode: skipping branch operations');
528
+ }
529
+ else {
530
+ logger.info(`Branch: ${taskBranch}`);
531
+ }
456
532
  events_1.events.emit('task.started', {
457
533
  taskName: task.name,
458
534
  taskBranch,
459
535
  index,
460
536
  });
461
- // Checkout task branch
462
- git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
537
+ // Checkout task branch (skip in noGit mode)
538
+ if (!noGit) {
539
+ git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
540
+ }
463
541
  // Apply dependency permissions
464
542
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
465
543
  // Run prompt
466
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
544
+ const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
467
545
  (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('user', prompt1, {
468
546
  task: task.name,
469
547
  model,
@@ -481,8 +559,9 @@ async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, r
481
559
  prompt: prompt1,
482
560
  model,
483
561
  signalDir: runDir,
484
- timeout: config.timeout,
562
+ timeout,
485
563
  enableIntervention: config.enableIntervention,
564
+ outputFormat: config.agentOutputFormat,
486
565
  });
487
566
  const duration = Date.now() - startTime;
488
567
  events_1.events.emit('agent.response_received', {
@@ -519,8 +598,38 @@ async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, r
519
598
  dependencyRequest: depReq.plan || null,
520
599
  };
521
600
  }
522
- // Push task branch
523
- git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
601
+ // Push task branch (skip in noGit mode)
602
+ if (!noGit) {
603
+ git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
604
+ }
605
+ // Automatic Review
606
+ const reviewEnabled = config.reviewAllTasks || task.acceptanceCriteria?.length || config.enableReview;
607
+ if (reviewEnabled) {
608
+ logger.section(`🔍 Reviewing Task: ${task.name}`);
609
+ const reviewResult = await (0, reviewer_1.runReviewLoop)({
610
+ taskResult: {
611
+ taskName: task.name,
612
+ taskBranch: taskBranch,
613
+ acceptanceCriteria: task.acceptanceCriteria,
614
+ },
615
+ worktreeDir,
616
+ runDir,
617
+ config,
618
+ workChatId: chatId,
619
+ model, // Use the same model as requested
620
+ cursorAgentSend,
621
+ cursorAgentCreateChat,
622
+ });
623
+ if (!reviewResult.approved) {
624
+ logger.error(`❌ Task review failed after ${reviewResult.iterations} iterations`);
625
+ return {
626
+ taskName: task.name,
627
+ taskBranch,
628
+ status: 'ERROR',
629
+ error: reviewResult.error || 'Task failed to pass review criteria',
630
+ };
631
+ }
632
+ }
524
633
  events_1.events.emit('task.completed', {
525
634
  taskName: task.name,
526
635
  taskBranch,
@@ -537,6 +646,10 @@ async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, r
537
646
  */
538
647
  async function runTasks(tasksFile, config, runDir, options = {}) {
539
648
  const startIndex = options.startIndex || 0;
649
+ const noGit = options.noGit || config.noGit || false;
650
+ if (noGit) {
651
+ logger.info('🚫 Running in noGit mode - Git operations will be skipped');
652
+ }
540
653
  // Validate configuration before starting
541
654
  logger.info('Validating task configuration...');
542
655
  try {
@@ -567,15 +680,25 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
567
680
  throw new Error('Cursor authentication required. Please authenticate and try again.');
568
681
  }
569
682
  logger.success('✓ Cursor authentication OK');
570
- const repoRoot = git.getRepoRoot();
683
+ // In noGit mode, we don't need repoRoot - use current directory
684
+ const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
571
685
  // Load existing state if resuming
572
686
  const statePath = path.join(runDir, 'state.json');
573
687
  let state = null;
574
- if (startIndex > 0 && fs.existsSync(statePath)) {
575
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
688
+ if (fs.existsSync(statePath)) {
689
+ try {
690
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
691
+ }
692
+ catch (e) {
693
+ logger.warn(`Failed to load existing state from ${statePath}: ${e}`);
694
+ }
576
695
  }
577
- const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
578
- const worktreeDir = state?.worktreeDir || path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
696
+ const randomSuffix = Math.random().toString(36).substring(2, 7);
697
+ const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
698
+ // In noGit mode, use a simple local directory instead of worktree
699
+ const worktreeDir = state?.worktreeDir || (noGit
700
+ ? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
701
+ : path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
579
702
  if (startIndex === 0) {
580
703
  logger.section('🚀 Starting Pipeline');
581
704
  }
@@ -585,12 +708,31 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
585
708
  logger.info(`Pipeline Branch: ${pipelineBranch}`);
586
709
  logger.info(`Worktree: ${worktreeDir}`);
587
710
  logger.info(`Tasks: ${config.tasks.length}`);
588
- // Create worktree only if starting fresh
589
- if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
590
- git.createWorktree(worktreeDir, pipelineBranch, {
591
- baseBranch: config.baseBranch || 'main',
592
- cwd: repoRoot,
593
- });
711
+ // Create worktree only if starting fresh and worktree doesn't exist
712
+ if (!fs.existsSync(worktreeDir)) {
713
+ if (noGit) {
714
+ // In noGit mode, just create the directory
715
+ logger.info(`Creating work directory: ${worktreeDir}`);
716
+ fs.mkdirSync(worktreeDir, { recursive: true });
717
+ }
718
+ else {
719
+ git.createWorktree(worktreeDir, pipelineBranch, {
720
+ baseBranch: config.baseBranch || 'main',
721
+ cwd: repoRoot,
722
+ });
723
+ }
724
+ }
725
+ else if (!noGit) {
726
+ // If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
727
+ logger.info(`Reusing existing worktree: ${worktreeDir}`);
728
+ try {
729
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
730
+ }
731
+ catch (e) {
732
+ // If checkout fails, maybe the worktree is in a weird state.
733
+ // For now, just log it. In a more robust impl, we might want to repair it.
734
+ logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
735
+ }
594
736
  }
595
737
  // Create chat
596
738
  logger.info('Creating chat session...');
@@ -610,6 +752,7 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
610
752
  dependencyRequest: null,
611
753
  tasksFile, // Store tasks file for resume
612
754
  dependsOn: config.dependsOn || [],
755
+ completedTasks: [],
613
756
  };
614
757
  }
615
758
  else {
@@ -617,10 +760,11 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
617
760
  state.error = null;
618
761
  state.dependencyRequest = null;
619
762
  state.dependsOn = config.dependsOn || [];
763
+ state.completedTasks = state.completedTasks || [];
620
764
  }
621
765
  (0, state_1.saveState)(statePath, state);
622
- // Merge dependencies if any
623
- if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
766
+ // Merge dependencies if any (skip in noGit mode)
767
+ if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
624
768
  logger.section('🔗 Merging Dependencies');
625
769
  // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
626
770
  const lanesRoot = path.dirname(runDir);
@@ -660,11 +804,74 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
660
804
  // Push the merged state
661
805
  git.push(pipelineBranch, { cwd: worktreeDir });
662
806
  }
807
+ else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
808
+ logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
809
+ // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
810
+ const lanesRoot = path.dirname(runDir);
811
+ for (const depName of config.dependsOn) {
812
+ const depRunDir = path.join(lanesRoot, depName);
813
+ const depStatePath = path.join(depRunDir, 'state.json');
814
+ if (!fs.existsSync(depStatePath)) {
815
+ continue;
816
+ }
817
+ try {
818
+ const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
819
+ if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
820
+ logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
821
+ // Use a simple recursive copy (excluding Git and internal dirs)
822
+ const copyFiles = (src, dest) => {
823
+ if (!fs.existsSync(dest))
824
+ fs.mkdirSync(dest, { recursive: true });
825
+ const entries = fs.readdirSync(src, { withFileTypes: true });
826
+ for (const entry of entries) {
827
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
828
+ continue;
829
+ const srcPath = path.join(src, entry.name);
830
+ const destPath = path.join(dest, entry.name);
831
+ if (entry.isDirectory()) {
832
+ copyFiles(srcPath, destPath);
833
+ }
834
+ else {
835
+ fs.copyFileSync(srcPath, destPath);
836
+ }
837
+ }
838
+ };
839
+ copyFiles(depState.worktreeDir, worktreeDir);
840
+ }
841
+ }
842
+ catch (e) {
843
+ logger.error(`Failed to copy dependency ${depName}: ${e}`);
844
+ }
845
+ }
846
+ }
663
847
  // Run tasks
664
848
  const results = [];
665
849
  for (let i = startIndex; i < config.tasks.length; i++) {
666
850
  const task = config.tasks[i];
667
851
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
852
+ // Handle task-level dependencies
853
+ if (task.dependsOn && task.dependsOn.length > 0) {
854
+ state.status = 'waiting';
855
+ state.waitingFor = task.dependsOn;
856
+ (0, state_1.saveState)(statePath, state);
857
+ try {
858
+ await waitForTaskDependencies(task.dependsOn, runDir);
859
+ if (!noGit) {
860
+ await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
861
+ }
862
+ state.status = 'running';
863
+ state.waitingFor = [];
864
+ (0, state_1.saveState)(statePath, state);
865
+ }
866
+ catch (e) {
867
+ state.status = 'failed';
868
+ state.waitingFor = [];
869
+ state.error = e.message;
870
+ (0, state_1.saveState)(statePath, state);
871
+ logger.error(`Task dependency wait/merge failed: ${e.message}`);
872
+ process.exit(1);
873
+ }
874
+ }
668
875
  const result = await runTask({
669
876
  task,
670
877
  config,
@@ -674,10 +881,15 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
674
881
  taskBranch,
675
882
  chatId,
676
883
  runDir,
884
+ noGit,
677
885
  });
678
886
  results.push(result);
679
887
  // Update state
680
888
  state.currentTaskIndex = i + 1;
889
+ state.completedTasks = state.completedTasks || [];
890
+ if (!state.completedTasks.includes(task.name)) {
891
+ state.completedTasks.push(task.name);
892
+ }
681
893
  (0, state_1.saveState)(statePath, state);
682
894
  // Handle blocked or error
683
895
  if (result.status === 'BLOCKED_DEPENDENCY') {
@@ -700,20 +912,61 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
700
912
  logger.error(`Task failed: ${result.error}`);
701
913
  process.exit(1);
702
914
  }
703
- // Merge into pipeline
704
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
705
- git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
706
- // Log changed files
707
- const stats = git.getLastOperationStats(worktreeDir);
708
- if (stats) {
709
- logger.info('Changed files:\n' + stats);
915
+ // Merge into pipeline (skip in noGit mode)
916
+ if (!noGit) {
917
+ logger.info(`Merging ${taskBranch} ${pipelineBranch}`);
918
+ git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
919
+ // Log changed files
920
+ const stats = git.getLastOperationStats(worktreeDir);
921
+ if (stats) {
922
+ logger.info('Changed files:\n' + stats);
923
+ }
924
+ git.push(pipelineBranch, { cwd: worktreeDir });
925
+ }
926
+ else {
927
+ logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
710
928
  }
711
- git.push(pipelineBranch, { cwd: worktreeDir });
712
929
  }
713
930
  // Complete
714
931
  state.status = 'completed';
715
932
  state.endTime = Date.now();
716
933
  (0, state_1.saveState)(statePath, state);
934
+ // Log final file summary
935
+ if (noGit) {
936
+ const getFileSummary = (dir) => {
937
+ let stats = { files: 0, dirs: 0 };
938
+ if (!fs.existsSync(dir))
939
+ return stats;
940
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
941
+ for (const entry of entries) {
942
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
943
+ continue;
944
+ if (entry.isDirectory()) {
945
+ stats.dirs++;
946
+ const sub = getFileSummary(path.join(dir, entry.name));
947
+ stats.files += sub.files;
948
+ stats.dirs += sub.dirs;
949
+ }
950
+ else {
951
+ stats.files++;
952
+ }
953
+ }
954
+ return stats;
955
+ };
956
+ const summary = getFileSummary(worktreeDir);
957
+ logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
958
+ }
959
+ else {
960
+ try {
961
+ const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
962
+ if (stats) {
963
+ logger.info('Final Workspace Summary (Git):\n' + stats);
964
+ }
965
+ }
966
+ catch (e) {
967
+ // Ignore
968
+ }
969
+ }
717
970
  logger.success('All tasks completed!');
718
971
  return results;
719
972
  }
@@ -730,6 +983,7 @@ if (require.main === module) {
730
983
  const runDirIdx = args.indexOf('--run-dir');
731
984
  const startIdxIdx = args.indexOf('--start-index');
732
985
  const pipelineBranchIdx = args.indexOf('--pipeline-branch');
986
+ const noGit = args.includes('--no-git');
733
987
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1] : '.';
734
988
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
735
989
  const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
@@ -738,9 +992,10 @@ if (require.main === module) {
738
992
  const runsIdx = parts.lastIndexOf('runs');
739
993
  const runId = runsIdx >= 0 && parts[runsIdx + 1] ? parts[runsIdx + 1] : `run-${Date.now()}`;
740
994
  events_1.events.setRunId(runId);
741
- // Load global config to register webhooks in this process
995
+ // Load global config for defaults and webhooks
996
+ let globalConfig;
742
997
  try {
743
- const globalConfig = (0, config_1.loadConfig)();
998
+ globalConfig = (0, config_1.loadConfig)();
744
999
  if (globalConfig.webhooks) {
745
1000
  (0, webhook_1.registerWebhooks)(globalConfig.webhooks);
746
1001
  }
@@ -764,13 +1019,15 @@ if (require.main === module) {
764
1019
  console.error(`Failed to load tasks file: ${error.message}`);
765
1020
  process.exit(1);
766
1021
  }
767
- // Add dependency policy defaults
1022
+ // Add defaults from global config or hardcoded
768
1023
  config.dependencyPolicy = config.dependencyPolicy || {
769
- allowDependencyChange: false,
770
- lockfileReadOnly: true,
1024
+ allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
1025
+ lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
771
1026
  };
1027
+ // Add agent output format default
1028
+ config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
772
1029
  // Run tasks
773
- runTasks(tasksFile, config, runDir, { startIndex })
1030
+ runTasks(tasksFile, config, runDir, { startIndex, noGit })
774
1031
  .then(() => {
775
1032
  process.exit(0);
776
1033
  })