@litmers/cursorflow-orchestrator 0.1.13 → 0.1.15

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 (76) hide show
  1. package/CHANGELOG.md +37 -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 +759 -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 +9 -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 +13 -1
  26. package/dist/core/orchestrator.js +396 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.d.ts +2 -0
  29. package/dist/core/reviewer.js +24 -2
  30. package/dist/core/reviewer.js.map +1 -1
  31. package/dist/core/runner.d.ts +9 -3
  32. package/dist/core/runner.js +266 -61
  33. package/dist/core/runner.js.map +1 -1
  34. package/dist/utils/config.js +38 -1
  35. package/dist/utils/config.js.map +1 -1
  36. package/dist/utils/enhanced-logger.d.ts +210 -0
  37. package/dist/utils/enhanced-logger.js +1030 -0
  38. package/dist/utils/enhanced-logger.js.map +1 -0
  39. package/dist/utils/events.d.ts +59 -0
  40. package/dist/utils/events.js +37 -0
  41. package/dist/utils/events.js.map +1 -0
  42. package/dist/utils/git.d.ts +11 -0
  43. package/dist/utils/git.js +40 -0
  44. package/dist/utils/git.js.map +1 -1
  45. package/dist/utils/logger.d.ts +2 -0
  46. package/dist/utils/logger.js +4 -1
  47. package/dist/utils/logger.js.map +1 -1
  48. package/dist/utils/types.d.ts +132 -1
  49. package/dist/utils/webhook.d.ts +5 -0
  50. package/dist/utils/webhook.js +109 -0
  51. package/dist/utils/webhook.js.map +1 -0
  52. package/examples/README.md +1 -1
  53. package/package.json +2 -1
  54. package/scripts/patches/test-cursor-agent.js +1 -1
  55. package/scripts/simple-logging-test.sh +97 -0
  56. package/scripts/test-real-cursor-lifecycle.sh +289 -0
  57. package/scripts/test-real-logging.sh +289 -0
  58. package/scripts/test-streaming-multi-task.sh +247 -0
  59. package/src/cli/clean.ts +170 -13
  60. package/src/cli/index.ts +4 -1
  61. package/src/cli/logs.ts +863 -0
  62. package/src/cli/monitor.ts +123 -30
  63. package/src/cli/prepare.ts +1 -1
  64. package/src/cli/resume.ts +463 -22
  65. package/src/cli/run.ts +10 -0
  66. package/src/cli/signal.ts +43 -27
  67. package/src/core/orchestrator.ts +458 -36
  68. package/src/core/reviewer.ts +40 -4
  69. package/src/core/runner.ts +293 -60
  70. package/src/utils/config.ts +41 -1
  71. package/src/utils/enhanced-logger.ts +1166 -0
  72. package/src/utils/events.ts +117 -0
  73. package/src/utils/git.ts +40 -0
  74. package/src/utils/logger.ts +4 -1
  75. package/src/utils/types.ts +160 -1
  76. package/src/utils/webhook.ts +85 -0
@@ -8,6 +8,7 @@ import * as logger from '../utils/logger';
8
8
  import { appendLog, createConversationEntry } from '../utils/state';
9
9
  import * as path from 'path';
10
10
  import { ReviewResult, ReviewIssue, TaskResult, RunnerConfig, AgentSendResult } from '../utils/types';
11
+ import { events } from '../utils/events';
11
12
 
12
13
  /**
13
14
  * Build review prompt
@@ -148,7 +149,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
148
149
  worktreeDir: string;
149
150
  runDir: string;
150
151
  config: RunnerConfig;
151
- cursorAgentSend: (options: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
152
+ cursorAgentSend: (options: {
153
+ workspaceDir: string;
154
+ chatId: string;
155
+ prompt: string;
156
+ model?: string;
157
+ outputFormat?: 'stream-json' | 'json' | 'plain';
158
+ }) => AgentSendResult;
152
159
  cursorAgentCreateChat: () => string;
153
160
  }): Promise<ReviewResult> {
154
161
  const reviewPrompt = buildReviewPrompt({
@@ -159,12 +166,18 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
159
166
 
160
167
  logger.info(`Reviewing: ${taskResult.taskName}`);
161
168
 
169
+ events.emit('review.started', {
170
+ taskName: taskResult.taskName,
171
+ taskBranch: taskResult.taskBranch,
172
+ });
173
+
162
174
  const reviewChatId = cursorAgentCreateChat();
163
- const reviewResult = cursorAgentSend({
175
+ const reviewResult = await cursorAgentSend({
164
176
  workspaceDir: worktreeDir,
165
177
  chatId: reviewChatId,
166
178
  prompt: reviewPrompt,
167
179
  model: config.reviewModel || 'sonnet-4.5-thinking',
180
+ outputFormat: config.agentOutputFormat,
168
181
  });
169
182
 
170
183
  const review = parseReviewResult(reviewResult.resultText || '');
@@ -178,6 +191,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
178
191
 
179
192
  logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
180
193
 
194
+ events.emit('review.completed', {
195
+ taskName: taskResult.taskName,
196
+ status: review.status,
197
+ issueCount: review.issues?.length || 0,
198
+ summary: review.summary,
199
+ });
200
+
181
201
  return review;
182
202
  }
183
203
 
@@ -190,7 +210,13 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
190
210
  runDir: string;
191
211
  config: RunnerConfig;
192
212
  workChatId: string;
193
- cursorAgentSend: (options: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
213
+ cursorAgentSend: (options: {
214
+ workspaceDir: string;
215
+ chatId: string;
216
+ prompt: string;
217
+ model?: string;
218
+ outputFormat?: 'stream-json' | 'json' | 'plain';
219
+ }) => AgentSendResult;
194
220
  cursorAgentCreateChat: () => string;
195
221
  }): Promise<{ approved: boolean; review: ReviewResult; iterations: number; error?: string }> {
196
222
  const maxIterations = config.maxReviewIterations || 3;
@@ -209,6 +235,10 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
209
235
 
210
236
  if (currentReview.status === 'approved') {
211
237
  logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
238
+ events.emit('review.approved', {
239
+ taskName: taskResult.taskName,
240
+ iterations: iteration + 1,
241
+ });
212
242
  return { approved: true, review: currentReview, iterations: iteration + 1 };
213
243
  }
214
244
 
@@ -216,6 +246,11 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
216
246
 
217
247
  if (iteration >= maxIterations) {
218
248
  logger.warn(`Max review iterations (${maxIterations}) reached: ${taskResult.taskName}`);
249
+ events.emit('review.rejected', {
250
+ taskName: taskResult.taskName,
251
+ reason: 'Max iterations reached',
252
+ iterations: iteration,
253
+ });
219
254
  break;
220
255
  }
221
256
 
@@ -223,11 +258,12 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
223
258
  logger.info(`Sending feedback (iteration ${iteration}/${maxIterations})`);
224
259
  const feedbackPrompt = buildFeedbackPrompt(currentReview);
225
260
 
226
- const fixResult = cursorAgentSend({
261
+ const fixResult = await cursorAgentSend({
227
262
  workspaceDir: worktreeDir,
228
263
  chatId: workChatId,
229
264
  prompt: feedbackPrompt,
230
265
  model: config.model,
266
+ outputFormat: config.agentOutputFormat,
231
267
  });
232
268
 
233
269
  if (!fixResult.ok) {
@@ -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;
@@ -171,7 +175,7 @@ export function validateTaskConfig(config: RunnerConfig): void {
171
175
  /**
172
176
  * Execute cursor-agent command with streaming and better error handling
173
177
  */
174
- export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }: {
178
+ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }: {
175
179
  workspaceDir: string;
176
180
  chatId: string;
177
181
  prompt: string;
@@ -180,10 +184,14 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
180
184
  timeout?: number;
181
185
  /** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
182
186
  enableIntervention?: boolean;
187
+ /** Output format for cursor-agent (default: 'stream-json') */
188
+ outputFormat?: 'stream-json' | 'json' | 'plain';
183
189
  }): Promise<AgentSendResult> {
190
+ // Use stream-json format for structured output with tool calls and results
191
+ const format = outputFormat || 'stream-json';
184
192
  const args = [
185
193
  '--print',
186
- '--output-format', 'json',
194
+ '--output-format', format,
187
195
  '--workspace', workspaceDir,
188
196
  ...(model ? ['--model', model] : []),
189
197
  '--resume', chatId,
@@ -240,6 +248,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
240
248
 
241
249
  let fullStdout = '';
242
250
  let fullStderr = '';
251
+ let timeoutHandle: NodeJS.Timeout;
243
252
 
244
253
  // Heartbeat logging to show progress
245
254
  let lastHeartbeat = Date.now();
@@ -251,13 +260,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
251
260
  }, HEARTBEAT_INTERVAL_MS);
252
261
  const startTime = Date.now();
253
262
 
254
- // Watch for "intervention.txt" signal file if any
263
+ // Watch for "intervention.txt" or "timeout.txt" signal files
255
264
  const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
256
- let interventionWatcher: fs.FSWatcher | null = null;
265
+ const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
266
+ let signalWatcher: fs.FSWatcher | null = null;
257
267
 
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)) {
268
+ if (signalDir && fs.existsSync(signalDir)) {
269
+ signalWatcher = fs.watch(signalDir, (event, filename) => {
270
+ // Handle intervention
271
+ if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
261
272
  try {
262
273
  const message = fs.readFileSync(interventionPath, 'utf8').trim();
263
274
  if (message) {
@@ -274,6 +285,40 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
274
285
  logger.warn('Failed to read intervention file');
275
286
  }
276
287
  }
288
+
289
+ // Handle dynamic timeout update
290
+ if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
291
+ try {
292
+ const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
293
+ const newTimeoutMs = parseInt(newTimeoutStr);
294
+
295
+ if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
296
+ logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
297
+
298
+ // Clear old timeout
299
+ if (timeoutHandle) clearTimeout(timeoutHandle);
300
+
301
+ // Set new timeout based on total elapsed time
302
+ const elapsed = Date.now() - startTime;
303
+ const remaining = Math.max(1000, newTimeoutMs - elapsed);
304
+
305
+ timeoutHandle = setTimeout(() => {
306
+ clearInterval(heartbeatInterval);
307
+ child.kill();
308
+ const totalSec = Math.round(newTimeoutMs / 1000);
309
+ resolve({
310
+ ok: false,
311
+ exitCode: -1,
312
+ error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
313
+ });
314
+ }, remaining);
315
+
316
+ fs.unlinkSync(timeoutPath); // Clear it
317
+ }
318
+ } catch (e) {
319
+ logger.warn('Failed to read timeout update file');
320
+ }
321
+ }
277
322
  });
278
323
  }
279
324
 
@@ -295,7 +340,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
295
340
  });
296
341
  }
297
342
 
298
- const timeoutHandle = setTimeout(() => {
343
+ timeoutHandle = setTimeout(() => {
299
344
  clearInterval(heartbeatInterval);
300
345
  child.kill();
301
346
  const timeoutSec = Math.round(timeoutMs / 1000);
@@ -309,7 +354,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
309
354
  child.on('close', (code) => {
310
355
  clearTimeout(timeoutHandle);
311
356
  clearInterval(heartbeatInterval);
312
- if (interventionWatcher) interventionWatcher.close();
357
+ if (signalWatcher) signalWatcher.close();
313
358
 
314
359
  const json = parseJsonFromStdout(fullStdout);
315
360
 
@@ -384,33 +429,31 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
384
429
  /**
385
430
  * Wrap prompt with dependency policy
386
431
  */
387
- export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
388
- if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
432
+ export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy, options: { noGit?: boolean } = {}): string {
433
+ const { noGit = false } = options;
434
+
435
+ if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
389
436
  return prompt;
390
437
  }
391
438
 
392
- return `# Dependency Policy (MUST FOLLOW)
393
-
394
- You are running in a restricted lane.
395
-
396
- - allowDependencyChange: ${policy.allowDependencyChange}
397
- - lockfileReadOnly: ${policy.lockfileReadOnly}
398
-
399
- Rules:
400
- - BEFORE making any code changes, decide whether dependency changes are required.
401
- - If dependency changes are required, DO NOT change any files. Instead reply with:
402
-
403
- DEPENDENCY_CHANGE_REQUIRED
404
- \`\`\`json
405
- { "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
406
- \`\`\`
407
-
408
- Then STOP.
409
- - If dependency changes are NOT required, proceed normally.
410
-
411
- ---
412
-
413
- ${prompt}`;
439
+ let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
440
+
441
+ rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
442
+ rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
443
+
444
+ if (noGit) {
445
+ rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
446
+ }
447
+
448
+ rules += '\nRules:\n';
449
+ rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
450
+ rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
451
+ rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
452
+ rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
453
+ rules += 'Then STOP.\n';
454
+ rules += '- If dependency changes are NOT required, proceed normally.\n';
455
+
456
+ return `${rules}\n---\n\n${prompt}`;
414
457
  }
415
458
 
416
459
  /**
@@ -449,9 +492,11 @@ export async function runTask({
449
492
  config,
450
493
  index,
451
494
  worktreeDir,
495
+ pipelineBranch,
452
496
  taskBranch,
453
497
  chatId,
454
498
  runDir,
499
+ noGit = false,
455
500
  }: {
456
501
  task: Task;
457
502
  config: RunnerConfig;
@@ -461,22 +506,35 @@ export async function runTask({
461
506
  taskBranch: string;
462
507
  chatId: string;
463
508
  runDir: string;
509
+ noGit?: boolean;
464
510
  }): Promise<TaskExecutionResult> {
465
511
  const model = task.model || config.model || 'sonnet-4.5';
466
512
  const convoPath = path.join(runDir, 'conversation.jsonl');
467
513
 
468
514
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
469
515
  logger.info(`Model: ${model}`);
470
- logger.info(`Branch: ${taskBranch}`);
516
+ if (noGit) {
517
+ logger.info('🚫 noGit mode: skipping branch operations');
518
+ } else {
519
+ logger.info(`Branch: ${taskBranch}`);
520
+ }
471
521
 
472
- // Checkout task branch
473
- git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
522
+ events.emit('task.started', {
523
+ taskName: task.name,
524
+ taskBranch,
525
+ index,
526
+ });
527
+
528
+ // Checkout task branch (skip in noGit mode)
529
+ if (!noGit) {
530
+ git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
531
+ }
474
532
 
475
533
  // Apply dependency permissions
476
534
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
477
535
 
478
536
  // Run prompt
479
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
537
+ const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
480
538
 
481
539
  appendLog(convoPath, createConversationEntry('user', prompt1, {
482
540
  task: task.name,
@@ -484,6 +542,13 @@ export async function runTask({
484
542
  }));
485
543
 
486
544
  logger.info('Sending prompt to agent...');
545
+ const startTime = Date.now();
546
+ events.emit('agent.prompt_sent', {
547
+ taskName: task.name,
548
+ model,
549
+ promptLength: prompt1.length,
550
+ });
551
+
487
552
  const r1 = await cursorAgentSend({
488
553
  workspaceDir: worktreeDir,
489
554
  chatId,
@@ -492,14 +557,29 @@ export async function runTask({
492
557
  signalDir: runDir,
493
558
  timeout: config.timeout,
494
559
  enableIntervention: config.enableIntervention,
560
+ outputFormat: config.agentOutputFormat,
495
561
  });
496
562
 
563
+ const duration = Date.now() - startTime;
564
+ events.emit('agent.response_received', {
565
+ taskName: task.name,
566
+ ok: r1.ok,
567
+ duration,
568
+ responseLength: r1.resultText?.length || 0,
569
+ error: r1.error,
570
+ });
571
+
497
572
  appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
498
573
  task: task.name,
499
574
  model,
500
575
  }));
501
576
 
502
577
  if (!r1.ok) {
578
+ events.emit('task.failed', {
579
+ taskName: task.name,
580
+ taskBranch,
581
+ error: r1.error,
582
+ });
503
583
  return {
504
584
  taskName: task.name,
505
585
  taskBranch,
@@ -519,9 +599,17 @@ export async function runTask({
519
599
  };
520
600
  }
521
601
 
522
- // Push task branch
523
- git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
602
+ // Push task branch (skip in noGit mode)
603
+ if (!noGit) {
604
+ git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
605
+ }
524
606
 
607
+ events.emit('task.completed', {
608
+ taskName: task.name,
609
+ taskBranch,
610
+ status: 'FINISHED',
611
+ });
612
+
525
613
  return {
526
614
  taskName: task.name,
527
615
  taskBranch,
@@ -532,8 +620,13 @@ export async function runTask({
532
620
  /**
533
621
  * Run all tasks in sequence
534
622
  */
535
- export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
623
+ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
536
624
  const startIndex = options.startIndex || 0;
625
+ const noGit = options.noGit || config.noGit || false;
626
+
627
+ if (noGit) {
628
+ logger.info('🚫 Running in noGit mode - Git operations will be skipped');
629
+ }
537
630
 
538
631
  // Validate configuration before starting
539
632
  logger.info('Validating task configuration...');
@@ -573,7 +666,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
573
666
 
574
667
  logger.success('✓ Cursor authentication OK');
575
668
 
576
- const repoRoot = git.getRepoRoot();
669
+ // In noGit mode, we don't need repoRoot - use current directory
670
+ const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
577
671
 
578
672
  // Load existing state if resuming
579
673
  const statePath = path.join(runDir, 'state.json');
@@ -584,7 +678,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
584
678
  }
585
679
 
586
680
  const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
587
- const worktreeDir = state?.worktreeDir || path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
681
+ // In noGit mode, use a simple local directory instead of worktree
682
+ const worktreeDir = state?.worktreeDir || (noGit
683
+ ? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
684
+ : path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
588
685
 
589
686
  if (startIndex === 0) {
590
687
  logger.section('🚀 Starting Pipeline');
@@ -598,10 +695,16 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
598
695
 
599
696
  // Create worktree only if starting fresh
600
697
  if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
601
- git.createWorktree(worktreeDir, pipelineBranch, {
602
- baseBranch: config.baseBranch || 'main',
603
- cwd: repoRoot,
604
- });
698
+ if (noGit) {
699
+ // In noGit mode, just create the directory
700
+ logger.info(`Creating work directory: ${worktreeDir}`);
701
+ fs.mkdirSync(worktreeDir, { recursive: true });
702
+ } else {
703
+ git.createWorktree(worktreeDir, pipelineBranch, {
704
+ baseBranch: config.baseBranch || 'main',
705
+ cwd: repoRoot,
706
+ });
707
+ }
605
708
  }
606
709
 
607
710
  // Create chat
@@ -633,8 +736,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
633
736
 
634
737
  saveState(statePath, state);
635
738
 
636
- // Merge dependencies if any
637
- if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
739
+ // Merge dependencies if any (skip in noGit mode)
740
+ if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
638
741
  logger.section('🔗 Merging Dependencies');
639
742
 
640
743
  // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
@@ -667,6 +770,12 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
667
770
  noFf: true,
668
771
  message: `chore: merge dependency ${depName} (${depState.pipelineBranch})`
669
772
  });
773
+
774
+ // Log changed files
775
+ const stats = git.getLastOperationStats(worktreeDir);
776
+ if (stats) {
777
+ logger.info('Changed files:\n' + stats);
778
+ }
670
779
  }
671
780
  } catch (e) {
672
781
  logger.error(`Failed to merge dependency ${depName}: ${e}`);
@@ -675,6 +784,50 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
675
784
 
676
785
  // Push the merged state
677
786
  git.push(pipelineBranch, { cwd: worktreeDir });
787
+ } else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
788
+ logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
789
+
790
+ // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
791
+ const lanesRoot = path.dirname(runDir);
792
+
793
+ for (const depName of config.dependsOn) {
794
+ const depRunDir = path.join(lanesRoot, depName);
795
+ const depStatePath = path.join(depRunDir, 'state.json');
796
+
797
+ if (!fs.existsSync(depStatePath)) {
798
+ continue;
799
+ }
800
+
801
+ try {
802
+ const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
803
+ if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
804
+ logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
805
+
806
+ // Use a simple recursive copy (excluding Git and internal dirs)
807
+ const copyFiles = (src: string, dest: string) => {
808
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
809
+ const entries = fs.readdirSync(src, { withFileTypes: true });
810
+
811
+ for (const entry of entries) {
812
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
813
+
814
+ const srcPath = path.join(src, entry.name);
815
+ const destPath = path.join(dest, entry.name);
816
+
817
+ if (entry.isDirectory()) {
818
+ copyFiles(srcPath, destPath);
819
+ } else {
820
+ fs.copyFileSync(srcPath, destPath);
821
+ }
822
+ }
823
+ };
824
+
825
+ copyFiles(depState.worktreeDir, worktreeDir);
826
+ }
827
+ } catch (e) {
828
+ logger.error(`Failed to copy dependency ${depName}: ${e}`);
829
+ }
830
+ }
678
831
  }
679
832
 
680
833
  // Run tasks
@@ -693,6 +846,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
693
846
  taskBranch,
694
847
  chatId,
695
848
  runDir,
849
+ noGit,
696
850
  });
697
851
 
698
852
  results.push(result);
@@ -706,6 +860,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
706
860
  state.status = 'failed';
707
861
  state.dependencyRequest = result.dependencyRequest || null;
708
862
  saveState(statePath, state);
863
+
864
+ if (result.dependencyRequest) {
865
+ events.emit('lane.dependency_requested', {
866
+ laneName: state.label,
867
+ dependencyRequest: result.dependencyRequest,
868
+ });
869
+ }
870
+
709
871
  logger.warn('Task blocked on dependency change');
710
872
  process.exit(2);
711
873
  }
@@ -718,10 +880,21 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
718
880
  process.exit(1);
719
881
  }
720
882
 
721
- // Merge into pipeline
722
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
723
- git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
724
- git.push(pipelineBranch, { cwd: worktreeDir });
883
+ // Merge into pipeline (skip in noGit mode)
884
+ if (!noGit) {
885
+ logger.info(`Merging ${taskBranch} ${pipelineBranch}`);
886
+ git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
887
+
888
+ // Log changed files
889
+ const stats = git.getLastOperationStats(worktreeDir);
890
+ if (stats) {
891
+ logger.info('Changed files:\n' + stats);
892
+ }
893
+
894
+ git.push(pipelineBranch, { cwd: worktreeDir });
895
+ } else {
896
+ logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
897
+ }
725
898
  }
726
899
 
727
900
  // Complete
@@ -729,6 +902,41 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
729
902
  state.endTime = Date.now();
730
903
  saveState(statePath, state);
731
904
 
905
+ // Log final file summary
906
+ if (noGit) {
907
+ const getFileSummary = (dir: string): { files: number; dirs: number } => {
908
+ let stats = { files: 0, dirs: 0 };
909
+ if (!fs.existsSync(dir)) return stats;
910
+
911
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
912
+ for (const entry of entries) {
913
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
914
+
915
+ if (entry.isDirectory()) {
916
+ stats.dirs++;
917
+ const sub = getFileSummary(path.join(dir, entry.name));
918
+ stats.files += sub.files;
919
+ stats.dirs += sub.dirs;
920
+ } else {
921
+ stats.files++;
922
+ }
923
+ }
924
+ return stats;
925
+ };
926
+
927
+ const summary = getFileSummary(worktreeDir);
928
+ logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
929
+ } else {
930
+ try {
931
+ const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
932
+ if (stats) {
933
+ logger.info('Final Workspace Summary (Git):\n' + stats);
934
+ }
935
+ } catch (e) {
936
+ // Ignore
937
+ }
938
+ }
939
+
732
940
  logger.success('All tasks completed!');
733
941
  return results;
734
942
  }
@@ -747,11 +955,30 @@ if (require.main === module) {
747
955
  const tasksFile = args[0]!;
748
956
  const runDirIdx = args.indexOf('--run-dir');
749
957
  const startIdxIdx = args.indexOf('--start-index');
750
- // const executorIdx = args.indexOf('--executor');
958
+ const pipelineBranchIdx = args.indexOf('--pipeline-branch');
959
+ const noGit = args.includes('--no-git');
751
960
 
752
961
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
753
962
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
754
- // const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
963
+ const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
964
+
965
+ // Extract runId from runDir (format: .../runs/run-123/lanes/lane-name)
966
+ const parts = runDir.split(path.sep);
967
+ const runsIdx = parts.lastIndexOf('runs');
968
+ const runId = runsIdx >= 0 && parts[runsIdx + 1] ? parts[runsIdx + 1]! : `run-${Date.now()}`;
969
+
970
+ events.setRunId(runId);
971
+
972
+ // Load global config for defaults and webhooks
973
+ let globalConfig;
974
+ try {
975
+ globalConfig = loadConfig();
976
+ if (globalConfig.webhooks) {
977
+ registerWebhooks(globalConfig.webhooks);
978
+ }
979
+ } catch (e) {
980
+ // Non-blocking
981
+ }
755
982
 
756
983
  if (!fs.existsSync(tasksFile)) {
757
984
  console.error(`Tasks file not found: ${tasksFile}`);
@@ -762,19 +989,25 @@ if (require.main === module) {
762
989
  let config: RunnerConfig;
763
990
  try {
764
991
  config = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
992
+ if (forcedPipelineBranch) {
993
+ config.pipelineBranch = forcedPipelineBranch;
994
+ }
765
995
  } catch (error: any) {
766
996
  console.error(`Failed to load tasks file: ${error.message}`);
767
997
  process.exit(1);
768
998
  }
769
999
 
770
- // Add dependency policy defaults
1000
+ // Add defaults from global config or hardcoded
771
1001
  config.dependencyPolicy = config.dependencyPolicy || {
772
- allowDependencyChange: false,
773
- lockfileReadOnly: true,
1002
+ allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
1003
+ lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
774
1004
  };
775
1005
 
1006
+ // Add agent output format default
1007
+ config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
1008
+
776
1009
  // Run tasks
777
- runTasks(tasksFile, config, runDir, { startIndex })
1010
+ runTasks(tasksFile, config, runDir, { startIndex, noGit })
778
1011
  .then(() => {
779
1012
  process.exit(0);
780
1013
  })