@covibes/zeroshot 1.0.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 (57) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/LICENSE +21 -0
  3. package/README.md +364 -0
  4. package/cli/index.js +3990 -0
  5. package/cluster-templates/base-templates/debug-workflow.json +181 -0
  6. package/cluster-templates/base-templates/full-workflow.json +455 -0
  7. package/cluster-templates/base-templates/single-worker.json +48 -0
  8. package/cluster-templates/base-templates/worker-validator.json +131 -0
  9. package/cluster-templates/conductor-bootstrap.json +122 -0
  10. package/cluster-templates/conductor-junior-bootstrap.json +69 -0
  11. package/docker/zeroshot-cluster/Dockerfile +132 -0
  12. package/lib/completion.js +174 -0
  13. package/lib/id-detector.js +53 -0
  14. package/lib/settings.js +97 -0
  15. package/lib/stream-json-parser.js +236 -0
  16. package/package.json +121 -0
  17. package/src/agent/agent-config.js +121 -0
  18. package/src/agent/agent-context-builder.js +241 -0
  19. package/src/agent/agent-hook-executor.js +329 -0
  20. package/src/agent/agent-lifecycle.js +555 -0
  21. package/src/agent/agent-stuck-detector.js +256 -0
  22. package/src/agent/agent-task-executor.js +1034 -0
  23. package/src/agent/agent-trigger-evaluator.js +67 -0
  24. package/src/agent-wrapper.js +459 -0
  25. package/src/agents/git-pusher-agent.json +20 -0
  26. package/src/attach/attach-client.js +438 -0
  27. package/src/attach/attach-server.js +543 -0
  28. package/src/attach/index.js +35 -0
  29. package/src/attach/protocol.js +220 -0
  30. package/src/attach/ring-buffer.js +121 -0
  31. package/src/attach/socket-discovery.js +242 -0
  32. package/src/claude-task-runner.js +468 -0
  33. package/src/config-router.js +80 -0
  34. package/src/config-validator.js +598 -0
  35. package/src/github.js +103 -0
  36. package/src/isolation-manager.js +1042 -0
  37. package/src/ledger.js +429 -0
  38. package/src/logic-engine.js +223 -0
  39. package/src/message-bus-bridge.js +139 -0
  40. package/src/message-bus.js +202 -0
  41. package/src/name-generator.js +232 -0
  42. package/src/orchestrator.js +1938 -0
  43. package/src/schemas/sub-cluster.js +156 -0
  44. package/src/sub-cluster-wrapper.js +545 -0
  45. package/src/task-runner.js +28 -0
  46. package/src/template-resolver.js +347 -0
  47. package/src/tui/CHANGES.txt +133 -0
  48. package/src/tui/LAYOUT.md +261 -0
  49. package/src/tui/README.txt +192 -0
  50. package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
  51. package/src/tui/data-poller.js +325 -0
  52. package/src/tui/demo.js +208 -0
  53. package/src/tui/formatters.js +123 -0
  54. package/src/tui/index.js +193 -0
  55. package/src/tui/keybindings.js +383 -0
  56. package/src/tui/layout.js +317 -0
  57. package/src/tui/renderer.js +194 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * TUI - Main interactive dashboard
3
+ *
4
+ * Coordinates:
5
+ * - Screen and layout
6
+ * - Data polling
7
+ * - Rendering
8
+ * - Keybindings
9
+ * - State management
10
+ */
11
+
12
+ const blessed = require('blessed');
13
+ const { createLayout } = require('./layout');
14
+ const DataPoller = require('./data-poller');
15
+ const Renderer = require('./renderer');
16
+ const { setupKeybindings } = require('./keybindings');
17
+
18
+ class TUI {
19
+ constructor(options) {
20
+ this.orchestrator = options.orchestrator;
21
+ this.filter = options.filter || 'running';
22
+ this.refreshRate = options.refreshRate || 1000;
23
+
24
+ // State
25
+ this.clusters = [];
26
+ this.resourceStats = new Map();
27
+ this.messages = [];
28
+ this.selectedIndex = 0;
29
+ this.poller = null;
30
+ this.renderer = null;
31
+ this.widgets = null;
32
+ this.screen = null;
33
+
34
+ // View mode: 'overview' or 'detail'
35
+ this.viewMode = 'overview';
36
+ this.detailClusterId = null;
37
+ }
38
+
39
+ start() {
40
+ // Create screen
41
+ this.screen = blessed.screen({
42
+ smartCSR: true,
43
+ title: 'Vibe Cluster Watch',
44
+ dockBorders: true,
45
+ fullUnicode: true,
46
+ });
47
+
48
+ // Create layout
49
+ this.widgets = createLayout(this.screen);
50
+
51
+ // Show immediate loading message
52
+ this.widgets.statsBox.setContent('{center}{bold}Loading...{/bold}{/center}');
53
+ this.screen.render();
54
+
55
+ // Create renderer
56
+ this.renderer = new Renderer(this.widgets, this.screen);
57
+
58
+ // Setup keybindings (pass TUI instance for state management)
59
+ setupKeybindings(this.screen, this.widgets, this, this.orchestrator);
60
+
61
+ // Create data poller
62
+ this.poller = new DataPoller(this.orchestrator, {
63
+ refreshRate: this.refreshRate,
64
+ onUpdate: (update) => this._handleUpdate(update),
65
+ });
66
+
67
+ // Initial message
68
+ this.messages.push({
69
+ timestamp: new Date().toISOString(),
70
+ text: 'TUI started. Press ? for help.',
71
+ level: 'info',
72
+ });
73
+ this.renderer.renderLogs(this.messages.slice(-20));
74
+
75
+ // Start polling
76
+ this.poller.start();
77
+
78
+ // Initial render
79
+ this.screen.render();
80
+ }
81
+
82
+ _handleUpdate(update) {
83
+ // Update state based on update.type
84
+ switch (update.type) {
85
+ case 'cluster_state':
86
+ // Update cluster list
87
+ this.clusters = update.clusters;
88
+
89
+ // Apply filter
90
+ let filteredClusters = this.clusters;
91
+ if (this.filter === 'running') {
92
+ // For "running" filter, only show truly active (running) clusters
93
+ // Exclude initializing, stopped, failed, etc.
94
+ filteredClusters = this.clusters.filter((c) => c.state === 'running');
95
+ } else if (this.filter !== 'all') {
96
+ // For other specific filters, match exact state
97
+ filteredClusters = this.clusters.filter((c) => c.state === this.filter);
98
+ }
99
+
100
+ // Ensure selectedIndex is valid
101
+ if (this.selectedIndex >= filteredClusters.length) {
102
+ this.selectedIndex = Math.max(0, filteredClusters.length - 1);
103
+ }
104
+
105
+ // Render clusters table
106
+ this.renderer.renderClustersTable(filteredClusters, this.selectedIndex);
107
+
108
+ // Render system stats
109
+ this.renderer.renderSystemStats(this.clusters, this.resourceStats);
110
+
111
+ // Update agent table for selected cluster (ONLY in detail view)
112
+ if (this.viewMode === 'detail' && this.detailClusterId) {
113
+ // In detail view, show agents for the detail cluster
114
+ try {
115
+ const status = this.orchestrator.getStatus(this.detailClusterId);
116
+ this.renderer.renderAgentTable(status.agents, this.resourceStats);
117
+ } catch {
118
+ // Cluster might have been stopped/killed
119
+ this.renderer.renderAgentTable([], this.resourceStats);
120
+ }
121
+ } else if (this.viewMode === 'overview') {
122
+ // In overview view, don't show agents (or show empty)
123
+ this.renderer.renderAgentTable([], this.resourceStats);
124
+ }
125
+ break;
126
+
127
+ case 'resource_stats':
128
+ // Update resource stats
129
+ this.resourceStats = update.stats;
130
+
131
+ // Re-render system stats
132
+ this.renderer.renderSystemStats(this.clusters, this.resourceStats);
133
+
134
+ // Update agent table with new resource stats (ONLY in detail view)
135
+ if (this.viewMode === 'detail' && this.detailClusterId) {
136
+ try {
137
+ const status = this.orchestrator.getStatus(this.detailClusterId);
138
+ this.renderer.renderAgentTable(status.agents, this.resourceStats);
139
+ } catch {
140
+ this.renderer.renderAgentTable([], this.resourceStats);
141
+ }
142
+ }
143
+ break;
144
+
145
+ case 'new_message':
146
+ // Only add messages from the selected cluster
147
+ const selectedClusterId = this.renderer.selectedClusterId;
148
+ if (selectedClusterId && update.clusterId === selectedClusterId) {
149
+ // Add new message to log
150
+ this.messages.push(update.message);
151
+
152
+ // Keep only last 100 messages in memory
153
+ if (this.messages.length > 100) {
154
+ this.messages = this.messages.slice(-100);
155
+ }
156
+
157
+ // Render last 20 messages
158
+ this.renderer.renderLogs(this.messages.slice(-20));
159
+ }
160
+ break;
161
+
162
+ case 'error':
163
+ // Add error to log
164
+ this.messages.push({
165
+ timestamp: new Date().toISOString(),
166
+ text: `✗ ${update.error}`,
167
+ level: 'error',
168
+ });
169
+
170
+ if (this.messages.length > 100) {
171
+ this.messages = this.messages.slice(-100);
172
+ }
173
+
174
+ this.renderer.renderLogs(this.messages.slice(-20));
175
+ break;
176
+ }
177
+
178
+ // Render screen
179
+ this.screen.render();
180
+ }
181
+
182
+ exit() {
183
+ if (this.poller) {
184
+ this.poller.stop();
185
+ }
186
+ if (this.screen) {
187
+ this.screen.destroy();
188
+ }
189
+ process.exit(0);
190
+ }
191
+ }
192
+
193
+ module.exports = TUI;
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Keybindings for TUI
3
+ *
4
+ * Handles:
5
+ * - Navigation (up/down, j/k)
6
+ * - Actions (kill, stop, export, logs)
7
+ * - Confirmations for destructive actions
8
+ */
9
+
10
+ const blessed = require('blessed');
11
+ const { spawn } = require('child_process');
12
+
13
+ function setupKeybindings(screen, widgets, tui, orchestrator) {
14
+ // Enter - drill into cluster detail view
15
+ screen.key(['enter'], () => {
16
+ if (tui.viewMode === 'overview' && tui.clusters.length > 0) {
17
+ const selectedCluster = tui.clusters[tui.selectedIndex];
18
+ if (selectedCluster) {
19
+ tui.viewMode = 'detail';
20
+ tui.detailClusterId = selectedCluster.id;
21
+ tui.renderer.setSelectedCluster(selectedCluster.id);
22
+ tui.messages = []; // Clear old messages
23
+
24
+ // Update help text
25
+ widgets.helpBar.setContent(
26
+ '{cyan-fg}[Esc]{/} Back {cyan-fg}[k]{/} Kill {cyan-fg}[s]{/} Stop {cyan-fg}[e]{/} Export {cyan-fg}[l]{/} Logs {cyan-fg}[r]{/} Refresh {cyan-fg}[q]{/} Quit'
27
+ );
28
+
29
+ // Switch to detail layout: hide clusters/stats, show agents/logs
30
+ widgets.clustersTable.hide();
31
+ widgets.statsBox.hide();
32
+ widgets.agentTable.show();
33
+ widgets.logsBox.show();
34
+ screen.render();
35
+ }
36
+ }
37
+ });
38
+
39
+ // Escape - back to overview
40
+ screen.key(['escape'], () => {
41
+ if (tui.viewMode === 'detail') {
42
+ tui.viewMode = 'overview';
43
+ tui.detailClusterId = null;
44
+ tui.renderer.setSelectedCluster(null);
45
+ tui.messages = []; // Clear messages
46
+
47
+ // Update help text
48
+ widgets.helpBar.setContent(
49
+ '{cyan-fg}[Enter]{/} View {cyan-fg}[↑/↓]{/} Navigate {cyan-fg}[k]{/} Kill {cyan-fg}[s]{/} Stop {cyan-fg}[l]{/} Logs {cyan-fg}[r]{/} Refresh {cyan-fg}[q]{/} Quit'
50
+ );
51
+
52
+ // Switch to overview layout: show clusters/stats, hide agents/logs
53
+ widgets.clustersTable.show();
54
+ widgets.statsBox.show();
55
+ widgets.agentTable.hide();
56
+ widgets.logsBox.hide();
57
+ screen.render();
58
+ }
59
+ });
60
+
61
+ // Navigation - up
62
+ screen.key(['up', 'k'], () => {
63
+ if (tui.clusters.length === 0) return;
64
+ tui.selectedIndex = Math.max(0, tui.selectedIndex - 1);
65
+ tui.renderer.renderClustersTable(tui.clusters, tui.selectedIndex);
66
+
67
+ // Update agent table and logs for newly selected cluster
68
+ const selectedCluster = tui.clusters[tui.selectedIndex];
69
+ if (selectedCluster) {
70
+ // CRITICAL: Tell renderer which cluster is selected
71
+ tui.renderer.setSelectedCluster(selectedCluster.id);
72
+
73
+ // Clear old messages from previous cluster
74
+ tui.messages = [];
75
+
76
+ const status = orchestrator.getStatus(selectedCluster.id);
77
+ tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
78
+ }
79
+
80
+ screen.render();
81
+ });
82
+
83
+ // Navigation - down
84
+ screen.key(['down', 'j'], () => {
85
+ if (tui.clusters.length === 0) return;
86
+ tui.selectedIndex = Math.min(tui.clusters.length - 1, tui.selectedIndex + 1);
87
+ tui.renderer.renderClustersTable(tui.clusters, tui.selectedIndex);
88
+
89
+ // Update agent table and logs for newly selected cluster
90
+ const selectedCluster = tui.clusters[tui.selectedIndex];
91
+ if (selectedCluster) {
92
+ // CRITICAL: Tell renderer which cluster is selected
93
+ tui.renderer.setSelectedCluster(selectedCluster.id);
94
+
95
+ // Clear old messages from previous cluster
96
+ tui.messages = [];
97
+
98
+ const status = orchestrator.getStatus(selectedCluster.id);
99
+ tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
100
+ }
101
+
102
+ screen.render();
103
+ });
104
+
105
+ // Kill selected cluster (with confirmation)
106
+ screen.key(['K'], () => {
107
+ if (tui.clusters.length === 0) return;
108
+ const selectedCluster = tui.clusters[tui.selectedIndex];
109
+ if (!selectedCluster) return;
110
+
111
+ // Create confirmation dialog
112
+ const question = blessed.question({
113
+ parent: screen,
114
+ border: 'line',
115
+ height: 'shrink',
116
+ width: 'half',
117
+ top: 'center',
118
+ left: 'center',
119
+ label: ' {bold}{red-fg}Confirm Kill{/red-fg}{/bold} ',
120
+ tags: true,
121
+ keys: true,
122
+ vi: true,
123
+ });
124
+
125
+ question.ask(
126
+ `Kill cluster ${selectedCluster.id}?\n\n(This will force-stop all agents)`,
127
+ async (err, value) => {
128
+ if (err) return;
129
+ if (value) {
130
+ try {
131
+ await orchestrator.kill(selectedCluster.id);
132
+ tui.messages.push({
133
+ timestamp: new Date().toISOString(),
134
+ text: `✓ Killed cluster ${selectedCluster.id}`,
135
+ level: 'success',
136
+ });
137
+ tui.renderer.renderLogs(tui.messages.slice(-20));
138
+ } catch (error) {
139
+ tui.messages.push({
140
+ timestamp: new Date().toISOString(),
141
+ text: `✗ Failed to kill cluster: ${error.message}`,
142
+ level: 'error',
143
+ });
144
+ tui.renderer.renderLogs(tui.messages.slice(-20));
145
+ }
146
+ screen.render();
147
+ }
148
+ }
149
+ );
150
+ });
151
+
152
+ // Stop selected cluster (with confirmation)
153
+ screen.key(['s'], () => {
154
+ if (tui.clusters.length === 0) return;
155
+ const selectedCluster = tui.clusters[tui.selectedIndex];
156
+ if (!selectedCluster) return;
157
+
158
+ // Create confirmation dialog
159
+ const question = blessed.question({
160
+ parent: screen,
161
+ border: 'line',
162
+ height: 'shrink',
163
+ width: 'half',
164
+ top: 'center',
165
+ left: 'center',
166
+ label: ' {bold}{yellow-fg}Confirm Stop{/yellow-fg}{/bold} ',
167
+ tags: true,
168
+ keys: true,
169
+ vi: true,
170
+ });
171
+
172
+ question.ask(
173
+ `Stop cluster ${selectedCluster.id}?\n\n(This will gracefully stop all agents)`,
174
+ async (err, value) => {
175
+ if (err) return;
176
+ if (value) {
177
+ try {
178
+ await orchestrator.stop(selectedCluster.id);
179
+ tui.messages.push({
180
+ timestamp: new Date().toISOString(),
181
+ text: `✓ Stopped cluster ${selectedCluster.id}`,
182
+ level: 'success',
183
+ });
184
+ tui.renderer.renderLogs(tui.messages.slice(-20));
185
+ } catch (error) {
186
+ tui.messages.push({
187
+ timestamp: new Date().toISOString(),
188
+ text: `✗ Failed to stop cluster: ${error.message}`,
189
+ level: 'error',
190
+ });
191
+ tui.renderer.renderLogs(tui.messages.slice(-20));
192
+ }
193
+ screen.render();
194
+ }
195
+ }
196
+ );
197
+ });
198
+
199
+ // Export selected cluster
200
+ screen.key(['e'], () => {
201
+ if (tui.clusters.length === 0) return;
202
+ const selectedCluster = tui.clusters[tui.selectedIndex];
203
+ if (!selectedCluster) return;
204
+
205
+ try {
206
+ const markdown = orchestrator.export(selectedCluster.id, 'markdown');
207
+ const fs = require('fs');
208
+ const filename = `${selectedCluster.id}-export.md`;
209
+ fs.writeFileSync(filename, markdown);
210
+
211
+ tui.messages.push({
212
+ timestamp: new Date().toISOString(),
213
+ text: `✓ Exported cluster to ${filename}`,
214
+ level: 'success',
215
+ });
216
+ tui.renderer.renderLogs(tui.messages.slice(-20));
217
+ screen.render();
218
+ } catch (error) {
219
+ tui.messages.push({
220
+ timestamp: new Date().toISOString(),
221
+ text: `✗ Failed to export cluster: ${error.message}`,
222
+ level: 'error',
223
+ });
224
+ tui.renderer.renderLogs(tui.messages.slice(-20));
225
+ screen.render();
226
+ }
227
+ });
228
+
229
+ // Show full logs (spawn zeroshot logs -f in new terminal)
230
+ screen.key(['l'], () => {
231
+ if (tui.clusters.length === 0) return;
232
+ const selectedCluster = tui.clusters[tui.selectedIndex];
233
+ if (!selectedCluster) return;
234
+
235
+ try {
236
+ // Detect terminal emulator
237
+ const term =
238
+ process.env.TERM_PROGRAM || (process.env.COLORTERM ? 'gnome-terminal' : null) || 'xterm';
239
+
240
+ let cmd, args;
241
+ if (term === 'iTerm.app' || term === 'Apple_Terminal') {
242
+ // macOS
243
+ cmd = 'osascript';
244
+ args = [
245
+ '-e',
246
+ `tell application "Terminal" to do script "zeroshot logs ${selectedCluster.id} -f"`,
247
+ ];
248
+ } else {
249
+ // Linux - try common terminal emulators
250
+ const terminals = ['gnome-terminal', 'konsole', 'xterm', 'urxvt', 'alacritty', 'kitty'];
251
+ cmd =
252
+ terminals.find((t) => {
253
+ try {
254
+ require('child_process').execSync(`which ${t}`, {
255
+ stdio: 'ignore',
256
+ });
257
+ return true;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }) || 'xterm';
262
+
263
+ if (cmd === 'gnome-terminal' || cmd === 'konsole') {
264
+ args = [
265
+ '--',
266
+ 'bash',
267
+ '-c',
268
+ `zeroshot logs ${selectedCluster.id} -f; read -p "Press enter to close..."`,
269
+ ];
270
+ } else {
271
+ args = [
272
+ '-e',
273
+ 'bash',
274
+ '-c',
275
+ `zeroshot logs ${selectedCluster.id} -f; read -p "Press enter to close..."`,
276
+ ];
277
+ }
278
+ }
279
+
280
+ spawn(cmd, args, { detached: true, stdio: 'ignore' });
281
+
282
+ tui.messages.push({
283
+ timestamp: new Date().toISOString(),
284
+ text: `✓ Opened logs for ${selectedCluster.id} in new terminal`,
285
+ level: 'success',
286
+ });
287
+ tui.renderer.renderLogs(tui.messages.slice(-20));
288
+ screen.render();
289
+ } catch (error) {
290
+ tui.messages.push({
291
+ timestamp: new Date().toISOString(),
292
+ text: `✗ Failed to open logs: ${error.message}`,
293
+ level: 'error',
294
+ });
295
+ tui.renderer.renderLogs(tui.messages.slice(-20));
296
+ screen.render();
297
+ }
298
+ });
299
+
300
+ // Force refresh
301
+ screen.key(['r'], () => {
302
+ tui.messages.push({
303
+ timestamp: new Date().toISOString(),
304
+ text: '↻ Refreshing...',
305
+ level: 'info',
306
+ });
307
+ tui.renderer.renderLogs(tui.messages.slice(-20));
308
+ screen.render();
309
+
310
+ // Trigger immediate poll
311
+ if (tui.poller) {
312
+ tui.poller.poll();
313
+ }
314
+ });
315
+
316
+ // Exit (with confirmation)
317
+ screen.key(['q', 'C-c'], () => {
318
+ const question = blessed.question({
319
+ parent: screen,
320
+ border: 'line',
321
+ height: 'shrink',
322
+ width: 'half',
323
+ top: 'center',
324
+ left: 'center',
325
+ label: ' {bold}Confirm Exit{/bold} ',
326
+ tags: true,
327
+ keys: true,
328
+ vi: true,
329
+ });
330
+
331
+ question.ask('Exit TUI?\n\n(Clusters will continue running)', (err, value) => {
332
+ if (err) return;
333
+ if (value) {
334
+ tui.exit();
335
+ }
336
+ });
337
+ });
338
+
339
+ // Help
340
+ screen.key(['?', 'h'], () => {
341
+ const helpBox = blessed.box({
342
+ parent: screen,
343
+ border: 'line',
344
+ height: '80%',
345
+ width: '60%',
346
+ top: 'center',
347
+ left: 'center',
348
+ label: ' {bold}Keybindings{/bold} ',
349
+ tags: true,
350
+ keys: true,
351
+ vi: true,
352
+ scrollable: true,
353
+ alwaysScroll: true,
354
+ content: `
355
+ {bold}Navigation:{/bold}
356
+ ↑/k Move selection up
357
+ ↓/j Move selection down
358
+
359
+ {bold}Actions:{/bold}
360
+ K Kill selected cluster (force stop)
361
+ s Stop selected cluster (graceful)
362
+ e Export selected cluster to markdown
363
+ l Show full logs in new terminal
364
+ r Force refresh
365
+
366
+ {bold}Other:{/bold}
367
+ ?/h Show this help
368
+ q/Ctrl-C Exit TUI
369
+
370
+ Press any key to close...
371
+ `.trim(),
372
+ });
373
+
374
+ helpBox.key(['escape', 'q', 'enter', 'space'], () => {
375
+ helpBox.destroy();
376
+ screen.render();
377
+ });
378
+
379
+ screen.render();
380
+ });
381
+ }
382
+
383
+ module.exports = { setupKeybindings };