@covibes/zeroshot 1.3.0 → 1.4.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,10 @@
1
+ # [1.4.0](https://github.com/covibes/zeroshot/compare/v1.3.0...v1.4.0) (2025-12-28)
2
+
3
+
4
+ ### Features
5
+
6
+ * **status-footer:** atomic writes + token cost display ([7baf0c2](https://github.com/covibes/zeroshot/commit/7baf0c228dd5f3489013f75a1782abe6cbe39661))
7
+
1
8
  # [1.3.0](https://github.com/covibes/zeroshot/compare/v1.2.0...v1.3.0) (2025-12-28)
2
9
 
3
10
 
package/cli/index.js CHANGED
@@ -613,6 +613,7 @@ Input formats:
613
613
  });
614
614
  statusFooter.setCluster(clusterId);
615
615
  statusFooter.setClusterState('running');
616
+ statusFooter.setMessageBus(cluster.messageBus);
616
617
 
617
618
  // Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
618
619
  const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@covibes/zeroshot",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Multi-agent orchestration engine for Claude - cluster coordinator and CLI",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -86,6 +86,7 @@ class StatusFooter {
86
86
  this.clusterId = null;
87
87
  this.clusterState = 'initializing';
88
88
  this.startTime = Date.now();
89
+ this.messageBus = null; // MessageBus for token usage tracking
89
90
 
90
91
  // Robust resize handling state
91
92
  this.isRendering = false; // Render lock - prevents concurrent renders
@@ -137,23 +138,60 @@ class StatusFooter {
137
138
  }
138
139
 
139
140
  /**
140
- * Clear all footer lines (uses last known height for safety)
141
+ * Generate move cursor ANSI sequence (returns string, doesn't write)
142
+ * Used for atomic buffered writes to prevent interleaving
143
+ * @param {number} row - 1-based row
144
+ * @param {number} col - 1-based column
145
+ * @returns {string} ANSI escape sequence
141
146
  * @private
142
147
  */
143
- _clearFooterArea() {
148
+ _moveToStr(row, col) {
149
+ return `${CSI}${row};${col}H`;
150
+ }
151
+
152
+ /**
153
+ * Generate clear line ANSI sequence (returns string, doesn't write)
154
+ * Used for atomic buffered writes to prevent interleaving
155
+ * @param {number} row - 1-based row number
156
+ * @returns {string} ANSI escape sequence
157
+ * @private
158
+ */
159
+ _clearLineStr(row) {
160
+ return `${CSI}${row};1H${CLEAR_LINE}`;
161
+ }
162
+
163
+ /**
164
+ * Generate ANSI sequences to clear all footer lines (returns string)
165
+ * Used for atomic buffered writes to prevent interleaving
166
+ * @returns {string} ANSI escape sequences
167
+ * @private
168
+ */
169
+ _clearFooterAreaStr() {
144
170
  const { rows } = this.getTerminalSize();
145
171
  // Use max of current and last footer height to ensure full cleanup
146
172
  const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
147
173
  const startRow = Math.max(1, rows - heightToClear + 1);
148
174
 
175
+ let buffer = '';
149
176
  for (let row = startRow; row <= rows; row++) {
150
- this._clearLine(row);
177
+ buffer += this._clearLineStr(row);
151
178
  }
179
+ return buffer;
180
+ }
181
+
182
+ /**
183
+ * Clear all footer lines (uses last known height for safety)
184
+ * Uses single atomic write to prevent interleaving with other processes
185
+ * @private
186
+ */
187
+ _clearFooterArea() {
188
+ process.stdout.write(this._clearFooterAreaStr());
152
189
  }
153
190
 
154
191
  /**
155
192
  * Set up scroll region to reserve space for footer
156
193
  * ROBUST: Clears footer area first, resets to full screen, then sets new region
194
+ * Uses single atomic write to prevent interleaving with other processes
157
195
  */
158
196
  setupScrollRegion() {
159
197
  if (!this.isTTY()) return;
@@ -178,39 +216,53 @@ class StatusFooter {
178
216
 
179
217
  const scrollEnd = rows - this.footerHeight;
180
218
 
181
- // CRITICAL: Save cursor before any manipulation
182
- process.stdout.write(SAVE_CURSOR);
183
- process.stdout.write(HIDE_CURSOR);
219
+ // BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
220
+ let buffer = '';
184
221
 
185
- // Step 1: Reset scroll region to full screen first (prevents artifacts)
186
- process.stdout.write(`${CSI}1;${rows}r`);
222
+ // Step 1: Save cursor before any manipulation
223
+ buffer += SAVE_CURSOR;
224
+ buffer += HIDE_CURSOR;
187
225
 
188
- // Step 2: Clear footer area completely (prevents ghosting)
189
- this._clearFooterArea();
226
+ // Step 2: Reset scroll region to full screen first (prevents artifacts)
227
+ buffer += `${CSI}1;${rows}r`;
190
228
 
191
- // Step 3: Set new scroll region (lines 1 to scrollEnd)
192
- process.stdout.write(`${CSI}1;${scrollEnd}r`);
229
+ // Step 3: Clear footer area completely (prevents ghosting)
230
+ buffer += this._clearFooterAreaStr();
193
231
 
194
- // Step 4: Move cursor to bottom of scroll region (safe position)
195
- process.stdout.write(`${CSI}${scrollEnd};1H`);
232
+ // Step 4: Set new scroll region (lines 1 to scrollEnd)
233
+ buffer += `${CSI}1;${scrollEnd}r`;
196
234
 
197
- // Restore cursor and show it
198
- process.stdout.write(RESTORE_CURSOR);
199
- process.stdout.write(SHOW_CURSOR);
235
+ // Step 5: Move cursor to bottom of scroll region (safe position)
236
+ buffer += this._moveToStr(scrollEnd, 1);
237
+
238
+ // Step 6: Restore cursor and show it
239
+ buffer += RESTORE_CURSOR;
240
+ buffer += SHOW_CURSOR;
241
+
242
+ // SINGLE ATOMIC WRITE - prevents interleaving
243
+ process.stdout.write(buffer);
200
244
 
201
245
  this.scrollRegionSet = true;
202
246
  this.lastKnownRows = rows;
203
247
  this.lastKnownCols = cols;
204
248
  }
205
249
 
250
+ /**
251
+ * Generate reset scroll region string (returns string, doesn't write)
252
+ * @private
253
+ */
254
+ _resetScrollRegionStr() {
255
+ const { rows } = this.getTerminalSize();
256
+ return `${CSI}1;${rows}r`;
257
+ }
258
+
206
259
  /**
207
260
  * Reset scroll region to full terminal
208
261
  */
209
262
  resetScrollRegion() {
210
263
  if (!this.isTTY()) return;
211
264
 
212
- const { rows } = this.getTerminalSize();
213
- process.stdout.write(`${CSI}1;${rows}r`);
265
+ process.stdout.write(this._resetScrollRegionStr());
214
266
  this.scrollRegionSet = false;
215
267
  }
216
268
 
@@ -251,6 +303,14 @@ class StatusFooter {
251
303
  this.clusterId = clusterId;
252
304
  }
253
305
 
306
+ /**
307
+ * Set message bus for token usage tracking
308
+ * @param {object} messageBus - MessageBus instance with getTokensByRole()
309
+ */
310
+ setMessageBus(messageBus) {
311
+ this.messageBus = messageBus;
312
+ }
313
+
254
314
  /**
255
315
  * Update cluster state
256
316
  * @param {string} state
@@ -401,32 +461,37 @@ class StatusFooter {
401
461
  const agentRows = this.buildAgentRows(executingAgents, cols);
402
462
  const summaryLine = this.buildSummaryLine(cols);
403
463
 
404
- // Save cursor, render footer, restore cursor
405
- process.stdout.write(SAVE_CURSOR);
406
- process.stdout.write(HIDE_CURSOR);
464
+ // BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
465
+ // This prevents interleaving with other processes writing to stdout
466
+ let buffer = '';
467
+ buffer += SAVE_CURSOR;
468
+ buffer += HIDE_CURSOR;
407
469
 
408
470
  // Render from top of footer area
409
471
  let currentRow = rows - this.footerHeight + 1;
410
472
 
411
473
  // Header line
412
- this.moveTo(currentRow++, 1);
413
- process.stdout.write(CLEAR_LINE);
414
- process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
474
+ buffer += this._moveToStr(currentRow++, 1);
475
+ buffer += CLEAR_LINE;
476
+ buffer += `${COLORS.bgBlack}${headerLine}${COLORS.reset}`;
415
477
 
416
478
  // Agent rows
417
479
  for (const agentRow of agentRows) {
418
- this.moveTo(currentRow++, 1);
419
- process.stdout.write(CLEAR_LINE);
420
- process.stdout.write(`${COLORS.bgBlack}${agentRow}${COLORS.reset}`);
480
+ buffer += this._moveToStr(currentRow++, 1);
481
+ buffer += CLEAR_LINE;
482
+ buffer += `${COLORS.bgBlack}${agentRow}${COLORS.reset}`;
421
483
  }
422
484
 
423
485
  // Summary line (with bottom border)
424
- this.moveTo(currentRow, 1);
425
- process.stdout.write(CLEAR_LINE);
426
- process.stdout.write(`${COLORS.bgBlack}${summaryLine}${COLORS.reset}`);
486
+ buffer += this._moveToStr(currentRow, 1);
487
+ buffer += CLEAR_LINE;
488
+ buffer += `${COLORS.bgBlack}${summaryLine}${COLORS.reset}`;
489
+
490
+ buffer += RESTORE_CURSOR;
491
+ buffer += SHOW_CURSOR;
427
492
 
428
- process.stdout.write(RESTORE_CURSOR);
429
- process.stdout.write(SHOW_CURSOR);
493
+ // SINGLE ATOMIC WRITE - prevents interleaving
494
+ process.stdout.write(buffer);
430
495
  } finally {
431
496
  this.isRendering = false;
432
497
 
@@ -554,6 +619,21 @@ class StatusFooter {
554
619
  const total = this.agents.size;
555
620
  parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
556
621
 
622
+ // Token cost (from message bus)
623
+ if (this.messageBus && this.clusterId) {
624
+ try {
625
+ const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
626
+ const totalCost = tokensByRole?._total?.totalCostUsd || 0;
627
+ if (totalCost > 0) {
628
+ // Format: $0.05 or $1.23 or $12.34
629
+ const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
630
+ parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
631
+ }
632
+ } catch {
633
+ // Ignore errors - token tracking is optional
634
+ }
635
+ }
636
+
557
637
  // Aggregate metrics
558
638
  let totalCpu = 0;
559
639
  let totalMem = 0;
@@ -621,7 +701,9 @@ class StatusFooter {
621
701
  process.stdout.on('resize', this._debouncedResize);
622
702
 
623
703
  // Start refresh interval
704
+ // Guard: Skip if previous render still running (prevents overlapping renders)
624
705
  this.intervalId = setInterval(() => {
706
+ if (this.isRendering) return;
625
707
  this.render();
626
708
  }, this.refreshInterval);
627
709
 
@@ -642,17 +724,25 @@ class StatusFooter {
642
724
  process.stdout.removeListener('resize', this._debouncedResize);
643
725
 
644
726
  if (this.isTTY() && !this.hidden) {
727
+ // BUILD SINGLE BUFFER for atomic shutdown write
728
+ // Prevents interleaving with agent output during cleanup
729
+ let buffer = '';
730
+
645
731
  // Reset scroll region
646
- this.resetScrollRegion();
732
+ buffer += this._resetScrollRegionStr();
733
+ this.scrollRegionSet = false;
647
734
 
648
735
  // Clear all footer lines
649
- this._clearFooterArea();
736
+ buffer += this._clearFooterAreaStr();
650
737
 
651
- // Move cursor to safe position
738
+ // Move cursor to safe position and show cursor
652
739
  const { rows } = this.getTerminalSize();
653
740
  const startRow = rows - this.footerHeight + 1;
654
- this.moveTo(startRow, 1);
655
- process.stdout.write(SHOW_CURSOR);
741
+ buffer += this._moveToStr(startRow, 1);
742
+ buffer += SHOW_CURSOR;
743
+
744
+ // SINGLE ATOMIC WRITE
745
+ process.stdout.write(buffer);
656
746
  }
657
747
  }
658
748
 
@@ -662,8 +752,11 @@ class StatusFooter {
662
752
  hide() {
663
753
  if (!this.isTTY()) return;
664
754
 
665
- this.resetScrollRegion();
666
- this._clearFooterArea();
755
+ // Single atomic write for hide operation
756
+ let buffer = this._resetScrollRegionStr();
757
+ this.scrollRegionSet = false;
758
+ buffer += this._clearFooterAreaStr();
759
+ process.stdout.write(buffer);
667
760
  }
668
761
 
669
762
  /**