@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 +50 -0
- package/cli/index.js +131 -49
- package/cli/message-formatters-normal.js +77 -38
- package/cluster-templates/base-templates/debug-workflow.json +11 -2
- package/cluster-templates/base-templates/full-workflow.json +20 -7
- package/cluster-templates/base-templates/single-worker.json +8 -1
- package/cluster-templates/base-templates/worker-validator.json +10 -2
- package/docker/zeroshot-cluster/Dockerfile +7 -0
- package/package.json +3 -1
- package/src/agent/agent-config.js +19 -6
- package/src/agent/agent-context-builder.js +9 -0
- package/src/agent/agent-task-executor.js +149 -65
- package/src/config-validator.js +13 -0
- package/src/isolation-manager.js +11 -7
- package/src/orchestrator.js +78 -1
- package/src/status-footer.js +188 -42
- package/src/template-resolver.js +23 -1
package/src/status-footer.js
CHANGED
|
@@ -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
|
|
@@ -95,6 +96,11 @@ class StatusFooter {
|
|
|
95
96
|
this.minRows = 8; // Minimum rows for footer display (graceful degradation)
|
|
96
97
|
this.hidden = false; // True when terminal too small for footer
|
|
97
98
|
|
|
99
|
+
// Output queue - serializes all stdout to prevent cursor corruption
|
|
100
|
+
// When scroll region is active, console.log() can corrupt cursor position
|
|
101
|
+
// All output must go through print() to coordinate with render cycles
|
|
102
|
+
this.printQueue = [];
|
|
103
|
+
|
|
98
104
|
// Debounced resize handler (100ms) - prevents rapid-fire redraws
|
|
99
105
|
this._debouncedResize = debounce(() => this._handleResize(), 100);
|
|
100
106
|
}
|
|
@@ -107,6 +113,38 @@ class StatusFooter {
|
|
|
107
113
|
return process.stdout.isTTY === true;
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Print text to stdout, coordinating with the render cycle.
|
|
118
|
+
* When a render is in progress, queues output to prevent cursor corruption.
|
|
119
|
+
* When no render is active, writes immediately.
|
|
120
|
+
*
|
|
121
|
+
* MUST be used instead of console.log() when status footer is active.
|
|
122
|
+
* @param {string} text - Text to print (newline will be added)
|
|
123
|
+
*/
|
|
124
|
+
print(text) {
|
|
125
|
+
if (this.isRendering) {
|
|
126
|
+
// Queue for later - render() will flush after restoring cursor
|
|
127
|
+
this.printQueue.push(text);
|
|
128
|
+
} else {
|
|
129
|
+
// Write immediately - no render in progress
|
|
130
|
+
process.stdout.write(text + '\n');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Flush queued output to stdout.
|
|
136
|
+
* Called after render() restores cursor to ensure proper positioning.
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_flushPrintQueue() {
|
|
140
|
+
if (this.printQueue.length === 0) return;
|
|
141
|
+
|
|
142
|
+
// Write all queued output
|
|
143
|
+
const output = this.printQueue.map(text => text + '\n').join('');
|
|
144
|
+
this.printQueue = [];
|
|
145
|
+
process.stdout.write(output);
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
/**
|
|
111
149
|
* Get terminal dimensions
|
|
112
150
|
* @returns {{ rows: number, cols: number }}
|
|
@@ -137,23 +175,60 @@ class StatusFooter {
|
|
|
137
175
|
}
|
|
138
176
|
|
|
139
177
|
/**
|
|
140
|
-
*
|
|
178
|
+
* Generate move cursor ANSI sequence (returns string, doesn't write)
|
|
179
|
+
* Used for atomic buffered writes to prevent interleaving
|
|
180
|
+
* @param {number} row - 1-based row
|
|
181
|
+
* @param {number} col - 1-based column
|
|
182
|
+
* @returns {string} ANSI escape sequence
|
|
141
183
|
* @private
|
|
142
184
|
*/
|
|
143
|
-
|
|
185
|
+
_moveToStr(row, col) {
|
|
186
|
+
return `${CSI}${row};${col}H`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate clear line ANSI sequence (returns string, doesn't write)
|
|
191
|
+
* Used for atomic buffered writes to prevent interleaving
|
|
192
|
+
* @param {number} row - 1-based row number
|
|
193
|
+
* @returns {string} ANSI escape sequence
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_clearLineStr(row) {
|
|
197
|
+
return `${CSI}${row};1H${CLEAR_LINE}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate ANSI sequences to clear all footer lines (returns string)
|
|
202
|
+
* Used for atomic buffered writes to prevent interleaving
|
|
203
|
+
* @returns {string} ANSI escape sequences
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_clearFooterAreaStr() {
|
|
144
207
|
const { rows } = this.getTerminalSize();
|
|
145
208
|
// Use max of current and last footer height to ensure full cleanup
|
|
146
209
|
const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
|
|
147
210
|
const startRow = Math.max(1, rows - heightToClear + 1);
|
|
148
211
|
|
|
212
|
+
let buffer = '';
|
|
149
213
|
for (let row = startRow; row <= rows; row++) {
|
|
150
|
-
this.
|
|
214
|
+
buffer += this._clearLineStr(row);
|
|
151
215
|
}
|
|
216
|
+
return buffer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clear all footer lines (uses last known height for safety)
|
|
221
|
+
* Uses single atomic write to prevent interleaving with other processes
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
_clearFooterArea() {
|
|
225
|
+
process.stdout.write(this._clearFooterAreaStr());
|
|
152
226
|
}
|
|
153
227
|
|
|
154
228
|
/**
|
|
155
229
|
* Set up scroll region to reserve space for footer
|
|
156
230
|
* ROBUST: Clears footer area first, resets to full screen, then sets new region
|
|
231
|
+
* Uses single atomic write to prevent interleaving with other processes
|
|
157
232
|
*/
|
|
158
233
|
setupScrollRegion() {
|
|
159
234
|
if (!this.isTTY()) return;
|
|
@@ -178,39 +253,53 @@ class StatusFooter {
|
|
|
178
253
|
|
|
179
254
|
const scrollEnd = rows - this.footerHeight;
|
|
180
255
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
256
|
+
// BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
|
|
257
|
+
let buffer = '';
|
|
258
|
+
|
|
259
|
+
// Step 1: Save cursor before any manipulation
|
|
260
|
+
buffer += SAVE_CURSOR;
|
|
261
|
+
buffer += HIDE_CURSOR;
|
|
184
262
|
|
|
185
|
-
// Step
|
|
186
|
-
|
|
263
|
+
// Step 2: Reset scroll region to full screen first (prevents artifacts)
|
|
264
|
+
buffer += `${CSI}1;${rows}r`;
|
|
187
265
|
|
|
188
|
-
// Step
|
|
189
|
-
this.
|
|
266
|
+
// Step 3: Clear footer area completely (prevents ghosting)
|
|
267
|
+
buffer += this._clearFooterAreaStr();
|
|
190
268
|
|
|
191
|
-
// Step
|
|
192
|
-
|
|
269
|
+
// Step 4: Set new scroll region (lines 1 to scrollEnd)
|
|
270
|
+
buffer += `${CSI}1;${scrollEnd}r`;
|
|
193
271
|
|
|
194
|
-
// Step
|
|
195
|
-
|
|
272
|
+
// Step 5: Move cursor to bottom of scroll region (safe position)
|
|
273
|
+
buffer += this._moveToStr(scrollEnd, 1);
|
|
196
274
|
|
|
197
|
-
// Restore cursor and show it
|
|
198
|
-
|
|
199
|
-
|
|
275
|
+
// Step 6: Restore cursor and show it
|
|
276
|
+
buffer += RESTORE_CURSOR;
|
|
277
|
+
buffer += SHOW_CURSOR;
|
|
278
|
+
|
|
279
|
+
// SINGLE ATOMIC WRITE - prevents interleaving
|
|
280
|
+
process.stdout.write(buffer);
|
|
200
281
|
|
|
201
282
|
this.scrollRegionSet = true;
|
|
202
283
|
this.lastKnownRows = rows;
|
|
203
284
|
this.lastKnownCols = cols;
|
|
204
285
|
}
|
|
205
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Generate reset scroll region string (returns string, doesn't write)
|
|
289
|
+
* @private
|
|
290
|
+
*/
|
|
291
|
+
_resetScrollRegionStr() {
|
|
292
|
+
const { rows } = this.getTerminalSize();
|
|
293
|
+
return `${CSI}1;${rows}r`;
|
|
294
|
+
}
|
|
295
|
+
|
|
206
296
|
/**
|
|
207
297
|
* Reset scroll region to full terminal
|
|
208
298
|
*/
|
|
209
299
|
resetScrollRegion() {
|
|
210
300
|
if (!this.isTTY()) return;
|
|
211
301
|
|
|
212
|
-
|
|
213
|
-
process.stdout.write(`${CSI}1;${rows}r`);
|
|
302
|
+
process.stdout.write(this._resetScrollRegionStr());
|
|
214
303
|
this.scrollRegionSet = false;
|
|
215
304
|
}
|
|
216
305
|
|
|
@@ -251,6 +340,14 @@ class StatusFooter {
|
|
|
251
340
|
this.clusterId = clusterId;
|
|
252
341
|
}
|
|
253
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Set message bus for token usage tracking
|
|
345
|
+
* @param {object} messageBus - MessageBus instance with getTokensByRole()
|
|
346
|
+
*/
|
|
347
|
+
setMessageBus(messageBus) {
|
|
348
|
+
this.messageBus = messageBus;
|
|
349
|
+
}
|
|
350
|
+
|
|
254
351
|
/**
|
|
255
352
|
* Update cluster state
|
|
256
353
|
* @param {string} state
|
|
@@ -401,35 +498,53 @@ class StatusFooter {
|
|
|
401
498
|
const agentRows = this.buildAgentRows(executingAgents, cols);
|
|
402
499
|
const summaryLine = this.buildSummaryLine(cols);
|
|
403
500
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
501
|
+
// BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
|
|
502
|
+
// This prevents interleaving with other processes writing to stdout
|
|
503
|
+
let buffer = '';
|
|
504
|
+
buffer += SAVE_CURSOR;
|
|
505
|
+
buffer += HIDE_CURSOR;
|
|
407
506
|
|
|
408
507
|
// Render from top of footer area
|
|
409
508
|
let currentRow = rows - this.footerHeight + 1;
|
|
410
509
|
|
|
411
510
|
// Header line
|
|
412
|
-
this.
|
|
413
|
-
|
|
414
|
-
|
|
511
|
+
buffer += this._moveToStr(currentRow++, 1);
|
|
512
|
+
buffer += CLEAR_LINE;
|
|
513
|
+
buffer += `${COLORS.bgBlack}${headerLine}${COLORS.reset}`;
|
|
415
514
|
|
|
416
515
|
// Agent rows
|
|
417
516
|
for (const agentRow of agentRows) {
|
|
418
|
-
this.
|
|
419
|
-
|
|
420
|
-
|
|
517
|
+
buffer += this._moveToStr(currentRow++, 1);
|
|
518
|
+
buffer += CLEAR_LINE;
|
|
519
|
+
buffer += `${COLORS.bgBlack}${agentRow}${COLORS.reset}`;
|
|
421
520
|
}
|
|
422
521
|
|
|
423
522
|
// Summary line (with bottom border)
|
|
424
|
-
this.
|
|
425
|
-
|
|
426
|
-
|
|
523
|
+
buffer += this._moveToStr(currentRow, 1);
|
|
524
|
+
buffer += CLEAR_LINE;
|
|
525
|
+
buffer += `${COLORS.bgBlack}${summaryLine}${COLORS.reset}`;
|
|
526
|
+
|
|
527
|
+
buffer += RESTORE_CURSOR;
|
|
528
|
+
buffer += SHOW_CURSOR;
|
|
427
529
|
|
|
428
|
-
|
|
429
|
-
process.stdout.write(
|
|
530
|
+
// SINGLE ATOMIC WRITE - prevents interleaving
|
|
531
|
+
process.stdout.write(buffer);
|
|
430
532
|
} finally {
|
|
431
533
|
this.isRendering = false;
|
|
432
534
|
|
|
535
|
+
// CRITICAL: Position cursor at bottom of scroll region before flushing
|
|
536
|
+
// Without this, output goes below footer if cursor was restored outside scroll region
|
|
537
|
+
// (RESTORE_CURSOR at line 527 may restore to row outside the scrollable area)
|
|
538
|
+
if (this.scrollRegionSet) {
|
|
539
|
+
const { rows } = this.getTerminalSize();
|
|
540
|
+
const scrollEnd = rows - this.footerHeight;
|
|
541
|
+
process.stdout.write(this._moveToStr(scrollEnd, 1));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Flush any output that was queued during render
|
|
545
|
+
// Must happen BEFORE pending resize to preserve output order
|
|
546
|
+
this._flushPrintQueue();
|
|
547
|
+
|
|
433
548
|
// Process pending resize if one was queued during render
|
|
434
549
|
if (this.pendingResize) {
|
|
435
550
|
this.pendingResize = false;
|
|
@@ -554,6 +669,21 @@ class StatusFooter {
|
|
|
554
669
|
const total = this.agents.size;
|
|
555
670
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
|
|
556
671
|
|
|
672
|
+
// Token cost (from message bus)
|
|
673
|
+
if (this.messageBus && this.clusterId) {
|
|
674
|
+
try {
|
|
675
|
+
const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
|
|
676
|
+
const totalCost = tokensByRole?._total?.totalCostUsd || 0;
|
|
677
|
+
if (totalCost > 0) {
|
|
678
|
+
// Format: $0.05 or $1.23 or $12.34
|
|
679
|
+
const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
|
|
680
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
// Ignore errors - token tracking is optional
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
557
687
|
// Aggregate metrics
|
|
558
688
|
let totalCpu = 0;
|
|
559
689
|
let totalMem = 0;
|
|
@@ -621,7 +751,9 @@ class StatusFooter {
|
|
|
621
751
|
process.stdout.on('resize', this._debouncedResize);
|
|
622
752
|
|
|
623
753
|
// Start refresh interval
|
|
754
|
+
// Guard: Skip if previous render still running (prevents overlapping renders)
|
|
624
755
|
this.intervalId = setInterval(() => {
|
|
756
|
+
if (this.isRendering) return;
|
|
625
757
|
this.render();
|
|
626
758
|
}, this.refreshInterval);
|
|
627
759
|
|
|
@@ -642,17 +774,27 @@ class StatusFooter {
|
|
|
642
774
|
process.stdout.removeListener('resize', this._debouncedResize);
|
|
643
775
|
|
|
644
776
|
if (this.isTTY() && !this.hidden) {
|
|
645
|
-
//
|
|
646
|
-
|
|
777
|
+
// BUILD SINGLE BUFFER for atomic shutdown write
|
|
778
|
+
// Prevents interleaving with agent output during cleanup
|
|
779
|
+
let buffer = '';
|
|
647
780
|
|
|
648
|
-
// Clear
|
|
649
|
-
|
|
781
|
+
// CRITICAL: Clear footer area BEFORE resetting scroll region
|
|
782
|
+
// While scroll region is active, footer area contains only status bar content
|
|
783
|
+
// After reset, those lines may contain scrolled output (which we DON'T want to clear)
|
|
784
|
+
buffer += this._clearFooterAreaStr();
|
|
650
785
|
|
|
651
|
-
//
|
|
786
|
+
// Now reset scroll region (full terminal is scrollable again)
|
|
787
|
+
buffer += this._resetScrollRegionStr();
|
|
788
|
+
this.scrollRegionSet = false;
|
|
789
|
+
|
|
790
|
+
// Move cursor to safe position and show cursor
|
|
652
791
|
const { rows } = this.getTerminalSize();
|
|
653
792
|
const startRow = rows - this.footerHeight + 1;
|
|
654
|
-
this.
|
|
655
|
-
|
|
793
|
+
buffer += this._moveToStr(startRow, 1);
|
|
794
|
+
buffer += SHOW_CURSOR;
|
|
795
|
+
|
|
796
|
+
// SINGLE ATOMIC WRITE
|
|
797
|
+
process.stdout.write(buffer);
|
|
656
798
|
}
|
|
657
799
|
}
|
|
658
800
|
|
|
@@ -662,8 +804,12 @@ class StatusFooter {
|
|
|
662
804
|
hide() {
|
|
663
805
|
if (!this.isTTY()) return;
|
|
664
806
|
|
|
665
|
-
|
|
666
|
-
|
|
807
|
+
// Single atomic write for hide operation
|
|
808
|
+
// CRITICAL: Clear footer BEFORE resetting scroll region (same reason as stop())
|
|
809
|
+
let buffer = this._clearFooterAreaStr();
|
|
810
|
+
buffer += this._resetScrollRegionStr();
|
|
811
|
+
this.scrollRegionSet = false;
|
|
812
|
+
process.stdout.write(buffer);
|
|
667
813
|
}
|
|
668
814
|
|
|
669
815
|
/**
|
package/src/template-resolver.js
CHANGED
|
@@ -43,8 +43,11 @@ class TemplateResolver {
|
|
|
43
43
|
// Validate required params
|
|
44
44
|
this._validateParams(template, params);
|
|
45
45
|
|
|
46
|
+
// Apply defaults for missing params (e.g., timeout: 0)
|
|
47
|
+
const paramsWithDefaults = this._applyDefaults(template, params);
|
|
48
|
+
|
|
46
49
|
// Deep clone and resolve
|
|
47
|
-
const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)),
|
|
50
|
+
const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), paramsWithDefaults);
|
|
48
51
|
|
|
49
52
|
// Filter out conditional agents that don't meet their condition
|
|
50
53
|
if (resolved.agents) {
|
|
@@ -86,6 +89,25 @@ class TemplateResolver {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Apply template defaults for any missing params
|
|
94
|
+
* @private
|
|
95
|
+
* @param {any} template
|
|
96
|
+
* @param {any} params
|
|
97
|
+
* @returns {any} params with defaults applied
|
|
98
|
+
*/
|
|
99
|
+
_applyDefaults(template, params) {
|
|
100
|
+
if (!template.params) return params;
|
|
101
|
+
|
|
102
|
+
const result = { ...params };
|
|
103
|
+
for (const [name, schema] of Object.entries(template.params)) {
|
|
104
|
+
if (result[name] === undefined && schema.default !== undefined) {
|
|
105
|
+
result[name] = schema.default;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
/**
|
|
90
112
|
* Recursively resolve placeholders in an object
|
|
91
113
|
* @private
|