@litmers/cursorflow-orchestrator 0.1.14 → 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.
@@ -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,14 @@ 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
+ '--output-format', format,
192
195
  '--workspace', workspaceDir,
193
196
  ...(model ? ['--model', model] : []),
194
197
  '--resume', chatId,
@@ -426,33 +429,31 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
426
429
  /**
427
430
  * Wrap prompt with dependency policy
428
431
  */
429
- export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
430
- 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) {
431
436
  return prompt;
432
437
  }
433
438
 
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}`;
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}`;
456
457
  }
457
458
 
458
459
  /**
@@ -491,9 +492,11 @@ export async function runTask({
491
492
  config,
492
493
  index,
493
494
  worktreeDir,
495
+ pipelineBranch,
494
496
  taskBranch,
495
497
  chatId,
496
498
  runDir,
499
+ noGit = false,
497
500
  }: {
498
501
  task: Task;
499
502
  config: RunnerConfig;
@@ -503,13 +506,18 @@ export async function runTask({
503
506
  taskBranch: string;
504
507
  chatId: string;
505
508
  runDir: string;
509
+ noGit?: boolean;
506
510
  }): Promise<TaskExecutionResult> {
507
511
  const model = task.model || config.model || 'sonnet-4.5';
508
512
  const convoPath = path.join(runDir, 'conversation.jsonl');
509
513
 
510
514
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
511
515
  logger.info(`Model: ${model}`);
512
- logger.info(`Branch: ${taskBranch}`);
516
+ if (noGit) {
517
+ logger.info('đŸšĢ noGit mode: skipping branch operations');
518
+ } else {
519
+ logger.info(`Branch: ${taskBranch}`);
520
+ }
513
521
 
514
522
  events.emit('task.started', {
515
523
  taskName: task.name,
@@ -517,14 +525,16 @@ export async function runTask({
517
525
  index,
518
526
  });
519
527
 
520
- // Checkout task branch
521
- git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
528
+ // Checkout task branch (skip in noGit mode)
529
+ if (!noGit) {
530
+ git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
531
+ }
522
532
 
523
533
  // Apply dependency permissions
524
534
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
525
535
 
526
536
  // Run prompt
527
- const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
537
+ const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
528
538
 
529
539
  appendLog(convoPath, createConversationEntry('user', prompt1, {
530
540
  task: task.name,
@@ -547,6 +557,7 @@ export async function runTask({
547
557
  signalDir: runDir,
548
558
  timeout: config.timeout,
549
559
  enableIntervention: config.enableIntervention,
560
+ outputFormat: config.agentOutputFormat,
550
561
  });
551
562
 
552
563
  const duration = Date.now() - startTime;
@@ -588,8 +599,10 @@ export async function runTask({
588
599
  };
589
600
  }
590
601
 
591
- // Push task branch
592
- 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
+ }
593
606
 
594
607
  events.emit('task.completed', {
595
608
  taskName: task.name,
@@ -607,8 +620,13 @@ export async function runTask({
607
620
  /**
608
621
  * Run all tasks in sequence
609
622
  */
610
- 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[]> {
611
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
+ }
612
630
 
613
631
  // Validate configuration before starting
614
632
  logger.info('Validating task configuration...');
@@ -648,7 +666,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
648
666
 
649
667
  logger.success('✓ Cursor authentication OK');
650
668
 
651
- const repoRoot = git.getRepoRoot();
669
+ // In noGit mode, we don't need repoRoot - use current directory
670
+ const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
652
671
 
653
672
  // Load existing state if resuming
654
673
  const statePath = path.join(runDir, 'state.json');
@@ -659,7 +678,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
659
678
  }
660
679
 
661
680
  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);
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));
663
685
 
664
686
  if (startIndex === 0) {
665
687
  logger.section('🚀 Starting Pipeline');
@@ -673,10 +695,16 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
673
695
 
674
696
  // Create worktree only if starting fresh
675
697
  if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
676
- git.createWorktree(worktreeDir, pipelineBranch, {
677
- baseBranch: config.baseBranch || 'main',
678
- cwd: repoRoot,
679
- });
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
+ }
680
708
  }
681
709
 
682
710
  // Create chat
@@ -708,8 +736,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
708
736
 
709
737
  saveState(statePath, state);
710
738
 
711
- // Merge dependencies if any
712
- 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) {
713
741
  logger.section('🔗 Merging Dependencies');
714
742
 
715
743
  // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
@@ -756,6 +784,50 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
756
784
 
757
785
  // Push the merged state
758
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
+ }
759
831
  }
760
832
 
761
833
  // Run tasks
@@ -774,6 +846,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
774
846
  taskBranch,
775
847
  chatId,
776
848
  runDir,
849
+ noGit,
777
850
  });
778
851
 
779
852
  results.push(result);
@@ -807,17 +880,21 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
807
880
  process.exit(1);
808
881
  }
809
882
 
810
- // Merge into pipeline
811
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
812
- git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
813
-
814
- // Log changed files
815
- const stats = git.getLastOperationStats(worktreeDir);
816
- if (stats) {
817
- logger.info('Changed files:\n' + stats);
818
- }
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
+ }
819
893
 
820
- git.push(pipelineBranch, { cwd: worktreeDir });
894
+ git.push(pipelineBranch, { cwd: worktreeDir });
895
+ } else {
896
+ logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
897
+ }
821
898
  }
822
899
 
823
900
  // Complete
@@ -825,6 +902,41 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
825
902
  state.endTime = Date.now();
826
903
  saveState(statePath, state);
827
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
+
828
940
  logger.success('All tasks completed!');
829
941
  return results;
830
942
  }
@@ -844,6 +956,7 @@ if (require.main === module) {
844
956
  const runDirIdx = args.indexOf('--run-dir');
845
957
  const startIdxIdx = args.indexOf('--start-index');
846
958
  const pipelineBranchIdx = args.indexOf('--pipeline-branch');
959
+ const noGit = args.includes('--no-git');
847
960
 
848
961
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
849
962
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
@@ -856,9 +969,10 @@ if (require.main === module) {
856
969
 
857
970
  events.setRunId(runId);
858
971
 
859
- // Load global config to register webhooks in this process
972
+ // Load global config for defaults and webhooks
973
+ let globalConfig;
860
974
  try {
861
- const globalConfig = loadConfig();
975
+ globalConfig = loadConfig();
862
976
  if (globalConfig.webhooks) {
863
977
  registerWebhooks(globalConfig.webhooks);
864
978
  }
@@ -883,14 +997,17 @@ if (require.main === module) {
883
997
  process.exit(1);
884
998
  }
885
999
 
886
- // Add dependency policy defaults
1000
+ // Add defaults from global config or hardcoded
887
1001
  config.dependencyPolicy = config.dependencyPolicy || {
888
- allowDependencyChange: false,
889
- lockfileReadOnly: true,
1002
+ allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
1003
+ lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
890
1004
  };
891
1005
 
1006
+ // Add agent output format default
1007
+ config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
1008
+
892
1009
  // Run tasks
893
- runTasks(tasksFile, config, runDir, { startIndex })
1010
+ runTasks(tasksFile, config, runDir, { startIndex, noGit })
894
1011
  .then(() => {
895
1012
  process.exit(0);
896
1013
  })
@@ -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);
@@ -72,6 +74,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
72
74
  // Advanced
73
75
  worktreePrefix: 'cursorflow-',
74
76
  maxConcurrentLanes: 10,
77
+ agentOutputFormat: 'stream-json',
75
78
 
76
79
  // Webhooks
77
80
  webhooks: [],
@@ -194,6 +197,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
194
197
  // Advanced
195
198
  worktreePrefix: 'cursorflow-',
196
199
  maxConcurrentLanes: 10,
200
+ agentOutputFormat: 'stream-json', // 'stream-json' | 'json' | 'plain'
197
201
 
198
202
  // Webhook configuration
199
203
  // webhooks: [
@@ -384,11 +384,13 @@ export class EnhancedLogManager {
384
384
  private cleanTransform: CleanLogTransform | null = null;
385
385
  private streamingParser: StreamingMessageParser | null = null;
386
386
  private lineBuffer: string = '';
387
+ private onParsedMessage?: (msg: ParsedMessage) => void;
387
388
 
388
- constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}) {
389
+ constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}, onParsedMessage?: (msg: ParsedMessage) => void) {
389
390
  this.config = { ...DEFAULT_LOG_CONFIG, ...config };
390
391
  this.session = session;
391
392
  this.logDir = logDir;
393
+ this.onParsedMessage = onParsedMessage;
392
394
 
393
395
  // Ensure log directory exists
394
396
  fs.mkdirSync(logDir, { recursive: true });
@@ -450,6 +452,9 @@ export class EnhancedLogManager {
450
452
  // Create streaming parser for readable log
451
453
  this.streamingParser = new StreamingMessageParser((msg) => {
452
454
  this.writeReadableMessage(msg);
455
+ if (this.onParsedMessage) {
456
+ this.onParsedMessage(msg);
457
+ }
453
458
  });
454
459
  }
455
460
 
@@ -464,41 +469,44 @@ export class EnhancedLogManager {
464
469
 
465
470
  switch (msg.type) {
466
471
  case 'system':
467
- formatted = `\n${ts}\n${msg.content}\n`;
472
+ formatted = `[${ts}] âš™ī¸ SYSTEM: ${msg.content}\n`;
468
473
  break;
469
474
 
470
475
  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
476
  case 'assistant':
479
477
  case 'result':
480
- // Format assistant response
478
+ // Format with brackets and line (compact)
479
+ const isUser = msg.type === 'user';
481
480
  const isResult = msg.type === 'result';
482
- const header = isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
481
+ const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
483
482
  const duration = msg.metadata?.duration_ms
484
483
  ? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
485
484
  : '';
486
- formatted = `\n${ts}\n┌─ ${header}${duration} ──────────────────────────────────\n${this.indentText(msg.content, '│ ')}\n└───────────────────────────────────────────────────────\n`;
485
+
486
+ const label = `[ ${headerText}${duration} ] `;
487
+ const totalWidth = 80;
488
+ const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
489
+ const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
490
+
491
+ const lines = msg.content.split('\n');
492
+ formatted = `[${ts}] ${topBorder}\n`;
493
+ for (const line of lines) {
494
+ formatted += `[${ts}] │ ${line}\n`;
495
+ }
496
+ formatted += `[${ts}] ${bottomBorder}\n`;
487
497
  break;
488
498
 
489
499
  case 'tool':
490
- // Format tool call
491
- formatted = `${ts} 🔧 ${msg.content}\n`;
500
+ formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
492
501
  break;
493
502
 
494
503
  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`;
504
+ const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
505
+ formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
498
506
  break;
499
507
 
500
508
  default:
501
- formatted = `${ts} ${msg.content}\n`;
509
+ formatted = `[${ts}] ${msg.content}\n`;
502
510
  }
503
511
 
504
512
  try {
@@ -661,9 +669,37 @@ export class EnhancedLogManager {
661
669
  this.cleanTransform.write(data);
662
670
  }
663
671
 
664
- // Parse streaming JSON for readable log
672
+ // Parse streaming JSON for readable log (handles boxes, messages, tool calls)
665
673
  this.parseStreamingData(text);
666
674
 
675
+ // Also include significant info/status lines in readable log (compact)
676
+ if (this.readableLogFd !== null) {
677
+ const lines = text.split('\n');
678
+ for (const line of lines) {
679
+ const cleanLine = stripAnsi(line).trim();
680
+ // Look for log lines: [ISO_DATE] [LEVEL] ...
681
+ if (cleanLine &&
682
+ !cleanLine.startsWith('{') &&
683
+ !this.isNoiseLog(cleanLine) &&
684
+ /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
685
+
686
+ try {
687
+ // Check if it has a level marker
688
+ if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
689
+ // Special formatting for summary
690
+ if (cleanLine.includes('Final Workspace Summary')) {
691
+ const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
692
+ const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
693
+ fs.writeSync(this.readableLogFd, `[${ts}] 📊 SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
694
+ } else {
695
+ fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
696
+ }
697
+ }
698
+ } catch {}
699
+ }
700
+ }
701
+ }
702
+
667
703
  // Write JSON entry (for significant lines only)
668
704
  if (this.config.writeJsonLog) {
669
705
  const cleanText = stripAnsi(text).trim();
@@ -717,6 +753,20 @@ export class EnhancedLogManager {
717
753
  if (this.cleanTransform) {
718
754
  this.cleanTransform.write(data);
719
755
  }
756
+
757
+ // Also include error lines in readable log (compact)
758
+ if (this.readableLogFd !== null) {
759
+ const lines = text.split('\n');
760
+ for (const line of lines) {
761
+ const cleanLine = stripAnsi(line).trim();
762
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
763
+ try {
764
+ const ts = new Date().toISOString();
765
+ fs.writeSync(this.readableLogFd, `[${ts}] ❌ STDERR: ${cleanLine}\n`);
766
+ } catch {}
767
+ }
768
+ }
769
+ }
720
770
 
721
771
  // Write JSON entry
722
772
  if (this.config.writeJsonLog) {
@@ -747,6 +797,15 @@ export class EnhancedLogManager {
747
797
  this.writeToRawLog(line);
748
798
  }
749
799
 
800
+ // Write to readable log (compact)
801
+ if (this.readableLogFd !== null) {
802
+ const typeLabel = level === 'error' ? '❌ ERROR' : level === 'info' ? 'â„šī¸ INFO' : '🔍 DEBUG';
803
+ const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
804
+ try {
805
+ fs.writeSync(this.readableLogFd, formatted);
806
+ } catch {}
807
+ }
808
+
750
809
  if (this.config.writeJsonLog) {
751
810
  this.writeJsonEntry({
752
811
  timestamp: new Date().toISOString(),
@@ -770,6 +829,15 @@ export class EnhancedLogManager {
770
829
  if (this.config.keepRawLogs) {
771
830
  this.writeToRawLog(line);
772
831
  }
832
+
833
+ // Write to readable log (compact)
834
+ if (this.readableLogFd !== null) {
835
+ const ts = new Date().toISOString();
836
+ const formatted = `[${ts}] ━━━ ${title} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
837
+ try {
838
+ fs.writeSync(this.readableLogFd, formatted);
839
+ } catch {}
840
+ }
773
841
  }
774
842
 
775
843
  /**
@@ -922,7 +990,8 @@ export class EnhancedLogManager {
922
990
  export function createLogManager(
923
991
  laneRunDir: string,
924
992
  laneName: string,
925
- config?: Partial<EnhancedLogConfig>
993
+ config?: Partial<EnhancedLogConfig>,
994
+ onParsedMessage?: (msg: ParsedMessage) => void
926
995
  ): EnhancedLogManager {
927
996
  const session: LogSession = {
928
997
  id: `${laneName}-${Date.now().toString(36)}`,
@@ -930,7 +999,7 @@ export function createLogManager(
930
999
  startTime: Date.now(),
931
1000
  };
932
1001
 
933
- return new EnhancedLogManager(laneRunDir, session, config);
1002
+ return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
934
1003
  }
935
1004
 
936
1005
  /**
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
  /**