@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,156 @@
1
+ /**
2
+ * Sub-Cluster Schema
3
+ *
4
+ * Defines the structure for nested clusters where an agent can be replaced
5
+ * with an entire cluster configuration, enabling recursive composition.
6
+ *
7
+ * Example:
8
+ * {
9
+ * "id": "implementation-cluster",
10
+ * "type": "subcluster",
11
+ * "config": {
12
+ * "agents": [
13
+ * { "id": "worker", "role": "implementation", ... },
14
+ * { "id": "validator", "role": "validator", ... }
15
+ * ]
16
+ * },
17
+ * "triggers": [{ "topic": "PLAN_READY" }],
18
+ * "hooks": {
19
+ * "onComplete": {
20
+ * "action": "publish_message",
21
+ * "config": { "topic": "IMPLEMENTATION_COMPLETE" }
22
+ * }
23
+ * }
24
+ * }
25
+ */
26
+
27
+ /**
28
+ * Validate sub-cluster agent configuration
29
+ * @param {Object} agentConfig - Agent config with type: 'subcluster'
30
+ * @param {Number} depth - Current nesting depth (for recursion limit)
31
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
32
+ */
33
+ function validateSubCluster(agentConfig, depth = 0) {
34
+ const errors = [];
35
+ const warnings = [];
36
+
37
+ // Max nesting depth to prevent infinite recursion
38
+ const MAX_DEPTH = 5;
39
+
40
+ if (depth > MAX_DEPTH) {
41
+ errors.push(`Sub-cluster '${agentConfig.id}' exceeds max nesting depth (${MAX_DEPTH})`);
42
+ return { valid: false, errors, warnings };
43
+ }
44
+
45
+ // Validate required fields
46
+ if (agentConfig.type !== 'subcluster') {
47
+ errors.push(`Agent '${agentConfig.id}' must have type: 'subcluster'`);
48
+ }
49
+
50
+ if (!agentConfig.config) {
51
+ errors.push(`Sub-cluster '${agentConfig.id}' missing config field`);
52
+ return { valid: false, errors, warnings };
53
+ }
54
+
55
+ if (!agentConfig.config.agents || !Array.isArray(agentConfig.config.agents)) {
56
+ errors.push(`Sub-cluster '${agentConfig.id}' config.agents must be an array`);
57
+ return { valid: false, errors, warnings };
58
+ }
59
+
60
+ if (agentConfig.config.agents.length === 0) {
61
+ errors.push(`Sub-cluster '${agentConfig.id}' config.agents cannot be empty`);
62
+ return { valid: false, errors, warnings };
63
+ }
64
+
65
+ // Recursively validate nested cluster config
66
+ const configValidator = require('../config-validator');
67
+ const childValidation = configValidator.validateConfig(agentConfig.config, depth + 1);
68
+
69
+ if (!childValidation.valid) {
70
+ errors.push(...childValidation.errors.map((e) => `Sub-cluster '${agentConfig.id}': ${e}`));
71
+ }
72
+
73
+ warnings.push(...childValidation.warnings.map((w) => `Sub-cluster '${agentConfig.id}': ${w}`));
74
+
75
+ // Validate triggers (sub-cluster must have triggers to activate)
76
+ if (!agentConfig.triggers || agentConfig.triggers.length === 0) {
77
+ errors.push(`Sub-cluster '${agentConfig.id}' must have triggers to activate`);
78
+ }
79
+
80
+ // Validate hooks structure
81
+ if (agentConfig.hooks) {
82
+ if (agentConfig.hooks.onComplete) {
83
+ const hook = agentConfig.hooks.onComplete;
84
+ if (!hook.action) {
85
+ errors.push(`Sub-cluster '${agentConfig.id}' onComplete hook missing action`);
86
+ }
87
+ if (hook.action === 'publish_message' && !hook.config?.topic) {
88
+ errors.push(`Sub-cluster '${agentConfig.id}' onComplete hook missing config.topic`);
89
+ }
90
+ }
91
+ }
92
+
93
+ // Check for context bridging configuration
94
+ if (agentConfig.contextStrategy?.parentTopics) {
95
+ if (!Array.isArray(agentConfig.contextStrategy.parentTopics)) {
96
+ errors.push(`Sub-cluster '${agentConfig.id}' contextStrategy.parentTopics must be an array`);
97
+ } else {
98
+ // Validate each parent topic is a string
99
+ for (const topic of agentConfig.contextStrategy.parentTopics) {
100
+ if (typeof topic !== 'string') {
101
+ errors.push(
102
+ `Sub-cluster '${agentConfig.id}' parentTopics must contain strings, got ${typeof topic}`
103
+ );
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ return {
110
+ valid: errors.length === 0,
111
+ errors,
112
+ warnings,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Get default sub-cluster template
118
+ * @returns {Object} Default sub-cluster configuration
119
+ */
120
+ function getDefaultSubCluster() {
121
+ return {
122
+ id: 'example-subcluster',
123
+ type: 'subcluster',
124
+ role: 'orchestrator',
125
+ config: {
126
+ agents: [
127
+ {
128
+ id: 'worker',
129
+ role: 'implementation',
130
+ triggers: [{ topic: 'PARENT_TRIGGER' }],
131
+ hooks: {
132
+ onComplete: {
133
+ action: 'publish_message',
134
+ config: { topic: 'WORK_COMPLETE' },
135
+ },
136
+ },
137
+ },
138
+ ],
139
+ },
140
+ triggers: [{ topic: 'START_WORK' }],
141
+ hooks: {
142
+ onComplete: {
143
+ action: 'publish_message',
144
+ config: { topic: 'SUBCLUSTER_COMPLETE' },
145
+ },
146
+ },
147
+ contextStrategy: {
148
+ parentTopics: ['ISSUE_OPENED', 'PLAN_READY'],
149
+ },
150
+ };
151
+ }
152
+
153
+ module.exports = {
154
+ validateSubCluster,
155
+ getDefaultSubCluster,
156
+ };
@@ -0,0 +1,545 @@
1
+ /**
2
+ * SubClusterWrapper - Manages nested cluster lifecycle
3
+ *
4
+ * Implements same interface as AgentWrapper but spawns a child Orchestrator
5
+ * instead of a Claude task. Enables recursive cluster composition.
6
+ *
7
+ * Lifecycle:
8
+ * - On trigger match: spawns nested Orchestrator with child cluster config
9
+ * - Passes parent context to child via ISSUE_OPENED equivalent
10
+ * - Listens for child CLUSTER_COMPLETE → executes onComplete hook
11
+ * - Listens for child CLUSTER_FAILED → executes onError hook
12
+ * - Supports maxIterations at subcluster level (restart child on failure)
13
+ */
14
+
15
+ const LogicEngine = require('./logic-engine');
16
+ const MessageBusBridge = require('./message-bus-bridge');
17
+ const { DEFAULT_MAX_ITERATIONS } = require('./agent/agent-config');
18
+
19
+ class SubClusterWrapper {
20
+ constructor(config, messageBus, parentCluster, options = {}) {
21
+ this.id = config.id;
22
+ this.role = config.role || 'orchestrator';
23
+ this.config = config;
24
+ this.messageBus = messageBus; // Parent message bus
25
+ this.parentCluster = parentCluster;
26
+ this.logicEngine = new LogicEngine(messageBus, parentCluster);
27
+
28
+ this.state = 'idle';
29
+ this.iteration = 0;
30
+ this.maxIterations = config.maxIterations || DEFAULT_MAX_ITERATIONS;
31
+ this.running = false;
32
+ this.unsubscribe = null;
33
+
34
+ // Child cluster state
35
+ this.childCluster = null; // { id, orchestrator, messageBus, bridge }
36
+ this.childClusterId = null;
37
+
38
+ this.quiet = options.quiet || false;
39
+ }
40
+
41
+ /**
42
+ * Log message (respects quiet mode)
43
+ * @private
44
+ */
45
+ _log(...args) {
46
+ if (!this.quiet) {
47
+ console.log(...args);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Publish lifecycle event to parent message bus
53
+ * @private
54
+ */
55
+ _publishLifecycle(event, details = {}) {
56
+ this.messageBus.publish({
57
+ cluster_id: this.parentCluster.id,
58
+ topic: 'AGENT_LIFECYCLE',
59
+ sender: this.id,
60
+ receiver: 'system',
61
+ content: {
62
+ text: `${this.id}: ${event}`,
63
+ data: {
64
+ event,
65
+ agent: this.id,
66
+ role: this.role,
67
+ state: this.state,
68
+ type: 'subcluster',
69
+ ...details,
70
+ },
71
+ },
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Start the sub-cluster wrapper (begin listening for triggers)
77
+ */
78
+ start() {
79
+ if (this.running) {
80
+ throw new Error(`SubCluster ${this.id} is already running`);
81
+ }
82
+
83
+ this.running = true;
84
+ this.state = 'idle';
85
+
86
+ // Subscribe to parent cluster messages
87
+ this.unsubscribe = this.messageBus.subscribe((message) => {
88
+ if (message.cluster_id === this.parentCluster.id) {
89
+ this._handleMessage(message).catch((error) => {
90
+ console.error(`\n${'='.repeat(80)}`);
91
+ console.error(`🔴 FATAL: SubCluster ${this.id} message handler crashed`);
92
+ console.error(`${'='.repeat(80)}`);
93
+ console.error(`Topic: ${message.topic}`);
94
+ console.error(`Error: ${error.message}`);
95
+ console.error(`Stack: ${error.stack}`);
96
+ console.error(`${'='.repeat(80)}\n`);
97
+ throw error;
98
+ });
99
+ }
100
+ });
101
+
102
+ this._log(`SubCluster ${this.id} started (role: ${this.role})`);
103
+ this._publishLifecycle('STARTED', {
104
+ triggers: this.config.triggers?.map((t) => t.topic) || [],
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Stop the sub-cluster wrapper
110
+ */
111
+ async stop() {
112
+ if (!this.running) {
113
+ return;
114
+ }
115
+
116
+ this.running = false;
117
+ this.state = 'stopped';
118
+
119
+ if (this.unsubscribe) {
120
+ this.unsubscribe();
121
+ this.unsubscribe = null;
122
+ }
123
+
124
+ // Stop child cluster if running
125
+ if (this.childCluster) {
126
+ await this._stopChildCluster();
127
+ }
128
+
129
+ this._log(`SubCluster ${this.id} stopped`);
130
+ }
131
+
132
+ /**
133
+ * Handle incoming message from parent cluster
134
+ * @private
135
+ */
136
+ async _handleMessage(message) {
137
+ // Check if any trigger matches
138
+ const matchingTrigger = this._findMatchingTrigger(message);
139
+ if (!matchingTrigger) {
140
+ return;
141
+ }
142
+
143
+ // Check state
144
+ if (!this.running) {
145
+ console.warn(`[${this.id}] ⚠️ DROPPING message (not running): ${message.topic}`);
146
+ return;
147
+ }
148
+ if (this.state !== 'idle') {
149
+ console.warn(
150
+ `[${this.id}] ⚠️ DROPPING message (busy, state=${this.state}): ${message.topic}`
151
+ );
152
+ return;
153
+ }
154
+
155
+ // Evaluate trigger logic
156
+ this.state = 'evaluating_logic';
157
+ const shouldExecute = this._evaluateTrigger(matchingTrigger, message);
158
+
159
+ if (!shouldExecute) {
160
+ this.state = 'idle';
161
+ return;
162
+ }
163
+
164
+ // Execute trigger action (spawn child cluster)
165
+ await this._handleTrigger(message);
166
+ }
167
+
168
+ /**
169
+ * Find trigger matching the message topic
170
+ * @private
171
+ */
172
+ _findMatchingTrigger(message) {
173
+ if (!this.config.triggers) {
174
+ return null;
175
+ }
176
+
177
+ return this.config.triggers.find((trigger) => {
178
+ if (trigger.topic === '*' || trigger.topic === message.topic) {
179
+ return true;
180
+ }
181
+ if (trigger.topic.endsWith('*')) {
182
+ const prefix = trigger.topic.slice(0, -1);
183
+ return message.topic.startsWith(prefix);
184
+ }
185
+ return false;
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Evaluate trigger logic
191
+ * @private
192
+ */
193
+ _evaluateTrigger(trigger, message) {
194
+ if (!trigger.logic || !trigger.logic.script) {
195
+ return true;
196
+ }
197
+
198
+ const agent = {
199
+ id: this.id,
200
+ role: this.role,
201
+ iteration: this.iteration,
202
+ cluster_id: this.parentCluster.id,
203
+ };
204
+
205
+ return this.logicEngine.evaluate(trigger.logic.script, agent, message);
206
+ }
207
+
208
+ /**
209
+ * Handle trigger: spawn child cluster
210
+ * @private
211
+ */
212
+ async _handleTrigger(triggeringMessage) {
213
+ // Check max iterations
214
+ if (this.iteration >= this.maxIterations) {
215
+ this._log(`[SubCluster ${this.id}] Hit max iterations (${this.maxIterations}), failing`);
216
+ this._publishLifecycle('MAX_ITERATIONS_REACHED', {
217
+ iteration: this.iteration,
218
+ maxIterations: this.maxIterations,
219
+ });
220
+
221
+ this.messageBus.publish({
222
+ cluster_id: this.parentCluster.id,
223
+ topic: 'CLUSTER_FAILED',
224
+ sender: this.id,
225
+ receiver: 'system',
226
+ content: {
227
+ text: `SubCluster ${this.id} hit max iterations limit (${this.maxIterations})`,
228
+ data: {
229
+ reason: 'max_iterations',
230
+ iteration: this.iteration,
231
+ maxIterations: this.maxIterations,
232
+ },
233
+ },
234
+ });
235
+
236
+ this.state = 'failed';
237
+ return;
238
+ }
239
+
240
+ this.iteration++;
241
+ this.state = 'spawning_child';
242
+
243
+ this._publishLifecycle('SPAWNING_CHILD', {
244
+ iteration: this.iteration,
245
+ triggeredBy: triggeringMessage.topic,
246
+ });
247
+
248
+ try {
249
+ // Build child cluster context from parent messages
250
+ const context = this._buildChildContext(triggeringMessage);
251
+
252
+ // Spawn child cluster
253
+ await this._spawnChildCluster(context);
254
+
255
+ this._publishLifecycle('CHILD_SPAWNED', {
256
+ childClusterId: this.childClusterId,
257
+ iteration: this.iteration,
258
+ });
259
+
260
+ this.state = 'monitoring_child';
261
+ } catch (error) {
262
+ console.error(`\n${'='.repeat(80)}`);
263
+ console.error(`🔴 CHILD CLUSTER SPAWN FAILED - ${this.id}`);
264
+ console.error(`${'='.repeat(80)}`);
265
+ console.error(`Error: ${error.message}`);
266
+ console.error(`Stack: ${error.stack}`);
267
+ console.error(`${'='.repeat(80)}\n`);
268
+
269
+ this.state = 'error';
270
+
271
+ // Execute onError hook
272
+ await this._executeHook('onError', { error, triggeringMessage });
273
+
274
+ // Return to idle if we haven't hit max iterations
275
+ if (this.iteration < this.maxIterations) {
276
+ this.state = 'idle';
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Build context for child cluster from parent messages
283
+ * @private
284
+ */
285
+ _buildChildContext(triggeringMessage) {
286
+ const parentTopics = this.config.contextStrategy?.parentTopics || [];
287
+
288
+ let context = `# Child Cluster Context\n\n`;
289
+ context += `Parent Cluster: ${this.parentCluster.id}\n`;
290
+ context += `SubCluster ID: ${this.id}\n`;
291
+ context += `Iteration: ${this.iteration}\n\n`;
292
+
293
+ // Add messages from specified parent topics
294
+ if (parentTopics.length > 0) {
295
+ context += `## Parent Cluster Messages\n\n`;
296
+
297
+ for (const topic of parentTopics) {
298
+ const messages = this.messageBus.query({
299
+ cluster_id: this.parentCluster.id,
300
+ topic,
301
+ limit: 10,
302
+ });
303
+
304
+ if (messages.length > 0) {
305
+ context += `### Topic: ${topic}\n\n`;
306
+ for (const msg of messages) {
307
+ context += `[${new Date(msg.timestamp).toISOString()}] ${msg.sender}:\n`;
308
+ if (msg.content?.text) {
309
+ context += `${msg.content.text}\n`;
310
+ }
311
+ if (msg.content?.data) {
312
+ context += `Data: ${JSON.stringify(msg.content.data, null, 2)}\n`;
313
+ }
314
+ context += '\n';
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ // Add triggering message
321
+ context += `\n## Triggering Message\n\n`;
322
+ context += `Topic: ${triggeringMessage.topic}\n`;
323
+ context += `Sender: ${triggeringMessage.sender}\n`;
324
+ if (triggeringMessage.content?.text) {
325
+ context += `\n${triggeringMessage.content.text}\n`;
326
+ }
327
+
328
+ return context;
329
+ }
330
+
331
+ /**
332
+ * Spawn child cluster with nested Orchestrator
333
+ * @private
334
+ */
335
+ async _spawnChildCluster(context) {
336
+ const Orchestrator = require('./orchestrator');
337
+ const path = require('path');
338
+
339
+ // Generate child cluster ID (namespaced under parent)
340
+ const childId = `${this.parentCluster.id}.${this.id}`;
341
+ this.childClusterId = childId;
342
+
343
+ // Create child orchestrator with separate database
344
+ const childOrchestrator = new Orchestrator({
345
+ quiet: this.quiet,
346
+ skipLoad: true,
347
+ storageDir: path.join(this.parentCluster.ledger.dbPath, '..', 'subclusters', childId),
348
+ });
349
+
350
+ // Start child cluster with text input (context from parent)
351
+ const childCluster = await childOrchestrator.start(
352
+ this.config.config, // Child cluster config
353
+ { text: context },
354
+ { testMode: false }
355
+ );
356
+
357
+ // Create message bridge
358
+ const bridge = new MessageBusBridge(this.messageBus, childCluster.messageBus, {
359
+ parentClusterId: this.parentCluster.id,
360
+ childClusterId: childId,
361
+ parentTopics: this.config.contextStrategy?.parentTopics || [],
362
+ });
363
+
364
+ // Store child cluster state
365
+ this.childCluster = {
366
+ id: childId,
367
+ orchestrator: childOrchestrator,
368
+ messageBus: childCluster.messageBus,
369
+ bridge,
370
+ };
371
+
372
+ // Listen for child cluster completion
373
+ childCluster.messageBus.subscribe((message) => {
374
+ if (message.topic === 'CLUSTER_COMPLETE' && message.cluster_id === childId) {
375
+ this._onChildComplete(message).catch((err) => {
376
+ console.error(`Failed to handle child completion: ${err.message}`);
377
+ });
378
+ }
379
+ });
380
+
381
+ // Listen for child cluster failure
382
+ childCluster.messageBus.subscribe((message) => {
383
+ if (message.topic === 'CLUSTER_FAILED' && message.cluster_id === childId) {
384
+ this._onChildFailed(message).catch((err) => {
385
+ console.error(`Failed to handle child failure: ${err.message}`);
386
+ });
387
+ }
388
+ });
389
+ }
390
+
391
+ /**
392
+ * Handle child cluster completion
393
+ * @private
394
+ */
395
+ async _onChildComplete(message) {
396
+ this._log(`[SubCluster ${this.id}] Child cluster completed`);
397
+
398
+ this._publishLifecycle('CHILD_COMPLETE', {
399
+ childClusterId: this.childClusterId,
400
+ iteration: this.iteration,
401
+ });
402
+
403
+ // Execute onComplete hook
404
+ await this._executeHook('onComplete', {
405
+ result: message,
406
+ triggeringMessage: null,
407
+ });
408
+
409
+ // Clean up child cluster
410
+ await this._stopChildCluster();
411
+
412
+ this.state = 'idle';
413
+ }
414
+
415
+ /**
416
+ * Handle child cluster failure
417
+ * @private
418
+ */
419
+ async _onChildFailed(message) {
420
+ this._log(`[SubCluster ${this.id}] Child cluster failed`);
421
+
422
+ this._publishLifecycle('CHILD_FAILED', {
423
+ childClusterId: this.childClusterId,
424
+ iteration: this.iteration,
425
+ error: message.content?.data?.reason,
426
+ });
427
+
428
+ // Execute onError hook
429
+ const error = new Error(message.content?.data?.reason || 'Child cluster failed');
430
+ await this._executeHook('onError', { error, triggeringMessage: null });
431
+
432
+ // Clean up child cluster
433
+ await this._stopChildCluster();
434
+
435
+ // Retry if within max iterations
436
+ if (this.iteration < this.maxIterations) {
437
+ this.state = 'idle';
438
+ } else {
439
+ this.state = 'failed';
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Stop child cluster
445
+ * @private
446
+ */
447
+ async _stopChildCluster() {
448
+ if (!this.childCluster) {
449
+ return;
450
+ }
451
+
452
+ // Close message bridge
453
+ if (this.childCluster.bridge) {
454
+ this.childCluster.bridge.close();
455
+ }
456
+
457
+ // Stop child orchestrator
458
+ try {
459
+ await this.childCluster.orchestrator.stop(this.childCluster.id);
460
+ } catch (err) {
461
+ console.warn(`Warning: Failed to stop child cluster ${this.childCluster.id}: ${err.message}`);
462
+ }
463
+
464
+ this.childCluster = null;
465
+ this.childClusterId = null;
466
+ }
467
+
468
+ /**
469
+ * Execute a hook
470
+ * @private
471
+ */
472
+ _executeHook(hookName, context) {
473
+ const hook = this.config.hooks?.[hookName];
474
+ if (!hook) {
475
+ return;
476
+ }
477
+
478
+ if (hook.action === 'publish_message') {
479
+ const message = this._substituteTemplate(hook.config, context);
480
+ this.messageBus.publish({
481
+ cluster_id: this.parentCluster.id,
482
+ sender: this.id,
483
+ ...message,
484
+ });
485
+ } else {
486
+ throw new Error(`Unknown hook action: ${hook.action}`);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Substitute template variables in hook config
492
+ * @private
493
+ */
494
+ _substituteTemplate(config, context) {
495
+ if (!config) {
496
+ throw new Error('_substituteTemplate: config is required');
497
+ }
498
+
499
+ const json = JSON.stringify(config);
500
+
501
+ let substituted = json
502
+ .replace(/\{\{cluster\.id\}\}/g, this.parentCluster.id)
503
+ .replace(/\{\{subcluster\.id\}\}/g, this.id)
504
+ .replace(/\{\{child\.id\}\}/g, this.childClusterId || '')
505
+ .replace(/\{\{iteration\}\}/g, String(this.iteration))
506
+ .replace(/\{\{error\.message\}\}/g, context.error?.message || '');
507
+
508
+ // Parse and validate
509
+ let result;
510
+ try {
511
+ result = JSON.parse(substituted);
512
+ } catch (e) {
513
+ console.error('JSON parse failed. Substituted string:');
514
+ console.error(substituted);
515
+ throw new Error(`Template substitution produced invalid JSON: ${e.message}`);
516
+ }
517
+
518
+ return result;
519
+ }
520
+
521
+ /**
522
+ * Resume sub-cluster task (not implemented for subclusters)
523
+ */
524
+ resume(_resumeContext) {
525
+ throw new Error('Resume not implemented for subclusters');
526
+ }
527
+
528
+ /**
529
+ * Get current sub-cluster state
530
+ */
531
+ getState() {
532
+ return {
533
+ id: this.id,
534
+ role: this.role,
535
+ state: this.state,
536
+ iteration: this.iteration,
537
+ maxIterations: this.maxIterations,
538
+ type: 'subcluster',
539
+ childClusterId: this.childClusterId,
540
+ childRunning: this.childCluster !== null,
541
+ };
542
+ }
543
+ }
544
+
545
+ module.exports = SubClusterWrapper;