@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
@@ -15,7 +15,7 @@ import { saveState, appendLog, createConversationEntry } from '../utils/state';
15
15
  import { events } from '../utils/events';
16
16
  import { loadConfig } from '../utils/config';
17
17
  import { registerWebhooks } from '../utils/webhook';
18
- import { stripAnsi } from '../utils/enhanced-logger';
18
+ import { runReviewLoop } from './reviewer';
19
19
  import {
20
20
  RunnerConfig,
21
21
  Task,
@@ -175,7 +175,7 @@ export function validateTaskConfig(config: RunnerConfig): void {
175
175
  /**
176
176
  * Execute cursor-agent command with streaming and better error handling
177
177
  */
178
- export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }: {
178
+ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }: {
179
179
  workspaceDir: string;
180
180
  chatId: string;
181
181
  prompt: string;
@@ -184,11 +184,16 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
184
184
  timeout?: number;
185
185
  /** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
186
186
  enableIntervention?: boolean;
187
+ /** Output format for cursor-agent (default: 'stream-json') */
188
+ outputFormat?: 'stream-json' | 'json' | 'plain';
187
189
  }): Promise<AgentSendResult> {
188
190
  // Use stream-json format for structured output with tool calls and results
191
+ const format = outputFormat || 'stream-json';
189
192
  const args = [
190
193
  '--print',
191
- '--output-format', 'stream-json',
194
+ '--force',
195
+ '--approve-mcps',
196
+ '--output-format', format,
192
197
  '--workspace', workspaceDir,
193
198
  ...(model ? ['--model', model] : []),
194
199
  '--resume', chatId,
@@ -229,17 +234,18 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
229
234
  env: childEnv,
230
235
  });
231
236
 
232
- // Save PID to state if possible
237
+ logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
238
+
239
+ // Save PID to state if possible (avoid TOCTOU by reading directly)
233
240
  if (child.pid && signalDir) {
234
241
  try {
235
242
  const statePath = path.join(signalDir, 'state.json');
236
- if (fs.existsSync(statePath)) {
237
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
238
- state.pid = child.pid;
239
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
240
- }
241
- } catch (e) {
242
- // Best effort
243
+ // Read directly without existence check to avoid race condition
244
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
245
+ state.pid = child.pid;
246
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
247
+ } catch {
248
+ // Best effort - file may not exist yet
243
249
  }
244
250
  }
245
251
 
@@ -426,33 +432,31 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
426
432
  /**
427
433
  * Wrap prompt with dependency policy
428
434
  */
429
- export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
430
- if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
435
+ export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy, options: { noGit?: boolean } = {}): string {
436
+ const { noGit = false } = options;
437
+
438
+ if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
431
439
  return prompt;
432
440
  }
433
441
 
434
- return `# Dependency Policy (MUST FOLLOW)
435
-
436
- You are running in a restricted lane.
437
-
438
- - allowDependencyChange: ${policy.allowDependencyChange}
439
- - lockfileReadOnly: ${policy.lockfileReadOnly}
440
-
441
- Rules:
442
- - BEFORE making any code changes, decide whether dependency changes are required.
443
- - If dependency changes are required, DO NOT change any files. Instead reply with:
444
-
445
- DEPENDENCY_CHANGE_REQUIRED
446
- \`\`\`json
447
- { "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
448
- \`\`\`
449
-
450
- Then STOP.
451
- - If dependency changes are NOT required, proceed normally.
452
-
453
- ---
454
-
455
- ${prompt}`;
442
+ let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
443
+
444
+ rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
445
+ rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
446
+
447
+ if (noGit) {
448
+ rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
449
+ }
450
+
451
+ rules += '\nRules:\n';
452
+ rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
453
+ rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
454
+ rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
455
+ rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
456
+ rules += 'Then STOP.\n';
457
+ rules += '- If dependency changes are NOT required, proceed normally.\n';
458
+
459
+ return `${rules}\n---\n\n${prompt}`;
456
460
  }
457
461
 
458
462
  /**
@@ -483,6 +487,82 @@ export function applyDependencyFilePermissions(worktreeDir: string, policy: Depe
483
487
  }
484
488
  }
485
489
 
490
+ /**
491
+ * Wait for task-level dependencies to be completed by other lanes
492
+ */
493
+ export async function waitForTaskDependencies(deps: string[], runDir: string): Promise<void> {
494
+ if (!deps || deps.length === 0) return;
495
+
496
+ const lanesRoot = path.dirname(runDir);
497
+ const pendingDeps = new Set(deps);
498
+
499
+ logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
500
+
501
+ while (pendingDeps.size > 0) {
502
+ for (const dep of pendingDeps) {
503
+ const [laneName, taskName] = dep.split(':');
504
+ if (!laneName || !taskName) {
505
+ logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
506
+ pendingDeps.delete(dep);
507
+ continue;
508
+ }
509
+
510
+ const depStatePath = path.join(lanesRoot, laneName, 'state.json');
511
+ if (fs.existsSync(depStatePath)) {
512
+ try {
513
+ const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
514
+ if (state.completedTasks && state.completedTasks.includes(taskName)) {
515
+ logger.info(`✓ Dependency met: ${dep}`);
516
+ pendingDeps.delete(dep);
517
+ } else if (state.status === 'failed') {
518
+ throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
519
+ }
520
+ } catch (e: any) {
521
+ if (e.message.includes('Dependency failed')) throw e;
522
+ // Ignore parse errors, file might be being written
523
+ }
524
+ }
525
+ }
526
+
527
+ if (pendingDeps.size > 0) {
528
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
529
+ }
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Merge branches from dependency lanes
535
+ */
536
+ export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string): Promise<void> {
537
+ if (!deps || deps.length === 0) return;
538
+
539
+ const lanesRoot = path.dirname(runDir);
540
+ const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
541
+
542
+ for (const laneName of lanesToMerge) {
543
+ const depStatePath = path.join(lanesRoot, laneName, 'state.json');
544
+ if (!fs.existsSync(depStatePath)) continue;
545
+
546
+ try {
547
+ const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
548
+ if (state.pipelineBranch) {
549
+ logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
550
+
551
+ // Ensure we have the latest
552
+ git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
553
+
554
+ git.merge(state.pipelineBranch, {
555
+ cwd: worktreeDir,
556
+ noFf: true,
557
+ message: `chore: merge task dependency from ${laneName}`
558
+ });
559
+ }
560
+ } catch (e) {
561
+ logger.error(`Failed to merge branch from ${laneName}: ${e}`);
562
+ }
563
+ }
564
+ }
565
+
486
566
  /**
487
567
  * Run a single task
488
568
  */
@@ -491,9 +571,11 @@ export async function runTask({
491
571
  config,
492
572
  index,
493
573
  worktreeDir,
574
+ pipelineBranch,
494
575
  taskBranch,
495
576
  chatId,
496
577
  runDir,
578
+ noGit = false,
497
579
  }: {
498
580
  task: Task;
499
581
  config: RunnerConfig;
@@ -503,13 +585,19 @@ export async function runTask({
503
585
  taskBranch: string;
504
586
  chatId: string;
505
587
  runDir: string;
588
+ noGit?: boolean;
506
589
  }): Promise<TaskExecutionResult> {
507
590
  const model = task.model || config.model || 'sonnet-4.5';
591
+ const timeout = task.timeout || config.timeout;
508
592
  const convoPath = path.join(runDir, 'conversation.jsonl');
509
593
 
510
594
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
511
595
  logger.info(`Model: ${model}`);
512
- logger.info(`Branch: ${taskBranch}`);
596
+ if (noGit) {
597
+ logger.info('🚫 noGit mode: skipping branch operations');
598
+ } else {
599
+ logger.info(`Branch: ${taskBranch}`);
600
+ }
513
601
 
514
602
  events.emit('task.started', {
515
603
  taskName: task.name,
@@ -517,14 +605,16 @@ export async function runTask({
517
605
  index,
518
606
  });
519
607
 
520
- // Checkout task branch
521
- git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
608
+ // Checkout task branch (skip in noGit mode)
609
+ if (!noGit) {
610
+ git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
611
+ }
522
612
 
523
613
  // Apply dependency permissions
524
614
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
525
615
 
526
616
  // Run prompt
527
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
617
+ const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
528
618
 
529
619
  appendLog(convoPath, createConversationEntry('user', prompt1, {
530
620
  task: task.name,
@@ -545,8 +635,9 @@ export async function runTask({
545
635
  prompt: prompt1,
546
636
  model,
547
637
  signalDir: runDir,
548
- timeout: config.timeout,
638
+ timeout,
549
639
  enableIntervention: config.enableIntervention,
640
+ outputFormat: config.agentOutputFormat,
550
641
  });
551
642
 
552
643
  const duration = Date.now() - startTime;
@@ -588,8 +679,41 @@ export async function runTask({
588
679
  };
589
680
  }
590
681
 
591
- // Push task branch
592
- git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
682
+ // Push task branch (skip in noGit mode)
683
+ if (!noGit) {
684
+ git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
685
+ }
686
+
687
+ // Automatic Review
688
+ const reviewEnabled = config.reviewAllTasks || task.acceptanceCriteria?.length || config.enableReview;
689
+
690
+ if (reviewEnabled) {
691
+ logger.section(`🔍 Reviewing Task: ${task.name}`);
692
+ const reviewResult = await runReviewLoop({
693
+ taskResult: {
694
+ taskName: task.name,
695
+ taskBranch: taskBranch,
696
+ acceptanceCriteria: task.acceptanceCriteria,
697
+ },
698
+ worktreeDir,
699
+ runDir,
700
+ config,
701
+ workChatId: chatId,
702
+ model, // Use the same model as requested
703
+ cursorAgentSend,
704
+ cursorAgentCreateChat,
705
+ });
706
+
707
+ if (!reviewResult.approved) {
708
+ logger.error(`❌ Task review failed after ${reviewResult.iterations} iterations`);
709
+ return {
710
+ taskName: task.name,
711
+ taskBranch,
712
+ status: 'ERROR',
713
+ error: reviewResult.error || 'Task failed to pass review criteria',
714
+ };
715
+ }
716
+ }
593
717
 
594
718
  events.emit('task.completed', {
595
719
  taskName: task.name,
@@ -607,8 +731,13 @@ export async function runTask({
607
731
  /**
608
732
  * Run all tasks in sequence
609
733
  */
610
- export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
734
+ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
611
735
  const startIndex = options.startIndex || 0;
736
+ const noGit = options.noGit || config.noGit || false;
737
+
738
+ if (noGit) {
739
+ logger.info('🚫 Running in noGit mode - Git operations will be skipped');
740
+ }
612
741
 
613
742
  // Validate configuration before starting
614
743
  logger.info('Validating task configuration...');
@@ -648,18 +777,27 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
648
777
 
649
778
  logger.success('✓ Cursor authentication OK');
650
779
 
651
- const repoRoot = git.getRepoRoot();
780
+ // In noGit mode, we don't need repoRoot - use current directory
781
+ const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
652
782
 
653
783
  // Load existing state if resuming
654
784
  const statePath = path.join(runDir, 'state.json');
655
785
  let state: LaneState | null = null;
656
786
 
657
- if (startIndex > 0 && fs.existsSync(statePath)) {
658
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
787
+ if (fs.existsSync(statePath)) {
788
+ try {
789
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
790
+ } catch (e) {
791
+ logger.warn(`Failed to load existing state from ${statePath}: ${e}`);
792
+ }
659
793
  }
660
794
 
661
- const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
662
- const worktreeDir = state?.worktreeDir || path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
795
+ const randomSuffix = Math.random().toString(36).substring(2, 7);
796
+ const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
797
+ // In noGit mode, use a simple local directory instead of worktree
798
+ const worktreeDir = state?.worktreeDir || (noGit
799
+ ? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
800
+ : path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
663
801
 
664
802
  if (startIndex === 0) {
665
803
  logger.section('🚀 Starting Pipeline');
@@ -671,12 +809,28 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
671
809
  logger.info(`Worktree: ${worktreeDir}`);
672
810
  logger.info(`Tasks: ${config.tasks.length}`);
673
811
 
674
- // Create worktree only if starting fresh
675
- if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
676
- git.createWorktree(worktreeDir, pipelineBranch, {
677
- baseBranch: config.baseBranch || 'main',
678
- cwd: repoRoot,
679
- });
812
+ // Create worktree only if starting fresh and worktree doesn't exist
813
+ if (!fs.existsSync(worktreeDir)) {
814
+ if (noGit) {
815
+ // In noGit mode, just create the directory
816
+ logger.info(`Creating work directory: ${worktreeDir}`);
817
+ fs.mkdirSync(worktreeDir, { recursive: true });
818
+ } else {
819
+ git.createWorktree(worktreeDir, pipelineBranch, {
820
+ baseBranch: config.baseBranch || 'main',
821
+ cwd: repoRoot,
822
+ });
823
+ }
824
+ } else if (!noGit) {
825
+ // If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
826
+ logger.info(`Reusing existing worktree: ${worktreeDir}`);
827
+ try {
828
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
829
+ } catch (e) {
830
+ // If checkout fails, maybe the worktree is in a weird state.
831
+ // For now, just log it. In a more robust impl, we might want to repair it.
832
+ logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
833
+ }
680
834
  }
681
835
 
682
836
  // Create chat
@@ -698,18 +852,20 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
698
852
  dependencyRequest: null,
699
853
  tasksFile, // Store tasks file for resume
700
854
  dependsOn: config.dependsOn || [],
855
+ completedTasks: [],
701
856
  };
702
857
  } else {
703
858
  state.status = 'running';
704
859
  state.error = null;
705
860
  state.dependencyRequest = null;
706
861
  state.dependsOn = config.dependsOn || [];
862
+ state.completedTasks = state.completedTasks || [];
707
863
  }
708
864
 
709
865
  saveState(statePath, state);
710
866
 
711
- // Merge dependencies if any
712
- if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
867
+ // Merge dependencies if any (skip in noGit mode)
868
+ if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
713
869
  logger.section('🔗 Merging Dependencies');
714
870
 
715
871
  // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
@@ -756,6 +912,50 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
756
912
 
757
913
  // Push the merged state
758
914
  git.push(pipelineBranch, { cwd: worktreeDir });
915
+ } else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
916
+ logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
917
+
918
+ // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
919
+ const lanesRoot = path.dirname(runDir);
920
+
921
+ for (const depName of config.dependsOn) {
922
+ const depRunDir = path.join(lanesRoot, depName);
923
+ const depStatePath = path.join(depRunDir, 'state.json');
924
+
925
+ if (!fs.existsSync(depStatePath)) {
926
+ continue;
927
+ }
928
+
929
+ try {
930
+ const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
931
+ if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
932
+ logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
933
+
934
+ // Use a simple recursive copy (excluding Git and internal dirs)
935
+ const copyFiles = (src: string, dest: string) => {
936
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
937
+ const entries = fs.readdirSync(src, { withFileTypes: true });
938
+
939
+ for (const entry of entries) {
940
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
941
+
942
+ const srcPath = path.join(src, entry.name);
943
+ const destPath = path.join(dest, entry.name);
944
+
945
+ if (entry.isDirectory()) {
946
+ copyFiles(srcPath, destPath);
947
+ } else {
948
+ fs.copyFileSync(srcPath, destPath);
949
+ }
950
+ }
951
+ };
952
+
953
+ copyFiles(depState.worktreeDir, worktreeDir);
954
+ }
955
+ } catch (e) {
956
+ logger.error(`Failed to copy dependency ${depName}: ${e}`);
957
+ }
958
+ }
759
959
  }
760
960
 
761
961
  // Run tasks
@@ -764,6 +964,32 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
764
964
  for (let i = startIndex; i < config.tasks.length; i++) {
765
965
  const task = config.tasks[i]!;
766
966
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
967
+
968
+ // Handle task-level dependencies
969
+ if (task.dependsOn && task.dependsOn.length > 0) {
970
+ state.status = 'waiting';
971
+ state.waitingFor = task.dependsOn;
972
+ saveState(statePath, state);
973
+
974
+ try {
975
+ await waitForTaskDependencies(task.dependsOn, runDir);
976
+
977
+ if (!noGit) {
978
+ await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
979
+ }
980
+
981
+ state.status = 'running';
982
+ state.waitingFor = [];
983
+ saveState(statePath, state);
984
+ } catch (e: any) {
985
+ state.status = 'failed';
986
+ state.waitingFor = [];
987
+ state.error = e.message;
988
+ saveState(statePath, state);
989
+ logger.error(`Task dependency wait/merge failed: ${e.message}`);
990
+ process.exit(1);
991
+ }
992
+ }
767
993
 
768
994
  const result = await runTask({
769
995
  task,
@@ -774,12 +1000,17 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
774
1000
  taskBranch,
775
1001
  chatId,
776
1002
  runDir,
1003
+ noGit,
777
1004
  });
778
1005
 
779
1006
  results.push(result);
780
1007
 
781
1008
  // Update state
782
1009
  state.currentTaskIndex = i + 1;
1010
+ state.completedTasks = state.completedTasks || [];
1011
+ if (!state.completedTasks.includes(task.name)) {
1012
+ state.completedTasks.push(task.name);
1013
+ }
783
1014
  saveState(statePath, state);
784
1015
 
785
1016
  // Handle blocked or error
@@ -807,17 +1038,21 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
807
1038
  process.exit(1);
808
1039
  }
809
1040
 
810
- // Merge into pipeline
811
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
812
- git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
1041
+ // Merge into pipeline (skip in noGit mode)
1042
+ if (!noGit) {
1043
+ logger.info(`Merging ${taskBranch} ${pipelineBranch}`);
1044
+ git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
813
1045
 
814
- // Log changed files
815
- const stats = git.getLastOperationStats(worktreeDir);
816
- if (stats) {
817
- logger.info('Changed files:\n' + stats);
818
- }
1046
+ // Log changed files
1047
+ const stats = git.getLastOperationStats(worktreeDir);
1048
+ if (stats) {
1049
+ logger.info('Changed files:\n' + stats);
1050
+ }
819
1051
 
820
- git.push(pipelineBranch, { cwd: worktreeDir });
1052
+ git.push(pipelineBranch, { cwd: worktreeDir });
1053
+ } else {
1054
+ logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
1055
+ }
821
1056
  }
822
1057
 
823
1058
  // Complete
@@ -825,6 +1060,41 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
825
1060
  state.endTime = Date.now();
826
1061
  saveState(statePath, state);
827
1062
 
1063
+ // Log final file summary
1064
+ if (noGit) {
1065
+ const getFileSummary = (dir: string): { files: number; dirs: number } => {
1066
+ let stats = { files: 0, dirs: 0 };
1067
+ if (!fs.existsSync(dir)) return stats;
1068
+
1069
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1070
+ for (const entry of entries) {
1071
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
1072
+
1073
+ if (entry.isDirectory()) {
1074
+ stats.dirs++;
1075
+ const sub = getFileSummary(path.join(dir, entry.name));
1076
+ stats.files += sub.files;
1077
+ stats.dirs += sub.dirs;
1078
+ } else {
1079
+ stats.files++;
1080
+ }
1081
+ }
1082
+ return stats;
1083
+ };
1084
+
1085
+ const summary = getFileSummary(worktreeDir);
1086
+ logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
1087
+ } else {
1088
+ try {
1089
+ const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
1090
+ if (stats) {
1091
+ logger.info('Final Workspace Summary (Git):\n' + stats);
1092
+ }
1093
+ } catch (e) {
1094
+ // Ignore
1095
+ }
1096
+ }
1097
+
828
1098
  logger.success('All tasks completed!');
829
1099
  return results;
830
1100
  }
@@ -844,6 +1114,7 @@ if (require.main === module) {
844
1114
  const runDirIdx = args.indexOf('--run-dir');
845
1115
  const startIdxIdx = args.indexOf('--start-index');
846
1116
  const pipelineBranchIdx = args.indexOf('--pipeline-branch');
1117
+ const noGit = args.includes('--no-git');
847
1118
 
848
1119
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
849
1120
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
@@ -856,9 +1127,10 @@ if (require.main === module) {
856
1127
 
857
1128
  events.setRunId(runId);
858
1129
 
859
- // Load global config to register webhooks in this process
1130
+ // Load global config for defaults and webhooks
1131
+ let globalConfig;
860
1132
  try {
861
- const globalConfig = loadConfig();
1133
+ globalConfig = loadConfig();
862
1134
  if (globalConfig.webhooks) {
863
1135
  registerWebhooks(globalConfig.webhooks);
864
1136
  }
@@ -883,14 +1155,17 @@ if (require.main === module) {
883
1155
  process.exit(1);
884
1156
  }
885
1157
 
886
- // Add dependency policy defaults
1158
+ // Add defaults from global config or hardcoded
887
1159
  config.dependencyPolicy = config.dependencyPolicy || {
888
- allowDependencyChange: false,
889
- lockfileReadOnly: true,
1160
+ allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
1161
+ lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
890
1162
  };
891
1163
 
1164
+ // Add agent output format default
1165
+ config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
1166
+
892
1167
  // Run tasks
893
- runTasks(tasksFile, config, runDir, { startIndex })
1168
+ runTasks(tasksFile, config, runDir, { startIndex, noGit })
894
1169
  .then(() => {
895
1170
  process.exit(0);
896
1171
  })