@covibes/zeroshot 1.4.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,46 @@
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
+
1
44
  # [1.4.0](https://github.com/covibes/zeroshot/compare/v1.3.0...v1.4.0) (2025-12-28)
2
45
 
3
46
 
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'
@@ -614,6 +647,8 @@ Input formats:
614
647
  statusFooter.setCluster(clusterId);
615
648
  statusFooter.setClusterState('running');
616
649
  statusFooter.setMessageBus(cluster.messageBus);
650
+ // Set module-level reference so safePrint/safeWrite route through footer
651
+ activeStatusFooter = statusFooter;
617
652
 
618
653
  // Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
619
654
  const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
@@ -698,14 +733,16 @@ Input formats:
698
733
  if (status.state !== 'running') {
699
734
  clearInterval(checkInterval);
700
735
  clearInterval(flushInterval);
701
- // Stop status footer
702
- statusFooter.stop();
703
736
  lifecycleUnsubscribe();
704
- // Final flush
737
+ // Final flush BEFORE stopping status footer
738
+ // (statusFooter.stop() sends ANSI codes that can clear terminal area)
705
739
  for (const sender of sendersWithOutput) {
706
740
  const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
707
741
  flushLineBuffer(prefix, sender);
708
742
  }
743
+ // Stop status footer AFTER output is done
744
+ statusFooter.stop();
745
+ activeStatusFooter = null;
709
746
  unsubscribe();
710
747
  resolve();
711
748
  }
@@ -714,6 +751,7 @@ Input formats:
714
751
  clearInterval(checkInterval);
715
752
  clearInterval(flushInterval);
716
753
  statusFooter.stop();
754
+ activeStatusFooter = null;
717
755
  lifecycleUnsubscribe();
718
756
  unsubscribe();
719
757
  resolve();
@@ -726,6 +764,7 @@ Input formats:
726
764
  process.on('SIGINT', async () => {
727
765
  // Stop status footer first to restore terminal
728
766
  statusFooter.stop();
767
+ activeStatusFooter = null;
729
768
  lifecycleUnsubscribe();
730
769
 
731
770
  console.log(chalk.dim('\n\n--- Interrupted ---'));
@@ -750,6 +789,22 @@ Input formats:
750
789
  }
751
790
 
752
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
+ }
753
808
  } catch (error) {
754
809
  console.error('Error:', error.message);
755
810
  process.exit(1);
@@ -3570,32 +3625,40 @@ function accumulateText(prefix, sender, text) {
3570
3625
  buf.textBuffer = buf.textBuffer.slice(newlineIdx + 1);
3571
3626
 
3572
3627
  // Word wrap and print the complete line
3628
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3573
3629
  const wrappedLines = wordWrap(completeLine, contentWidth);
3630
+ let outputBuffer = '';
3631
+
3574
3632
  for (let i = 0; i < wrappedLines.length; i++) {
3575
3633
  const wrappedLine = wrappedLines[i];
3576
3634
 
3577
3635
  // Print prefix (real or continuation)
3578
3636
  if (buf.needsPrefix) {
3579
- process.stdout.write(`${prefix} `);
3637
+ outputBuffer += `${prefix} `;
3580
3638
  buf.needsPrefix = false;
3581
3639
  } else if (i > 0) {
3582
- process.stdout.write(`${continuationPrefix}`);
3640
+ outputBuffer += `${continuationPrefix}`;
3583
3641
  }
3584
3642
 
3585
3643
  if (wrappedLine.trim()) {
3586
- process.stdout.write(formatInlineMarkdown(wrappedLine));
3644
+ outputBuffer += formatInlineMarkdown(wrappedLine);
3587
3645
  }
3588
3646
 
3589
3647
  // Newline after each wrapped segment
3590
3648
  if (i < wrappedLines.length - 1) {
3591
- process.stdout.write('\n');
3649
+ outputBuffer += '\n';
3592
3650
  }
3593
3651
  }
3594
3652
 
3595
3653
  // Complete the line
3596
- process.stdout.write('\n');
3654
+ outputBuffer += '\n';
3597
3655
  buf.needsPrefix = true;
3598
3656
  buf.pendingNewline = false;
3657
+
3658
+ // Single atomic write prevents interleaving
3659
+ if (outputBuffer) {
3660
+ safeWrite(outputBuffer);
3661
+ }
3599
3662
  }
3600
3663
 
3601
3664
  // Mark that we have pending text (no newline yet)
@@ -3620,35 +3683,45 @@ function accumulateThinking(prefix, sender, text) {
3620
3683
  const newlineIdx = remaining.indexOf('\n');
3621
3684
  const rawLine = newlineIdx === -1 ? remaining : remaining.slice(0, newlineIdx);
3622
3685
 
3686
+ // CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
3623
3687
  const wrappedLines = wordWrap(rawLine, contentWidth);
3688
+ let outputBuffer = '';
3624
3689
 
3625
3690
  for (let i = 0; i < wrappedLines.length; i++) {
3626
3691
  const wrappedLine = wrappedLines[i];
3627
3692
 
3628
3693
  if (buf.thinkingNeedsPrefix) {
3629
- process.stdout.write(`${prefix} ${chalk.dim.italic('💭 ')}`);
3694
+ outputBuffer += `${prefix} ${chalk.dim.italic('💭 ')}`;
3630
3695
  buf.thinkingNeedsPrefix = false;
3631
3696
  } else if (i > 0) {
3632
- process.stdout.write(`${continuationPrefix}`);
3697
+ outputBuffer += `${continuationPrefix}`;
3633
3698
  }
3634
3699
 
3635
3700
  if (wrappedLine.trim()) {
3636
- process.stdout.write(chalk.dim.italic(wrappedLine));
3701
+ outputBuffer += chalk.dim.italic(wrappedLine);
3637
3702
  }
3638
3703
 
3639
3704
  if (i < wrappedLines.length - 1) {
3640
- process.stdout.write('\n');
3705
+ outputBuffer += '\n';
3641
3706
  }
3642
3707
  }
3643
3708
 
3644
3709
  if (newlineIdx === -1) {
3645
3710
  buf.thinkingPendingNewline = true;
3711
+ // Single atomic write
3712
+ if (outputBuffer) {
3713
+ safeWrite(outputBuffer);
3714
+ }
3646
3715
  break;
3647
3716
  } else {
3648
- process.stdout.write('\n');
3717
+ outputBuffer += '\n';
3649
3718
  buf.thinkingNeedsPrefix = true;
3650
3719
  buf.thinkingPendingNewline = false;
3651
3720
  remaining = remaining.slice(newlineIdx + 1);
3721
+ // Single atomic write
3722
+ if (outputBuffer) {
3723
+ safeWrite(outputBuffer);
3724
+ }
3652
3725
  }
3653
3726
  }
3654
3727
  }
@@ -3658,7 +3731,10 @@ function flushLineBuffer(prefix, sender) {
3658
3731
  const buf = lineBuffers.get(sender);
3659
3732
  if (!buf) return;
3660
3733
 
3661
- // 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)
3662
3738
  if (buf.textBuffer && buf.textBuffer.length > 0) {
3663
3739
  // Calculate widths for word wrapping (same as accumulateText)
3664
3740
  const prefixLen = chalk.reset(prefix).replace(/\\x1b\[[0-9;]*m/g, '').length + 1;
@@ -3671,18 +3747,18 @@ function flushLineBuffer(prefix, sender) {
3671
3747
  const wrappedLine = wrappedLines[i];
3672
3748
 
3673
3749
  if (buf.needsPrefix) {
3674
- process.stdout.write(`${prefix} `);
3750
+ outputBuffer += `${prefix} `;
3675
3751
  buf.needsPrefix = false;
3676
3752
  } else if (i > 0) {
3677
- process.stdout.write(`${continuationPrefix}`);
3753
+ outputBuffer += `${continuationPrefix}`;
3678
3754
  }
3679
3755
 
3680
3756
  if (wrappedLine.trim()) {
3681
- process.stdout.write(formatInlineMarkdown(wrappedLine));
3757
+ outputBuffer += formatInlineMarkdown(wrappedLine);
3682
3758
  }
3683
3759
 
3684
3760
  if (i < wrappedLines.length - 1) {
3685
- process.stdout.write('\n');
3761
+ outputBuffer += '\n';
3686
3762
  }
3687
3763
  }
3688
3764
 
@@ -3692,15 +3768,20 @@ function flushLineBuffer(prefix, sender) {
3692
3768
  }
3693
3769
 
3694
3770
  if (buf.pendingNewline) {
3695
- process.stdout.write('\n');
3771
+ outputBuffer += '\n';
3696
3772
  buf.needsPrefix = true;
3697
3773
  buf.pendingNewline = false;
3698
3774
  }
3699
3775
  if (buf.thinkingPendingNewline) {
3700
- process.stdout.write('\n');
3776
+ outputBuffer += '\n';
3701
3777
  buf.thinkingNeedsPrefix = true;
3702
3778
  buf.thinkingPendingNewline = false;
3703
3779
  }
3780
+
3781
+ // Single atomic write prevents interleaving
3782
+ if (outputBuffer) {
3783
+ safeWrite(outputBuffer);
3784
+ }
3704
3785
  }
3705
3786
 
3706
3787
  // Lines to filter out (noise, metadata, errors)
@@ -3783,17 +3864,17 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3783
3864
  }
3784
3865
 
3785
3866
  if (msg.topic === 'PR_CREATED') {
3786
- formatPrCreated(msg, prefix, timestamp);
3867
+ formatPrCreated(msg, prefix, timestamp, safePrint);
3787
3868
  return;
3788
3869
  }
3789
3870
 
3790
3871
  if (msg.topic === 'CLUSTER_COMPLETE') {
3791
- formatClusterComplete(msg, prefix, timestamp);
3872
+ formatClusterComplete(msg, prefix, timestamp, safePrint);
3792
3873
  return;
3793
3874
  }
3794
3875
 
3795
3876
  if (msg.topic === 'CLUSTER_FAILED') {
3796
- formatClusterFailed(msg, prefix, timestamp);
3877
+ formatClusterFailed(msg, prefix, timestamp, safePrint);
3797
3878
  return;
3798
3879
  }
3799
3880
 
@@ -3819,7 +3900,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3819
3900
  if (event.text) {
3820
3901
  accumulateThinking(prefix, msg.sender, event.text);
3821
3902
  } else if (event.type === 'thinking_start') {
3822
- console.log(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
3903
+ safePrint(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
3823
3904
  }
3824
3905
  break;
3825
3906
 
@@ -3833,7 +3914,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3833
3914
  flushLineBuffer(prefix, msg.sender);
3834
3915
  const icon = getToolIcon(event.toolName);
3835
3916
  const toolDesc = formatToolCall(event.toolName, event.input);
3836
- console.log(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
3917
+ safePrint(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
3837
3918
  // Store tool call info for matching with result
3838
3919
  currentToolCall.set(msg.sender, {
3839
3920
  toolName: event.toolName,
@@ -3855,7 +3936,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3855
3936
  toolCall?.toolName,
3856
3937
  toolCall?.input
3857
3938
  );
3858
- console.log(`${prefix} ${status} ${resultDesc}`);
3939
+ safePrint(`${prefix} ${status} ${resultDesc}`);
3859
3940
  // Clear stored tool call after result
3860
3941
  currentToolCall.delete(msg.sender);
3861
3942
  break;
@@ -3865,7 +3946,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3865
3946
  flushLineBuffer(prefix, msg.sender);
3866
3947
  // Final result - only show errors (success text already streamed)
3867
3948
  if (!event.success) {
3868
- console.log(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
3949
+ safePrint(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
3869
3950
  }
3870
3951
  break;
3871
3952
 
@@ -3906,7 +3987,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3906
3987
  // Skip duplicate content
3907
3988
  if (isDuplicate(trimmed)) continue;
3908
3989
 
3909
- console.log(`${prefix} ${line}`);
3990
+ safePrint(`${prefix} ${line}`);
3910
3991
  }
3911
3992
  }
3912
3993
  return;
@@ -3914,22 +3995,22 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3914
3995
 
3915
3996
  // AGENT_ERROR: Show errors with visual prominence
3916
3997
  if (msg.topic === 'AGENT_ERROR') {
3917
- console.log(''); // Blank line before error
3918
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
3919
- 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')}`);
3920
4001
  if (msg.content?.text) {
3921
- console.log(`${prefix} ${chalk.red(msg.content.text)}`);
4002
+ safePrint(`${prefix} ${chalk.red(msg.content.text)}`);
3922
4003
  }
3923
4004
  if (msg.content?.data?.stack) {
3924
4005
  // Show first 5 lines of stack trace
3925
4006
  const stackLines = msg.content.data.stack.split('\n').slice(0, 5);
3926
4007
  for (const line of stackLines) {
3927
4008
  if (line.trim()) {
3928
- console.log(`${prefix} ${chalk.dim(line)}`);
4009
+ safePrint(`${prefix} ${chalk.dim(line)}`);
3929
4010
  }
3930
4011
  }
3931
4012
  }
3932
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
4013
+ safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
3933
4014
  return;
3934
4015
  }
3935
4016
 
@@ -3941,29 +4022,29 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3941
4022
  }
3942
4023
  shownNewTaskForCluster.add(msg.cluster_id);
3943
4024
 
3944
- console.log(''); // Blank line before new task
3945
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
3946
- 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')}`);
3947
4028
  if (msg.content?.text) {
3948
4029
  // Show task description (first 3 lines max)
3949
4030
  const lines = msg.content.text.split('\n').slice(0, 3);
3950
4031
  for (const line of lines) {
3951
4032
  if (line.trim() && line.trim() !== '# Manual Input') {
3952
- console.log(`${prefix} ${chalk.white(line)}`);
4033
+ safePrint(`${prefix} ${chalk.white(line)}`);
3953
4034
  }
3954
4035
  }
3955
4036
  }
3956
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
4037
+ safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
3957
4038
  return;
3958
4039
  }
3959
4040
 
3960
4041
  // IMPLEMENTATION_READY: milestone marker
3961
4042
  if (msg.topic === 'IMPLEMENTATION_READY') {
3962
- console.log(
4043
+ safePrint(
3963
4044
  `${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`
3964
4045
  );
3965
4046
  if (msg.content?.data?.commit) {
3966
- console.log(
4047
+ safePrint(
3967
4048
  `${prefix} ${chalk.gray('Commit:')} ${chalk.cyan(msg.content.data.commit.substring(0, 8))}`
3968
4049
  );
3969
4050
  }
@@ -3976,33 +4057,33 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3976
4057
  const approved = data.approved === true || data.approved === 'true';
3977
4058
  const status = approved ? chalk.bold.green('✓ APPROVED') : chalk.bold.red('✗ REJECTED');
3978
4059
 
3979
- console.log(`${prefix} ${chalk.gray(timestamp)} ${status}`);
4060
+ safePrint(`${prefix} ${chalk.gray(timestamp)} ${status}`);
3980
4061
 
3981
4062
  // Show summary if present and not a template variable
3982
4063
  if (msg.content?.text && !msg.content.text.includes('{{')) {
3983
- console.log(`${prefix} ${msg.content.text.substring(0, 100)}`);
4064
+ safePrint(`${prefix} ${msg.content.text.substring(0, 100)}`);
3984
4065
  }
3985
4066
 
3986
4067
  // Show full JSON data structure
3987
- console.log(
4068
+ safePrint(
3988
4069
  `${prefix} ${chalk.dim(JSON.stringify(data, null, 2).split('\n').join(`\n${prefix} `))}`
3989
4070
  );
3990
4071
 
3991
4072
  // Show errors/issues if any
3992
4073
  if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
3993
- console.log(`${prefix} ${chalk.red('Errors:')}`);
4074
+ safePrint(`${prefix} ${chalk.red('Errors:')}`);
3994
4075
  data.errors.forEach((err) => {
3995
4076
  if (err && typeof err === 'string') {
3996
- console.log(`${prefix} - ${err}`);
4077
+ safePrint(`${prefix} - ${err}`);
3997
4078
  }
3998
4079
  });
3999
4080
  }
4000
4081
 
4001
4082
  if (data.issues && Array.isArray(data.issues) && data.issues.length > 0) {
4002
- console.log(`${prefix} ${chalk.yellow('Issues:')}`);
4083
+ safePrint(`${prefix} ${chalk.yellow('Issues:')}`);
4003
4084
  data.issues.forEach((issue) => {
4004
4085
  if (issue && typeof issue === 'string') {
4005
- console.log(`${prefix} - ${issue}`);
4086
+ safePrint(`${prefix} - ${issue}`);
4006
4087
  }
4007
4088
  });
4008
4089
  }
@@ -4010,7 +4091,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
4010
4091
  }
4011
4092
 
4012
4093
  // Fallback: generic message display for unknown topics
4013
- formatGenericMessage(msg, prefix, timestamp);
4094
+ formatGenericMessage(msg, prefix, timestamp, safePrint);
4014
4095
  }
4015
4096
 
4016
4097
  // Default command handling: if first arg doesn't match a known command, treat it as 'run'