@covibes/zeroshot 1.3.0 → 1.5.0

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,53 @@
1
+ # [1.5.0](https://github.com/covibes/zeroshot/compare/v1.4.0...v1.5.0) (2025-12-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **agent:** stop polling after max failures and fix status matching ([7a0fbfe](https://github.com/covibes/zeroshot/commit/7a0fbfe5439f428bdf8e0bcadbd308542221b6f1))
7
+ * **ci:** skip pre-push hook in CI environment ([352b013](https://github.com/covibes/zeroshot/commit/352b013b71fcea7c2d484c8274fe7c42139c65ea))
8
+ * **cli:** prevent terminal garbling with status footer coordination ([2716ce5](https://github.com/covibes/zeroshot/commit/2716ce55eae9a08107788200ab798c3f76815820))
9
+ * **config:** add timeout: 0 default to agent configuration ([6ff66c0](https://github.com/covibes/zeroshot/commit/6ff66c093bf8dfd5048b468fbd250cbfc0d9dbc1))
10
+ * **deps:** regenerate package-lock.json for jscpd dependencies ([c46d84c](https://github.com/covibes/zeroshot/commit/c46d84c3fc1fbb3d00585922613ff36d829d917a))
11
+ * **infra:** improve container cleanup and npm install robustness ([6c04b46](https://github.com/covibes/zeroshot/commit/6c04b46374bd3041a8b5c185ca163009f2fb6635))
12
+ * **orchestrator:** prevent 0-message clusters on SIGINT during init ([33ed8f9](https://github.com/covibes/zeroshot/commit/33ed8f9b90d92da6bf9caf2a7b5e52eadbcecc9f))
13
+ * **template-resolver:** apply param defaults before resolving placeholders ([eafdd62](https://github.com/covibes/zeroshot/commit/eafdd62fce381a7b9a7cb9787cd06e13f421171b))
14
+ * **templates:** add timeout parameter to all base templates ([f853ed3](https://github.com/covibes/zeroshot/commit/f853ed39e0e566afaf31040ce94923a2dcc7bfb9))
15
+
16
+
17
+ ### Features
18
+
19
+ * **agents:** add git prohibition + minimal output instructions ([6f6496c](https://github.com/covibes/zeroshot/commit/6f6496c5db29073ebbeb6229ac128a5f62d7591f))
20
+ * mechanical enforcement of 7 antipatterns ([4286091](https://github.com/covibes/zeroshot/commit/428609163f9405a8d4b9e84adaee0edbc6bbb7d1)), closes [#1](https://github.com/covibes/zeroshot/issues/1) [#5](https://github.com/covibes/zeroshot/issues/5) [#2](https://github.com/covibes/zeroshot/issues/2) [#4](https://github.com/covibes/zeroshot/issues/4) [#3](https://github.com/covibes/zeroshot/issues/3) [#7](https://github.com/covibes/zeroshot/issues/7) [covibes/covibes#635](https://github.com/covibes/covibes/issues/635)
21
+ * **templates:** add timeout parameter to worker-validator agents ([ee8b17b](https://github.com/covibes/zeroshot/commit/ee8b17bc76aa29bb692965fddbc5a993749f11f9))
22
+
23
+ # [1.5.0](https://github.com/covibes/zeroshot/compare/v1.4.0...v1.5.0) (2025-12-28)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **agent:** stop polling after max failures and fix status matching ([7a0fbfe](https://github.com/covibes/zeroshot/commit/7a0fbfe5439f428bdf8e0bcadbd308542221b6f1))
29
+ * **cli:** prevent terminal garbling with status footer coordination ([2716ce5](https://github.com/covibes/zeroshot/commit/2716ce55eae9a08107788200ab798c3f76815820))
30
+ * **config:** add timeout: 0 default to agent configuration ([6ff66c0](https://github.com/covibes/zeroshot/commit/6ff66c093bf8dfd5048b468fbd250cbfc0d9dbc1))
31
+ * **deps:** regenerate package-lock.json for jscpd dependencies ([c46d84c](https://github.com/covibes/zeroshot/commit/c46d84c3fc1fbb3d00585922613ff36d829d917a))
32
+ * **infra:** improve container cleanup and npm install robustness ([6c04b46](https://github.com/covibes/zeroshot/commit/6c04b46374bd3041a8b5c185ca163009f2fb6635))
33
+ * **orchestrator:** prevent 0-message clusters on SIGINT during init ([33ed8f9](https://github.com/covibes/zeroshot/commit/33ed8f9b90d92da6bf9caf2a7b5e52eadbcecc9f))
34
+ * **template-resolver:** apply param defaults before resolving placeholders ([eafdd62](https://github.com/covibes/zeroshot/commit/eafdd62fce381a7b9a7cb9787cd06e13f421171b))
35
+ * **templates:** add timeout parameter to all base templates ([f853ed3](https://github.com/covibes/zeroshot/commit/f853ed39e0e566afaf31040ce94923a2dcc7bfb9))
36
+
37
+
38
+ ### Features
39
+
40
+ * **agents:** add git prohibition + minimal output instructions ([6f6496c](https://github.com/covibes/zeroshot/commit/6f6496c5db29073ebbeb6229ac128a5f62d7591f))
41
+ * mechanical enforcement of 7 antipatterns ([4286091](https://github.com/covibes/zeroshot/commit/428609163f9405a8d4b9e84adaee0edbc6bbb7d1)), closes [#1](https://github.com/covibes/zeroshot/issues/1) [#5](https://github.com/covibes/zeroshot/issues/5) [#2](https://github.com/covibes/zeroshot/issues/2) [#4](https://github.com/covibes/zeroshot/issues/4) [#3](https://github.com/covibes/zeroshot/issues/3) [#7](https://github.com/covibes/zeroshot/issues/7) [covibes/covibes#635](https://github.com/covibes/covibes/issues/635)
42
+ * **templates:** add timeout parameter to worker-validator agents ([ee8b17b](https://github.com/covibes/zeroshot/commit/ee8b17bc76aa29bb692965fddbc5a993749f11f9))
43
+
44
+ # [1.4.0](https://github.com/covibes/zeroshot/compare/v1.3.0...v1.4.0) (2025-12-28)
45
+
46
+
47
+ ### Features
48
+
49
+ * **status-footer:** atomic writes + token cost display ([7baf0c2](https://github.com/covibes/zeroshot/commit/7baf0c228dd5f3489013f75a1782abe6cbe39661))
50
+
1
51
  # [1.3.0](https://github.com/covibes/zeroshot/compare/v1.2.0...v1.3.0) (2025-12-28)
2
52
 
3
53
 
package/cli/index.js CHANGED
@@ -60,6 +60,39 @@ let activeClusterId = null;
60
60
  /** @type {import('../src/orchestrator') | null} */
61
61
  let orchestratorInstance = null;
62
62
 
63
+ // Track active status footer for safe output routing
64
+ // When set, all output routes through statusFooter.print() to prevent garbling
65
+ /** @type {import('../src/status-footer').StatusFooter | null} */
66
+ let activeStatusFooter = null;
67
+
68
+ /**
69
+ * Safe print - routes through statusFooter when active to prevent garbling
70
+ * @param {...any} args - Arguments to print (like console.log)
71
+ */
72
+ function safePrint(...args) {
73
+ const text = args.map(arg =>
74
+ typeof arg === 'string' ? arg : String(arg)
75
+ ).join(' ');
76
+
77
+ if (activeStatusFooter) {
78
+ activeStatusFooter.print(text + '\n');
79
+ } else {
80
+ console.log(...args);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Safe write - routes through statusFooter when active
86
+ * @param {string} text - Text to write
87
+ */
88
+ function safeWrite(text) {
89
+ if (activeStatusFooter) {
90
+ activeStatusFooter.print(text);
91
+ } else {
92
+ process.stdout.write(text);
93
+ }
94
+ }
95
+
63
96
  /**
64
97
  * Handle fatal errors: log, cleanup cluster state, exit
65
98
  * @param {string} type - 'uncaughtException' or 'unhandledRejection'
@@ -613,6 +646,9 @@ Input formats:
613
646
  });
614
647
  statusFooter.setCluster(clusterId);
615
648
  statusFooter.setClusterState('running');
649
+ statusFooter.setMessageBus(cluster.messageBus);
650
+ // Set module-level reference so safePrint/safeWrite route through footer
651
+ activeStatusFooter = statusFooter;
616
652
 
617
653
  // Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
618
654
  const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
@@ -697,14 +733,16 @@ Input formats:
697
733
  if (status.state !== 'running') {
698
734
  clearInterval(checkInterval);
699
735
  clearInterval(flushInterval);
700
- // Stop status footer
701
- statusFooter.stop();
702
736
  lifecycleUnsubscribe();
703
- // Final flush
737
+ // Final flush BEFORE stopping status footer
738
+ // (statusFooter.stop() sends ANSI codes that can clear terminal area)
704
739
  for (const sender of sendersWithOutput) {
705
740
  const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
706
741
  flushLineBuffer(prefix, sender);
707
742
  }
743
+ // Stop status footer AFTER output is done
744
+ statusFooter.stop();
745
+ activeStatusFooter = null;
708
746
  unsubscribe();
709
747
  resolve();
710
748
  }
@@ -713,6 +751,7 @@ Input formats:
713
751
  clearInterval(checkInterval);
714
752
  clearInterval(flushInterval);
715
753
  statusFooter.stop();
754
+ activeStatusFooter = null;
716
755
  lifecycleUnsubscribe();
717
756
  unsubscribe();
718
757
  resolve();
@@ -725,6 +764,7 @@ Input formats:
725
764
  process.on('SIGINT', async () => {
726
765
  // Stop status footer first to restore terminal
727
766
  statusFooter.stop();
767
+ activeStatusFooter = null;
728
768
  lifecycleUnsubscribe();
729
769
 
730
770
  console.log(chalk.dim('\n\n--- Interrupted ---'));
@@ -749,6 +789,22 @@ Input formats:
749
789
  }
750
790
 
751
791
  // Daemon mode: cluster runs in background, stay alive via orchestrator's setInterval
792
+ // Add cleanup handlers for daemon mode to ensure container cleanup on process exit
793
+ // CRITICAL: Without this, containers become orphaned when daemon process dies
794
+ if (process.env.CREW_DAEMON) {
795
+ const cleanup = async (signal) => {
796
+ console.log(`\n[DAEMON] Received ${signal}, cleaning up cluster ${clusterId}...`);
797
+ try {
798
+ await orchestrator.stop(clusterId);
799
+ console.log(`[DAEMON] Cluster ${clusterId} stopped.`);
800
+ } catch (e) {
801
+ console.error(`[DAEMON] Cleanup error: ${e.message}`);
802
+ }
803
+ process.exit(0);
804
+ };
805
+ process.on('SIGTERM', () => cleanup('SIGTERM'));
806
+ process.on('SIGINT', () => cleanup('SIGINT'));
807
+ }
752
808
  } catch (error) {
753
809
  console.error('Error:', error.message);
754
810
  process.exit(1);
@@ -3569,32 +3625,40 @@ function accumulateText(prefix, sender, text) {
3569
3625
  buf.textBuffer = buf.textBuffer.slice(newlineIdx + 1);
3570
3626
 
3571
3627
  // Word wrap and print the complete line
3628
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3572
3629
  const wrappedLines = wordWrap(completeLine, contentWidth);
3630
+ let outputBuffer = '';
3631
+
3573
3632
  for (let i = 0; i < wrappedLines.length; i++) {
3574
3633
  const wrappedLine = wrappedLines[i];
3575
3634
 
3576
3635
  // Print prefix (real or continuation)
3577
3636
  if (buf.needsPrefix) {
3578
- process.stdout.write(`${prefix} `);
3637
+ outputBuffer += `${prefix} `;
3579
3638
  buf.needsPrefix = false;
3580
3639
  } else if (i > 0) {
3581
- process.stdout.write(`${continuationPrefix}`);
3640
+ outputBuffer += `${continuationPrefix}`;
3582
3641
  }
3583
3642
 
3584
3643
  if (wrappedLine.trim()) {
3585
- process.stdout.write(formatInlineMarkdown(wrappedLine));
3644
+ outputBuffer += formatInlineMarkdown(wrappedLine);
3586
3645
  }
3587
3646
 
3588
3647
  // Newline after each wrapped segment
3589
3648
  if (i < wrappedLines.length - 1) {
3590
- process.stdout.write('\n');
3649
+ outputBuffer += '\n';
3591
3650
  }
3592
3651
  }
3593
3652
 
3594
3653
  // Complete the line
3595
- process.stdout.write('\n');
3654
+ outputBuffer += '\n';
3596
3655
  buf.needsPrefix = true;
3597
3656
  buf.pendingNewline = false;
3657
+
3658
+ // Single atomic write prevents interleaving
3659
+ if (outputBuffer) {
3660
+ safeWrite(outputBuffer);
3661
+ }
3598
3662
  }
3599
3663
 
3600
3664
  // Mark that we have pending text (no newline yet)
@@ -3619,35 +3683,45 @@ function accumulateThinking(prefix, sender, text) {
3619
3683
  const newlineIdx = remaining.indexOf('\n');
3620
3684
  const rawLine = newlineIdx === -1 ? remaining : remaining.slice(0, newlineIdx);
3621
3685
 
3686
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3622
3687
  const wrappedLines = wordWrap(rawLine, contentWidth);
3688
+ let outputBuffer = '';
3623
3689
 
3624
3690
  for (let i = 0; i < wrappedLines.length; i++) {
3625
3691
  const wrappedLine = wrappedLines[i];
3626
3692
 
3627
3693
  if (buf.thinkingNeedsPrefix) {
3628
- process.stdout.write(`${prefix} ${chalk.dim.italic('💭 ')}`);
3694
+ outputBuffer += `${prefix} ${chalk.dim.italic('💭 ')}`;
3629
3695
  buf.thinkingNeedsPrefix = false;
3630
3696
  } else if (i > 0) {
3631
- process.stdout.write(`${continuationPrefix}`);
3697
+ outputBuffer += `${continuationPrefix}`;
3632
3698
  }
3633
3699
 
3634
3700
  if (wrappedLine.trim()) {
3635
- process.stdout.write(chalk.dim.italic(wrappedLine));
3701
+ outputBuffer += chalk.dim.italic(wrappedLine);
3636
3702
  }
3637
3703
 
3638
3704
  if (i < wrappedLines.length - 1) {
3639
- process.stdout.write('\n');
3705
+ outputBuffer += '\n';
3640
3706
  }
3641
3707
  }
3642
3708
 
3643
3709
  if (newlineIdx === -1) {
3644
3710
  buf.thinkingPendingNewline = true;
3711
+ // Single atomic write
3712
+ if (outputBuffer) {
3713
+ safeWrite(outputBuffer);
3714
+ }
3645
3715
  break;
3646
3716
  } else {
3647
- process.stdout.write('\n');
3717
+ outputBuffer += '\n';
3648
3718
  buf.thinkingNeedsPrefix = true;
3649
3719
  buf.thinkingPendingNewline = false;
3650
3720
  remaining = remaining.slice(newlineIdx + 1);
3721
+ // Single atomic write
3722
+ if (outputBuffer) {
3723
+ safeWrite(outputBuffer);
3724
+ }
3651
3725
  }
3652
3726
  }
3653
3727
  }
@@ -3657,7 +3731,10 @@ function flushLineBuffer(prefix, sender) {
3657
3731
  const buf = lineBuffers.get(sender);
3658
3732
  if (!buf) return;
3659
3733
 
3660
- // CRITICAL: Flush any remaining text in textBuffer (text without trailing newline)
3734
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3735
+ let outputBuffer = '';
3736
+
3737
+ // Flush any remaining text in textBuffer (text without trailing newline)
3661
3738
  if (buf.textBuffer && buf.textBuffer.length > 0) {
3662
3739
  // Calculate widths for word wrapping (same as accumulateText)
3663
3740
  const prefixLen = chalk.reset(prefix).replace(/\\x1b\[[0-9;]*m/g, '').length + 1;
@@ -3670,18 +3747,18 @@ function flushLineBuffer(prefix, sender) {
3670
3747
  const wrappedLine = wrappedLines[i];
3671
3748
 
3672
3749
  if (buf.needsPrefix) {
3673
- process.stdout.write(`${prefix} `);
3750
+ outputBuffer += `${prefix} `;
3674
3751
  buf.needsPrefix = false;
3675
3752
  } else if (i > 0) {
3676
- process.stdout.write(`${continuationPrefix}`);
3753
+ outputBuffer += `${continuationPrefix}`;
3677
3754
  }
3678
3755
 
3679
3756
  if (wrappedLine.trim()) {
3680
- process.stdout.write(formatInlineMarkdown(wrappedLine));
3757
+ outputBuffer += formatInlineMarkdown(wrappedLine);
3681
3758
  }
3682
3759
 
3683
3760
  if (i < wrappedLines.length - 1) {
3684
- process.stdout.write('\n');
3761
+ outputBuffer += '\n';
3685
3762
  }
3686
3763
  }
3687
3764
 
@@ -3691,15 +3768,20 @@ function flushLineBuffer(prefix, sender) {
3691
3768
  }
3692
3769
 
3693
3770
  if (buf.pendingNewline) {
3694
- process.stdout.write('\n');
3771
+ outputBuffer += '\n';
3695
3772
  buf.needsPrefix = true;
3696
3773
  buf.pendingNewline = false;
3697
3774
  }
3698
3775
  if (buf.thinkingPendingNewline) {
3699
- process.stdout.write('\n');
3776
+ outputBuffer += '\n';
3700
3777
  buf.thinkingNeedsPrefix = true;
3701
3778
  buf.thinkingPendingNewline = false;
3702
3779
  }
3780
+
3781
+ // Single atomic write prevents interleaving
3782
+ if (outputBuffer) {
3783
+ safeWrite(outputBuffer);
3784
+ }
3703
3785
  }
3704
3786
 
3705
3787
  // Lines to filter out (noise, metadata, errors)
@@ -3782,17 +3864,17 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3782
3864
  }
3783
3865
 
3784
3866
  if (msg.topic === 'PR_CREATED') {
3785
- formatPrCreated(msg, prefix, timestamp);
3867
+ formatPrCreated(msg, prefix, timestamp, safePrint);
3786
3868
  return;
3787
3869
  }
3788
3870
 
3789
3871
  if (msg.topic === 'CLUSTER_COMPLETE') {
3790
- formatClusterComplete(msg, prefix, timestamp);
3872
+ formatClusterComplete(msg, prefix, timestamp, safePrint);
3791
3873
  return;
3792
3874
  }
3793
3875
 
3794
3876
  if (msg.topic === 'CLUSTER_FAILED') {
3795
- formatClusterFailed(msg, prefix, timestamp);
3877
+ formatClusterFailed(msg, prefix, timestamp, safePrint);
3796
3878
  return;
3797
3879
  }
3798
3880
 
@@ -3818,7 +3900,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3818
3900
  if (event.text) {
3819
3901
  accumulateThinking(prefix, msg.sender, event.text);
3820
3902
  } else if (event.type === 'thinking_start') {
3821
- console.log(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
3903
+ safePrint(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
3822
3904
  }
3823
3905
  break;
3824
3906
 
@@ -3832,7 +3914,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3832
3914
  flushLineBuffer(prefix, msg.sender);
3833
3915
  const icon = getToolIcon(event.toolName);
3834
3916
  const toolDesc = formatToolCall(event.toolName, event.input);
3835
- console.log(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
3917
+ safePrint(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
3836
3918
  // Store tool call info for matching with result
3837
3919
  currentToolCall.set(msg.sender, {
3838
3920
  toolName: event.toolName,
@@ -3854,7 +3936,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3854
3936
  toolCall?.toolName,
3855
3937
  toolCall?.input
3856
3938
  );
3857
- console.log(`${prefix} ${status} ${resultDesc}`);
3939
+ safePrint(`${prefix} ${status} ${resultDesc}`);
3858
3940
  // Clear stored tool call after result
3859
3941
  currentToolCall.delete(msg.sender);
3860
3942
  break;
@@ -3864,7 +3946,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3864
3946
  flushLineBuffer(prefix, msg.sender);
3865
3947
  // Final result - only show errors (success text already streamed)
3866
3948
  if (!event.success) {
3867
- console.log(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
3949
+ safePrint(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
3868
3950
  }
3869
3951
  break;
3870
3952
 
@@ -3905,7 +3987,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3905
3987
  // Skip duplicate content
3906
3988
  if (isDuplicate(trimmed)) continue;
3907
3989
 
3908
- console.log(`${prefix} ${line}`);
3990
+ safePrint(`${prefix} ${line}`);
3909
3991
  }
3910
3992
  }
3911
3993
  return;
@@ -3913,22 +3995,22 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3913
3995
 
3914
3996
  // AGENT_ERROR: Show errors with visual prominence
3915
3997
  if (msg.topic === 'AGENT_ERROR') {
3916
- console.log(''); // Blank line before error
3917
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
3918
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
3998
+ safePrint(''); // Blank line before error
3999
+ safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
4000
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
3919
4001
  if (msg.content?.text) {
3920
- console.log(`${prefix} ${chalk.red(msg.content.text)}`);
4002
+ safePrint(`${prefix} ${chalk.red(msg.content.text)}`);
3921
4003
  }
3922
4004
  if (msg.content?.data?.stack) {
3923
4005
  // Show first 5 lines of stack trace
3924
4006
  const stackLines = msg.content.data.stack.split('\n').slice(0, 5);
3925
4007
  for (const line of stackLines) {
3926
4008
  if (line.trim()) {
3927
- console.log(`${prefix} ${chalk.dim(line)}`);
4009
+ safePrint(`${prefix} ${chalk.dim(line)}`);
3928
4010
  }
3929
4011
  }
3930
4012
  }
3931
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
4013
+ safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
3932
4014
  return;
3933
4015
  }
3934
4016
 
@@ -3940,29 +4022,29 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3940
4022
  }
3941
4023
  shownNewTaskForCluster.add(msg.cluster_id);
3942
4024
 
3943
- console.log(''); // Blank line before new task
3944
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
3945
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
4025
+ safePrint(''); // Blank line before new task
4026
+ safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
4027
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
3946
4028
  if (msg.content?.text) {
3947
4029
  // Show task description (first 3 lines max)
3948
4030
  const lines = msg.content.text.split('\n').slice(0, 3);
3949
4031
  for (const line of lines) {
3950
4032
  if (line.trim() && line.trim() !== '# Manual Input') {
3951
- console.log(`${prefix} ${chalk.white(line)}`);
4033
+ safePrint(`${prefix} ${chalk.white(line)}`);
3952
4034
  }
3953
4035
  }
3954
4036
  }
3955
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
4037
+ safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
3956
4038
  return;
3957
4039
  }
3958
4040
 
3959
4041
  // IMPLEMENTATION_READY: milestone marker
3960
4042
  if (msg.topic === 'IMPLEMENTATION_READY') {
3961
- console.log(
4043
+ safePrint(
3962
4044
  `${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`
3963
4045
  );
3964
4046
  if (msg.content?.data?.commit) {
3965
- console.log(
4047
+ safePrint(
3966
4048
  `${prefix} ${chalk.gray('Commit:')} ${chalk.cyan(msg.content.data.commit.substring(0, 8))}`
3967
4049
  );
3968
4050
  }
@@ -3975,33 +4057,33 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3975
4057
  const approved = data.approved === true || data.approved === 'true';
3976
4058
  const status = approved ? chalk.bold.green('✓ APPROVED') : chalk.bold.red('✗ REJECTED');
3977
4059
 
3978
- console.log(`${prefix} ${chalk.gray(timestamp)} ${status}`);
4060
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${status}`);
3979
4061
 
3980
4062
  // Show summary if present and not a template variable
3981
4063
  if (msg.content?.text && !msg.content.text.includes('{{')) {
3982
- console.log(`${prefix} ${msg.content.text.substring(0, 100)}`);
4064
+ safePrint(`${prefix} ${msg.content.text.substring(0, 100)}`);
3983
4065
  }
3984
4066
 
3985
4067
  // Show full JSON data structure
3986
- console.log(
4068
+ safePrint(
3987
4069
  `${prefix} ${chalk.dim(JSON.stringify(data, null, 2).split('\n').join(`\n${prefix} `))}`
3988
4070
  );
3989
4071
 
3990
4072
  // Show errors/issues if any
3991
4073
  if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
3992
- console.log(`${prefix} ${chalk.red('Errors:')}`);
4074
+ safePrint(`${prefix} ${chalk.red('Errors:')}`);
3993
4075
  data.errors.forEach((err) => {
3994
4076
  if (err && typeof err === 'string') {
3995
- console.log(`${prefix} - ${err}`);
4077
+ safePrint(`${prefix} - ${err}`);
3996
4078
  }
3997
4079
  });
3998
4080
  }
3999
4081
 
4000
4082
  if (data.issues && Array.isArray(data.issues) && data.issues.length > 0) {
4001
- console.log(`${prefix} ${chalk.yellow('Issues:')}`);
4083
+ safePrint(`${prefix} ${chalk.yellow('Issues:')}`);
4002
4084
  data.issues.forEach((issue) => {
4003
4085
  if (issue && typeof issue === 'string') {
4004
- console.log(`${prefix} - ${issue}`);
4086
+ safePrint(`${prefix} - ${issue}`);
4005
4087
  }
4006
4088
  });
4007
4089
  }
@@ -4009,7 +4091,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4009
4091
  }
4010
4092
 
4011
4093
  // Fallback: generic message display for unknown topics
4012
- formatGenericMessage(msg, prefix, timestamp);
4094
+ formatGenericMessage(msg, prefix, timestamp, safePrint);
4013
4095
  }
4014
4096
 
4015
4097
  // Default command handling: if first arg doesn't match a known command, treat it as 'run'