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