@covibes/zeroshot 5.2.1 → 5.4.0

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 (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
@@ -0,0 +1,122 @@
1
+ function parseToolPart(part) {
2
+ const state = part.state || {};
3
+
4
+ if (state.status === 'pending' || state.status === 'running') {
5
+ return {
6
+ type: 'tool_call',
7
+ toolName: part.tool,
8
+ toolId: part.callID,
9
+ input: state.input || {},
10
+ };
11
+ }
12
+
13
+ if (state.status === 'completed') {
14
+ return {
15
+ type: 'tool_result',
16
+ toolId: part.callID,
17
+ content: state.output || '',
18
+ isError: false,
19
+ };
20
+ }
21
+
22
+ if (state.status === 'error') {
23
+ return {
24
+ type: 'tool_result',
25
+ toolId: part.callID,
26
+ content: state.error || '',
27
+ isError: true,
28
+ };
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ function parseStepFinish(part) {
35
+ const tokens = part.tokens || {};
36
+ return {
37
+ type: 'result',
38
+ success: true,
39
+ inputTokens: tokens.input || 0,
40
+ outputTokens: tokens.output || 0,
41
+ };
42
+ }
43
+
44
+ function parsePart(part) {
45
+ if (!part || typeof part !== 'object') return null;
46
+
47
+ if (part.type === 'text' && part.text) {
48
+ return { type: 'text', text: part.text };
49
+ }
50
+
51
+ if (part.type === 'reasoning' && part.text) {
52
+ return { type: 'thinking', text: part.text };
53
+ }
54
+
55
+ if (part.type === 'tool') {
56
+ return parseToolPart(part);
57
+ }
58
+
59
+ if (part.type === 'step-finish') {
60
+ return parseStepFinish(part);
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ function parseErrorEvent(event) {
67
+ const error = event.error || {};
68
+ return {
69
+ type: 'result',
70
+ success: false,
71
+ error: error.data?.message || error.message || error.name || 'Unknown error',
72
+ };
73
+ }
74
+
75
+ function parseEvent(line) {
76
+ let event;
77
+ try {
78
+ event = JSON.parse(line);
79
+ } catch {
80
+ return null;
81
+ }
82
+
83
+ if (!event || typeof event !== 'object') return null;
84
+
85
+ if (event.type === 'error') {
86
+ return parseErrorEvent(event);
87
+ }
88
+
89
+ if (event.type === 'text' || event.type === 'step_start' || event.type === 'step_finish') {
90
+ return parsePart(event.part || event);
91
+ }
92
+
93
+ if (event.type === 'message.part.updated') {
94
+ return parsePart(event.properties?.part);
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ function parseChunk(chunk) {
101
+ const events = [];
102
+ const lines = chunk.split('\n');
103
+
104
+ for (const line of lines) {
105
+ if (!line.trim()) continue;
106
+ const event = parseEvent(line);
107
+ if (!event) continue;
108
+ if (Array.isArray(event)) {
109
+ events.push(...event);
110
+ } else {
111
+ events.push(event);
112
+ }
113
+ }
114
+
115
+ return events;
116
+ }
117
+
118
+ module.exports = {
119
+ parseEvent,
120
+ parseChunk,
121
+ parsePart,
122
+ };
@@ -30,16 +30,18 @@
30
30
  * @param {Number} depth - Current nesting depth (for recursion limit)
31
31
  * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
32
32
  */
33
+ const MAX_SUBCLUSTER_DEPTH = 5;
34
+
33
35
  function validateSubCluster(agentConfig, depth = 0) {
34
36
  const errors = [];
35
37
  const warnings = [];
36
38
 
37
39
  // 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 };
40
+ if (depth > MAX_SUBCLUSTER_DEPTH) {
41
+ errors.push(
42
+ `Sub-cluster '${agentConfig.id}' exceeds max nesting depth (${MAX_SUBCLUSTER_DEPTH})`
43
+ );
44
+ return buildValidationResult(errors, warnings);
43
45
  }
44
46
 
45
47
  // Validate required fields
@@ -47,19 +49,44 @@ function validateSubCluster(agentConfig, depth = 0) {
47
49
  errors.push(`Agent '${agentConfig.id}' must have type: 'subcluster'`);
48
50
  }
49
51
 
52
+ if (!validateSubClusterConfig(agentConfig, depth, errors, warnings)) {
53
+ return buildValidationResult(errors, warnings);
54
+ }
55
+
56
+ // Validate triggers (sub-cluster must have triggers to activate)
57
+ validateSubClusterTriggers(agentConfig, errors);
58
+
59
+ // Validate hooks structure
60
+ validateSubClusterHooks(agentConfig, errors);
61
+
62
+ // Check for context bridging configuration
63
+ validateContextStrategy(agentConfig, errors);
64
+
65
+ return buildValidationResult(errors, warnings);
66
+ }
67
+
68
+ function buildValidationResult(errors, warnings) {
69
+ return {
70
+ valid: errors.length === 0,
71
+ errors,
72
+ warnings,
73
+ };
74
+ }
75
+
76
+ function validateSubClusterConfig(agentConfig, depth, errors, warnings) {
50
77
  if (!agentConfig.config) {
51
78
  errors.push(`Sub-cluster '${agentConfig.id}' missing config field`);
52
- return { valid: false, errors, warnings };
79
+ return false;
53
80
  }
54
81
 
55
82
  if (!agentConfig.config.agents || !Array.isArray(agentConfig.config.agents)) {
56
83
  errors.push(`Sub-cluster '${agentConfig.id}' config.agents must be an array`);
57
- return { valid: false, errors, warnings };
84
+ return false;
58
85
  }
59
86
 
60
87
  if (agentConfig.config.agents.length === 0) {
61
88
  errors.push(`Sub-cluster '${agentConfig.id}' config.agents cannot be empty`);
62
- return { valid: false, errors, warnings };
89
+ return false;
63
90
  }
64
91
 
65
92
  // Recursively validate nested cluster config
@@ -71,46 +98,48 @@ function validateSubCluster(agentConfig, depth = 0) {
71
98
  }
72
99
 
73
100
  warnings.push(...childValidation.warnings.map((w) => `Sub-cluster '${agentConfig.id}': ${w}`));
101
+ return true;
102
+ }
74
103
 
75
- // Validate triggers (sub-cluster must have triggers to activate)
104
+ function validateSubClusterTriggers(agentConfig, errors) {
76
105
  if (!agentConfig.triggers || agentConfig.triggers.length === 0) {
77
106
  errors.push(`Sub-cluster '${agentConfig.id}' must have triggers to activate`);
78
107
  }
108
+ }
79
109
 
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
- }
110
+ function validateSubClusterHooks(agentConfig, errors) {
111
+ if (!agentConfig.hooks?.onComplete) {
112
+ return;
91
113
  }
92
114
 
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
- }
115
+ const hook = agentConfig.hooks.onComplete;
116
+ if (!hook.action) {
117
+ errors.push(`Sub-cluster '${agentConfig.id}' onComplete hook missing action`);
118
+ }
119
+ if (hook.action === 'publish_message' && !hook.config?.topic) {
120
+ errors.push(`Sub-cluster '${agentConfig.id}' onComplete hook missing config.topic`);
107
121
  }
122
+ }
108
123
 
109
- return {
110
- valid: errors.length === 0,
111
- errors,
112
- warnings,
113
- };
124
+ function validateContextStrategy(agentConfig, errors) {
125
+ const parentTopics = agentConfig.contextStrategy?.parentTopics;
126
+ if (!parentTopics) {
127
+ return;
128
+ }
129
+
130
+ if (!Array.isArray(parentTopics)) {
131
+ errors.push(`Sub-cluster '${agentConfig.id}' contextStrategy.parentTopics must be an array`);
132
+ return;
133
+ }
134
+
135
+ // Validate each parent topic is a string
136
+ for (const topic of parentTopics) {
137
+ if (typeof topic !== 'string') {
138
+ errors.push(
139
+ `Sub-cluster '${agentConfig.id}' parentTopics must contain strings, got ${typeof topic}`
140
+ );
141
+ }
142
+ }
114
143
  }
115
144
 
116
145
  /**
@@ -58,7 +58,7 @@ const AGENT_STATE = Object.freeze({
58
58
  // Agent states that indicate "active" work (from agent-lifecycle.js)
59
59
  // These should show in the footer with metrics
60
60
  const ACTIVE_STATES = new Set([
61
- AGENT_STATE.EXECUTING_TASK, // Actually running claude CLI
61
+ AGENT_STATE.EXECUTING_TASK, // Actually running claude CLI
62
62
  AGENT_STATE.EVALUATING_LOGIC, // Evaluating trigger conditions
63
63
  AGENT_STATE.BUILDING_CONTEXT, // Building context for task
64
64
  ]);
@@ -81,7 +81,8 @@ function debounce(fn, ms) {
81
81
  * @typedef {Object} AgentState
82
82
  * @property {string} id - Agent ID
83
83
  * @property {string} state - Agent state (idle, executing, etc.)
84
- * @property {number|null} pid - Process ID if running
84
+ * @property {number|null} processPid - Process ID if running (preferred)
85
+ * @property {number|null} pid - Legacy alias for processPid
85
86
  * @property {number} iteration - Current iteration
86
87
  */
87
88
 
@@ -175,7 +176,7 @@ class StatusFooter {
175
176
  if (this.printQueue.length === 0) return;
176
177
 
177
178
  // Write all queued output
178
- const output = this.printQueue.map(text => text + '\n').join('');
179
+ const output = this.printQueue.map((text) => text + '\n').join('');
179
180
  this.printQueue = [];
180
181
  process.stdout.write(output);
181
182
  }
@@ -396,8 +397,11 @@ class StatusFooter {
396
397
  * @param {AgentState} agentState
397
398
  */
398
399
  updateAgent(agentState) {
400
+ const processPid = agentState.processPid ?? agentState.pid ?? null;
399
401
  this.agents.set(agentState.id, {
400
402
  ...agentState,
403
+ processPid,
404
+ pid: processPid,
401
405
  lastUpdate: Date.now(),
402
406
  });
403
407
  }
@@ -418,11 +422,12 @@ class StatusFooter {
418
422
  */
419
423
  async _sampleMetrics() {
420
424
  for (const [agentId, agent] of this.agents) {
421
- if (!agent.pid) continue;
425
+ const processPid = agent.processPid ?? agent.pid;
426
+ if (!processPid) continue;
422
427
 
423
428
  try {
424
429
  // Get actual metrics with 200ms sample window (for CPU calculation)
425
- const raw = await getProcessMetrics(agent.pid, { samplePeriodMs: 200 });
430
+ const raw = await getProcessMetrics(processPid, { samplePeriodMs: 200 });
426
431
  const existing = this.interpolatedMetrics.get(agentId);
427
432
 
428
433
  this.interpolatedMetrics.set(agentId, {
@@ -458,14 +463,8 @@ class StatusFooter {
458
463
  if (!data.target || !data.current) continue;
459
464
 
460
465
  // Lerp CPU and RAM toward target (they fluctuate)
461
- data.current.cpuPercent = this._lerp(
462
- data.current.cpuPercent,
463
- data.target.cpuPercent
464
- );
465
- data.current.memoryMB = this._lerp(
466
- data.current.memoryMB,
467
- data.target.memoryMB
468
- );
466
+ data.current.cpuPercent = this._lerp(data.current.cpuPercent, data.target.cpuPercent);
467
+ data.current.memoryMB = this._lerp(data.current.memoryMB, data.target.memoryMB);
469
468
  // Network is cumulative counter - no interpolation, just use latest
470
469
  }
471
470
  }
@@ -767,60 +766,107 @@ class StatusFooter {
767
766
  // Border with corner
768
767
  parts.push(`${COLORS.gray}└─${COLORS.reset}`);
769
768
 
770
- // Cluster state
769
+ this.appendClusterStateSummary(parts);
770
+ this.appendDurationSummary(parts);
771
+ this.appendAgentCountSummary(parts);
772
+ this.appendTokenCostSummary(parts);
773
+ this.appendAggregateMetricsSummary(parts);
774
+
775
+ // Pad and close with bottom corner
776
+ return this.padSummaryLine(parts, width);
777
+ }
778
+
779
+ appendClusterStateSummary(parts) {
771
780
  const stateColor = this.clusterState === 'running' ? COLORS.green : COLORS.yellow;
772
781
  parts.push(` ${stateColor}${this.clusterState}${COLORS.reset}`);
782
+ }
773
783
 
774
- // Duration
784
+ appendDurationSummary(parts) {
775
785
  const duration = this.formatDuration(Date.now() - this.startTime);
776
786
  parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
787
+ }
777
788
 
778
- // Agent counts
779
- const executing = Array.from(this.agents.values()).filter((a) => ACTIVE_STATES.has(a.state)).length;
780
- const total = this.agents.size;
781
- parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
789
+ appendAgentCountSummary(parts) {
790
+ const { executing, total } = this.getActiveAgentCounts();
791
+ parts.push(
792
+ ` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`
793
+ );
794
+ }
782
795
 
783
- // Token cost (from message bus)
784
- if (this.messageBus && this.clusterId) {
785
- try {
786
- const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
787
- const totalCost = tokensByRole?._total?.totalCostUsd || 0;
788
- if (totalCost > 0) {
789
- // Format: $0.05 or $1.23 or $12.34
790
- const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
791
- parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
792
- }
793
- } catch {
794
- // Ignore errors - token tracking is optional
796
+ getActiveAgentCounts() {
797
+ const executing = Array.from(this.agents.values()).filter((a) =>
798
+ ACTIVE_STATES.has(a.state)
799
+ ).length;
800
+ return { executing, total: this.agents.size };
801
+ }
802
+
803
+ appendTokenCostSummary(parts) {
804
+ const costStr = this.getTokenCostSummary();
805
+ if (!costStr) {
806
+ return;
807
+ }
808
+ parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
809
+ }
810
+
811
+ getTokenCostSummary() {
812
+ if (!this.messageBus || !this.clusterId) {
813
+ return null;
814
+ }
815
+
816
+ try {
817
+ const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
818
+ const totalCost = tokensByRole?._total?.totalCostUsd || 0;
819
+ if (totalCost <= 0) {
820
+ return null;
795
821
  }
822
+
823
+ // Format: $0.05 or $1.23 or $12.34
824
+ return totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
825
+ } catch {
826
+ // Ignore errors - token tracking is optional
827
+ return null;
796
828
  }
829
+ }
797
830
 
798
- // Aggregate metrics (using interpolated values for smooth display)
831
+ appendAggregateMetricsSummary(parts) {
832
+ const { totalCpu, totalMem, totalBytesSent, totalBytesReceived } = this.getAggregateMetrics();
833
+
834
+ if (totalCpu <= 0 && totalMem <= 0) {
835
+ return;
836
+ }
837
+
838
+ parts.push(` ${COLORS.gray}│${COLORS.reset}`);
839
+ let aggregateStr = ` ${COLORS.cyan}Σ${COLORS.reset} `;
840
+ aggregateStr += `${COLORS.dim}CPU:${COLORS.reset}${totalCpu.toFixed(1)}%`;
841
+ aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(1)}MB`;
842
+ if (totalBytesSent > 0 || totalBytesReceived > 0) {
843
+ aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
844
+ }
845
+ parts.push(aggregateStr);
846
+ }
847
+
848
+ getAggregateMetrics() {
799
849
  let totalCpu = 0;
800
850
  let totalMem = 0;
801
851
  let totalBytesSent = 0;
802
852
  let totalBytesReceived = 0;
853
+
854
+ // Aggregate metrics (using interpolated values for smooth display)
803
855
  for (const data of this.interpolatedMetrics.values()) {
804
- if (data.exists && data.current) {
805
- totalCpu += data.current.cpuPercent;
806
- totalMem += data.current.memoryMB;
807
- totalBytesSent += data.network?.bytesSent || 0;
808
- totalBytesReceived += data.network?.bytesReceived || 0;
856
+ if (!data.exists || !data.current) {
857
+ continue;
809
858
  }
810
- }
811
859
 
812
- if (totalCpu > 0 || totalMem > 0) {
813
- parts.push(` ${COLORS.gray}│${COLORS.reset}`);
814
- let aggregateStr = ` ${COLORS.cyan}Σ${COLORS.reset} `;
815
- aggregateStr += `${COLORS.dim}CPU:${COLORS.reset}${totalCpu.toFixed(1)}%`;
816
- aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(1)}MB`;
817
- if (totalBytesSent > 0 || totalBytesReceived > 0) {
818
- aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
819
- }
820
- parts.push(aggregateStr);
860
+ totalCpu += data.current.cpuPercent;
861
+ totalMem += data.current.memoryMB;
862
+ totalBytesSent += data.network?.bytesSent || 0;
863
+ totalBytesReceived += data.network?.bytesReceived || 0;
821
864
  }
822
865
 
823
- // Pad and close with bottom corner
866
+ return { totalCpu, totalMem, totalBytesSent, totalBytesReceived };
867
+ }
868
+
869
+ padSummaryLine(parts, width) {
824
870
  const content = parts.join('');
825
871
  const contentLen = this.stripAnsi(content).length;
826
872
  const padding = Math.max(0, width - contentLen - 1);
@@ -36,6 +36,7 @@ class SubClusterWrapper {
36
36
  this.childClusterId = null;
37
37
 
38
38
  this.quiet = options.quiet || false;
39
+ this.modelOverride = options.modelOverride || null;
39
40
  }
40
41
 
41
42
  /**
@@ -285,47 +286,88 @@ class SubClusterWrapper {
285
286
  _buildChildContext(triggeringMessage) {
286
287
  const parentTopics = this.config.contextStrategy?.parentTopics || [];
287
288
 
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`;
289
+ const lines = [
290
+ '# Child Cluster Context',
291
+ '',
292
+ `Parent Cluster: ${this.parentCluster.id}`,
293
+ `SubCluster ID: ${this.id}`,
294
+ `Iteration: ${this.iteration}`,
295
+ '',
296
+ ];
292
297
 
293
- // Add messages from specified parent topics
294
- if (parentTopics.length > 0) {
295
- context += `## Parent Cluster Messages\n\n`;
298
+ this._appendParentTopicContext(lines, parentTopics);
299
+ this._appendTriggeringMessageContext(lines, triggeringMessage);
296
300
 
297
- for (const topic of parentTopics) {
298
- const messages = this.messageBus.query({
299
- cluster_id: this.parentCluster.id,
300
- topic,
301
- limit: 10,
302
- });
301
+ return lines.join('\n');
302
+ }
303
+
304
+ _appendParentTopicContext(lines, parentTopics) {
305
+ if (parentTopics.length === 0) {
306
+ return;
307
+ }
308
+
309
+ lines.push('## Parent Cluster Messages', '');
303
310
 
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
- }
311
+ for (const topic of parentTopics) {
312
+ const topicLines = this._buildTopicContextLines(topic);
313
+ if (topicLines.length === 0) {
314
+ continue;
317
315
  }
316
+
317
+ lines.push(...topicLines);
318
+ }
319
+ }
320
+
321
+ _buildTopicContextLines(topic) {
322
+ const messages = this.messageBus.query({
323
+ cluster_id: this.parentCluster.id,
324
+ topic,
325
+ limit: 10,
326
+ });
327
+
328
+ if (messages.length === 0) {
329
+ return [];
330
+ }
331
+
332
+ const lines = [`### Topic: ${topic}`, ''];
333
+
334
+ for (const message of messages) {
335
+ lines.push(...this._buildMessageContextLines(message));
336
+ lines.push('');
337
+ }
338
+
339
+ return lines;
340
+ }
341
+
342
+ _buildMessageContextLines(message) {
343
+ const lines = [`[${new Date(message.timestamp).toISOString()}] ${message.sender}:`];
344
+ const text = message.content?.text;
345
+ const data = message.content?.data;
346
+
347
+ if (text) {
348
+ lines.push(text);
318
349
  }
319
350
 
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`;
351
+ if (data) {
352
+ lines.push(`Data: ${JSON.stringify(data, null, 2)}`);
326
353
  }
327
354
 
328
- return context;
355
+ return lines;
356
+ }
357
+
358
+ _appendTriggeringMessageContext(lines, triggeringMessage) {
359
+ lines.push(
360
+ '',
361
+ '## Triggering Message',
362
+ '',
363
+ `Topic: ${triggeringMessage.topic}`,
364
+ `Sender: ${triggeringMessage.sender}`
365
+ );
366
+
367
+ const text = triggeringMessage.content?.text;
368
+ if (text) {
369
+ lines.push('', text);
370
+ }
329
371
  }
330
372
 
331
373
  /**
@@ -341,17 +383,31 @@ class SubClusterWrapper {
341
383
  this.childClusterId = childId;
342
384
 
343
385
  // Create child orchestrator with separate database
344
- const childOrchestrator = new Orchestrator({
386
+ const childOrchestrator = await Orchestrator.create({
345
387
  quiet: this.quiet,
346
388
  skipLoad: true,
347
389
  storageDir: path.join(this.parentCluster.ledger.dbPath, '..', 'subclusters', childId),
348
390
  });
349
391
 
392
+ const childConfig = JSON.parse(JSON.stringify(this.config.config));
393
+ const parentConfig = this.parentCluster?.config || {};
394
+
395
+ if (parentConfig.forceProvider) {
396
+ childConfig.forceProvider = parentConfig.forceProvider;
397
+ childConfig.defaultProvider = parentConfig.forceProvider;
398
+ if (parentConfig.forceLevel) {
399
+ childConfig.forceLevel = parentConfig.forceLevel;
400
+ childConfig.defaultLevel = parentConfig.forceLevel;
401
+ }
402
+ } else if (parentConfig.defaultProvider && !childConfig.defaultProvider) {
403
+ childConfig.defaultProvider = parentConfig.defaultProvider;
404
+ }
405
+
350
406
  // Start child cluster with text input (context from parent)
351
407
  const childCluster = await childOrchestrator.start(
352
- this.config.config, // Child cluster config
408
+ childConfig, // Child cluster config
353
409
  { text: context },
354
- { testMode: false }
410
+ { testMode: false, modelOverride: this.modelOverride || undefined }
355
411
  );
356
412
 
357
413
  // Create message bridge
@@ -1,18 +1,20 @@
1
1
  /**
2
- * TaskRunner - Strategy Pattern interface for executing Claude tasks
2
+ * TaskRunner - Strategy Pattern interface for executing provider tasks
3
3
  *
4
- * Implementations must provide a `run()` method that executes a Claude task
4
+ * Implementations must provide a `run()` method that executes a task
5
5
  * with the given context and options. Different runners can implement various
6
- * execution strategies (Claude CLI, mock responses, etc).
6
+ * execution strategies (CLI, mock responses, etc).
7
7
  */
8
8
  class TaskRunner {
9
9
  /**
10
- * Execute a Claude task with the given context and options
10
+ * Execute a provider task with the given context and options
11
11
  *
12
- * @param {string} _context - Full prompt/context for Claude to process
12
+ * @param {string} _context - Full prompt/context for the provider to process
13
13
  * @param {Object} _options - Execution options
14
14
  * @param {string} _options.agentId - Identifier for this agent/task
15
- * @param {string} _options.model - Model to use (e.g., 'opus', 'sonnet', 'haiku')
15
+ * @param {string} _options.model - Model to use (provider-specific model id)
16
+ * @param {string} [_options.provider] - Provider to use (claude|codex|gemini)
17
+ * @param {object} [_options.modelSpec] - Resolved model spec (level/model/reasoningEffort)
16
18
  * @param {string} [_options.outputFormat] - Output format ('text', 'json', 'stream-json')
17
19
  * @param {Object} [_options.jsonSchema] - JSON schema for structured output validation
18
20
  * @param {string} [_options.cwd] - Working directory for task execution