@covibes/zeroshot 5.3.0 → 5.4.1

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.
Files changed (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -385
  3. package/cli/index.js +108 -4615
  4. package/package.json +16 -125
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -150
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -253
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -697
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -152
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -212
  21. package/lib/id-detector.js +0 -53
  22. package/lib/provider-detection.js +0 -59
  23. package/lib/provider-names.js +0 -56
  24. package/lib/settings.js +0 -405
  25. package/lib/stream-json-parser.js +0 -10
  26. package/scripts/fix-node-pty-permissions.js +0 -75
  27. package/scripts/record-demo.sh +0 -279
  28. package/scripts/test-install.sh +0 -40
  29. package/scripts/validate-templates.js +0 -100
  30. package/src/agent/agent-config.js +0 -193
  31. package/src/agent/agent-context-builder.js +0 -335
  32. package/src/agent/agent-hook-executor.js +0 -402
  33. package/src/agent/agent-lifecycle.js +0 -683
  34. package/src/agent/agent-stuck-detector.js +0 -256
  35. package/src/agent/agent-task-executor.js +0 -1431
  36. package/src/agent/agent-trigger-evaluator.js +0 -67
  37. package/src/agent/output-extraction.js +0 -200
  38. package/src/agent/output-reformatter.js +0 -175
  39. package/src/agent/schema-utils.js +0 -111
  40. package/src/agent-wrapper.js +0 -570
  41. package/src/agents/git-pusher-agent.json +0 -20
  42. package/src/attach/attach-client.js +0 -438
  43. package/src/attach/attach-server.js +0 -543
  44. package/src/attach/index.js +0 -35
  45. package/src/attach/protocol.js +0 -220
  46. package/src/attach/ring-buffer.js +0 -121
  47. package/src/attach/socket-discovery.js +0 -267
  48. package/src/claude-task-runner.js +0 -523
  49. package/src/config-router.js +0 -80
  50. package/src/config-validator.js +0 -1660
  51. package/src/copy-worker.js +0 -43
  52. package/src/github.js +0 -139
  53. package/src/isolation-manager.js +0 -1381
  54. package/src/ledger.js +0 -614
  55. package/src/logic-engine.js +0 -223
  56. package/src/message-bus-bridge.js +0 -139
  57. package/src/message-bus.js +0 -249
  58. package/src/name-generator.js +0 -232
  59. package/src/orchestrator.js +0 -2519
  60. package/src/preflight.js +0 -485
  61. package/src/process-metrics.js +0 -566
  62. package/src/providers/anthropic/cli-builder.js +0 -45
  63. package/src/providers/anthropic/index.js +0 -134
  64. package/src/providers/anthropic/models.js +0 -23
  65. package/src/providers/anthropic/output-parser.js +0 -159
  66. package/src/providers/base-provider.js +0 -181
  67. package/src/providers/capabilities.js +0 -51
  68. package/src/providers/google/cli-builder.js +0 -55
  69. package/src/providers/google/index.js +0 -116
  70. package/src/providers/google/models.js +0 -24
  71. package/src/providers/google/output-parser.js +0 -92
  72. package/src/providers/index.js +0 -75
  73. package/src/providers/openai/cli-builder.js +0 -122
  74. package/src/providers/openai/index.js +0 -135
  75. package/src/providers/openai/models.js +0 -21
  76. package/src/providers/openai/output-parser.js +0 -129
  77. package/src/schemas/sub-cluster.js +0 -156
  78. package/src/status-footer.js +0 -980
  79. package/src/sub-cluster-wrapper.js +0 -560
  80. package/src/task-runner.js +0 -30
  81. package/src/template-resolver.js +0 -369
  82. package/src/tui/CHANGES.txt +0 -133
  83. package/src/tui/LAYOUT.md +0 -261
  84. package/src/tui/README.txt +0 -192
  85. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  86. package/src/tui/data-poller.js +0 -325
  87. package/src/tui/demo.js +0 -208
  88. package/src/tui/formatters.js +0 -128
  89. package/src/tui/index.js +0 -193
  90. package/src/tui/keybindings.js +0 -383
  91. package/src/tui/layout.js +0 -334
  92. package/src/tui/renderer.js +0 -194
  93. package/task-lib/attachable-watcher.js +0 -272
  94. package/task-lib/claude-recovery.js +0 -119
  95. package/task-lib/commands/clean.js +0 -50
  96. package/task-lib/commands/get-log-path.js +0 -23
  97. package/task-lib/commands/kill.js +0 -32
  98. package/task-lib/commands/list.js +0 -105
  99. package/task-lib/commands/logs.js +0 -411
  100. package/task-lib/commands/resume.js +0 -42
  101. package/task-lib/commands/run.js +0 -57
  102. package/task-lib/commands/schedule.js +0 -105
  103. package/task-lib/commands/scheduler-cmd.js +0 -96
  104. package/task-lib/commands/schedules.js +0 -98
  105. package/task-lib/commands/status.js +0 -44
  106. package/task-lib/commands/unschedule.js +0 -16
  107. package/task-lib/completion.js +0 -9
  108. package/task-lib/config.js +0 -10
  109. package/task-lib/name-generator.js +0 -230
  110. package/task-lib/package.json +0 -3
  111. package/task-lib/runner.js +0 -144
  112. package/task-lib/scheduler.js +0 -252
  113. package/task-lib/store.js +0 -230
  114. package/task-lib/tui/formatters.js +0 -166
  115. package/task-lib/tui/index.js +0 -197
  116. package/task-lib/tui/layout.js +0 -111
  117. package/task-lib/tui/renderer.js +0 -119
  118. package/task-lib/tui.js +0 -384
  119. package/task-lib/watcher.js +0 -188
@@ -1,980 +0,0 @@
1
- /**
2
- * StatusFooter - Persistent terminal status bar for live agent monitoring
3
- *
4
- * Displays:
5
- * - Agent status icons (🟢 running, ⏳ waiting, 🔄 processing, etc.)
6
- * - Real-time CPU, memory, network metrics per agent
7
- * - Cluster summary stats
8
- *
9
- * Uses ANSI escape sequences to maintain a fixed footer while
10
- * allowing normal terminal output to scroll above it.
11
- *
12
- * ROBUST RESIZE HANDLING:
13
- * - Debounced resize events (100ms) prevent rapid-fire redraws
14
- * - Render lock prevents concurrent renders from corrupting state
15
- * - Full footer clear before scroll region reset prevents artifacts
16
- * - Dimension checkpointing skips unnecessary redraws
17
- * - Graceful degradation for terminals < 8 rows
18
- */
19
-
20
- const { getProcessMetrics } = require('./process-metrics');
21
-
22
- // ANSI escape codes
23
- const ESC = '\x1b';
24
- const CSI = `${ESC}[`;
25
-
26
- // Terminal manipulation
27
- const SAVE_CURSOR = `${CSI}s`;
28
- const RESTORE_CURSOR = `${CSI}u`;
29
- const CLEAR_LINE = `${CSI}2K`;
30
- const HIDE_CURSOR = `${CSI}?25l`;
31
- const SHOW_CURSOR = `${CSI}?25h`;
32
-
33
- // Colors
34
- const COLORS = {
35
- reset: `${CSI}0m`,
36
- bold: `${CSI}1m`,
37
- dim: `${CSI}2m`,
38
- cyan: `${CSI}36m`,
39
- green: `${CSI}32m`,
40
- yellow: `${CSI}33m`,
41
- red: `${CSI}31m`,
42
- gray: `${CSI}90m`,
43
- white: `${CSI}37m`,
44
- bgBlack: `${CSI}40m`,
45
- };
46
-
47
- /**
48
- * Canonical agent state constants - SINGLE SOURCE OF TRUTH
49
- * Use these instead of magic strings throughout the codebase
50
- */
51
- const AGENT_STATE = Object.freeze({
52
- IDLE: 'idle',
53
- EXECUTING_TASK: 'executing_task',
54
- EVALUATING_LOGIC: 'evaluating_logic',
55
- BUILDING_CONTEXT: 'building_context',
56
- });
57
-
58
- // Agent states that indicate "active" work (from agent-lifecycle.js)
59
- // These should show in the footer with metrics
60
- const ACTIVE_STATES = new Set([
61
- AGENT_STATE.EXECUTING_TASK, // Actually running claude CLI
62
- AGENT_STATE.EVALUATING_LOGIC, // Evaluating trigger conditions
63
- AGENT_STATE.BUILDING_CONTEXT, // Building context for task
64
- ]);
65
-
66
- /**
67
- * Debounce function - prevents rapid-fire calls during resize
68
- * @param {Function} fn - Function to debounce
69
- * @param {number} ms - Debounce delay in milliseconds
70
- * @returns {Function} Debounced function
71
- */
72
- function debounce(fn, ms) {
73
- let timeout;
74
- return (...args) => {
75
- clearTimeout(timeout);
76
- timeout = setTimeout(() => fn(...args), ms);
77
- };
78
- }
79
-
80
- /**
81
- * @typedef {Object} AgentState
82
- * @property {string} id - Agent ID
83
- * @property {string} state - Agent state (idle, executing, etc.)
84
- * @property {number|null} pid - Process ID if running
85
- * @property {number} iteration - Current iteration
86
- */
87
-
88
- class StatusFooter {
89
- /**
90
- * @param {Object} options
91
- * @param {number} [options.samplingInterval=500] - Background metrics sampling interval in ms
92
- * @param {number} [options.refreshInterval=100] - Display refresh interval in ms (10 fps)
93
- * @param {number} [options.interpolationSpeed=0.15] - Lerp factor (0-1) per refresh tick
94
- * @param {boolean} [options.enabled=true] - Whether footer is enabled
95
- * @param {number} [options.maxAgentRows=5] - Max agent rows to display
96
- */
97
- constructor(options = {}) {
98
- // INTERPOLATION ARCHITECTURE:
99
- // - Sample actual metrics every 500ms (background, non-blocking)
100
- // - Display updates every 100ms (10 fps - appears continuous)
101
- // - Values smoothly drift toward actual measurements via lerp
102
- // - Result: Real-time seeming monitoring even with less frequent polling
103
- this.samplingInterval = options.samplingInterval || 500; // Actual metrics poll (ms)
104
- this.refreshInterval = options.refreshInterval || 100; // Display update rate (ms) - was 1000
105
- this.interpolationSpeed = options.interpolationSpeed || 0.15; // Lerp factor per tick
106
- this.enabled = options.enabled !== false;
107
- this.maxAgentRows = options.maxAgentRows || 5;
108
- this.intervalId = null;
109
- this.samplingIntervalId = null; // Background metrics sampler
110
- this.agents = new Map(); // agentId -> AgentState
111
- // Interpolated metrics: { current: {...}, target: {...}, lastSampleTime, exists }
112
- // - current: Displayed value (interpolated toward target)
113
- // - target: Actual measured value from last sample
114
- this.interpolatedMetrics = new Map(); // agentId -> InterpolatedMetrics
115
- this.footerHeight = 3; // Minimum: header + 1 agent row + summary
116
- this.lastFooterHeight = 3;
117
- this.scrollRegionSet = false;
118
- this.clusterId = null;
119
- this.clusterState = 'initializing';
120
- this.startTime = Date.now();
121
- this.messageBus = null; // MessageBus for token usage tracking
122
-
123
- // Robust resize handling state
124
- this.isRendering = false; // Render lock - prevents concurrent renders
125
- this.pendingResize = false; // Queue resize if render in progress
126
- this.lastKnownRows = 0; // Track terminal dimensions for change detection
127
- this.lastKnownCols = 0;
128
- this.minRows = 8; // Minimum rows for footer display (graceful degradation)
129
- this.hidden = false; // True when terminal too small for footer
130
-
131
- // Output queue - serializes all stdout to prevent cursor corruption
132
- // When scroll region is active, console.log() can corrupt cursor position
133
- // All output must go through print() to coordinate with render cycles
134
- this.printQueue = [];
135
-
136
- // Blink state for executing agents - toggles on each render for visual pulse
137
- this.blinkState = false;
138
-
139
- // Debounced resize handler (100ms) - prevents rapid-fire redraws
140
- this._debouncedResize = debounce(() => this._handleResize(), 100);
141
- }
142
-
143
- /**
144
- * Check if we're in a TTY that supports the footer
145
- * @returns {boolean}
146
- */
147
- isTTY() {
148
- return process.stdout.isTTY === true;
149
- }
150
-
151
- /**
152
- * Print text to stdout, coordinating with the render cycle.
153
- * When a render is in progress, queues output to prevent cursor corruption.
154
- * When no render is active, writes immediately.
155
- *
156
- * MUST be used instead of console.log() when status footer is active.
157
- * @param {string} text - Text to print (newline will be added)
158
- */
159
- print(text) {
160
- if (this.isRendering) {
161
- // Queue for later - render() will flush after restoring cursor
162
- this.printQueue.push(text);
163
- } else {
164
- // Write immediately - no render in progress
165
- process.stdout.write(text + '\n');
166
- }
167
- }
168
-
169
- /**
170
- * Flush queued output to stdout.
171
- * Called after render() restores cursor to ensure proper positioning.
172
- * @private
173
- */
174
- _flushPrintQueue() {
175
- if (this.printQueue.length === 0) return;
176
-
177
- // Write all queued output
178
- const output = this.printQueue.map(text => text + '\n').join('');
179
- this.printQueue = [];
180
- process.stdout.write(output);
181
- }
182
-
183
- /**
184
- * Get terminal dimensions
185
- * @returns {{ rows: number, cols: number }}
186
- */
187
- getTerminalSize() {
188
- return {
189
- rows: process.stdout.rows || 24,
190
- cols: process.stdout.columns || 80,
191
- };
192
- }
193
-
194
- /**
195
- * Move cursor to specific position
196
- * @param {number} row - 1-based row
197
- * @param {number} col - 1-based column
198
- */
199
- moveTo(row, col) {
200
- process.stdout.write(`${CSI}${row};${col}H`);
201
- }
202
-
203
- /**
204
- * Clear a specific line completely
205
- * @param {number} row - 1-based row number
206
- * @private
207
- */
208
- _clearLine(row) {
209
- process.stdout.write(`${CSI}${row};1H${CLEAR_LINE}`);
210
- }
211
-
212
- /**
213
- * Generate move cursor ANSI sequence (returns string, doesn't write)
214
- * Used for atomic buffered writes to prevent interleaving
215
- * @param {number} row - 1-based row
216
- * @param {number} col - 1-based column
217
- * @returns {string} ANSI escape sequence
218
- * @private
219
- */
220
- _moveToStr(row, col) {
221
- return `${CSI}${row};${col}H`;
222
- }
223
-
224
- /**
225
- * Generate clear line ANSI sequence (returns string, doesn't write)
226
- * Used for atomic buffered writes to prevent interleaving
227
- * @param {number} row - 1-based row number
228
- * @returns {string} ANSI escape sequence
229
- * @private
230
- */
231
- _clearLineStr(row) {
232
- return `${CSI}${row};1H${CLEAR_LINE}`;
233
- }
234
-
235
- /**
236
- * Generate ANSI sequences to clear all footer lines (returns string)
237
- * Used for atomic buffered writes to prevent interleaving
238
- * @returns {string} ANSI escape sequences
239
- * @private
240
- */
241
- _clearFooterAreaStr() {
242
- const { rows } = this.getTerminalSize();
243
- // Use max of current and last footer height to ensure full cleanup
244
- const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
245
- const startRow = Math.max(1, rows - heightToClear + 1);
246
-
247
- let buffer = '';
248
- for (let row = startRow; row <= rows; row++) {
249
- buffer += this._clearLineStr(row);
250
- }
251
- return buffer;
252
- }
253
-
254
- /**
255
- * Clear all footer lines (uses last known height for safety)
256
- * Uses single atomic write to prevent interleaving with other processes
257
- * @private
258
- */
259
- _clearFooterArea() {
260
- process.stdout.write(this._clearFooterAreaStr());
261
- }
262
-
263
- /**
264
- * Set up scroll region to reserve space for footer
265
- * ROBUST: Clears footer area first, resets to full screen, then sets new region
266
- * Uses single atomic write to prevent interleaving with other processes
267
- */
268
- setupScrollRegion() {
269
- if (!this.isTTY()) return;
270
-
271
- const { rows, cols } = this.getTerminalSize();
272
-
273
- // Graceful degradation: hide footer if terminal too small
274
- if (rows < this.minRows) {
275
- if (!this.hidden) {
276
- this.hidden = true;
277
- // Reset to full screen scroll
278
- process.stdout.write(`${CSI}1;${rows}r`);
279
- this.scrollRegionSet = false;
280
- }
281
- return;
282
- }
283
-
284
- // Restore footer if terminal grew large enough
285
- if (this.hidden) {
286
- this.hidden = false;
287
- }
288
-
289
- const scrollEnd = rows - this.footerHeight;
290
-
291
- // BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
292
- let buffer = '';
293
-
294
- // Step 1: Save cursor before any manipulation
295
- buffer += SAVE_CURSOR;
296
- buffer += HIDE_CURSOR;
297
-
298
- // Step 2: Reset scroll region to full screen first (prevents artifacts)
299
- buffer += `${CSI}1;${rows}r`;
300
-
301
- // Step 3: Clear footer area completely (prevents ghosting)
302
- buffer += this._clearFooterAreaStr();
303
-
304
- // Step 4: Set new scroll region (lines 1 to scrollEnd)
305
- buffer += `${CSI}1;${scrollEnd}r`;
306
-
307
- // Step 5: Move cursor to bottom of scroll region (safe position)
308
- buffer += this._moveToStr(scrollEnd, 1);
309
-
310
- // Step 6: Restore cursor and show it
311
- buffer += RESTORE_CURSOR;
312
- buffer += SHOW_CURSOR;
313
-
314
- // SINGLE ATOMIC WRITE - prevents interleaving
315
- process.stdout.write(buffer);
316
-
317
- this.scrollRegionSet = true;
318
- this.lastKnownRows = rows;
319
- this.lastKnownCols = cols;
320
- }
321
-
322
- /**
323
- * Generate reset scroll region string (returns string, doesn't write)
324
- * @private
325
- */
326
- _resetScrollRegionStr() {
327
- const { rows } = this.getTerminalSize();
328
- return `${CSI}1;${rows}r`;
329
- }
330
-
331
- /**
332
- * Reset scroll region to full terminal
333
- */
334
- resetScrollRegion() {
335
- if (!this.isTTY()) return;
336
-
337
- process.stdout.write(this._resetScrollRegionStr());
338
- this.scrollRegionSet = false;
339
- }
340
-
341
- /**
342
- * Handle terminal resize event
343
- * Called via debounced wrapper to prevent rapid-fire redraws
344
- * @private
345
- */
346
- _handleResize() {
347
- if (!this.isTTY()) return;
348
-
349
- const { rows, cols } = this.getTerminalSize();
350
-
351
- // Skip if dimensions haven't actually changed (debounce may still fire)
352
- if (rows === this.lastKnownRows && cols === this.lastKnownCols) {
353
- return;
354
- }
355
-
356
- // If render in progress, queue resize for after
357
- if (this.isRendering) {
358
- this.pendingResize = true;
359
- return;
360
- }
361
-
362
- // Update dimensions and reconfigure
363
- this.lastKnownRows = rows;
364
- this.lastKnownCols = cols;
365
-
366
- this.setupScrollRegion();
367
- this.render();
368
- }
369
-
370
- /**
371
- * Register cluster for monitoring
372
- * @param {string} clusterId
373
- */
374
- setCluster(clusterId) {
375
- this.clusterId = clusterId;
376
- }
377
-
378
- /**
379
- * Set message bus for token usage tracking
380
- * @param {object} messageBus - MessageBus instance with getTokensByRole()
381
- */
382
- setMessageBus(messageBus) {
383
- this.messageBus = messageBus;
384
- }
385
-
386
- /**
387
- * Update cluster state
388
- * @param {string} state
389
- */
390
- setClusterState(state) {
391
- this.clusterState = state;
392
- }
393
-
394
- /**
395
- * Register an agent for monitoring
396
- * @param {AgentState} agentState
397
- */
398
- updateAgent(agentState) {
399
- this.agents.set(agentState.id, {
400
- ...agentState,
401
- lastUpdate: Date.now(),
402
- });
403
- }
404
-
405
- /**
406
- * Remove an agent from monitoring
407
- * @param {string} agentId
408
- */
409
- removeAgent(agentId) {
410
- this.agents.delete(agentId);
411
- this.interpolatedMetrics.delete(agentId);
412
- }
413
-
414
- /**
415
- * Background metrics sampling - polls actual CPU/RAM every samplingInterval ms
416
- * Updates target values that display will interpolate toward
417
- * @private
418
- */
419
- async _sampleMetrics() {
420
- for (const [agentId, agent] of this.agents) {
421
- if (!agent.pid) continue;
422
-
423
- try {
424
- // Get actual metrics with 200ms sample window (for CPU calculation)
425
- const raw = await getProcessMetrics(agent.pid, { samplePeriodMs: 200 });
426
- const existing = this.interpolatedMetrics.get(agentId);
427
-
428
- this.interpolatedMetrics.set(agentId, {
429
- // Keep interpolating from current position (smooth transition)
430
- current: existing?.current || {
431
- cpuPercent: raw.cpuPercent,
432
- memoryMB: raw.memoryMB,
433
- },
434
- // New target to approach
435
- target: {
436
- cpuPercent: raw.cpuPercent,
437
- memoryMB: raw.memoryMB,
438
- },
439
- // Network is cumulative - just use latest value (no interpolation)
440
- network: raw.network,
441
- lastSampleTime: Date.now(),
442
- exists: raw.exists,
443
- });
444
- } catch {
445
- // Process may have exited
446
- this.interpolatedMetrics.delete(agentId);
447
- }
448
- }
449
- }
450
-
451
- /**
452
- * Interpolate current values toward target values
453
- * Called each render tick (100ms) for smooth visual updates
454
- * @private
455
- */
456
- _interpolateMetrics() {
457
- for (const [, data] of this.interpolatedMetrics) {
458
- if (!data.target || !data.current) continue;
459
-
460
- // Lerp CPU and RAM toward target (they fluctuate)
461
- data.current.cpuPercent = this._lerp(
462
- data.current.cpuPercent,
463
- data.target.cpuPercent
464
- );
465
- data.current.memoryMB = this._lerp(
466
- data.current.memoryMB,
467
- data.target.memoryMB
468
- );
469
- // Network is cumulative counter - no interpolation, just use latest
470
- }
471
- }
472
-
473
- /**
474
- * Linear interpolation helper
475
- * Moves current 15% of the way toward target each tick
476
- * At 100ms ticks: reaches ~87% of target after 1 second
477
- * @param {number} current - Current displayed value
478
- * @param {number} target - Target measured value
479
- * @returns {number} New interpolated value
480
- * @private
481
- */
482
- _lerp(current, target) {
483
- if (current === null || current === undefined) return target;
484
- if (target === null || target === undefined) return current;
485
- const diff = target - current;
486
- return current + diff * this.interpolationSpeed;
487
- }
488
-
489
- /**
490
- * Get status icon for agent state
491
- * Uses blinking dot for active states (executing, evaluating, building_context)
492
- * @param {string} state
493
- * @returns {string}
494
- */
495
- getAgentIcon(state) {
496
- // Blinking indicator for active states - alternates between bright and dim
497
- const blinkOn = `${COLORS.green}●${COLORS.reset}`;
498
- const blinkOff = `${COLORS.dim}○${COLORS.reset}`;
499
- const blinkIndicator = this.blinkState ? blinkOn : blinkOff;
500
-
501
- switch (state) {
502
- case 'idle':
503
- return `${COLORS.gray}○${COLORS.reset}`; // Waiting for trigger
504
- case 'evaluating_logic':
505
- return blinkIndicator; // Evaluating triggers (blinking)
506
- case 'building_context':
507
- return blinkIndicator; // Building context (blinking)
508
- case 'executing_task':
509
- return blinkIndicator; // Running task (blinking)
510
- case 'stopped':
511
- return `${COLORS.gray}■${COLORS.reset}`; // Stopped
512
- case 'error':
513
- return `${COLORS.red}●${COLORS.reset}`; // Error
514
- default:
515
- return `${COLORS.gray}○${COLORS.reset}`;
516
- }
517
- }
518
-
519
- /**
520
- * Format duration in human-readable form
521
- * @param {number} ms
522
- * @returns {string}
523
- */
524
- formatDuration(ms) {
525
- const seconds = Math.floor(ms / 1000);
526
- const minutes = Math.floor(seconds / 60);
527
- const hours = Math.floor(minutes / 60);
528
-
529
- if (hours > 0) {
530
- return `${hours}h${minutes % 60}m`;
531
- }
532
- if (minutes > 0) {
533
- return `${minutes}m${seconds % 60}s`;
534
- }
535
- return `${seconds}s`;
536
- }
537
-
538
- /**
539
- * Format bytes in human-readable form
540
- * @param {number} bytes
541
- * @returns {string}
542
- */
543
- formatBytes(bytes) {
544
- if (bytes < 1024) {
545
- return `${bytes}B`;
546
- }
547
- if (bytes < 1024 * 1024) {
548
- return `${(bytes / 1024).toFixed(1)}KB`;
549
- }
550
- if (bytes < 1024 * 1024 * 1024) {
551
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
552
- }
553
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
554
- }
555
-
556
- /**
557
- * Render the footer
558
- * ROBUST: Uses render lock to prevent concurrent renders from corrupting state
559
- */
560
- render() {
561
- if (!this.enabled || !this.isTTY()) return;
562
-
563
- // Graceful degradation: don't render if hidden
564
- if (this.hidden) return;
565
-
566
- // Render lock: prevent concurrent renders
567
- if (this.isRendering) {
568
- return;
569
- }
570
- this.isRendering = true;
571
-
572
- // Toggle blink state for visual pulse effect on executing agents
573
- this.blinkState = !this.blinkState;
574
-
575
- try {
576
- const { rows, cols } = this.getTerminalSize();
577
-
578
- // Double-check terminal size (may have changed since last check)
579
- if (rows < this.minRows) {
580
- this.hidden = true;
581
- this.resetScrollRegion();
582
- return;
583
- }
584
-
585
- // Interpolate metrics toward targets (called every 100ms for smooth visuals)
586
- // Background sampler updates targets every 500ms
587
- this._interpolateMetrics();
588
-
589
- // Get executing agents for display
590
- const executingAgents = Array.from(this.agents.entries())
591
- .filter(([, agent]) => ACTIVE_STATES.has(agent.state))
592
- .slice(0, this.maxAgentRows);
593
-
594
- // Calculate dynamic footer height: header + agent rows + summary
595
- // Minimum 3 lines (header + "no agents" message + summary)
596
- const agentRowCount = Math.max(1, executingAgents.length);
597
- const newHeight = 2 + agentRowCount + 1; // header + agents + summary
598
-
599
- // Update scroll region if height changed
600
- if (newHeight !== this.footerHeight) {
601
- this.lastFooterHeight = this.footerHeight;
602
- this.footerHeight = newHeight;
603
- this.setupScrollRegion();
604
- }
605
-
606
- // Build footer lines
607
- const headerLine = this.buildHeaderLine(cols);
608
- const agentRows = this.buildAgentRows(executingAgents, cols);
609
- const summaryLine = this.buildSummaryLine(cols);
610
-
611
- // BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
612
- // This prevents interleaving with other processes writing to stdout
613
- let buffer = '';
614
- buffer += SAVE_CURSOR;
615
- buffer += HIDE_CURSOR;
616
-
617
- // Render from top of footer area
618
- let currentRow = rows - this.footerHeight + 1;
619
-
620
- // Header line
621
- buffer += this._moveToStr(currentRow++, 1);
622
- buffer += CLEAR_LINE;
623
- buffer += `${COLORS.bgBlack}${headerLine}${COLORS.reset}`;
624
-
625
- // Agent rows
626
- for (const agentRow of agentRows) {
627
- buffer += this._moveToStr(currentRow++, 1);
628
- buffer += CLEAR_LINE;
629
- buffer += `${COLORS.bgBlack}${agentRow}${COLORS.reset}`;
630
- }
631
-
632
- // Summary line (with bottom border)
633
- buffer += this._moveToStr(currentRow, 1);
634
- buffer += CLEAR_LINE;
635
- buffer += `${COLORS.bgBlack}${summaryLine}${COLORS.reset}`;
636
-
637
- buffer += RESTORE_CURSOR;
638
- buffer += SHOW_CURSOR;
639
-
640
- // SINGLE ATOMIC WRITE - prevents interleaving
641
- process.stdout.write(buffer);
642
- } finally {
643
- this.isRendering = false;
644
-
645
- // CRITICAL: Position cursor at bottom of scroll region before flushing
646
- // Without this, output goes below footer if cursor was restored outside scroll region
647
- // (RESTORE_CURSOR at line 527 may restore to row outside the scrollable area)
648
- if (this.scrollRegionSet) {
649
- const { rows } = this.getTerminalSize();
650
- const scrollEnd = rows - this.footerHeight;
651
- process.stdout.write(this._moveToStr(scrollEnd, 1));
652
- }
653
-
654
- // Flush any output that was queued during render
655
- // Must happen BEFORE pending resize to preserve output order
656
- this._flushPrintQueue();
657
-
658
- // Process pending resize if one was queued during render
659
- if (this.pendingResize) {
660
- this.pendingResize = false;
661
- // Use setImmediate to avoid deep recursion
662
- setImmediate(() => this._handleResize());
663
- }
664
- }
665
- }
666
-
667
- /**
668
- * Build the header line with cluster ID
669
- * @param {number} width - Terminal width
670
- * @returns {string}
671
- */
672
- buildHeaderLine(width) {
673
- let content = `${COLORS.gray}┌─${COLORS.reset}`;
674
-
675
- // Cluster ID
676
- if (this.clusterId) {
677
- const shortId = this.clusterId.replace('cluster-', '');
678
- content += ` ${COLORS.cyan}${COLORS.bold}${shortId}${COLORS.reset} `;
679
- }
680
-
681
- // Fill with border
682
- const contentLen = this.stripAnsi(content).length;
683
- const padding = Math.max(0, width - contentLen - 1);
684
- return content + `${COLORS.gray}${'─'.repeat(padding)}┐${COLORS.reset}`;
685
- }
686
-
687
- /**
688
- * Build agent rows (one row per executing agent)
689
- * @param {Array} executingAgents - Array of [agentId, agent] pairs
690
- * @param {number} width - Terminal width
691
- * @returns {Array<string>} Array of formatted rows
692
- */
693
- buildAgentRows(executingAgents, width) {
694
- if (executingAgents.length === 0) {
695
- // No agents row
696
- const content = `${COLORS.gray}│${COLORS.reset} ${COLORS.dim}No active agents${COLORS.reset}`;
697
- const contentLen = this.stripAnsi(content).length;
698
- const padding = Math.max(0, width - contentLen - 1);
699
- return [content + ' '.repeat(padding) + `${COLORS.gray}│${COLORS.reset}`];
700
- }
701
-
702
- const rows = [];
703
- for (const [agentId, agent] of executingAgents) {
704
- const icon = this.getAgentIcon(agent.state);
705
- const data = this.interpolatedMetrics.get(agentId);
706
- const metrics = data?.current;
707
-
708
- // Build columns with fixed widths for alignment
709
- const iconCol = icon;
710
- const nameCol = agentId.padEnd(14).slice(0, 14); // Max 14 chars for name
711
-
712
- let metricsStr = '';
713
- if (metrics && data.exists) {
714
- const cpuColor = metrics.cpuPercent > 50 ? COLORS.yellow : COLORS.green;
715
- const cpuVal = `${metrics.cpuPercent.toFixed(1)}%`.padStart(6);
716
- const ramVal = `${metrics.memoryMB.toFixed(1)}MB`.padStart(8);
717
-
718
- metricsStr += `${COLORS.dim}CPU:${COLORS.reset}${cpuColor}${cpuVal}${COLORS.reset}`;
719
- metricsStr += ` ${COLORS.dim}RAM:${COLORS.reset}${COLORS.gray}${ramVal}${COLORS.reset}`;
720
-
721
- // Network bytes (cumulative - no interpolation)
722
- const net = data.network;
723
- if (net && (net.bytesSent > 0 || net.bytesReceived > 0)) {
724
- const sent = this.formatBytes(net.bytesSent).padStart(7);
725
- const recv = this.formatBytes(net.bytesReceived).padStart(7);
726
- metricsStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${sent} ↓${recv}${COLORS.reset}`;
727
- }
728
- } else {
729
- metricsStr = `${COLORS.dim}(starting...)${COLORS.reset}`;
730
- }
731
-
732
- // Iteration number
733
- const iterStr = agent.iteration > 0 ? `${COLORS.dim}#${agent.iteration}${COLORS.reset}` : '';
734
-
735
- // Build the row
736
- let content = `${COLORS.gray}│${COLORS.reset} ${iconCol} ${COLORS.white}${nameCol}${COLORS.reset} ${metricsStr}`;
737
- if (iterStr) {
738
- content += ` ${iterStr}`;
739
- }
740
-
741
- const contentLen = this.stripAnsi(content).length;
742
- const padding = Math.max(0, width - contentLen - 1);
743
- rows.push(content + ' '.repeat(padding) + `${COLORS.gray}│${COLORS.reset}`);
744
- }
745
-
746
- return rows;
747
- }
748
-
749
- /**
750
- * Build a single status line for testing/display
751
- * Alias for buildSummaryLine for backward compatibility
752
- * @param {number} width - Terminal width
753
- * @returns {string}
754
- */
755
- buildStatusLine(width) {
756
- return this.buildSummaryLine(width);
757
- }
758
-
759
- /**
760
- * Build the summary line with aggregated metrics
761
- * @param {number} width - Terminal width
762
- * @returns {string}
763
- */
764
- buildSummaryLine(width) {
765
- const parts = [];
766
-
767
- // Border with corner
768
- parts.push(`${COLORS.gray}└─${COLORS.reset}`);
769
-
770
- // Cluster state
771
- const stateColor = this.clusterState === 'running' ? COLORS.green : COLORS.yellow;
772
- parts.push(` ${stateColor}${this.clusterState}${COLORS.reset}`);
773
-
774
- // Duration
775
- const duration = this.formatDuration(Date.now() - this.startTime);
776
- parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
777
-
778
- // Agent counts
779
- const executing = Array.from(this.agents.values()).filter((a) => ACTIVE_STATES.has(a.state)).length;
780
- const total = this.agents.size;
781
- parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
782
-
783
- // Token cost (from message bus)
784
- if (this.messageBus && this.clusterId) {
785
- try {
786
- const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
787
- const totalCost = tokensByRole?._total?.totalCostUsd || 0;
788
- if (totalCost > 0) {
789
- // Format: $0.05 or $1.23 or $12.34
790
- const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
791
- parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
792
- }
793
- } catch {
794
- // Ignore errors - token tracking is optional
795
- }
796
- }
797
-
798
- // Aggregate metrics (using interpolated values for smooth display)
799
- let totalCpu = 0;
800
- let totalMem = 0;
801
- let totalBytesSent = 0;
802
- let totalBytesReceived = 0;
803
- for (const data of this.interpolatedMetrics.values()) {
804
- if (data.exists && data.current) {
805
- totalCpu += data.current.cpuPercent;
806
- totalMem += data.current.memoryMB;
807
- totalBytesSent += data.network?.bytesSent || 0;
808
- totalBytesReceived += data.network?.bytesReceived || 0;
809
- }
810
- }
811
-
812
- if (totalCpu > 0 || totalMem > 0) {
813
- parts.push(` ${COLORS.gray}│${COLORS.reset}`);
814
- let aggregateStr = ` ${COLORS.cyan}Σ${COLORS.reset} `;
815
- aggregateStr += `${COLORS.dim}CPU:${COLORS.reset}${totalCpu.toFixed(1)}%`;
816
- aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(1)}MB`;
817
- if (totalBytesSent > 0 || totalBytesReceived > 0) {
818
- aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
819
- }
820
- parts.push(aggregateStr);
821
- }
822
-
823
- // Pad and close with bottom corner
824
- const content = parts.join('');
825
- const contentLen = this.stripAnsi(content).length;
826
- const padding = Math.max(0, width - contentLen - 1);
827
- return content + `${COLORS.gray}${'─'.repeat(padding)}┘${COLORS.reset}`;
828
- }
829
-
830
- /**
831
- * Strip ANSI codes from string for length calculation
832
- * @param {string} str
833
- * @returns {string}
834
- */
835
- stripAnsi(str) {
836
- // eslint-disable-next-line no-control-regex
837
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
838
- }
839
-
840
- /**
841
- * Start the status footer
842
- */
843
- start() {
844
- if (!this.enabled || !this.isTTY()) {
845
- return;
846
- }
847
-
848
- // Initialize dimension tracking
849
- const { rows, cols } = this.getTerminalSize();
850
- this.lastKnownRows = rows;
851
- this.lastKnownCols = cols;
852
-
853
- // Check for graceful degradation at startup
854
- if (rows < this.minRows) {
855
- this.hidden = true;
856
- return; // Don't set up scroll region for tiny terminals
857
- }
858
-
859
- this.setupScrollRegion();
860
-
861
- // Handle terminal resize with debounced handler
862
- process.stdout.on('resize', this._debouncedResize);
863
-
864
- // Start background metrics sampling (every 500ms)
865
- // Non-blocking - updates target values that display will interpolate toward
866
- this.samplingIntervalId = setInterval(() => {
867
- this._sampleMetrics().catch(() => {
868
- // Ignore sampling errors - display will show stale data
869
- });
870
- }, this.samplingInterval);
871
-
872
- // Initial metrics sample (async, don't block startup)
873
- this._sampleMetrics().catch(() => {});
874
-
875
- // Start display refresh interval (every 100ms - 10 fps)
876
- // Interpolates current values toward targets for smooth visual updates
877
- this.intervalId = setInterval(() => {
878
- if (this.isRendering) return;
879
- this.render();
880
- }, this.refreshInterval);
881
-
882
- // Initial render
883
- this.render();
884
- }
885
-
886
- /**
887
- * Stop the status footer and cleanup
888
- */
889
- stop() {
890
- if (this.intervalId) {
891
- clearInterval(this.intervalId);
892
- this.intervalId = null;
893
- }
894
-
895
- // Stop background metrics sampling
896
- if (this.samplingIntervalId) {
897
- clearInterval(this.samplingIntervalId);
898
- this.samplingIntervalId = null;
899
- }
900
-
901
- // Remove resize listener
902
- process.stdout.removeListener('resize', this._debouncedResize);
903
-
904
- // ALWAYS perform cleanup if TTY, regardless of hidden state
905
- // BUG FIX: Previously skipped cleanup when hidden=true, but footer content
906
- // may still be rendered from before terminal was resized small.
907
- // The cleanup operations are harmless if footer was never rendered.
908
- if (this.isTTY()) {
909
- // BUILD SINGLE BUFFER for atomic shutdown write
910
- // Prevents interleaving with agent output during cleanup
911
- let buffer = '';
912
-
913
- // CRITICAL: Clear footer area BEFORE resetting scroll region
914
- // While scroll region is active, footer area contains only status bar content
915
- // After reset, those lines may contain scrolled output (which we DON'T want to clear)
916
- buffer += this._clearFooterAreaStr();
917
-
918
- // Now reset scroll region (full terminal is scrollable again)
919
- buffer += this._resetScrollRegionStr();
920
- this.scrollRegionSet = false;
921
-
922
- // Move cursor to safe position and show cursor
923
- const { rows } = this.getTerminalSize();
924
- const startRow = rows - this.footerHeight + 1;
925
- buffer += this._moveToStr(startRow, 1);
926
- buffer += SHOW_CURSOR;
927
-
928
- // SINGLE ATOMIC WRITE
929
- process.stdout.write(buffer);
930
- }
931
- }
932
-
933
- /**
934
- * Temporarily hide footer for clean output
935
- */
936
- hide() {
937
- if (!this.isTTY()) return;
938
-
939
- // Single atomic write for hide operation
940
- // CRITICAL: Clear footer BEFORE resetting scroll region (same reason as stop())
941
- let buffer = this._clearFooterAreaStr();
942
- buffer += this._resetScrollRegionStr();
943
- this.scrollRegionSet = false;
944
- process.stdout.write(buffer);
945
- }
946
-
947
- /**
948
- * Restore footer after hiding
949
- */
950
- show() {
951
- if (!this.isTTY()) return;
952
-
953
- // Reset hidden state and check terminal size
954
- const { rows } = this.getTerminalSize();
955
- if (rows < this.minRows) {
956
- this.hidden = true;
957
- return;
958
- }
959
-
960
- this.hidden = false;
961
- this.setupScrollRegion();
962
- this.render();
963
- }
964
- }
965
-
966
- /**
967
- * Create a singleton footer instance
968
- * @param {Object} options
969
- * @returns {StatusFooter}
970
- */
971
- function createStatusFooter(options = {}) {
972
- return new StatusFooter(options);
973
- }
974
-
975
- module.exports = {
976
- StatusFooter,
977
- createStatusFooter,
978
- AGENT_STATE,
979
- ACTIVE_STATES,
980
- };