@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,543 @@
1
+ /**
2
+ * AttachServer - PTY process manager with socket server for attach/detach
3
+ *
4
+ * Lifecycle:
5
+ * 1. Created when task/agent spawns
6
+ * 2. Spawns command via node-pty
7
+ * 3. Listens on Unix socket for client connections
8
+ * 4. Buffers output in ring buffer (for late-joining clients)
9
+ * 5. Broadcasts output to all connected clients
10
+ * 6. Cleans up when process exits
11
+ *
12
+ * Features:
13
+ * - Multi-client support (multiple terminals can attach)
14
+ * - Output history replay on attach
15
+ * - Signal forwarding (SIGINT, SIGTERM)
16
+ * - Window resize support
17
+ * - Graceful cleanup on exit
18
+ */
19
+
20
+ const net = require('net');
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const EventEmitter = require('events');
24
+
25
+ const RingBuffer = require('./ring-buffer');
26
+ const protocol = require('./protocol');
27
+ const { cleanupStaleSocket } = require('./socket-discovery');
28
+
29
+ // FAIL FAST: Check for node-pty at module load time, not at spawn time
30
+ let pty;
31
+ try {
32
+ pty = require('node-pty');
33
+ } catch (e) {
34
+ throw new Error(
35
+ `AttachServer: node-pty not installed. Run: npm install node-pty\n` +
36
+ `Original error: ${e.message}`
37
+ );
38
+ }
39
+
40
+ // Default output buffer size: 1MB
41
+ const DEFAULT_BUFFER_SIZE = 1024 * 1024;
42
+
43
+ class AttachServer extends EventEmitter {
44
+ /**
45
+ * @param {object} options
46
+ * @param {string} options.id - Task or agent ID
47
+ * @param {string} options.socketPath - Unix socket path
48
+ * @param {string} options.command - Command to spawn
49
+ * @param {string[]} options.args - Command arguments
50
+ * @param {string} [options.cwd] - Working directory
51
+ * @param {object} [options.env] - Environment variables
52
+ * @param {number} [options.cols] - Terminal columns (default 120)
53
+ * @param {number} [options.rows] - Terminal rows (default 30)
54
+ * @param {number} [options.bufferSize] - Output buffer size (default 1MB)
55
+ */
56
+ constructor(options) {
57
+ super();
58
+
59
+ if (!options.id) throw new Error('AttachServer: id is required');
60
+ if (!options.socketPath) throw new Error('AttachServer: socketPath is required');
61
+ if (!options.command) throw new Error('AttachServer: command is required');
62
+
63
+ this.id = options.id;
64
+ this.socketPath = options.socketPath;
65
+ this.command = options.command;
66
+ this.args = options.args || [];
67
+ this.cwd = options.cwd || process.cwd();
68
+ this.env = options.env || process.env;
69
+ this.cols = options.cols || 120;
70
+ this.rows = options.rows || 30;
71
+
72
+ this.outputBuffer = new RingBuffer(options.bufferSize || DEFAULT_BUFFER_SIZE);
73
+ this.clients = new Map(); // clientId -> { socket, decoder }
74
+ this.pty = null;
75
+ this.server = null;
76
+ this.state = 'stopped'; // stopped, starting, running, exiting, exited
77
+ this.exitCode = null;
78
+ this.exitSignal = null;
79
+ this.pid = null;
80
+
81
+ // Bind cleanup handlers
82
+ this._onProcessExit = this._onProcessExit.bind(this);
83
+ this._onServerError = this._onServerError.bind(this);
84
+ }
85
+
86
+ /**
87
+ * Start the PTY process and socket server
88
+ * @returns {Promise<void>}
89
+ */
90
+ async start() {
91
+ if (this.state !== 'stopped') {
92
+ throw new Error(`AttachServer: Cannot start from state '${this.state}'`);
93
+ }
94
+
95
+ this.state = 'starting';
96
+
97
+ // Ensure socket directory exists
98
+ const socketDir = path.dirname(this.socketPath);
99
+ if (!fs.existsSync(socketDir)) {
100
+ fs.mkdirSync(socketDir, { recursive: true });
101
+ }
102
+
103
+ // Clean up stale socket if exists
104
+ await cleanupStaleSocket(this.socketPath);
105
+
106
+ // Check if socket is still in use (another process)
107
+ if (fs.existsSync(this.socketPath)) {
108
+ throw new Error(`AttachServer: Socket already in use: ${this.socketPath}`);
109
+ }
110
+
111
+ // Start socket server FIRST (so clients can connect immediately)
112
+ await this._startServer();
113
+
114
+ // Spawn PTY process
115
+ await this._spawnPty();
116
+
117
+ this.state = 'running';
118
+ this.emit('start', { id: this.id, pid: this.pid });
119
+ }
120
+
121
+ /**
122
+ * Stop the server and kill the PTY process
123
+ * @param {string} [signal='SIGTERM'] - Signal to send
124
+ * @returns {Promise<void>}
125
+ */
126
+ async stop(signal = 'SIGTERM') {
127
+ if (this.state !== 'running') {
128
+ return;
129
+ }
130
+
131
+ this.state = 'exiting';
132
+
133
+ // Kill PTY process
134
+ if (this.pty) {
135
+ try {
136
+ this.pty.kill(signal);
137
+ } catch {
138
+ // Process may already be dead
139
+ }
140
+ }
141
+
142
+ // Wait for process to exit (with timeout)
143
+ await new Promise((resolve) => {
144
+ const timeout = setTimeout(() => {
145
+ // Force kill if still running
146
+ if (this.pty) {
147
+ try {
148
+ this.pty.kill('SIGKILL');
149
+ } catch {
150
+ // Ignore
151
+ }
152
+ }
153
+ resolve();
154
+ }, 5000);
155
+
156
+ const checkExit = () => {
157
+ if (this.state === 'exited') {
158
+ clearTimeout(timeout);
159
+ resolve();
160
+ } else {
161
+ setTimeout(checkExit, 100);
162
+ }
163
+ };
164
+ checkExit();
165
+ });
166
+
167
+ await this._cleanup();
168
+ }
169
+
170
+ /**
171
+ * Send a signal to the PTY process
172
+ * @param {string} signal - Signal name (SIGINT, SIGTERM, etc.)
173
+ */
174
+ sendSignal(signal) {
175
+ if (!this.pty || this.state !== 'running') {
176
+ return false;
177
+ }
178
+
179
+ try {
180
+ this.pty.kill(signal);
181
+ return true;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Resize the PTY
189
+ * @param {number} cols - Columns
190
+ * @param {number} rows - Rows
191
+ */
192
+ resize(cols, rows) {
193
+ if (!this.pty || this.state !== 'running') {
194
+ return;
195
+ }
196
+
197
+ this.cols = cols;
198
+ this.rows = rows;
199
+
200
+ try {
201
+ this.pty.resize(cols, rows);
202
+ } catch {
203
+ // Ignore resize errors
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Write to PTY stdin (for future interactive mode)
209
+ * @param {Buffer|string} data - Data to write
210
+ */
211
+ write(data) {
212
+ if (!this.pty || this.state !== 'running') {
213
+ return false;
214
+ }
215
+
216
+ try {
217
+ this.pty.write(data);
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get current server state
226
+ * @returns {object}
227
+ */
228
+ getState() {
229
+ return {
230
+ id: this.id,
231
+ state: this.state,
232
+ pid: this.pid,
233
+ exitCode: this.exitCode,
234
+ exitSignal: this.exitSignal,
235
+ clientCount: this.clients.size,
236
+ bufferSize: this.outputBuffer.getSize(),
237
+ };
238
+ }
239
+
240
+ // ─────────────────────────────────────────────────────────────────
241
+ // Private methods
242
+ // ─────────────────────────────────────────────────────────────────
243
+
244
+ /**
245
+ * Start the Unix socket server
246
+ * @private
247
+ */
248
+ _startServer() {
249
+ return new Promise((resolve, reject) => {
250
+ this.server = net.createServer((socket) => {
251
+ this._handleClientConnection(socket);
252
+ });
253
+
254
+ this.server.on('error', this._onServerError);
255
+
256
+ this.server.listen(this.socketPath, () => {
257
+ // Set socket permissions (owner read/write only)
258
+ try {
259
+ fs.chmodSync(this.socketPath, 0o600);
260
+ } catch {
261
+ // Ignore permission errors
262
+ }
263
+ resolve();
264
+ });
265
+
266
+ this.server.on('error', (err) => {
267
+ if (this.state === 'starting') {
268
+ reject(err);
269
+ }
270
+ });
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Spawn the PTY process
276
+ * @private
277
+ */
278
+ _spawnPty() {
279
+ this.pty = pty.spawn(this.command, this.args, {
280
+ name: 'xterm-256color',
281
+ cols: this.cols,
282
+ rows: this.rows,
283
+ cwd: this.cwd,
284
+ env: this.env,
285
+ });
286
+
287
+ this.pid = this.pty.pid;
288
+
289
+ // Handle PTY output
290
+ this.pty.onData((data) => {
291
+ this._handlePtyOutput(data);
292
+ });
293
+
294
+ // Handle PTY exit
295
+ this.pty.onExit(({ exitCode, signal }) => {
296
+ this._onProcessExit(exitCode, signal);
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Handle PTY output
302
+ * @private
303
+ */
304
+ _handlePtyOutput(data) {
305
+ // Buffer output for late-joining clients
306
+ this.outputBuffer.write(data);
307
+
308
+ // Broadcast to all connected clients
309
+ const message = protocol.encode(protocol.createOutputMessage(data));
310
+
311
+ for (const [clientId, client] of this.clients) {
312
+ try {
313
+ client.socket.write(message);
314
+ } catch {
315
+ // Client disconnected, will be cleaned up
316
+ this._removeClient(clientId);
317
+ }
318
+ }
319
+
320
+ // Emit for local listeners (e.g., logging)
321
+ this.emit('output', data);
322
+ }
323
+
324
+ /**
325
+ * Handle new client connection
326
+ * @private
327
+ */
328
+ _handleClientConnection(socket) {
329
+ const decoder = new protocol.MessageDecoder();
330
+ let clientId = null;
331
+
332
+ socket.on('data', (data) => {
333
+ try {
334
+ const messages = decoder.feed(data);
335
+ for (const msg of messages) {
336
+ this._handleClientMessage(socket, msg, (id) => {
337
+ clientId = id;
338
+ });
339
+ }
340
+ } catch (e) {
341
+ // Protocol error, close connection
342
+ this._sendError(socket, `Protocol error: ${e.message}`);
343
+ socket.end();
344
+ }
345
+ });
346
+
347
+ socket.on('close', () => {
348
+ if (clientId) {
349
+ this._removeClient(clientId);
350
+ }
351
+ });
352
+
353
+ socket.on('error', () => {
354
+ if (clientId) {
355
+ this._removeClient(clientId);
356
+ }
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Handle message from client
362
+ * @private
363
+ */
364
+ _handleClientMessage(socket, message, setClientId) {
365
+ switch (message.type) {
366
+ case protocol.MessageType.ATTACH: {
367
+ const { clientId, cols, rows } = message;
368
+ if (!clientId) {
369
+ this._sendError(socket, 'ATTACH requires clientId');
370
+ return;
371
+ }
372
+
373
+ // Register client
374
+ this.clients.set(clientId, {
375
+ socket,
376
+ decoder: new protocol.MessageDecoder(),
377
+ });
378
+ setClientId(clientId);
379
+
380
+ // Send history (buffered output)
381
+ const history = this.outputBuffer.read();
382
+ if (history.length > 0) {
383
+ socket.write(protocol.encode(protocol.createHistoryMessage(history)));
384
+ }
385
+
386
+ // Send current state
387
+ socket.write(protocol.encode(protocol.createStateMessage(this.getState())));
388
+
389
+ // Resize PTY to client dimensions (if provided)
390
+ if (cols && rows) {
391
+ this.resize(cols, rows);
392
+ }
393
+
394
+ this.emit('clientAttach', { clientId });
395
+ break;
396
+ }
397
+
398
+ case protocol.MessageType.DETACH: {
399
+ const { clientId } = message;
400
+ this._removeClient(clientId);
401
+ socket.end();
402
+ break;
403
+ }
404
+
405
+ case protocol.MessageType.RESIZE: {
406
+ const { cols, rows } = message;
407
+ if (cols && rows) {
408
+ this.resize(cols, rows);
409
+ }
410
+ break;
411
+ }
412
+
413
+ case protocol.MessageType.SIGNAL: {
414
+ const { signal } = message;
415
+ this.sendSignal(signal);
416
+ break;
417
+ }
418
+
419
+ case protocol.MessageType.STDIN: {
420
+ // Future interactive mode
421
+ const data = protocol.decodeData(message);
422
+ if (data) {
423
+ this.write(data);
424
+ }
425
+ break;
426
+ }
427
+
428
+ default:
429
+ this._sendError(socket, `Unknown message type: ${message.type}`);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Remove a client
435
+ * @private
436
+ */
437
+ _removeClient(clientId) {
438
+ const client = this.clients.get(clientId);
439
+ if (client) {
440
+ this.clients.delete(clientId);
441
+ this.emit('clientDetach', { clientId });
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Send error message to client
447
+ * @private
448
+ */
449
+ _sendError(socket, message) {
450
+ try {
451
+ socket.write(protocol.encode(protocol.createErrorMessage(message)));
452
+ } catch {
453
+ // Client disconnected
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Handle PTY process exit
459
+ * @private
460
+ */
461
+ _onProcessExit(exitCode, signal) {
462
+ this.exitCode = exitCode;
463
+ this.exitSignal = signal;
464
+ this.state = 'exited';
465
+
466
+ // Notify all clients
467
+ const exitMessage = protocol.encode(protocol.createExitMessage(exitCode, signal));
468
+
469
+ for (const [, client] of this.clients) {
470
+ try {
471
+ client.socket.write(exitMessage);
472
+ client.socket.end();
473
+ } catch {
474
+ // Client already disconnected
475
+ }
476
+ }
477
+
478
+ this.emit('exit', { exitCode, signal });
479
+
480
+ // Clean up after short delay (allow clients to receive exit message)
481
+ setTimeout(() => {
482
+ this._cleanup();
483
+ }, 500);
484
+ }
485
+
486
+ /**
487
+ * Handle server error
488
+ * @private
489
+ */
490
+ _onServerError(err) {
491
+ this.emit('error', err);
492
+ }
493
+
494
+ /**
495
+ * Clean up resources
496
+ * @private
497
+ */
498
+ async _cleanup() {
499
+ // Close all client connections
500
+ for (const [, client] of this.clients) {
501
+ try {
502
+ client.socket.destroy();
503
+ } catch {
504
+ // Ignore
505
+ }
506
+ }
507
+ this.clients.clear();
508
+
509
+ // Close server
510
+ if (this.server) {
511
+ await new Promise((resolve) => {
512
+ this.server.close(() => resolve());
513
+ });
514
+ this.server = null;
515
+ }
516
+
517
+ // Remove socket file
518
+ if (fs.existsSync(this.socketPath)) {
519
+ try {
520
+ fs.unlinkSync(this.socketPath);
521
+ } catch {
522
+ // Ignore
523
+ }
524
+ }
525
+
526
+ // Clean up empty parent directory for cluster sockets
527
+ const socketDir = path.dirname(this.socketPath);
528
+ if (socketDir.includes('cluster-')) {
529
+ try {
530
+ const files = fs.readdirSync(socketDir);
531
+ if (files.length === 0) {
532
+ fs.rmdirSync(socketDir);
533
+ }
534
+ } catch {
535
+ // Ignore
536
+ }
537
+ }
538
+
539
+ this.emit('cleanup');
540
+ }
541
+ }
542
+
543
+ module.exports = AttachServer;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Attach/Detach System - tmux-style session management for zeroshot tasks and clusters
3
+ *
4
+ * Usage:
5
+ * const { AttachServer, AttachClient, socketDiscovery } = require('./attach');
6
+ *
7
+ * // Server side (in watcher.js or agent-wrapper.js)
8
+ * const server = new AttachServer({
9
+ * id: 'task-xxx',
10
+ * socketPath: socketDiscovery.getTaskSocketPath('task-xxx'),
11
+ * command: 'claude',
12
+ * args: ['--print', '--output-format', 'stream-json', prompt],
13
+ * });
14
+ * await server.start();
15
+ *
16
+ * // Client side (in CLI attach command)
17
+ * const client = new AttachClient({
18
+ * socketPath: socketDiscovery.getTaskSocketPath('task-xxx'),
19
+ * });
20
+ * await client.connect();
21
+ */
22
+
23
+ const AttachServer = require('./attach-server');
24
+ const AttachClient = require('./attach-client');
25
+ const RingBuffer = require('./ring-buffer');
26
+ const protocol = require('./protocol');
27
+ const socketDiscovery = require('./socket-discovery');
28
+
29
+ module.exports = {
30
+ AttachServer,
31
+ AttachClient,
32
+ RingBuffer,
33
+ protocol,
34
+ socketDiscovery,
35
+ };