@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
@@ -17,7 +17,9 @@ export function findProjectRoot(cwd = process.cwd()): string {
17
17
 
18
18
  while (current !== path.parse(current).root) {
19
19
  const packagePath = path.join(current, 'package.json');
20
- if (fs.existsSync(packagePath)) {
20
+ const configPath = path.join(current, 'cursorflow.config.js');
21
+
22
+ if (fs.existsSync(packagePath) || fs.existsSync(configPath)) {
21
23
  return current;
22
24
  }
23
25
  current = path.dirname(current);
@@ -57,6 +59,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
57
59
  // Review
58
60
  enableReview: false,
59
61
  reviewModel: 'sonnet-4.5-thinking',
62
+ reviewAllTasks: false,
60
63
  maxReviewIterations: 3,
61
64
 
62
65
  // Lane defaults
@@ -72,6 +75,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
72
75
  // Advanced
73
76
  worktreePrefix: 'cursorflow-',
74
77
  maxConcurrentLanes: 10,
78
+ agentOutputFormat: 'stream-json',
75
79
 
76
80
  // Webhooks
77
81
  webhooks: [],
@@ -155,10 +159,6 @@ export function validateConfig(config: CursorFlowConfig): boolean {
155
159
  export function createDefaultConfig(projectRoot: string, force = false): string {
156
160
  const configPath = path.join(projectRoot, 'cursorflow.config.js');
157
161
 
158
- if (fs.existsSync(configPath) && !force) {
159
- throw new Error(`Config file already exists: ${configPath}`);
160
- }
161
-
162
162
  const template = `module.exports = {
163
163
  // Directory configuration
164
164
  tasksDir: '_cursorflow/tasks',
@@ -179,6 +179,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
179
179
  // Review configuration
180
180
  enableReview: false,
181
181
  reviewModel: 'sonnet-4.5-thinking',
182
+ reviewAllTasks: false,
182
183
  maxReviewIterations: 3,
183
184
 
184
185
  // Lane configuration
@@ -194,6 +195,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
194
195
  // Advanced
195
196
  worktreePrefix: 'cursorflow-',
196
197
  maxConcurrentLanes: 10,
198
+ agentOutputFormat: 'stream-json', // 'stream-json' | 'json' | 'plain'
197
199
 
198
200
  // Webhook configuration
199
201
  // webhooks: [
@@ -218,6 +220,15 @@ export function createDefaultConfig(projectRoot: string, force = false): string
218
220
  };
219
221
  `;
220
222
 
221
- fs.writeFileSync(configPath, template, 'utf8');
223
+ // Use atomic write with wx flag to avoid TOCTOU race condition (unless force is set)
224
+ try {
225
+ const writeFlag = force ? 'w' : 'wx';
226
+ fs.writeFileSync(configPath, template, { encoding: 'utf8', flag: writeFlag });
227
+ } catch (err: any) {
228
+ if (err.code === 'EEXIST') {
229
+ throw new Error(`Config file already exists: ${configPath}`);
230
+ }
231
+ throw err;
232
+ }
222
233
  return configPath;
223
234
  }
@@ -486,16 +486,46 @@ function validateBranchNames(
486
486
  const remoteBranches = getAllRemoteBranches(repoRoot);
487
487
  const allExistingBranches = new Set([...localBranches, ...remoteBranches]);
488
488
 
489
- // Collect branch prefixes from lanes
489
+ // Collect branch prefixes and pipeline branches from lanes
490
490
  const branchPrefixes: { laneName: string; prefix: string }[] = [];
491
+ const pipelineBranches: { laneName: string; branch: string }[] = [];
491
492
 
492
493
  for (const lane of lanes) {
493
494
  const branchPrefix = lane.json?.branchPrefix;
494
495
  if (branchPrefix) {
495
496
  branchPrefixes.push({ laneName: lane.fileName, prefix: branchPrefix });
496
497
  }
498
+
499
+ const pipelineBranch = lane.json?.pipelineBranch;
500
+ if (pipelineBranch) {
501
+ pipelineBranches.push({ laneName: lane.fileName, branch: pipelineBranch });
502
+ }
497
503
  }
498
504
 
505
+ // Check for pipeline branch collisions
506
+ const pipeMap = new Map<string, string[]>();
507
+ for (const { laneName, branch } of pipelineBranches) {
508
+ const existing = pipeMap.get(branch) || [];
509
+ existing.push(laneName);
510
+ pipeMap.set(branch, existing);
511
+ }
512
+
513
+ for (const [branch, laneNames] of pipeMap) {
514
+ if (laneNames.length > 1) {
515
+ addIssue(issues, {
516
+ id: 'branch.pipeline_collision',
517
+ severity: 'error',
518
+ title: 'Pipeline branch collision',
519
+ message: `Multiple lanes use the same pipelineBranch "${branch}": ${laneNames.join(', ')}`,
520
+ details: 'Each lane should have a unique pipelineBranch to avoid worktree conflicts during parallel execution.',
521
+ fixes: [
522
+ 'Update the pipelineBranch in each lane JSON file to be unique',
523
+ 'Or remove pipelineBranch to let CursorFlow generate unique ones',
524
+ ],
525
+ });
526
+ }
527
+ }
528
+
499
529
  // Check for branch prefix collisions between lanes
500
530
  const prefixMap = new Map<string, string[]>();
501
531
  for (const { laneName, prefix } of branchPrefixes) {
@@ -13,7 +13,7 @@
13
13
 
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
- import { PassThrough, Transform, TransformCallback } from 'stream';
16
+ import { Transform, TransformCallback } from 'stream';
17
17
  import { EnhancedLogConfig } from './types';
18
18
 
19
19
  // Re-export for backwards compatibility
@@ -163,6 +163,23 @@ export class StreamingMessageParser {
163
163
  },
164
164
  });
165
165
  break;
166
+
167
+ case 'thinking':
168
+ // Thinking message (Claude 3.7+ etc.)
169
+ if (json.subtype === 'delta' && json.text) {
170
+ // Check if this is a new message or continuation
171
+ if (this.currentRole !== 'thinking') {
172
+ // Flush previous message if any
173
+ this.flush();
174
+ this.currentRole = 'thinking';
175
+ this.messageStartTime = json.timestamp_ms || Date.now();
176
+ }
177
+ this.currentMessage += json.text;
178
+ } else if (json.subtype === 'completed') {
179
+ // Thinking completed - flush immediately
180
+ this.flush();
181
+ }
182
+ break;
166
183
  }
167
184
  }
168
185
 
@@ -191,7 +208,7 @@ export class StreamingMessageParser {
191
208
  }
192
209
 
193
210
  export interface ParsedMessage {
194
- type: 'system' | 'user' | 'assistant' | 'tool' | 'tool_result' | 'result';
211
+ type: 'system' | 'user' | 'assistant' | 'tool' | 'tool_result' | 'result' | 'thinking';
195
212
  role: string;
196
213
  content: string;
197
214
  timestamp: number;
@@ -384,11 +401,13 @@ export class EnhancedLogManager {
384
401
  private cleanTransform: CleanLogTransform | null = null;
385
402
  private streamingParser: StreamingMessageParser | null = null;
386
403
  private lineBuffer: string = '';
404
+ private onParsedMessage?: (msg: ParsedMessage) => void;
387
405
 
388
- constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}) {
406
+ constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}, onParsedMessage?: (msg: ParsedMessage) => void) {
389
407
  this.config = { ...DEFAULT_LOG_CONFIG, ...config };
390
408
  this.session = session;
391
409
  this.logDir = logDir;
410
+ this.onParsedMessage = onParsedMessage;
392
411
 
393
412
  // Ensure log directory exists
394
413
  fs.mkdirSync(logDir, { recursive: true });
@@ -450,6 +469,9 @@ export class EnhancedLogManager {
450
469
  // Create streaming parser for readable log
451
470
  this.streamingParser = new StreamingMessageParser((msg) => {
452
471
  this.writeReadableMessage(msg);
472
+ if (this.onParsedMessage) {
473
+ this.onParsedMessage(msg);
474
+ }
453
475
  });
454
476
  }
455
477
 
@@ -464,41 +486,59 @@ export class EnhancedLogManager {
464
486
 
465
487
  switch (msg.type) {
466
488
  case 'system':
467
- formatted = `\n${ts}\n${msg.content}\n`;
489
+ formatted = `[${ts}] ⚙️ SYSTEM: ${msg.content}\n`;
468
490
  break;
469
491
 
470
492
  case 'user':
471
- // Format user prompt nicely
472
- const promptPreview = msg.content.length > 200
473
- ? msg.content.substring(0, 200) + '...'
474
- : msg.content;
475
- formatted = `\n${ts}\n┌─ 🧑 USER ─────────────────────────────────────────────\n${this.indentText(promptPreview, '│ ')}\n└───────────────────────────────────────────────────────\n`;
476
- break;
477
-
478
493
  case 'assistant':
479
494
  case 'result':
480
- // Format assistant response
495
+ // Format with brackets and line (compact)
496
+ const isUser = msg.type === 'user';
481
497
  const isResult = msg.type === 'result';
482
- const header = isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
498
+ const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
483
499
  const duration = msg.metadata?.duration_ms
484
500
  ? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
485
501
  : '';
486
- formatted = `\n${ts}\n┌─ ${header}${duration} ──────────────────────────────────\n${this.indentText(msg.content, '│ ')}\n└───────────────────────────────────────────────────────\n`;
502
+
503
+ const label = `[ ${headerText}${duration} ] `;
504
+ const totalWidth = 80;
505
+ const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
506
+ const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
507
+
508
+ const lines = msg.content.split('\n');
509
+ formatted = `[${ts}] ${topBorder}\n`;
510
+ for (const line of lines) {
511
+ formatted += `[${ts}] │ ${line}\n`;
512
+ }
513
+ formatted += `[${ts}] ${bottomBorder}\n`;
487
514
  break;
488
515
 
489
516
  case 'tool':
490
- // Format tool call
491
- formatted = `${ts} 🔧 ${msg.content}\n`;
517
+ formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
492
518
  break;
493
519
 
494
520
  case 'tool_result':
495
- // Format tool result (truncated)
496
- const lines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
497
- formatted = `${ts} 📄 ${msg.metadata?.toolName || 'Tool'}${lines}\n`;
521
+ const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
522
+ formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
523
+ break;
524
+
525
+ case 'thinking':
526
+ // Format thinking block
527
+ const thinkLabel = `[ 🤔 THINKING ] `;
528
+ const thinkWidth = 80;
529
+ const thinkTop = `┌─${thinkLabel}${'─'.repeat(Math.max(0, thinkWidth - thinkLabel.length - 2))}`;
530
+ const thinkBottom = `└─${'─'.repeat(thinkWidth - 2)}`;
531
+
532
+ const thinkLines = msg.content.trim().split('\n');
533
+ formatted = `[${ts}] ${thinkTop}\n`;
534
+ for (const line of thinkLines) {
535
+ formatted += `[${ts}] │ ${line}\n`;
536
+ }
537
+ formatted += `[${ts}] ${thinkBottom}\n`;
498
538
  break;
499
539
 
500
540
  default:
501
- formatted = `${ts} ${msg.content}\n`;
541
+ formatted = `[${ts}] ${msg.content}\n`;
502
542
  }
503
543
 
504
544
  try {
@@ -661,45 +701,106 @@ export class EnhancedLogManager {
661
701
  this.cleanTransform.write(data);
662
702
  }
663
703
 
664
- // Parse streaming JSON for readable log
665
- this.parseStreamingData(text);
704
+ // Process lines for readable log and JSON entries
705
+ this.lineBuffer += text;
706
+ const lines = this.lineBuffer.split('\n');
707
+ this.lineBuffer = lines.pop() || '';
666
708
 
667
- // Write JSON entry (for significant lines only)
668
- if (this.config.writeJsonLog) {
669
- const cleanText = stripAnsi(text).trim();
670
- if (cleanText && !this.isNoiseLog(cleanText)) {
709
+ for (const line of lines) {
710
+ const cleanLine = stripAnsi(line).trim();
711
+ if (!cleanLine) continue;
712
+
713
+ // Handle streaming JSON messages (for boxes, etc. in readable log)
714
+ if (cleanLine.startsWith('{')) {
715
+ if (this.streamingParser) {
716
+ this.streamingParser.parseLine(cleanLine);
717
+ }
718
+
719
+ // Special handling for terminal.jsonl entries for AI messages
720
+ if (this.config.writeJsonLog) {
721
+ try {
722
+ const json = JSON.parse(cleanLine);
723
+ let displayMsg = cleanLine;
724
+ let metadata = { ...json };
725
+
726
+ // Extract cleaner text for significant AI message types
727
+ if (json.type === 'thinking' && json.text) {
728
+ displayMsg = json.text;
729
+ } else if (json.type === 'assistant' && json.message?.content) {
730
+ displayMsg = json.message.content
731
+ .filter((c: any) => c.type === 'text')
732
+ .map((c: any) => c.text)
733
+ .join('');
734
+ } else if (json.type === 'user' && json.message?.content) {
735
+ displayMsg = json.message.content
736
+ .filter((c: any) => c.type === 'text')
737
+ .map((c: any) => c.text)
738
+ .join('');
739
+ } else if (json.type === 'tool_call' && json.subtype === 'started') {
740
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
741
+ const args = json.tool_call[toolName]?.args || {};
742
+ displayMsg = `🔧 CALL: ${toolName}(${JSON.stringify(args)})`;
743
+ } else if (json.type === 'tool_call' && json.subtype === 'completed') {
744
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
745
+ displayMsg = `📄 RESL: ${toolName}`;
746
+ } else if (json.type === 'result') {
747
+ displayMsg = json.result || 'Task completed';
748
+ }
749
+
750
+ this.writeJsonEntry({
751
+ timestamp: new Date().toISOString(),
752
+ level: 'stdout',
753
+ lane: this.session.laneName,
754
+ task: this.session.taskName,
755
+ message: displayMsg.substring(0, 2000), // Larger limit for AI text
756
+ metadata,
757
+ });
758
+ continue; // Already logged this JSON line
759
+ } catch {
760
+ // Not valid JSON or error, fall through to regular logging
761
+ }
762
+ }
763
+ }
764
+
765
+ // Also include significant info/status lines in readable log (compact)
766
+ if (this.readableLogFd !== null) {
767
+ // Look for log lines: [ISO_DATE] [LEVEL] ...
768
+ if (!this.isNoiseLog(cleanLine) && /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
769
+ try {
770
+ // Check if it has a level marker
771
+ if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
772
+ // Special formatting for summary
773
+ if (cleanLine.includes('Final Workspace Summary')) {
774
+ const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
775
+ const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
776
+ fs.writeSync(this.readableLogFd, `[${ts}] 📊 SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
777
+ } else {
778
+ fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
779
+ }
780
+ }
781
+ } catch {}
782
+ }
783
+ }
784
+
785
+ // Write regular non-JSON lines to terminal.jsonl
786
+ if (this.config.writeJsonLog && !this.isNoiseLog(cleanLine)) {
671
787
  this.writeJsonEntry({
672
788
  timestamp: new Date().toISOString(),
673
789
  level: 'stdout',
674
790
  lane: this.session.laneName,
675
791
  task: this.session.taskName,
676
- message: cleanText.substring(0, 1000), // Truncate very long lines
677
- raw: this.config.keepRawLogs ? undefined : text.substring(0, 1000),
792
+ message: cleanLine.substring(0, 1000),
793
+ raw: this.config.keepRawLogs ? undefined : line.substring(0, 1000),
678
794
  });
679
795
  }
680
796
  }
681
797
  }
682
798
 
683
799
  /**
684
- * Parse streaming JSON data for readable log
800
+ * Parse streaming JSON data for readable log - legacy, integrated into writeStdout
685
801
  */
686
802
  private parseStreamingData(text: string): void {
687
- if (!this.streamingParser) return;
688
-
689
- // Buffer incomplete lines
690
- this.lineBuffer += text;
691
- const lines = this.lineBuffer.split('\n');
692
-
693
- // Keep the last incomplete line in buffer
694
- this.lineBuffer = lines.pop() || '';
695
-
696
- // Parse complete lines
697
- for (const line of lines) {
698
- const trimmed = line.trim();
699
- if (trimmed.startsWith('{')) {
700
- this.streamingParser.parseLine(trimmed);
701
- }
702
- }
803
+ // Legacy method, no longer used but kept for internal references if any
703
804
  }
704
805
 
705
806
  /**
@@ -717,6 +818,20 @@ export class EnhancedLogManager {
717
818
  if (this.cleanTransform) {
718
819
  this.cleanTransform.write(data);
719
820
  }
821
+
822
+ // Also include error lines in readable log (compact)
823
+ if (this.readableLogFd !== null) {
824
+ const lines = text.split('\n');
825
+ for (const line of lines) {
826
+ const cleanLine = stripAnsi(line).trim();
827
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
828
+ try {
829
+ const ts = new Date().toISOString();
830
+ fs.writeSync(this.readableLogFd, `[${ts}] ❌ STDERR: ${cleanLine}\n`);
831
+ } catch {}
832
+ }
833
+ }
834
+ }
720
835
 
721
836
  // Write JSON entry
722
837
  if (this.config.writeJsonLog) {
@@ -747,6 +862,15 @@ export class EnhancedLogManager {
747
862
  this.writeToRawLog(line);
748
863
  }
749
864
 
865
+ // Write to readable log (compact)
866
+ if (this.readableLogFd !== null) {
867
+ const typeLabel = level === 'error' ? '❌ ERROR' : level === 'info' ? 'ℹ️ INFO' : '🔍 DEBUG';
868
+ const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
869
+ try {
870
+ fs.writeSync(this.readableLogFd, formatted);
871
+ } catch {}
872
+ }
873
+
750
874
  if (this.config.writeJsonLog) {
751
875
  this.writeJsonEntry({
752
876
  timestamp: new Date().toISOString(),
@@ -770,6 +894,15 @@ export class EnhancedLogManager {
770
894
  if (this.config.keepRawLogs) {
771
895
  this.writeToRawLog(line);
772
896
  }
897
+
898
+ // Write to readable log (compact)
899
+ if (this.readableLogFd !== null) {
900
+ const ts = new Date().toISOString();
901
+ const formatted = `[${ts}] ━━━ ${title} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
902
+ try {
903
+ fs.writeSync(this.readableLogFd, formatted);
904
+ } catch {}
905
+ }
773
906
  }
774
907
 
775
908
  /**
@@ -805,7 +938,7 @@ export class EnhancedLogManager {
805
938
 
806
939
  // Skip common progress/spinner patterns
807
940
  const noisePatterns = [
808
- /^[\s│├└─┌┐┘┴┬┤├]+$/, // Box drawing only
941
+ /^[\s│├└─┌┐┘┴┬┤]+$/, // Box drawing only (removed duplicate ├)
809
942
  /^[.\s]+$/, // Dots only
810
943
  /^[=>\s-]+$/, // Progress bar characters
811
944
  /^\d+%$/, // Percentage only
@@ -922,7 +1055,8 @@ export class EnhancedLogManager {
922
1055
  export function createLogManager(
923
1056
  laneRunDir: string,
924
1057
  laneName: string,
925
- config?: Partial<EnhancedLogConfig>
1058
+ config?: Partial<EnhancedLogConfig>,
1059
+ onParsedMessage?: (msg: ParsedMessage) => void
926
1060
  ): EnhancedLogManager {
927
1061
  const session: LogSession = {
928
1062
  id: `${laneName}-${Date.now().toString(36)}`,
@@ -930,7 +1064,7 @@ export function createLogManager(
930
1064
  startTime: Date.now(),
931
1065
  };
932
1066
 
933
- return new EnhancedLogManager(laneRunDir, session, config);
1067
+ return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
934
1068
  }
935
1069
 
936
1070
  /**
package/src/utils/git.ts CHANGED
@@ -217,12 +217,27 @@ export function commit(message: string, options: { cwd?: string; addAll?: boolea
217
217
  runGit(['commit', '-m', message], { cwd });
218
218
  }
219
219
 
220
+ /**
221
+ * Check if a remote exists
222
+ */
223
+ export function remoteExists(remoteName = 'origin', options: { cwd?: string } = {}): boolean {
224
+ const result = runGitResult(['remote'], { cwd: options.cwd });
225
+ if (!result.success) return false;
226
+ return result.stdout.split('\n').map(r => r.trim()).includes(remoteName);
227
+ }
228
+
220
229
  /**
221
230
  * Push to remote
222
231
  */
223
232
  export function push(branchName: string, options: { cwd?: string; force?: boolean; setUpstream?: boolean } = {}): void {
224
233
  const { cwd, force = false, setUpstream = false } = options;
225
234
 
235
+ // Check if origin exists before pushing
236
+ if (!remoteExists('origin', { cwd })) {
237
+ // If no origin, just skip pushing (useful for local tests)
238
+ return;
239
+ }
240
+
226
241
  const args = ['push'];
227
242
 
228
243
  if (force) {
@@ -16,7 +16,9 @@ export const COLORS = {
16
16
  green: '\x1b[32m',
17
17
  blue: '\x1b[34m',
18
18
  cyan: '\x1b[36m',
19
+ magenta: '\x1b[35m',
19
20
  gray: '\x1b[90m',
21
+ bold: '\x1b[1m',
20
22
  };
21
23
 
22
24
  let currentLogLevel: number = LogLevel.info;
@@ -38,7 +40,8 @@ export function setLogLevel(level: string | number): void {
38
40
  function formatMessage(level: string, message: string, emoji = ''): string {
39
41
  const timestamp = new Date().toISOString();
40
42
  const prefix = emoji ? `${emoji} ` : '';
41
- return `[${timestamp}] [${level.toUpperCase()}] ${prefix}${message}`;
43
+ const lines = String(message).split('\n');
44
+ return lines.map(line => `[${timestamp}] [${level.toUpperCase()}] ${prefix}${line}`).join('\n');
42
45
  }
43
46
 
44
47
  /**
@@ -0,0 +1,54 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { createLogManager } from './enhanced-logger';
4
+
5
+ async function testThinkingLogs() {
6
+ const testDir = path.join(process.cwd(), '_test_thinking_logs');
7
+ if (fs.existsSync(testDir)) {
8
+ fs.rmSync(testDir, { recursive: true });
9
+ }
10
+ fs.mkdirSync(testDir, { recursive: true });
11
+
12
+ console.log('--- Initializing Log Manager ---');
13
+ const manager = createLogManager(testDir, 'test-lane-thinking', {
14
+ writeJsonLog: true,
15
+ keepRawLogs: true
16
+ });
17
+
18
+ manager.setTask('repro-thinking-task', 'sonnet-4.5-thinking');
19
+
20
+ const logLines = [
21
+ '{"type":"tool_call","subtype":"started","call_id":"0_tool_54a8fcc9-6981-4f59-aeb6-3ab6d37b2","tool_call":{"readToolCall":{"args":{"path":"/home/eugene/workbench/workbench-os-eungjin/_cursorflow/worktrees/cursorflow/run-mjfxp57i/agent_output.txt"}}}}',
22
+ '{"type":"thinking","subtype":"delta","text":"**Defining Installation Strategy**\\n\\nI\'ve considered the `package.json` file as the central point for installation in automated environments. Thinking now about how that impacts the user\'s ultimate goal, given this is how the process begins in these environments.\\n\\n\\n"}',
23
+ '{"type":"thinking","subtype":"delta","text":"**Clarifying Execution Context**\\n\\nI\'m focused on the user\'s explicit command: `pnpm add @convex-dev/agent ai @ai-sdk/google zod`. My inability to directly execute this is a key constraint. I\'m exploring ways to inform the user about this. I\'ve considered that, without the tools required to run the original command, any attempt to run a command like `grep` or `date` is pointless and will fail.\\n\\n\\n"}',
24
+ '{"type":"tool_call","subtype":"started","call_id":"0_tool_d8f826c8-9d8f-4cab-9ff8-1c47d1ac1","tool_call":{"shellToolCall":{"args":{"command":"date"}}}}'
25
+ ];
26
+
27
+ console.log('\n--- Feeding Log Lines to Manager ---');
28
+ for (const line of logLines) {
29
+ console.log('Processing:', line.substring(0, 100) + '...');
30
+ manager.writeStdout(line + '\n');
31
+ }
32
+
33
+ manager.close();
34
+
35
+ console.log('\n--- Verifying terminal-readable.log ---');
36
+ const readableLog = fs.readFileSync(path.join(testDir, 'terminal-readable.log'), 'utf8');
37
+ console.log(readableLog);
38
+
39
+ console.log('\n--- Verifying terminal.jsonl (last 3 entries) ---');
40
+ const jsonlLog = fs.readFileSync(path.join(testDir, 'terminal.jsonl'), 'utf8');
41
+ const lines = jsonlLog.trim().split('\n');
42
+ for (const line of lines.slice(-3)) {
43
+ const parsed = JSON.parse(line);
44
+ console.log(JSON.stringify({
45
+ level: parsed.level,
46
+ message: parsed.message.substring(0, 50) + '...',
47
+ hasMetadata: !!parsed.metadata,
48
+ metadataType: parsed.metadata?.type
49
+ }, null, 2));
50
+ }
51
+ }
52
+
53
+ testThinkingLogs().catch(console.error);
54
+
@@ -18,6 +18,7 @@ export interface CursorFlowConfig {
18
18
  lockfileReadOnly: boolean;
19
19
  enableReview: boolean;
20
20
  reviewModel: string;
21
+ reviewAllTasks?: boolean;
21
22
  maxReviewIterations: number;
22
23
  defaultLaneConfig: LaneConfig;
23
24
  logLevel: string;
@@ -25,6 +26,8 @@ export interface CursorFlowConfig {
25
26
  worktreePrefix: string;
26
27
  maxConcurrentLanes: number;
27
28
  projectRoot: string;
29
+ /** Output format for cursor-agent (default: 'stream-json') */
30
+ agentOutputFormat: 'stream-json' | 'json' | 'plain';
28
31
  webhooks?: WebhookConfig[];
29
32
  /** Enhanced logging configuration */
30
33
  enhancedLogging?: Partial<EnhancedLogConfig>;
@@ -187,6 +190,10 @@ export interface Task {
187
190
  model?: string;
188
191
  /** Acceptance criteria for the AI reviewer to validate */
189
192
  acceptanceCriteria?: string[];
193
+ /** Task-level dependencies (format: "lane:task") */
194
+ dependsOn?: string[];
195
+ /** Task execution timeout in milliseconds. Overrides lane-level timeout. */
196
+ timeout?: number;
190
197
  }
191
198
 
192
199
  export interface RunnerConfig {
@@ -198,7 +205,11 @@ export interface RunnerConfig {
198
205
  baseBranch?: string;
199
206
  model?: string;
200
207
  dependencyPolicy: DependencyPolicy;
208
+ enableReview?: boolean;
209
+ /** Output format for cursor-agent (default: 'stream-json') */
210
+ agentOutputFormat?: 'stream-json' | 'json' | 'plain';
201
211
  reviewModel?: string;
212
+ reviewAllTasks?: boolean;
202
213
  maxReviewIterations?: number;
203
214
  acceptanceCriteria?: string[];
204
215
  /** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
@@ -209,6 +220,12 @@ export interface RunnerConfig {
209
220
  * Default: false
210
221
  */
211
222
  enableIntervention?: boolean;
223
+ /**
224
+ * Disable Git operations (worktree, branch, push, commit).
225
+ * Useful for testing or environments without Git remote.
226
+ * Default: false
227
+ */
228
+ noGit?: boolean;
212
229
  }
213
230
 
214
231
  export interface DependencyRequestPlan {
@@ -253,6 +270,7 @@ export interface ReviewResult {
253
270
  export interface TaskResult {
254
271
  taskName: string;
255
272
  taskBranch: string;
273
+ acceptanceCriteria?: string[];
256
274
  [key: string]: any;
257
275
  }
258
276
 
@@ -271,6 +289,10 @@ export interface LaneState {
271
289
  tasksFile?: string; // Original tasks file path
272
290
  dependsOn?: string[];
273
291
  pid?: number;
292
+ /** List of completed task names in this lane */
293
+ completedTasks?: string[];
294
+ /** Task-level dependencies currently being waited for (format: "lane:task") */
295
+ waitingFor?: string[];
274
296
  }
275
297
 
276
298
  export interface ConversationEntry {
@@ -56,6 +56,9 @@ async function sendWebhook(config: WebhookConfig, event: CursorFlowEvent) {
56
56
  const controller = new AbortController();
57
57
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
58
58
 
59
+ // SECURITY NOTE: Intentionally sending event data to configured webhook URLs.
60
+ // This is the expected behavior - users explicitly configure webhook endpoints
61
+ // to receive CursorFlow events. The data is JSON-serialized event metadata.
59
62
  const response = await fetch(config.url, {
60
63
  method: 'POST',
61
64
  headers,