@covibes/zeroshot 1.0.1 → 1.1.3

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 (42) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +2 -0
  3. package/cli/index.js +151 -208
  4. package/cli/message-formatter-utils.js +75 -0
  5. package/cli/message-formatters-normal.js +214 -0
  6. package/cli/message-formatters-watch.js +181 -0
  7. package/cluster-templates/base-templates/full-workflow.json +10 -5
  8. package/docker/zeroshot-cluster/Dockerfile +6 -0
  9. package/package.json +5 -2
  10. package/src/agent/agent-task-executor.js +237 -112
  11. package/src/isolation-manager.js +94 -51
  12. package/src/orchestrator.js +45 -10
  13. package/src/preflight.js +383 -0
  14. package/src/process-metrics.js +546 -0
  15. package/src/status-footer.js +543 -0
  16. package/task-lib/attachable-watcher.js +202 -0
  17. package/task-lib/commands/clean.js +50 -0
  18. package/task-lib/commands/get-log-path.js +23 -0
  19. package/task-lib/commands/kill.js +32 -0
  20. package/task-lib/commands/list.js +105 -0
  21. package/task-lib/commands/logs.js +411 -0
  22. package/task-lib/commands/resume.js +41 -0
  23. package/task-lib/commands/run.js +48 -0
  24. package/task-lib/commands/schedule.js +105 -0
  25. package/task-lib/commands/scheduler-cmd.js +96 -0
  26. package/task-lib/commands/schedules.js +98 -0
  27. package/task-lib/commands/status.js +44 -0
  28. package/task-lib/commands/unschedule.js +16 -0
  29. package/task-lib/completion.js +9 -0
  30. package/task-lib/config.js +10 -0
  31. package/task-lib/name-generator.js +230 -0
  32. package/task-lib/package.json +3 -0
  33. package/task-lib/runner.js +123 -0
  34. package/task-lib/scheduler.js +252 -0
  35. package/task-lib/store.js +217 -0
  36. package/task-lib/tui/formatters.js +166 -0
  37. package/task-lib/tui/index.js +197 -0
  38. package/task-lib/tui/layout.js +111 -0
  39. package/task-lib/tui/renderer.js +119 -0
  40. package/task-lib/tui.js +384 -0
  41. package/task-lib/watcher.js +162 -0
  42. package/cluster-templates/conductor-junior-bootstrap.json +0 -69
@@ -0,0 +1,543 @@
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
+
13
+ const { getProcessMetrics } = require('./process-metrics');
14
+
15
+ // ANSI escape codes
16
+ const ESC = '\x1b';
17
+ const CSI = `${ESC}[`;
18
+
19
+ // Terminal manipulation
20
+ const SAVE_CURSOR = `${CSI}s`;
21
+ const RESTORE_CURSOR = `${CSI}u`;
22
+ const CLEAR_LINE = `${CSI}2K`;
23
+ const HIDE_CURSOR = `${CSI}?25l`;
24
+ const SHOW_CURSOR = `${CSI}?25h`;
25
+
26
+ // Colors
27
+ const COLORS = {
28
+ reset: `${CSI}0m`,
29
+ bold: `${CSI}1m`,
30
+ dim: `${CSI}2m`,
31
+ cyan: `${CSI}36m`,
32
+ green: `${CSI}32m`,
33
+ yellow: `${CSI}33m`,
34
+ red: `${CSI}31m`,
35
+ gray: `${CSI}90m`,
36
+ white: `${CSI}37m`,
37
+ bgBlack: `${CSI}40m`,
38
+ };
39
+
40
+ /**
41
+ * @typedef {Object} AgentState
42
+ * @property {string} id - Agent ID
43
+ * @property {string} state - Agent state (idle, executing, etc.)
44
+ * @property {number|null} pid - Process ID if running
45
+ * @property {number} iteration - Current iteration
46
+ */
47
+
48
+ class StatusFooter {
49
+ /**
50
+ * @param {Object} options
51
+ * @param {number} [options.refreshInterval=1000] - Refresh interval in ms
52
+ * @param {boolean} [options.enabled=true] - Whether footer is enabled
53
+ * @param {number} [options.maxAgentRows=5] - Max agent rows to display
54
+ */
55
+ constructor(options = {}) {
56
+ this.refreshInterval = options.refreshInterval || 1000;
57
+ this.enabled = options.enabled !== false;
58
+ this.maxAgentRows = options.maxAgentRows || 5;
59
+ this.intervalId = null;
60
+ this.agents = new Map(); // agentId -> AgentState
61
+ this.metricsCache = new Map(); // agentId -> ProcessMetrics
62
+ this.footerHeight = 3; // Minimum: header + 1 agent row + summary
63
+ this.lastFooterHeight = 3;
64
+ this.scrollRegionSet = false;
65
+ this.clusterId = null;
66
+ this.clusterState = 'initializing';
67
+ this.startTime = Date.now();
68
+ }
69
+
70
+ /**
71
+ * Check if we're in a TTY that supports the footer
72
+ * @returns {boolean}
73
+ */
74
+ isTTY() {
75
+ return process.stdout.isTTY === true;
76
+ }
77
+
78
+ /**
79
+ * Get terminal dimensions
80
+ * @returns {{ rows: number, cols: number }}
81
+ */
82
+ getTerminalSize() {
83
+ return {
84
+ rows: process.stdout.rows || 24,
85
+ cols: process.stdout.columns || 80,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Move cursor to specific position
91
+ * @param {number} row - 1-based row
92
+ * @param {number} col - 1-based column
93
+ */
94
+ moveTo(row, col) {
95
+ process.stdout.write(`${CSI}${row};${col}H`);
96
+ }
97
+
98
+ /**
99
+ * Set up scroll region to reserve space for footer
100
+ */
101
+ setupScrollRegion() {
102
+ if (!this.isTTY()) return;
103
+
104
+ const { rows } = this.getTerminalSize();
105
+ const scrollEnd = rows - this.footerHeight;
106
+
107
+ // Set scroll region (lines 1 to scrollEnd)
108
+ process.stdout.write(`${CSI}1;${scrollEnd}r`);
109
+
110
+ // Move cursor to top of scroll region
111
+ process.stdout.write(`${CSI}1;1H`);
112
+
113
+ this.scrollRegionSet = true;
114
+ }
115
+
116
+ /**
117
+ * Reset scroll region to full terminal
118
+ */
119
+ resetScrollRegion() {
120
+ if (!this.isTTY()) return;
121
+
122
+ const { rows } = this.getTerminalSize();
123
+ process.stdout.write(`${CSI}1;${rows}r`);
124
+ this.scrollRegionSet = false;
125
+ }
126
+
127
+ /**
128
+ * Register cluster for monitoring
129
+ * @param {string} clusterId
130
+ */
131
+ setCluster(clusterId) {
132
+ this.clusterId = clusterId;
133
+ }
134
+
135
+ /**
136
+ * Update cluster state
137
+ * @param {string} state
138
+ */
139
+ setClusterState(state) {
140
+ this.clusterState = state;
141
+ }
142
+
143
+ /**
144
+ * Register an agent for monitoring
145
+ * @param {AgentState} agentState
146
+ */
147
+ updateAgent(agentState) {
148
+ this.agents.set(agentState.id, {
149
+ ...agentState,
150
+ lastUpdate: Date.now(),
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Remove an agent from monitoring
156
+ * @param {string} agentId
157
+ */
158
+ removeAgent(agentId) {
159
+ this.agents.delete(agentId);
160
+ this.metricsCache.delete(agentId);
161
+ }
162
+
163
+ /**
164
+ * Get status icon for agent state
165
+ * @param {string} state
166
+ * @returns {string}
167
+ */
168
+ getAgentIcon(state) {
169
+ switch (state) {
170
+ case 'idle':
171
+ return '⏳'; // Waiting for trigger
172
+ case 'evaluating':
173
+ return '🔍'; // Evaluating triggers
174
+ case 'building_context':
175
+ return '📝'; // Building context
176
+ case 'executing':
177
+ return '🔄'; // Running task
178
+ case 'stopped':
179
+ return '⏹️'; // Stopped
180
+ case 'error':
181
+ return '❌'; // Error
182
+ default:
183
+ return '⚪';
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Format duration in human-readable form
189
+ * @param {number} ms
190
+ * @returns {string}
191
+ */
192
+ formatDuration(ms) {
193
+ const seconds = Math.floor(ms / 1000);
194
+ const minutes = Math.floor(seconds / 60);
195
+ const hours = Math.floor(minutes / 60);
196
+
197
+ if (hours > 0) {
198
+ return `${hours}h${minutes % 60}m`;
199
+ }
200
+ if (minutes > 0) {
201
+ return `${minutes}m${seconds % 60}s`;
202
+ }
203
+ return `${seconds}s`;
204
+ }
205
+
206
+ /**
207
+ * Format bytes in human-readable form
208
+ * @param {number} bytes
209
+ * @returns {string}
210
+ */
211
+ formatBytes(bytes) {
212
+ if (bytes < 1024) {
213
+ return `${bytes}B`;
214
+ }
215
+ if (bytes < 1024 * 1024) {
216
+ return `${(bytes / 1024).toFixed(1)}KB`;
217
+ }
218
+ if (bytes < 1024 * 1024 * 1024) {
219
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
220
+ }
221
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
222
+ }
223
+
224
+ /**
225
+ * Render the footer
226
+ */
227
+ async render() {
228
+ if (!this.enabled || !this.isTTY()) return;
229
+
230
+ const { rows, cols } = this.getTerminalSize();
231
+
232
+ // Collect metrics for all agents with PIDs
233
+ for (const [agentId, agent] of this.agents) {
234
+ if (agent.pid) {
235
+ try {
236
+ const metrics = await getProcessMetrics(agent.pid, { samplePeriodMs: 500 });
237
+ this.metricsCache.set(agentId, metrics);
238
+ } catch {
239
+ // Process may have exited
240
+ this.metricsCache.delete(agentId);
241
+ }
242
+ }
243
+ }
244
+
245
+ // Get executing agents for display
246
+ const executingAgents = Array.from(this.agents.entries())
247
+ .filter(([, agent]) => agent.state === 'executing')
248
+ .slice(0, this.maxAgentRows);
249
+
250
+ // Calculate dynamic footer height: header + agent rows + separator + summary
251
+ // Minimum 3 lines (header + "no agents" message + summary)
252
+ const agentRowCount = Math.max(1, executingAgents.length);
253
+ const newHeight = 2 + agentRowCount + 1; // header + agents + summary (separator merged with summary)
254
+
255
+ // Update scroll region if height changed
256
+ if (newHeight !== this.footerHeight) {
257
+ this.footerHeight = newHeight;
258
+ this.setupScrollRegion();
259
+ }
260
+
261
+ // Build footer lines
262
+ const headerLine = this.buildHeaderLine(cols);
263
+ const agentRows = this.buildAgentRows(executingAgents, cols);
264
+ const summaryLine = this.buildSummaryLine(cols);
265
+
266
+ // Save cursor, render footer, restore cursor
267
+ process.stdout.write(SAVE_CURSOR);
268
+ process.stdout.write(HIDE_CURSOR);
269
+
270
+ // Render from top of footer area
271
+ let currentRow = rows - this.footerHeight + 1;
272
+
273
+ // Header line
274
+ this.moveTo(currentRow++, 1);
275
+ process.stdout.write(CLEAR_LINE);
276
+ process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
277
+
278
+ // Agent rows
279
+ for (const agentRow of agentRows) {
280
+ this.moveTo(currentRow++, 1);
281
+ process.stdout.write(CLEAR_LINE);
282
+ process.stdout.write(`${COLORS.bgBlack}${agentRow}${COLORS.reset}`);
283
+ }
284
+
285
+ // Summary line (with bottom border)
286
+ this.moveTo(currentRow, 1);
287
+ process.stdout.write(CLEAR_LINE);
288
+ process.stdout.write(`${COLORS.bgBlack}${summaryLine}${COLORS.reset}`);
289
+
290
+ process.stdout.write(RESTORE_CURSOR);
291
+ process.stdout.write(SHOW_CURSOR);
292
+ }
293
+
294
+ /**
295
+ * Build the header line with cluster ID
296
+ * @param {number} width - Terminal width
297
+ * @returns {string}
298
+ */
299
+ buildHeaderLine(width) {
300
+ let content = `${COLORS.gray}┌─${COLORS.reset}`;
301
+
302
+ // Cluster ID
303
+ if (this.clusterId) {
304
+ const shortId = this.clusterId.replace('cluster-', '');
305
+ content += ` ${COLORS.cyan}${COLORS.bold}${shortId}${COLORS.reset} `;
306
+ }
307
+
308
+ // Fill with border
309
+ const contentLen = this.stripAnsi(content).length;
310
+ const padding = Math.max(0, width - contentLen - 1);
311
+ return content + `${COLORS.gray}${'─'.repeat(padding)}┐${COLORS.reset}`;
312
+ }
313
+
314
+ /**
315
+ * Build agent rows (one row per executing agent)
316
+ * @param {Array} executingAgents - Array of [agentId, agent] pairs
317
+ * @param {number} width - Terminal width
318
+ * @returns {Array<string>} Array of formatted rows
319
+ */
320
+ buildAgentRows(executingAgents, width) {
321
+ if (executingAgents.length === 0) {
322
+ // No agents row
323
+ const content = `${COLORS.gray}│${COLORS.reset} ${COLORS.dim}No active agents${COLORS.reset}`;
324
+ const contentLen = this.stripAnsi(content).length;
325
+ const padding = Math.max(0, width - contentLen - 1);
326
+ return [content + ' '.repeat(padding) + `${COLORS.gray}│${COLORS.reset}`];
327
+ }
328
+
329
+ const rows = [];
330
+ for (const [agentId, agent] of executingAgents) {
331
+ const icon = this.getAgentIcon(agent.state);
332
+ const metrics = this.metricsCache.get(agentId);
333
+
334
+ // Build columns with fixed widths for alignment
335
+ const iconCol = icon;
336
+ const nameCol = agentId.padEnd(14).slice(0, 14); // Max 14 chars for name
337
+
338
+ let metricsStr = '';
339
+ if (metrics && metrics.exists) {
340
+ const cpuColor = metrics.cpuPercent > 50 ? COLORS.yellow : COLORS.green;
341
+ const cpuVal = `${metrics.cpuPercent}%`.padStart(4);
342
+ const ramVal = `${metrics.memoryMB}MB`.padStart(6);
343
+
344
+ metricsStr += `${COLORS.dim}CPU:${COLORS.reset}${cpuColor}${cpuVal}${COLORS.reset}`;
345
+ metricsStr += ` ${COLORS.dim}RAM:${COLORS.reset}${COLORS.gray}${ramVal}${COLORS.reset}`;
346
+
347
+ // Network bytes
348
+ const net = metrics.network;
349
+ if (net.bytesSent > 0 || net.bytesReceived > 0) {
350
+ const sent = this.formatBytes(net.bytesSent).padStart(7);
351
+ const recv = this.formatBytes(net.bytesReceived).padStart(7);
352
+ metricsStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${sent} ↓${recv}${COLORS.reset}`;
353
+ }
354
+ } else {
355
+ metricsStr = `${COLORS.dim}(starting...)${COLORS.reset}`;
356
+ }
357
+
358
+ // Iteration number
359
+ const iterStr = agent.iteration > 0 ? `${COLORS.dim}#${agent.iteration}${COLORS.reset}` : '';
360
+
361
+ // Build the row
362
+ let content = `${COLORS.gray}│${COLORS.reset} ${iconCol} ${COLORS.white}${nameCol}${COLORS.reset} ${metricsStr}`;
363
+ if (iterStr) {
364
+ content += ` ${iterStr}`;
365
+ }
366
+
367
+ const contentLen = this.stripAnsi(content).length;
368
+ const padding = Math.max(0, width - contentLen - 1);
369
+ rows.push(content + ' '.repeat(padding) + `${COLORS.gray}│${COLORS.reset}`);
370
+ }
371
+
372
+ return rows;
373
+ }
374
+
375
+ /**
376
+ * Build a single status line for testing/display
377
+ * Alias for buildSummaryLine for backward compatibility
378
+ * @param {number} width - Terminal width
379
+ * @returns {string}
380
+ */
381
+ buildStatusLine(width) {
382
+ return this.buildSummaryLine(width);
383
+ }
384
+
385
+ /**
386
+ * Build the summary line with aggregated metrics
387
+ * @param {number} width - Terminal width
388
+ * @returns {string}
389
+ */
390
+ buildSummaryLine(width) {
391
+ const parts = [];
392
+
393
+ // Border with corner
394
+ parts.push(`${COLORS.gray}└─${COLORS.reset}`);
395
+
396
+ // Cluster state
397
+ const stateColor = this.clusterState === 'running' ? COLORS.green : COLORS.yellow;
398
+ parts.push(` ${stateColor}${this.clusterState}${COLORS.reset}`);
399
+
400
+ // Duration
401
+ const duration = this.formatDuration(Date.now() - this.startTime);
402
+ parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
403
+
404
+ // Agent counts
405
+ const executing = Array.from(this.agents.values()).filter(a => a.state === 'executing').length;
406
+ const total = this.agents.size;
407
+ parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
408
+
409
+ // Aggregate metrics
410
+ let totalCpu = 0;
411
+ let totalMem = 0;
412
+ let totalBytesSent = 0;
413
+ let totalBytesReceived = 0;
414
+ for (const metrics of this.metricsCache.values()) {
415
+ if (metrics.exists) {
416
+ totalCpu += metrics.cpuPercent;
417
+ totalMem += metrics.memoryMB;
418
+ totalBytesSent += metrics.network.bytesSent || 0;
419
+ totalBytesReceived += metrics.network.bytesReceived || 0;
420
+ }
421
+ }
422
+
423
+ if (totalCpu > 0 || totalMem > 0) {
424
+ parts.push(` ${COLORS.gray}│${COLORS.reset}`);
425
+ let aggregateStr = ` ${COLORS.cyan}Σ${COLORS.reset} `;
426
+ aggregateStr += `${COLORS.dim}CPU:${COLORS.reset}${totalCpu.toFixed(0)}%`;
427
+ aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(0)}MB`;
428
+ if (totalBytesSent > 0 || totalBytesReceived > 0) {
429
+ aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
430
+ }
431
+ parts.push(aggregateStr);
432
+ }
433
+
434
+ // Pad and close with bottom corner
435
+ const content = parts.join('');
436
+ const contentLen = this.stripAnsi(content).length;
437
+ const padding = Math.max(0, width - contentLen - 1);
438
+ return content + `${COLORS.gray}${'─'.repeat(padding)}┘${COLORS.reset}`;
439
+ }
440
+
441
+ /**
442
+ * Strip ANSI codes from string for length calculation
443
+ * @param {string} str
444
+ * @returns {string}
445
+ */
446
+ stripAnsi(str) {
447
+ // eslint-disable-next-line no-control-regex
448
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
449
+ }
450
+
451
+ /**
452
+ * Start the status footer
453
+ */
454
+ start() {
455
+ if (!this.enabled || !this.isTTY()) {
456
+ return;
457
+ }
458
+
459
+ this.setupScrollRegion();
460
+
461
+ // Handle terminal resize
462
+ process.stdout.on('resize', () => {
463
+ this.setupScrollRegion();
464
+ this.render();
465
+ });
466
+
467
+ // Start refresh interval
468
+ this.intervalId = setInterval(() => {
469
+ this.render();
470
+ }, this.refreshInterval);
471
+
472
+ // Initial render
473
+ this.render();
474
+ }
475
+
476
+ /**
477
+ * Stop the status footer and cleanup
478
+ */
479
+ stop() {
480
+ if (this.intervalId) {
481
+ clearInterval(this.intervalId);
482
+ this.intervalId = null;
483
+ }
484
+
485
+ if (this.isTTY()) {
486
+ // Reset scroll region
487
+ this.resetScrollRegion();
488
+
489
+ // Clear all footer lines (dynamic height)
490
+ const { rows } = this.getTerminalSize();
491
+ const startRow = rows - this.footerHeight + 1;
492
+ for (let row = startRow; row <= rows; row++) {
493
+ this.moveTo(row, 1);
494
+ process.stdout.write(CLEAR_LINE);
495
+ }
496
+
497
+ // Move cursor to safe position
498
+ this.moveTo(startRow, 1);
499
+ process.stdout.write(SHOW_CURSOR);
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Temporarily hide footer for clean output
505
+ */
506
+ hide() {
507
+ if (!this.isTTY()) return;
508
+
509
+ this.resetScrollRegion();
510
+
511
+ // Clear all footer lines (dynamic height)
512
+ const { rows } = this.getTerminalSize();
513
+ const startRow = rows - this.footerHeight + 1;
514
+ for (let row = startRow; row <= rows; row++) {
515
+ this.moveTo(row, 1);
516
+ process.stdout.write(CLEAR_LINE);
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Restore footer after hiding
522
+ */
523
+ show() {
524
+ if (!this.isTTY()) return;
525
+
526
+ this.setupScrollRegion();
527
+ this.render();
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Create a singleton footer instance
533
+ * @param {Object} options
534
+ * @returns {StatusFooter}
535
+ */
536
+ function createStatusFooter(options = {}) {
537
+ return new StatusFooter(options);
538
+ }
539
+
540
+ module.exports = {
541
+ StatusFooter,
542
+ createStatusFooter,
543
+ };