@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,220 @@
1
+ /**
2
+ * Protocol - Message framing for attach/detach IPC
3
+ *
4
+ * Uses length-prefixed JSON messages over Unix sockets.
5
+ * Format: [4-byte length (BE)] [JSON payload]
6
+ *
7
+ * Message Types:
8
+ *
9
+ * Client → Server:
10
+ * ATTACH { type: 'attach', clientId, cols, rows }
11
+ * DETACH { type: 'detach', clientId }
12
+ * RESIZE { type: 'resize', cols, rows }
13
+ * SIGNAL { type: 'signal', signal: 'SIGINT' | 'SIGTERM' }
14
+ * STDIN { type: 'stdin', data: base64 } (future interactive mode)
15
+ *
16
+ * Server → Client:
17
+ * OUTPUT { type: 'output', data: base64, timestamp }
18
+ * HISTORY { type: 'history', data: base64 }
19
+ * STATE { type: 'state', status, pid, ... }
20
+ * EXIT { type: 'exit', code, signal }
21
+ * ERROR { type: 'error', message }
22
+ */
23
+
24
+ const MAX_MESSAGE_SIZE = 10 * 1024 * 1024; // 10MB max message
25
+
26
+ /**
27
+ * Encode a message for transmission
28
+ * @param {object} message - Message object to encode
29
+ * @returns {Buffer} - Length-prefixed encoded message
30
+ */
31
+ function encode(message) {
32
+ const json = JSON.stringify(message);
33
+ const payload = Buffer.from(json, 'utf8');
34
+
35
+ if (payload.length > MAX_MESSAGE_SIZE) {
36
+ throw new Error(`Message too large: ${payload.length} bytes (max ${MAX_MESSAGE_SIZE})`);
37
+ }
38
+
39
+ const frame = Buffer.alloc(4 + payload.length);
40
+ frame.writeUInt32BE(payload.length, 0);
41
+ payload.copy(frame, 4);
42
+
43
+ return frame;
44
+ }
45
+
46
+ /**
47
+ * MessageDecoder - Streaming decoder for framed messages
48
+ *
49
+ * Handles partial reads and message reassembly.
50
+ */
51
+ class MessageDecoder {
52
+ constructor() {
53
+ this.buffer = Buffer.alloc(0);
54
+ }
55
+
56
+ /**
57
+ * Feed data into the decoder
58
+ * @param {Buffer} data - Received data chunk
59
+ * @returns {object[]} - Array of decoded messages (may be empty)
60
+ */
61
+ feed(data) {
62
+ this.buffer = Buffer.concat([this.buffer, data]);
63
+ const messages = [];
64
+
65
+ while (this.buffer.length >= 4) {
66
+ const length = this.buffer.readUInt32BE(0);
67
+
68
+ if (length > MAX_MESSAGE_SIZE) {
69
+ throw new Error(`Message too large: ${length} bytes (max ${MAX_MESSAGE_SIZE})`);
70
+ }
71
+
72
+ if (this.buffer.length < 4 + length) {
73
+ // Incomplete message, wait for more data
74
+ break;
75
+ }
76
+
77
+ const payload = this.buffer.slice(4, 4 + length);
78
+ this.buffer = this.buffer.slice(4 + length);
79
+
80
+ try {
81
+ const message = JSON.parse(payload.toString('utf8'));
82
+ messages.push(message);
83
+ } catch (e) {
84
+ throw new Error(`Invalid JSON in message: ${e.message}`);
85
+ }
86
+ }
87
+
88
+ return messages;
89
+ }
90
+
91
+ /**
92
+ * Reset decoder state
93
+ */
94
+ reset() {
95
+ this.buffer = Buffer.alloc(0);
96
+ }
97
+ }
98
+
99
+ // Message type constants
100
+ const MessageType = {
101
+ // Client → Server
102
+ ATTACH: 'attach',
103
+ DETACH: 'detach',
104
+ RESIZE: 'resize',
105
+ SIGNAL: 'signal',
106
+ STDIN: 'stdin',
107
+
108
+ // Server → Client
109
+ OUTPUT: 'output',
110
+ HISTORY: 'history',
111
+ STATE: 'state',
112
+ EXIT: 'exit',
113
+ ERROR: 'error',
114
+ };
115
+
116
+ // Helper functions to create messages
117
+
118
+ /**
119
+ * Create an ATTACH message
120
+ */
121
+ function createAttachMessage(clientId, cols, rows) {
122
+ return { type: MessageType.ATTACH, clientId, cols, rows };
123
+ }
124
+
125
+ /**
126
+ * Create a DETACH message
127
+ */
128
+ function createDetachMessage(clientId) {
129
+ return { type: MessageType.DETACH, clientId };
130
+ }
131
+
132
+ /**
133
+ * Create a RESIZE message
134
+ */
135
+ function createResizeMessage(cols, rows) {
136
+ return { type: MessageType.RESIZE, cols, rows };
137
+ }
138
+
139
+ /**
140
+ * Create a SIGNAL message
141
+ */
142
+ function createSignalMessage(signal) {
143
+ if (!['SIGINT', 'SIGTERM', 'SIGKILL', 'SIGTSTP'].includes(signal)) {
144
+ throw new Error(`Invalid signal: ${signal}`);
145
+ }
146
+ return { type: MessageType.SIGNAL, signal };
147
+ }
148
+
149
+ /**
150
+ * Create a STDIN message (for future interactive mode)
151
+ */
152
+ function createStdinMessage(data) {
153
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
154
+ return { type: MessageType.STDIN, data: buf.toString('base64') };
155
+ }
156
+
157
+ /**
158
+ * Create an OUTPUT message
159
+ */
160
+ function createOutputMessage(data, timestamp = Date.now()) {
161
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
162
+ return { type: MessageType.OUTPUT, data: buf.toString('base64'), timestamp };
163
+ }
164
+
165
+ /**
166
+ * Create a HISTORY message
167
+ */
168
+ function createHistoryMessage(data) {
169
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
170
+ return { type: MessageType.HISTORY, data: buf.toString('base64') };
171
+ }
172
+
173
+ /**
174
+ * Create a STATE message
175
+ */
176
+ function createStateMessage(state) {
177
+ return { type: MessageType.STATE, ...state };
178
+ }
179
+
180
+ /**
181
+ * Create an EXIT message
182
+ */
183
+ function createExitMessage(code, signal) {
184
+ return { type: MessageType.EXIT, code, signal };
185
+ }
186
+
187
+ /**
188
+ * Create an ERROR message
189
+ */
190
+ function createErrorMessage(message) {
191
+ return { type: MessageType.ERROR, message };
192
+ }
193
+
194
+ /**
195
+ * Decode base64 data field from OUTPUT/HISTORY/STDIN messages
196
+ */
197
+ function decodeData(message) {
198
+ if (message.data) {
199
+ return Buffer.from(message.data, 'base64');
200
+ }
201
+ return null;
202
+ }
203
+
204
+ module.exports = {
205
+ encode,
206
+ MessageDecoder,
207
+ MessageType,
208
+ createAttachMessage,
209
+ createDetachMessage,
210
+ createResizeMessage,
211
+ createSignalMessage,
212
+ createStdinMessage,
213
+ createOutputMessage,
214
+ createHistoryMessage,
215
+ createStateMessage,
216
+ createExitMessage,
217
+ createErrorMessage,
218
+ decodeData,
219
+ MAX_MESSAGE_SIZE,
220
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * RingBuffer - Fixed-size circular buffer for output history
3
+ *
4
+ * Used by AttachServer to store recent output for late-joining clients.
5
+ * When buffer fills, oldest data is overwritten.
6
+ *
7
+ * Design:
8
+ * - Fixed allocation (no dynamic resizing)
9
+ * - O(1) write and read operations
10
+ * - Thread-safe for single writer (Node.js is single-threaded)
11
+ */
12
+
13
+ class RingBuffer {
14
+ /**
15
+ * @param {number} maxSize - Maximum buffer size in bytes (default 1MB)
16
+ */
17
+ constructor(maxSize = 1024 * 1024) {
18
+ if (maxSize <= 0) {
19
+ throw new Error('RingBuffer maxSize must be positive');
20
+ }
21
+ this.buffer = Buffer.alloc(maxSize);
22
+ this.maxSize = maxSize;
23
+ this.writePos = 0; // Next write position
24
+ this.size = 0; // Current data size (0 to maxSize)
25
+ }
26
+
27
+ /**
28
+ * Write data to the buffer
29
+ * @param {Buffer|string} data - Data to write
30
+ */
31
+ write(data) {
32
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
33
+
34
+ if (buf.length === 0) return;
35
+
36
+ // If data is larger than buffer, only keep the last maxSize bytes
37
+ if (buf.length >= this.maxSize) {
38
+ buf.copy(this.buffer, 0, buf.length - this.maxSize);
39
+ this.writePos = 0;
40
+ this.size = this.maxSize;
41
+ return;
42
+ }
43
+
44
+ // Calculate how much wraps around
45
+ const spaceToEnd = this.maxSize - this.writePos;
46
+
47
+ if (buf.length <= spaceToEnd) {
48
+ // No wrap needed
49
+ buf.copy(this.buffer, this.writePos);
50
+ this.writePos += buf.length;
51
+ } else {
52
+ // Wrap around
53
+ buf.copy(this.buffer, this.writePos, 0, spaceToEnd);
54
+ buf.copy(this.buffer, 0, spaceToEnd);
55
+ this.writePos = buf.length - spaceToEnd;
56
+ }
57
+
58
+ // Update size (capped at maxSize)
59
+ this.size = Math.min(this.size + buf.length, this.maxSize);
60
+ }
61
+
62
+ /**
63
+ * Read all buffered data
64
+ * @returns {Buffer} - All data currently in buffer
65
+ */
66
+ read() {
67
+ if (this.size === 0) {
68
+ return Buffer.alloc(0);
69
+ }
70
+
71
+ const result = Buffer.alloc(this.size);
72
+
73
+ if (this.size < this.maxSize) {
74
+ // Buffer not full yet, data starts at 0
75
+ this.buffer.copy(result, 0, 0, this.size);
76
+ } else {
77
+ // Buffer is full, data starts at writePos (oldest data)
78
+ const startPos = this.writePos;
79
+ const firstChunkSize = this.maxSize - startPos;
80
+
81
+ this.buffer.copy(result, 0, startPos, this.maxSize);
82
+ this.buffer.copy(result, firstChunkSize, 0, startPos);
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * Clear the buffer
90
+ */
91
+ clear() {
92
+ this.writePos = 0;
93
+ this.size = 0;
94
+ }
95
+
96
+ /**
97
+ * Get current data size
98
+ * @returns {number}
99
+ */
100
+ getSize() {
101
+ return this.size;
102
+ }
103
+
104
+ /**
105
+ * Check if buffer is empty
106
+ * @returns {boolean}
107
+ */
108
+ isEmpty() {
109
+ return this.size === 0;
110
+ }
111
+
112
+ /**
113
+ * Check if buffer is full
114
+ * @returns {boolean}
115
+ */
116
+ isFull() {
117
+ return this.size === this.maxSize;
118
+ }
119
+ }
120
+
121
+ module.exports = RingBuffer;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Socket Discovery - Utilities for socket path management
3
+ *
4
+ * Socket locations:
5
+ * - Tasks: ~/.zeroshot/sockets/task-<id>.sock
6
+ * - Clusters: ~/.zeroshot/sockets/cluster-<id>.sock (cluster-level, future)
7
+ * - Agents: ~/.zeroshot/sockets/cluster-<id>/<agent-id>.sock
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const net = require('net');
14
+
15
+ const CREW_DIR = path.join(os.homedir(), '.zeroshot');
16
+ const SOCKET_DIR = path.join(CREW_DIR, 'sockets');
17
+
18
+ /**
19
+ * Ensure socket directory exists
20
+ */
21
+ function ensureSocketDir() {
22
+ if (!fs.existsSync(SOCKET_DIR)) {
23
+ fs.mkdirSync(SOCKET_DIR, { recursive: true });
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Get socket path for a task
29
+ * @param {string} taskId - Task ID (e.g., 'task-swift-falcon')
30
+ * @returns {string} - Socket path
31
+ */
32
+ function getTaskSocketPath(taskId) {
33
+ ensureSocketDir();
34
+ return path.join(SOCKET_DIR, `${taskId}.sock`);
35
+ }
36
+
37
+ /**
38
+ * Get socket path for a cluster agent
39
+ * @param {string} clusterId - Cluster ID (e.g., 'cluster-bold-eagle')
40
+ * @param {string} agentId - Agent ID (e.g., 'worker')
41
+ * @returns {string} - Socket path
42
+ */
43
+ function getAgentSocketPath(clusterId, agentId) {
44
+ const clusterDir = path.join(SOCKET_DIR, clusterId);
45
+ if (!fs.existsSync(clusterDir)) {
46
+ fs.mkdirSync(clusterDir, { recursive: true });
47
+ }
48
+ return path.join(clusterDir, `${agentId}.sock`);
49
+ }
50
+
51
+ /**
52
+ * Get socket path for any ID (auto-detects task vs cluster)
53
+ * @param {string} id - Task or cluster ID
54
+ * @param {string} [agentId] - Optional agent ID for clusters
55
+ * @returns {string} - Socket path
56
+ */
57
+ function getSocketPath(id, agentId = null) {
58
+ if (id.startsWith('task-')) {
59
+ return getTaskSocketPath(id);
60
+ }
61
+ if (id.startsWith('cluster-')) {
62
+ if (agentId) {
63
+ return getAgentSocketPath(id, agentId);
64
+ }
65
+ // Cluster-level socket (future use)
66
+ ensureSocketDir();
67
+ return path.join(SOCKET_DIR, `${id}.sock`);
68
+ }
69
+ // Unknown format, treat as task
70
+ return getTaskSocketPath(id);
71
+ }
72
+
73
+ /**
74
+ * Check if a socket exists and is connectable
75
+ * @param {string} socketPath - Path to socket file
76
+ * @returns {Promise<boolean>} - True if socket is live
77
+ */
78
+ function isSocketAlive(socketPath) {
79
+ if (!fs.existsSync(socketPath)) {
80
+ return Promise.resolve(false);
81
+ }
82
+
83
+ return new Promise((resolve) => {
84
+ const socket = net.createConnection(socketPath);
85
+ const timeout = setTimeout(() => {
86
+ socket.destroy();
87
+ resolve(false);
88
+ }, 1000);
89
+
90
+ socket.on('connect', () => {
91
+ clearTimeout(timeout);
92
+ socket.end();
93
+ resolve(true);
94
+ });
95
+
96
+ socket.on('error', () => {
97
+ clearTimeout(timeout);
98
+ resolve(false);
99
+ });
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Remove stale socket file if not connectable
105
+ * @param {string} socketPath - Path to socket file
106
+ * @returns {Promise<boolean>} - True if socket was removed (stale)
107
+ */
108
+ async function cleanupStaleSocket(socketPath) {
109
+ if (!fs.existsSync(socketPath)) {
110
+ return false;
111
+ }
112
+
113
+ const alive = await isSocketAlive(socketPath);
114
+ if (!alive) {
115
+ try {
116
+ fs.unlinkSync(socketPath);
117
+ return true;
118
+ } catch {
119
+ // Ignore errors (file may have been removed already)
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ /**
126
+ * List all attachable tasks
127
+ * Task sockets are .sock files directly in SOCKET_DIR (not in subdirectories)
128
+ * Excludes cluster-level sockets (cluster-xxx.sock) since those aren't tasks
129
+ * @returns {Promise<string[]>} - Array of task IDs with live sockets
130
+ */
131
+ async function listAttachableTasks() {
132
+ ensureSocketDir();
133
+ const entries = fs.readdirSync(SOCKET_DIR, { withFileTypes: true });
134
+ const tasks = [];
135
+
136
+ for (const entry of entries) {
137
+ // Check socket files (Unix sockets report isSocket(), not isFile())
138
+ // Also accept regular files for compatibility
139
+ const isSocketFile = (entry.isSocket() || entry.isFile()) && entry.name.endsWith('.sock');
140
+ if (isSocketFile && !entry.isDirectory()) {
141
+ const id = entry.name.slice(0, -5); // Remove .sock
142
+
143
+ // Skip cluster-level sockets (cluster-xxx.sock)
144
+ if (id.startsWith('cluster-')) {
145
+ continue;
146
+ }
147
+
148
+ const socketPath = path.join(SOCKET_DIR, entry.name);
149
+ if (await isSocketAlive(socketPath)) {
150
+ tasks.push(id);
151
+ }
152
+ }
153
+ }
154
+
155
+ return tasks;
156
+ }
157
+
158
+ /**
159
+ * List all attachable agents for a cluster
160
+ * @param {string} clusterId - Cluster ID
161
+ * @returns {Promise<string[]>} - Array of agent IDs with live sockets
162
+ */
163
+ async function listAttachableAgents(clusterId) {
164
+ const clusterDir = path.join(SOCKET_DIR, clusterId);
165
+ if (!fs.existsSync(clusterDir)) {
166
+ return [];
167
+ }
168
+
169
+ const files = fs.readdirSync(clusterDir);
170
+ const agents = [];
171
+
172
+ for (const file of files) {
173
+ if (file.endsWith('.sock')) {
174
+ const agentId = file.slice(0, -5); // Remove .sock
175
+ const socketPath = path.join(clusterDir, file);
176
+ if (await isSocketAlive(socketPath)) {
177
+ agents.push(agentId);
178
+ }
179
+ }
180
+ }
181
+
182
+ return agents;
183
+ }
184
+
185
+ /**
186
+ * List all attachable clusters
187
+ * @returns {Promise<string[]>} - Array of cluster IDs with at least one live agent socket
188
+ */
189
+ async function listAttachableClusters() {
190
+ ensureSocketDir();
191
+ const entries = fs.readdirSync(SOCKET_DIR, { withFileTypes: true });
192
+ const clusters = [];
193
+
194
+ for (const entry of entries) {
195
+ if (entry.isDirectory() && entry.name.startsWith('cluster-')) {
196
+ const agents = await listAttachableAgents(entry.name);
197
+ if (agents.length > 0) {
198
+ clusters.push(entry.name);
199
+ }
200
+ }
201
+ }
202
+
203
+ return clusters;
204
+ }
205
+
206
+ /**
207
+ * Cleanup all sockets for a cluster (on cluster stop)
208
+ * @param {string} clusterId - Cluster ID
209
+ */
210
+ function cleanupClusterSockets(clusterId) {
211
+ const clusterDir = path.join(SOCKET_DIR, clusterId);
212
+ if (fs.existsSync(clusterDir)) {
213
+ const files = fs.readdirSync(clusterDir);
214
+ for (const file of files) {
215
+ try {
216
+ fs.unlinkSync(path.join(clusterDir, file));
217
+ } catch {
218
+ // Ignore
219
+ }
220
+ }
221
+ try {
222
+ fs.rmdirSync(clusterDir);
223
+ } catch {
224
+ // Ignore
225
+ }
226
+ }
227
+ }
228
+
229
+ module.exports = {
230
+ CREW_DIR,
231
+ SOCKET_DIR,
232
+ ensureSocketDir,
233
+ getTaskSocketPath,
234
+ getAgentSocketPath,
235
+ getSocketPath,
236
+ isSocketAlive,
237
+ cleanupStaleSocket,
238
+ listAttachableTasks,
239
+ listAttachableAgents,
240
+ listAttachableClusters,
241
+ cleanupClusterSockets,
242
+ };