@covibes/zeroshot 5.3.0 → 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 (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -1,12 +1,14 @@
1
1
  const AnthropicProvider = require('./anthropic');
2
2
  const OpenAIProvider = require('./openai');
3
3
  const GoogleProvider = require('./google');
4
+ const OpencodeProvider = require('./opencode');
4
5
  const { normalizeProviderName } = require('../../lib/provider-names');
5
6
 
6
7
  const PROVIDERS = {
7
8
  claude: AnthropicProvider,
8
9
  codex: OpenAIProvider,
9
10
  gemini: GoogleProvider,
11
+ opencode: OpencodeProvider,
10
12
  };
11
13
 
12
14
  function getProvider(name) {
@@ -35,10 +37,24 @@ function listProviders() {
35
37
 
36
38
  function stripTimestampPrefix(line) {
37
39
  if (!line || typeof line !== 'string') return '';
38
- const trimmed = line.trim().replace(/\r$/, '');
40
+ let trimmed = line.trim().replace(/\r$/, '');
39
41
  if (!trimmed) return '';
40
- const match = trimmed.match(/^\[(\d{13})\](.*)$/);
41
- return match ? match[2] : trimmed;
42
+
43
+ const tsMatch = trimmed.match(/^\[(\d{13})\](.*)$/);
44
+ if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
45
+
46
+ // In cluster logs, lines are often prefixed like:
47
+ // "validator | {json...}"
48
+ // Strip the "<agent> | " prefix so provider parsers can JSON.parse the event.
49
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
50
+ const pipeMatch = trimmed.match(/^[^|]{1,40}\|\s*(.*)$/);
51
+ if (pipeMatch) {
52
+ const afterPipe = (pipeMatch[1] || '').trimStart();
53
+ if (afterPipe.startsWith('{') || afterPipe.startsWith('[')) return afterPipe;
54
+ }
55
+ }
56
+
57
+ return trimmed;
42
58
  }
43
59
 
44
60
  function parseChunkWithProvider(provider, chunk) {
@@ -3,10 +3,12 @@ const path = require('path');
3
3
  const os = require('os');
4
4
 
5
5
  /**
6
- * Codex structured outputs require additionalProperties: false on ALL object schemas.
7
- * This function recursively adds that constraint to ensure schema validation passes.
6
+ * OpenAI structured outputs have strict schema requirements:
7
+ * 1. additionalProperties: false on ALL object schemas
8
+ * 2. 'required' must include ALL keys in 'properties'
9
+ * This function recursively enforces both constraints.
8
10
  * @param {Object} schema - JSON Schema object
9
- * @returns {Object} - Modified schema with additionalProperties: false on all objects
11
+ * @returns {Object} - Modified schema compliant with OpenAI structured output requirements
10
12
  */
11
13
  function enforceStrictSchema(schema) {
12
14
  if (!schema || typeof schema !== 'object') return schema;
@@ -14,8 +16,12 @@ function enforceStrictSchema(schema) {
14
16
  const result = { ...schema };
15
17
 
16
18
  // Add additionalProperties: false to object types
19
+ // OpenAI also requires 'required' to include ALL property keys
17
20
  if (result.type === 'object') {
18
21
  result.additionalProperties = false;
22
+ if (result.properties) {
23
+ result.required = Object.keys(result.properties);
24
+ }
19
25
  }
20
26
 
21
27
  // Recurse into properties
@@ -72,6 +78,11 @@ function buildCommand(context, options = {}) {
72
78
  args.push('--dangerously-bypass-approvals-and-sandbox');
73
79
  }
74
80
 
81
+ // Always skip git repo check - zeroshot runs non-interactively and may run in any directory
82
+ if (cliFeatures.supportsSkipGitRepoCheck !== false) {
83
+ args.push('--skip-git-repo-check');
84
+ }
85
+
75
86
  // Augment context with schema if CLI doesn't support native --output-schema
76
87
  let finalContext = context;
77
88
  if (jsonSchema && cliFeatures.supportsOutputSchema) {
@@ -54,6 +54,7 @@ class OpenAIProvider extends BaseProvider {
54
54
  supportsCwd: unknown ? true : /\s-C\b/.test(help) || /--cwd\b/.test(help),
55
55
  supportsConfigOverride: unknown ? true : /--config\b/.test(help),
56
56
  supportsModel: unknown ? true : /\s-m\b/.test(help) || /--model\b/.test(help),
57
+ supportsSkipGitRepoCheck: unknown ? true : /--skip-git-repo-check\b/.test(help),
57
58
  unknown,
58
59
  };
59
60
 
@@ -6,24 +6,53 @@ function safeJsonParse(value, fallback) {
6
6
  }
7
7
  }
8
8
 
9
+ function parseAssistantMessage(item) {
10
+ const events = [];
11
+ const content = Array.isArray(item.content)
12
+ ? item.content
13
+ : [{ type: 'text', text: item.content }];
14
+ const text = content
15
+ .filter((c) => c.type === 'text')
16
+ .map((c) => c.text)
17
+ .join('');
18
+ const thinking = content
19
+ .filter((c) => c.type === 'thinking' || c.type === 'reasoning')
20
+ .map((c) => c.text)
21
+ .join('');
22
+ if (text) events.push({ type: 'text', text });
23
+ if (thinking) events.push({ type: 'thinking', text: thinking });
24
+ return events;
25
+ }
26
+
27
+ function parseFunctionCall(item) {
28
+ const toolId = item.call_id || item.id || item.tool_call_id || item.tool_id;
29
+ const args =
30
+ typeof item.arguments === 'string' ? safeJsonParse(item.arguments, {}) : item.arguments || {};
31
+ return {
32
+ type: 'tool_call',
33
+ toolName: item.name,
34
+ toolId,
35
+ input: args,
36
+ };
37
+ }
38
+
39
+ function parseFunctionCallOutput(item) {
40
+ const toolId = item.call_id || item.id || item.tool_call_id || item.tool_id;
41
+ const content = item.output ?? item.result ?? item.content ?? '';
42
+ return {
43
+ type: 'tool_result',
44
+ toolId,
45
+ content,
46
+ isError: !!item.error,
47
+ };
48
+ }
49
+
9
50
  function parseItem(item) {
10
51
  const events = [];
11
52
 
12
53
  // Handle assistant messages (Claude-style: type=message, role=assistant)
13
54
  if (item.type === 'message' && item.role === 'assistant') {
14
- const content = Array.isArray(item.content)
15
- ? item.content
16
- : [{ type: 'text', text: item.content }];
17
- const text = content
18
- .filter((c) => c.type === 'text')
19
- .map((c) => c.text)
20
- .join('');
21
- const thinking = content
22
- .filter((c) => c.type === 'thinking' || c.type === 'reasoning')
23
- .map((c) => c.text)
24
- .join('');
25
- if (text) events.push({ type: 'text', text });
26
- if (thinking) events.push({ type: 'thinking', text: thinking });
55
+ events.push(...parseAssistantMessage(item));
27
56
  }
28
57
 
29
58
  // Handle agent messages (Codex-style: type=agent_message, text=string)
@@ -32,26 +61,11 @@ function parseItem(item) {
32
61
  }
33
62
 
34
63
  if (item.type === 'function_call') {
35
- const toolId = item.call_id || item.id || item.tool_call_id || item.tool_id;
36
- const args =
37
- typeof item.arguments === 'string' ? safeJsonParse(item.arguments, {}) : item.arguments || {};
38
- events.push({
39
- type: 'tool_call',
40
- toolName: item.name,
41
- toolId,
42
- input: args,
43
- });
64
+ events.push(parseFunctionCall(item));
44
65
  }
45
66
 
46
67
  if (item.type === 'function_call_output') {
47
- const toolId = item.call_id || item.id || item.tool_call_id || item.tool_id;
48
- const content = item.output ?? item.result ?? item.content ?? '';
49
- events.push({
50
- type: 'tool_result',
51
- toolId,
52
- content,
53
- isError: !!item.error,
54
- });
68
+ events.push(parseFunctionCallOutput(item));
55
69
  }
56
70
 
57
71
  if (events.length === 1) return events[0];
@@ -0,0 +1,42 @@
1
+ function buildCommand(context, options = {}) {
2
+ const { modelSpec, outputFormat, jsonSchema, cwd, cliFeatures = {} } = options;
3
+
4
+ let finalContext = context;
5
+ if (jsonSchema) {
6
+ const schemaStr =
7
+ typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
8
+ finalContext =
9
+ context +
10
+ `\n\n## OUTPUT FORMAT (CRITICAL - REQUIRED)\n\nYou MUST respond with a JSON object that exactly matches this schema. NO markdown, NO explanation, NO code blocks. ONLY the raw JSON object.\n\nSchema:\n\`\`\`json\n${schemaStr}\n\`\`\`\n\nYour response must be ONLY valid JSON. Start with { and end with }. Nothing else.`;
11
+ }
12
+
13
+ const args = ['run'];
14
+
15
+ if ((outputFormat === 'stream-json' || outputFormat === 'json') && cliFeatures.supportsJson) {
16
+ args.push('--format', 'json');
17
+ }
18
+
19
+ if (modelSpec?.model) {
20
+ args.push('--model', modelSpec.model);
21
+ }
22
+
23
+ if (modelSpec?.reasoningEffort && cliFeatures.supportsVariant) {
24
+ args.push('--variant', modelSpec.reasoningEffort);
25
+ }
26
+
27
+ if (cwd && cliFeatures.supportsCwd) {
28
+ args.push('--cwd', cwd);
29
+ }
30
+
31
+ args.push(finalContext);
32
+
33
+ return {
34
+ binary: 'opencode',
35
+ args,
36
+ env: {},
37
+ };
38
+ }
39
+
40
+ module.exports = {
41
+ buildCommand,
42
+ };
@@ -0,0 +1,103 @@
1
+ const BaseProvider = require('../base-provider');
2
+ const { commandExists, getCommandPath, getHelpOutput } = require('../../../lib/provider-detection');
3
+ const { buildCommand } = require('./cli-builder');
4
+ const { parseEvent } = require('./output-parser');
5
+ const {
6
+ MODEL_CATALOG,
7
+ LEVEL_MAPPING,
8
+ DEFAULT_LEVEL,
9
+ DEFAULT_MAX_LEVEL,
10
+ DEFAULT_MIN_LEVEL,
11
+ } = require('./models');
12
+
13
+ const warned = new Set();
14
+
15
+ class OpencodeProvider extends BaseProvider {
16
+ constructor() {
17
+ super({ name: 'opencode', displayName: 'Opencode', cliCommand: 'opencode' });
18
+ this._cliFeatures = null;
19
+ }
20
+
21
+ isAvailable() {
22
+ return commandExists(this.cliCommand);
23
+ }
24
+
25
+ getCliPath() {
26
+ return getCommandPath(this.cliCommand) || this.cliCommand;
27
+ }
28
+
29
+ getInstallInstructions() {
30
+ return 'See https://opencode.ai for installation instructions.';
31
+ }
32
+
33
+ getAuthInstructions() {
34
+ return 'opencode auth login';
35
+ }
36
+
37
+ getCliFeatures() {
38
+ if (this._cliFeatures) return this._cliFeatures;
39
+ const help = getHelpOutput(this.cliCommand, ['run']);
40
+ const unknown = !help;
41
+
42
+ const features = {
43
+ supportsJson: unknown ? true : /--format\b/.test(help),
44
+ supportsModel: unknown ? true : /--model\b/.test(help),
45
+ supportsVariant: unknown ? true : /--variant\b/.test(help),
46
+ supportsCwd: unknown ? false : /--cwd\b/.test(help),
47
+ supportsAutoApprove: false,
48
+ unknown,
49
+ };
50
+
51
+ this._cliFeatures = features;
52
+ return features;
53
+ }
54
+
55
+ getCredentialPaths() {
56
+ return ['~/.local/share/opencode'];
57
+ }
58
+
59
+ buildCommand(context, options) {
60
+ const cliFeatures = options.cliFeatures || {};
61
+
62
+ if (options.modelSpec?.reasoningEffort && cliFeatures.supportsVariant === false) {
63
+ this._warnOnce(
64
+ 'opencode-variant',
65
+ 'Opencode CLI does not support --variant; skipping reasoningEffort.'
66
+ );
67
+ }
68
+
69
+ return buildCommand(context, { ...options, cliFeatures });
70
+ }
71
+
72
+ parseEvent(line) {
73
+ return parseEvent(line);
74
+ }
75
+
76
+ getModelCatalog() {
77
+ return MODEL_CATALOG;
78
+ }
79
+
80
+ getLevelMapping() {
81
+ return LEVEL_MAPPING;
82
+ }
83
+
84
+ getDefaultLevel() {
85
+ return DEFAULT_LEVEL;
86
+ }
87
+
88
+ getDefaultMaxLevel() {
89
+ return DEFAULT_MAX_LEVEL;
90
+ }
91
+
92
+ getDefaultMinLevel() {
93
+ return DEFAULT_MIN_LEVEL;
94
+ }
95
+
96
+ _warnOnce(key, message) {
97
+ if (warned.has(key)) return;
98
+ warned.add(key);
99
+ console.warn(`⚠️ ${message}`);
100
+ }
101
+ }
102
+
103
+ module.exports = OpencodeProvider;
@@ -0,0 +1,55 @@
1
+ const MODEL_CATALOG = {
2
+ 'opencode/big-pickle': { rank: 1 },
3
+ 'opencode/glm-4.7-free': { rank: 1 },
4
+ 'opencode/gpt-5-nano': { rank: 1 },
5
+ 'opencode/grok-code': { rank: 1 },
6
+ 'opencode/minimax-m2.1-free': { rank: 1 },
7
+ 'google/gemini-1.5-flash': { rank: 1 },
8
+ 'google/gemini-1.5-flash-8b': { rank: 1 },
9
+ 'google/gemini-1.5-pro': { rank: 1 },
10
+ 'google/gemini-2.0-flash': { rank: 1 },
11
+ 'google/gemini-2.0-flash-lite': { rank: 1 },
12
+ 'google/gemini-2.5-flash': { rank: 1 },
13
+ 'google/gemini-2.5-flash-image': { rank: 1 },
14
+ 'google/gemini-2.5-flash-image-preview': { rank: 1 },
15
+ 'google/gemini-2.5-flash-lite': { rank: 1 },
16
+ 'google/gemini-2.5-flash-lite-preview-06-17': { rank: 1 },
17
+ 'google/gemini-2.5-flash-lite-preview-09-2025': { rank: 1 },
18
+ 'google/gemini-2.5-flash-preview-04-17': { rank: 1 },
19
+ 'google/gemini-2.5-flash-preview-05-20': { rank: 1 },
20
+ 'google/gemini-2.5-flash-preview-09-2025': { rank: 1 },
21
+ 'google/gemini-2.5-flash-preview-tts': { rank: 1 },
22
+ 'google/gemini-2.5-pro': { rank: 1 },
23
+ 'google/gemini-2.5-pro-preview-05-06': { rank: 1 },
24
+ 'google/gemini-2.5-pro-preview-06-05': { rank: 1 },
25
+ 'google/gemini-2.5-pro-preview-tts': { rank: 1 },
26
+ 'google/gemini-3-flash-preview': { rank: 1 },
27
+ 'google/gemini-3-pro-preview': { rank: 1 },
28
+ 'google/gemini-embedding-001': { rank: 1 },
29
+ 'google/gemini-flash-latest': { rank: 1 },
30
+ 'google/gemini-flash-lite-latest': { rank: 1 },
31
+ 'google/gemini-live-2.5-flash': { rank: 1 },
32
+ 'google/gemini-live-2.5-flash-preview-native-audio': { rank: 1 },
33
+ 'openai/gpt-5.1-codex-max': { rank: 1 },
34
+ 'openai/gpt-5.1-codex-mini': { rank: 1 },
35
+ 'openai/gpt-5.2': { rank: 1 },
36
+ 'openai/gpt-5.2-codex': { rank: 1 },
37
+ };
38
+
39
+ const LEVEL_MAPPING = {
40
+ level1: { rank: 1, model: null, reasoningEffort: 'low' },
41
+ level2: { rank: 2, model: null, reasoningEffort: 'medium' },
42
+ level3: { rank: 3, model: null, reasoningEffort: 'high' },
43
+ };
44
+
45
+ const DEFAULT_LEVEL = 'level2';
46
+ const DEFAULT_MAX_LEVEL = 'level3';
47
+ const DEFAULT_MIN_LEVEL = 'level1';
48
+
49
+ module.exports = {
50
+ MODEL_CATALOG,
51
+ LEVEL_MAPPING,
52
+ DEFAULT_LEVEL,
53
+ DEFAULT_MAX_LEVEL,
54
+ DEFAULT_MIN_LEVEL,
55
+ };
@@ -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
  /**