@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
@@ -11,6 +11,11 @@
11
11
  * Run at config load time to fail fast before spawning agents.
12
12
  */
13
13
 
14
+ const { loadSettings } = require('../lib/settings');
15
+ const { VALID_PROVIDERS, normalizeProviderName } = require('../lib/provider-names');
16
+ const { getProvider } = require('./providers');
17
+ const { CAPABILITIES } = require('./providers/capabilities');
18
+
14
19
  /**
15
20
  * Check if config is a conductor-bootstrap style config
16
21
  * Conductor configs dynamically spawn agents via CLUSTER_OPERATIONS
@@ -37,6 +42,7 @@ function isConductorConfig(config) {
37
42
  function validateConfig(config, depth = 0) {
38
43
  const errors = [];
39
44
  const warnings = [];
45
+ const settings = loadSettings();
40
46
 
41
47
  // Max nesting depth check
42
48
  const MAX_DEPTH = 5;
@@ -99,6 +105,11 @@ function validateConfig(config, depth = 0) {
99
105
  errors.push(...configResult.errors);
100
106
  warnings.push(...configResult.warnings);
101
107
 
108
+ // === PHASE 10: Provider feature validation ===
109
+ const providerResult = validateProviderFeatures(config, settings);
110
+ errors.push(...providerResult.errors);
111
+ warnings.push(...providerResult.warnings);
112
+
102
113
  return {
103
114
  valid: errors.length === 0,
104
115
  errors,
@@ -106,6 +117,100 @@ function validateConfig(config, depth = 0) {
106
117
  };
107
118
  }
108
119
 
120
+ function validateAgentIdentity(agent, prefix, seenIds, errors) {
121
+ if (!agent.id) {
122
+ errors.push(`${prefix}.id is required`);
123
+ } else if (typeof agent.id !== 'string') {
124
+ errors.push(`${prefix}.id must be a string`);
125
+ } else if (seenIds.has(agent.id)) {
126
+ errors.push(`Duplicate agent id: "${agent.id}"`);
127
+ } else {
128
+ seenIds.add(agent.id);
129
+ }
130
+
131
+ if (!agent.role) {
132
+ errors.push(`${prefix}.role is required`);
133
+ }
134
+ }
135
+
136
+ function validateSubclusterAgent(agent, depth, errors, warnings) {
137
+ const subClusterSchema = require('./schemas/sub-cluster');
138
+ const subResult = subClusterSchema.validateSubCluster(agent, depth);
139
+ errors.push(...subResult.errors);
140
+ warnings.push(...subResult.warnings);
141
+ }
142
+
143
+ function validateTrigger(trigger, triggerPrefix, errors) {
144
+ if (!trigger.topic) {
145
+ errors.push(`${triggerPrefix}.topic is required`);
146
+ }
147
+
148
+ if (trigger.action && !['execute_task', 'stop_cluster'].includes(trigger.action)) {
149
+ errors.push(
150
+ `${triggerPrefix}.action must be 'execute_task' or 'stop_cluster', got '${trigger.action}'`
151
+ );
152
+ }
153
+
154
+ if (trigger.logic) {
155
+ if (!trigger.logic.script) {
156
+ errors.push(`${triggerPrefix}.logic.script is required when logic is specified`);
157
+ }
158
+ if (trigger.logic.engine && trigger.logic.engine !== 'javascript') {
159
+ errors.push(
160
+ `${triggerPrefix}.logic.engine must be 'javascript', got '${trigger.logic.engine}'`
161
+ );
162
+ }
163
+ }
164
+ }
165
+
166
+ function validateAgentTriggers(agent, prefix, errors) {
167
+ if (!agent.triggers || !Array.isArray(agent.triggers)) {
168
+ errors.push(`${prefix}.triggers array is required`);
169
+ return;
170
+ }
171
+
172
+ if (agent.triggers.length === 0) {
173
+ errors.push(`${prefix}.triggers cannot be empty (agent would never activate)`);
174
+ }
175
+
176
+ for (let j = 0; j < agent.triggers.length; j++) {
177
+ const trigger = agent.triggers[j];
178
+ const triggerPrefix = `${prefix}.triggers[${j}]`;
179
+ validateTrigger(trigger, triggerPrefix, errors);
180
+ }
181
+ }
182
+
183
+ function validateModelRule(rule, rulePrefix, errors) {
184
+ if (!rule.iterations) {
185
+ errors.push(`${rulePrefix}.iterations is required`);
186
+ } else if (!isValidIterationPattern(rule.iterations)) {
187
+ errors.push(
188
+ `${rulePrefix}.iterations '${rule.iterations}' is invalid. Valid: "1", "1-3", "5+", "all"`
189
+ );
190
+ }
191
+
192
+ if (!rule.model && !rule.modelLevel) {
193
+ errors.push(`${rulePrefix}.model or modelLevel is required`);
194
+ }
195
+ }
196
+
197
+ function validateAgentModelRules(agent, prefix, errors) {
198
+ if (!agent.modelRules) {
199
+ return;
200
+ }
201
+
202
+ if (!Array.isArray(agent.modelRules)) {
203
+ errors.push(`${prefix}.modelRules must be an array`);
204
+ return;
205
+ }
206
+
207
+ for (let j = 0; j < agent.modelRules.length; j++) {
208
+ const rule = agent.modelRules[j];
209
+ const rulePrefix = `${prefix}.modelRules[${j}]`;
210
+ validateModelRule(rule, rulePrefix, errors);
211
+ }
212
+ }
213
+
109
214
  /**
110
215
  * Phase 1: Validate basic structure (fields, types, duplicates)
111
216
  */
@@ -132,95 +237,21 @@ function validateBasicStructure(config, depth = 0) {
132
237
  // Check if this is a subcluster
133
238
  const isSubCluster = agent.type === 'subcluster';
134
239
 
135
- // Required fields
136
- if (!agent.id) {
137
- errors.push(`${prefix}.id is required`);
138
- } else if (typeof agent.id !== 'string') {
139
- errors.push(`${prefix}.id must be a string`);
140
- } else if (seenIds.has(agent.id)) {
141
- errors.push(`Duplicate agent id: "${agent.id}"`);
142
- } else {
143
- seenIds.add(agent.id);
144
- }
145
-
146
- if (!agent.role) {
147
- errors.push(`${prefix}.role is required`);
148
- }
240
+ validateAgentIdentity(agent, prefix, seenIds, errors);
149
241
 
150
242
  // Validate subclusters
151
243
  if (isSubCluster) {
152
- const subClusterSchema = require('./schemas/sub-cluster');
153
- const subResult = subClusterSchema.validateSubCluster(agent, depth);
154
- errors.push(...subResult.errors);
155
- warnings.push(...subResult.warnings);
244
+ validateSubclusterAgent(agent, depth, errors, warnings);
156
245
  continue; // Skip regular agent validation
157
246
  }
158
247
 
159
248
  // Regular agent validation
160
- if (!agent.triggers || !Array.isArray(agent.triggers)) {
161
- errors.push(`${prefix}.triggers array is required`);
162
- } else if (agent.triggers.length === 0) {
163
- errors.push(`${prefix}.triggers cannot be empty (agent would never activate)`);
164
- }
165
-
166
- // Validate triggers structure
167
- if (agent.triggers) {
168
- for (let j = 0; j < agent.triggers.length; j++) {
169
- const trigger = agent.triggers[j];
170
- const triggerPrefix = `${prefix}.triggers[${j}]`;
171
-
172
- if (!trigger.topic) {
173
- errors.push(`${triggerPrefix}.topic is required`);
174
- }
175
-
176
- if (trigger.action && !['execute_task', 'stop_cluster'].includes(trigger.action)) {
177
- errors.push(
178
- `${triggerPrefix}.action must be 'execute_task' or 'stop_cluster', got '${trigger.action}'`
179
- );
180
- }
181
-
182
- if (trigger.logic) {
183
- if (!trigger.logic.script) {
184
- errors.push(`${triggerPrefix}.logic.script is required when logic is specified`);
185
- }
186
- if (trigger.logic.engine && trigger.logic.engine !== 'javascript') {
187
- errors.push(
188
- `${triggerPrefix}.logic.engine must be 'javascript', got '${trigger.logic.engine}'`
189
- );
190
- }
191
- }
192
- }
193
- }
249
+ validateAgentTriggers(agent, prefix, errors);
194
250
 
195
251
  // Validate model rules if present
196
- if (agent.modelRules) {
197
- if (!Array.isArray(agent.modelRules)) {
198
- errors.push(`${prefix}.modelRules must be an array`);
199
- } else {
200
- for (let j = 0; j < agent.modelRules.length; j++) {
201
- const rule = agent.modelRules[j];
202
- const rulePrefix = `${prefix}.modelRules[${j}]`;
203
-
204
- if (!rule.iterations) {
205
- errors.push(`${rulePrefix}.iterations is required`);
206
- } else if (!isValidIterationPattern(rule.iterations)) {
207
- errors.push(
208
- `${rulePrefix}.iterations '${rule.iterations}' is invalid. Valid: "1", "1-3", "5+", "all"`
209
- );
210
- }
211
-
212
- if (!rule.model) {
213
- errors.push(`${rulePrefix}.model is required`);
214
- } else if (!['opus', 'sonnet', 'haiku'].includes(rule.model)) {
215
- errors.push(
216
- `${rulePrefix}.model must be 'opus', 'sonnet', or 'haiku', got '${rule.model}'`
217
- );
218
- }
219
- }
252
+ validateAgentModelRules(agent, prefix, errors);
220
253
 
221
- // Note: Detailed coverage gap checking (iteration ranges) is done in Phase 7
222
- }
223
- }
254
+ // Note: Detailed coverage gap checking (iteration ranges) is done in Phase 7
224
255
  }
225
256
 
226
257
  return { errors, warnings };
@@ -229,45 +260,77 @@ function validateBasicStructure(config, depth = 0) {
229
260
  /**
230
261
  * Phase 2: Analyze message flow for structural problems
231
262
  */
232
- function analyzeMessageFlow(config) {
233
- const errors = [];
234
- const warnings = [];
263
+ function ensureTopicList(map, topic) {
264
+ if (!map.has(topic)) {
265
+ map.set(topic, []);
266
+ }
267
+ return map.get(topic);
268
+ }
269
+
270
+ function recordAgentTriggers(agent, topicConsumers, agentInputTopics) {
271
+ for (const trigger of agent.triggers || []) {
272
+ const topic = trigger.topic;
273
+ ensureTopicList(topicConsumers, topic).push(agent.id);
274
+ agentInputTopics.get(agent.id).push(topic);
275
+ }
276
+ }
277
+
278
+ function recordAgentOutputs(agent, topicProducers, agentOutputTopics) {
279
+ const outputTopic = agent.hooks?.onComplete?.config?.topic;
280
+ if (outputTopic) {
281
+ ensureTopicList(topicProducers, outputTopic).push(agent.id);
282
+ agentOutputTopics.get(agent.id).push(outputTopic);
283
+ }
284
+
285
+ const hookLogicScript = agent.hooks?.onComplete?.logic?.script;
286
+ if (!hookLogicScript || typeof hookLogicScript !== 'string') {
287
+ return;
288
+ }
289
+
290
+ const topicMatches = hookLogicScript.match(/topic:\s*['"]([A-Z_]+)['"]/g) || [];
291
+ for (const match of topicMatches) {
292
+ const dynamicTopic = match.match(/['"]([A-Z_]+)['"]/)?.[1];
293
+ if (!dynamicTopic || dynamicTopic === outputTopic) {
294
+ continue;
295
+ }
296
+
297
+ const producers = ensureTopicList(topicProducers, dynamicTopic);
298
+ if (!producers.includes(agent.id)) {
299
+ producers.push(`${agent.id}*`);
300
+ }
235
301
 
236
- // Build topic graph
237
- const topicProducers = new Map(); // topic -> [agentIds that produce it]
238
- const topicConsumers = new Map(); // topic -> [agentIds that consume it]
239
- const agentOutputTopics = new Map(); // agentId -> [topics it produces]
240
- const agentInputTopics = new Map(); // agentId -> [topics it consumes]
302
+ const outputs = agentOutputTopics.get(agent.id);
303
+ if (!outputs.includes(dynamicTopic)) {
304
+ outputs.push(dynamicTopic);
305
+ }
306
+ }
307
+ }
308
+
309
+ function buildMessageFlowGraph(config) {
310
+ const topicProducers = new Map();
311
+ const topicConsumers = new Map();
312
+ const agentOutputTopics = new Map();
313
+ const agentInputTopics = new Map();
241
314
 
242
- // System always produces ISSUE_OPENED
243
315
  topicProducers.set('ISSUE_OPENED', ['system']);
244
316
 
245
317
  for (const agent of config.agents) {
246
318
  agentInputTopics.set(agent.id, []);
247
319
  agentOutputTopics.set(agent.id, []);
248
320
 
249
- // Track what topics this agent consumes (triggers)
250
- for (const trigger of agent.triggers || []) {
251
- const topic = trigger.topic;
252
- if (!topicConsumers.has(topic)) {
253
- topicConsumers.set(topic, []);
254
- }
255
- topicConsumers.get(topic).push(agent.id);
256
- agentInputTopics.get(agent.id).push(topic);
257
- }
258
-
259
- // Track what topics this agent produces (hooks)
260
- const outputTopic = agent.hooks?.onComplete?.config?.topic;
261
- if (outputTopic) {
262
- if (!topicProducers.has(outputTopic)) {
263
- topicProducers.set(outputTopic, []);
264
- }
265
- topicProducers.get(outputTopic).push(agent.id);
266
- agentOutputTopics.get(agent.id).push(outputTopic);
267
- }
321
+ recordAgentTriggers(agent, topicConsumers, agentInputTopics);
322
+ recordAgentOutputs(agent, topicProducers, agentOutputTopics);
268
323
  }
269
324
 
270
- // === CHECK 1: No bootstrap trigger ===
325
+ return {
326
+ topicProducers,
327
+ topicConsumers,
328
+ agentOutputTopics,
329
+ agentInputTopics,
330
+ };
331
+ }
332
+
333
+ function reportMissingBootstrap(topicConsumers, errors) {
271
334
  const issueOpenedConsumers = topicConsumers.get('ISSUE_OPENED') || [];
272
335
  if (issueOpenedConsumers.length === 0) {
273
336
  errors.push(
@@ -275,8 +338,9 @@ function analyzeMessageFlow(config) {
275
338
  'Add a trigger: { "topic": "ISSUE_OPENED", "action": "execute_task" }'
276
339
  );
277
340
  }
341
+ }
278
342
 
279
- // === CHECK 2: No completion handler ===
343
+ function reportCompletionHandlers(config, errors, warnings) {
280
344
  const completionHandlers = config.agents.filter(
281
345
  (a) =>
282
346
  a.triggers?.some((t) => t.action === 'stop_cluster') ||
@@ -284,22 +348,31 @@ function analyzeMessageFlow(config) {
284
348
  a.id === 'git-pusher' ||
285
349
  a.hooks?.onComplete?.config?.topic === 'CLUSTER_COMPLETE'
286
350
  );
351
+ const isTemplateConfig = config.params && Object.keys(config.params).length > 0;
287
352
 
288
353
  if (completionHandlers.length === 0) {
289
- errors.push(
354
+ const message =
290
355
  'No completion handler found. Cluster will run until idle timeout (2 min). ' +
291
- 'Add an agent with trigger action: "stop_cluster"'
292
- );
356
+ 'Add an agent with trigger action: "stop_cluster"';
357
+ if (isTemplateConfig) {
358
+ warnings.push(`${message} (template will rely on orchestrator injection)`);
359
+ } else {
360
+ errors.push(message);
361
+ }
293
362
  } else if (completionHandlers.length > 1) {
294
363
  errors.push(
295
364
  `Multiple completion handlers: [${completionHandlers.map((a) => a.id).join(', ')}]. ` +
296
365
  'This causes race conditions. Keep only one.'
297
366
  );
298
367
  }
368
+ }
299
369
 
300
- // === CHECK 3: Orphan topics (produced but never consumed) ===
370
+ function reportOrphanTopics(topicProducers, topicConsumers, warnings) {
301
371
  for (const [topic, producers] of topicProducers) {
302
- if (topic === 'CLUSTER_COMPLETE') continue; // System handles this
372
+ if (topic === 'CLUSTER_COMPLETE') {
373
+ continue;
374
+ }
375
+
303
376
  const consumers = topicConsumers.get(topic) || [];
304
377
  if (consumers.length === 0) {
305
378
  warnings.push(
@@ -307,11 +380,17 @@ function analyzeMessageFlow(config) {
307
380
  );
308
381
  }
309
382
  }
383
+ }
310
384
 
311
- // === CHECK 4: Waiting for topics that are never produced ===
385
+ function reportUnproducedTopics(topicConsumers, topicProducers, errors) {
312
386
  for (const [topic, consumers] of topicConsumers) {
313
- if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue; // System produces
314
- if (topic.endsWith('*')) continue; // Wildcard pattern
387
+ if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') {
388
+ continue;
389
+ }
390
+ if (topic.endsWith('*')) {
391
+ continue;
392
+ }
393
+
315
394
  const producers = topicProducers.get(topic) || [];
316
395
  if (producers.length === 0) {
317
396
  errors.push(
@@ -320,35 +399,45 @@ function analyzeMessageFlow(config) {
320
399
  );
321
400
  }
322
401
  }
402
+ }
323
403
 
324
- // === CHECK 5: Self-triggering agents (instant infinite loop) ===
404
+ function reportSelfTriggeringAgents(config, agentInputTopics, agentOutputTopics, errors) {
325
405
  for (const agent of config.agents) {
326
406
  const inputs = agentInputTopics.get(agent.id) || [];
327
407
  const outputs = agentOutputTopics.get(agent.id) || [];
328
408
  const selfTrigger = inputs.find((t) => outputs.includes(t));
329
- if (selfTrigger) {
409
+ if (!selfTrigger) {
410
+ continue;
411
+ }
412
+
413
+ const triggerHasLogic = agent.triggers?.some((t) => t.topic === selfTrigger && t.logic?.script);
414
+ const hookHasLogic = agent.hooks?.onComplete?.logic?.script;
415
+
416
+ if (!triggerHasLogic && !hookHasLogic) {
330
417
  errors.push(
331
418
  `Agent '${agent.id}' triggers on '${selfTrigger}' and produces '${selfTrigger}'. ` +
332
419
  'Instant infinite loop.'
333
420
  );
334
421
  }
335
422
  }
423
+ }
336
424
 
337
- // === CHECK 6: Two-agent circular dependency ===
425
+ function reportTwoAgentCycles(config, agentInputTopics, agentOutputTopics, warnings) {
338
426
  for (const agentA of config.agents) {
339
427
  const outputsA = agentOutputTopics.get(agentA.id) || [];
340
428
  for (const agentB of config.agents) {
341
- if (agentA.id === agentB.id) continue;
429
+ if (agentA.id === agentB.id) {
430
+ continue;
431
+ }
432
+
342
433
  const inputsB = agentInputTopics.get(agentB.id) || [];
343
434
  const outputsB = agentOutputTopics.get(agentB.id) || [];
344
435
  const inputsA = agentInputTopics.get(agentA.id) || [];
345
436
 
346
- // A produces what B consumes, AND B produces what A consumes
347
437
  const aToB = outputsA.some((t) => inputsB.includes(t));
348
438
  const bToA = outputsB.some((t) => inputsA.includes(t));
349
439
 
350
440
  if (aToB && bToA) {
351
- // This might be intentional (rejection loop), check if there's an escape
352
441
  const hasEscapeLogic =
353
442
  agentA.triggers?.some((t) => t.logic) || agentB.triggers?.some((t) => t.logic);
354
443
  if (!hasEscapeLogic) {
@@ -360,35 +449,45 @@ function analyzeMessageFlow(config) {
360
449
  }
361
450
  }
362
451
  }
452
+ }
363
453
 
364
- // === CHECK 7: Validator without worker re-trigger ===
454
+ function reportMissingValidationTriggers(config, errors) {
365
455
  const validators = config.agents.filter((a) => a.role === 'validator');
366
456
  const workers = config.agents.filter((a) => a.role === 'implementation');
367
457
 
368
- if (validators.length > 0 && workers.length > 0) {
369
- for (const worker of workers) {
370
- const triggersOnValidation = worker.triggers?.some(
371
- (t) => t.topic === 'VALIDATION_RESULT' || t.topic.includes('VALIDATION')
458
+ if (validators.length === 0 || workers.length === 0) {
459
+ return;
460
+ }
461
+
462
+ for (const worker of workers) {
463
+ const triggersOnValidation = worker.triggers?.some(
464
+ (t) => t.topic === 'VALIDATION_RESULT' || t.topic.includes('VALIDATION')
465
+ );
466
+ if (!triggersOnValidation) {
467
+ errors.push(
468
+ `Worker '${worker.id}' has validators but doesn't trigger on VALIDATION_RESULT. ` +
469
+ 'Rejections will be ignored. Add trigger: { "topic": "VALIDATION_RESULT", "logic": {...} }'
372
470
  );
373
- if (!triggersOnValidation) {
374
- errors.push(
375
- `Worker '${worker.id}' has validators but doesn't trigger on VALIDATION_RESULT. ` +
376
- 'Rejections will be ignored. Add trigger: { "topic": "VALIDATION_RESULT", "logic": {...} }'
377
- );
378
- }
379
471
  }
380
472
  }
473
+ }
381
474
 
382
- // === CHECK 8: Context strategy missing trigger topics ===
475
+ function reportMissingContextTopics(config, warnings) {
383
476
  for (const agent of config.agents) {
384
- if (!agent.contextStrategy?.sources) continue;
477
+ if (!agent.contextStrategy?.sources) {
478
+ continue;
479
+ }
385
480
 
386
481
  const triggerTopics = (agent.triggers || []).map((t) => t.topic);
387
482
  const contextTopics = agent.contextStrategy.sources.map((s) => s.topic);
388
483
 
389
484
  for (const triggerTopic of triggerTopics) {
390
- if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED') continue;
391
- if (triggerTopic.endsWith('*')) continue;
485
+ if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED') {
486
+ continue;
487
+ }
488
+ if (triggerTopic.endsWith('*')) {
489
+ continue;
490
+ }
392
491
 
393
492
  if (!contextTopics.includes(triggerTopic)) {
394
493
  warnings.push(
@@ -398,6 +497,23 @@ function analyzeMessageFlow(config) {
398
497
  }
399
498
  }
400
499
  }
500
+ }
501
+
502
+ function analyzeMessageFlow(config) {
503
+ const errors = [];
504
+ const warnings = [];
505
+
506
+ const { topicProducers, topicConsumers, agentOutputTopics, agentInputTopics } =
507
+ buildMessageFlowGraph(config);
508
+
509
+ reportMissingBootstrap(topicConsumers, errors);
510
+ reportCompletionHandlers(config, errors, warnings);
511
+ reportOrphanTopics(topicProducers, topicConsumers, warnings);
512
+ reportUnproducedTopics(topicConsumers, topicProducers, errors);
513
+ reportSelfTriggeringAgents(config, agentInputTopics, agentOutputTopics, errors);
514
+ reportTwoAgentCycles(config, agentInputTopics, agentOutputTopics, warnings);
515
+ reportMissingValidationTriggers(config, errors);
516
+ reportMissingContextTopics(config, warnings);
401
517
 
402
518
  return { errors, warnings };
403
519
  }
@@ -405,93 +521,135 @@ function analyzeMessageFlow(config) {
405
521
  /**
406
522
  * Phase 3: Validate agent-specific configurations
407
523
  */
408
- function validateAgents(config) {
409
- const errors = [];
410
- const warnings = [];
524
+ function recordAgentRole(roles, agent) {
525
+ if (!roles.has(agent.role)) {
526
+ roles.set(agent.role, []);
527
+ }
528
+ roles.get(agent.role).push(agent.id);
529
+ }
411
530
 
412
- const roles = new Map(); // role -> [agentIds]
531
+ function agentExecutesTask(agent) {
532
+ return agent.triggers?.some((t) => t.action === 'execute_task' || (!t.action && !t.logic));
533
+ }
413
534
 
414
- for (const agent of config.agents) {
415
- // Track roles
416
- if (!roles.has(agent.role)) {
417
- roles.set(agent.role, []);
535
+ function validateOrchestratorTriggers(agent, warnings) {
536
+ if (agent.role !== 'orchestrator') {
537
+ return;
538
+ }
539
+
540
+ if (agentExecutesTask(agent)) {
541
+ warnings.push(
542
+ `Orchestrator '${agent.id}' has execute_task triggers. ` +
543
+ 'Orchestrators typically use action: "stop_cluster". This may waste API calls.'
544
+ );
545
+ }
546
+ }
547
+
548
+ function validateValidatorGitUsage(agent, errors) {
549
+ if (agent.role !== 'validator') {
550
+ return;
551
+ }
552
+
553
+ const prompt = typeof agent.prompt === 'string' ? agent.prompt : agent.prompt?.system;
554
+ const gitPatterns = ['git diff', 'git status', 'git log', 'git show'];
555
+ for (const pattern of gitPatterns) {
556
+ if (prompt?.includes(pattern)) {
557
+ errors.push(`Validator '${agent.id}' uses '${pattern}' - git state is unreliable in agents`);
418
558
  }
419
- roles.get(agent.role).push(agent.id);
559
+ }
560
+ }
420
561
 
421
- // Orchestrator should not execute tasks
422
- if (agent.role === 'orchestrator') {
423
- const executesTask = agent.triggers?.some(
424
- (t) => t.action === 'execute_task' || (!t.action && !t.logic)
425
- );
426
- if (executesTask) {
427
- warnings.push(
428
- `Orchestrator '${agent.id}' has execute_task triggers. ` +
429
- 'Orchestrators typically use action: "stop_cluster". This may waste API calls.'
430
- );
562
+ function validateJsonOutputSchema(agent, warnings) {
563
+ if (agent.outputFormat === 'json' && !agent.jsonSchema) {
564
+ warnings.push(
565
+ `Agent '${agent.id}' has outputFormat: 'json' but no jsonSchema. ` +
566
+ 'Output parsing may be unreliable.'
567
+ );
568
+ }
569
+ }
570
+
571
+ function validateMaxIterations(agent, warnings) {
572
+ if (agent.maxIterations && agent.maxIterations > 50) {
573
+ warnings.push(
574
+ `Agent '${agent.id}' has maxIterations: ${agent.maxIterations}. ` +
575
+ 'This may consume significant API credits if stuck in a loop.'
576
+ );
577
+ }
578
+ }
579
+
580
+ function validateImplementationIterations(agent, warnings) {
581
+ if (agent.role === 'implementation' && !agent.maxIterations) {
582
+ warnings.push(
583
+ `Implementation agent '${agent.id}' has no maxIterations. ` +
584
+ 'Defaults to 30, but consider setting explicitly.'
585
+ );
586
+ }
587
+ }
588
+
589
+ function validateModelSpec(agent, errors) {
590
+ if (agent.model) {
591
+ errors.push(
592
+ `Agent '${agent.id}' uses 'model: "${agent.model}"'. ` +
593
+ `Use 'modelLevel: "level1|level2|level3"' instead for provider-agnostic model selection.`
594
+ );
595
+ }
596
+ }
597
+
598
+ function findRoleReferences(script) {
599
+ const matches = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
600
+ if (!matches) {
601
+ return [];
602
+ }
603
+
604
+ return matches.map((match) => match.match(/['"](\w+)['"]/)[1]);
605
+ }
606
+
607
+ function warnMissingRoleReferences(agents, roles, warnings) {
608
+ for (const agent of agents) {
609
+ for (const trigger of agent.triggers || []) {
610
+ if (!trigger.logic?.script) {
611
+ continue;
612
+ }
613
+
614
+ const script = trigger.logic.script;
615
+ const rolesReferenced = findRoleReferences(script);
616
+ if (rolesReferenced.length === 0) {
617
+ continue;
431
618
  }
432
- }
433
619
 
434
- // Check for git operations in validator prompts (unreliable in agents)
435
- if (agent.role === 'validator') {
436
- const prompt = typeof agent.prompt === 'string' ? agent.prompt : agent.prompt?.system;
437
- const gitPatterns = ['git diff', 'git status', 'git log', 'git show'];
438
- for (const pattern of gitPatterns) {
439
- if (prompt?.includes(pattern)) {
440
- errors.push(
441
- `Validator '${agent.id}' uses '${pattern}' - git state is unreliable in agents`
620
+ for (const role of rolesReferenced) {
621
+ if (!roles.has(role)) {
622
+ warnings.push(
623
+ `Agent '${agent.id}' logic references role '${role}' but no agent has that role. ` +
624
+ `Trigger may be a no-op. Available roles: [${Array.from(roles.keys()).join(', ')}]`
442
625
  );
443
626
  }
444
627
  }
445
628
  }
629
+ }
630
+ }
446
631
 
447
- // JSON output without schema
448
- if (agent.outputFormat === 'json' && !agent.jsonSchema) {
449
- warnings.push(
450
- `Agent '${agent.id}' has outputFormat: 'json' but no jsonSchema. ` +
451
- 'Output parsing may be unreliable.'
452
- );
453
- }
632
+ function validateAgents(config) {
633
+ const errors = [];
634
+ const warnings = [];
454
635
 
455
- // Very high maxIterations
456
- if (agent.maxIterations && agent.maxIterations > 50) {
457
- warnings.push(
458
- `Agent '${agent.id}' has maxIterations: ${agent.maxIterations}. ` +
459
- 'This may consume significant API credits if stuck in a loop.'
460
- );
461
- }
636
+ const roles = new Map(); // role -> [agentIds]
462
637
 
463
- // No maxIterations on implementation agent (unbounded retries)
464
- if (agent.role === 'implementation' && !agent.maxIterations) {
465
- warnings.push(
466
- `Implementation agent '${agent.id}' has no maxIterations. ` +
467
- 'Defaults to 30, but consider setting explicitly.'
468
- );
469
- }
638
+ for (const agent of config.agents) {
639
+ recordAgentRole(roles, agent);
640
+ validateOrchestratorTriggers(agent, warnings);
641
+ validateValidatorGitUsage(agent, errors);
642
+ validateJsonOutputSchema(agent, warnings);
643
+ validateMaxIterations(agent, warnings);
644
+ validateImplementationIterations(agent, warnings);
645
+ validateModelSpec(agent, errors);
470
646
  }
471
647
 
472
648
  // Check for role references in logic scripts
473
649
  // IMPORTANT: Changed from error to warning because some triggers are designed to be
474
650
  // no-ops when the referenced role doesn't exist (e.g., worker's VALIDATION_RESULT
475
651
  // trigger returns false when validators.length === 0)
476
- for (const agent of config.agents) {
477
- for (const trigger of agent.triggers || []) {
478
- if (trigger.logic?.script) {
479
- const script = trigger.logic.script;
480
- const roleMatch = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
481
- if (roleMatch) {
482
- for (const match of roleMatch) {
483
- const role = match.match(/['"](\w+)['"]/)[1];
484
- if (!roles.has(role)) {
485
- warnings.push(
486
- `Agent '${agent.id}' logic references role '${role}' but no agent has that role. ` +
487
- `Trigger may be a no-op. Available roles: [${Array.from(roles.keys()).join(', ')}]`
488
- );
489
- }
490
- }
491
- }
492
- }
493
- }
494
- }
652
+ warnMissingRoleReferences(config.agents, roles, warnings);
495
653
 
496
654
  return { errors, warnings };
497
655
  }
@@ -819,6 +977,93 @@ function formatValidationResult(result) {
819
977
  * @param {Object} config - Cluster configuration
820
978
  * @returns {{ errors: string[], warnings: string[] }}
821
979
  */
980
+ function validateHookAction(hook, prefix, errors) {
981
+ if (!hook.action) {
982
+ errors.push(
983
+ `[Gap 1] ${prefix}: Missing 'action' field. ` +
984
+ `Fix: Add "action": "publish_message" or "action": "execute_system_command"`
985
+ );
986
+ }
987
+ }
988
+
989
+ function validateTransformScript(hook, prefix, errors) {
990
+ if (!hook.transform?.script) {
991
+ return;
992
+ }
993
+
994
+ const script = hook.transform.script;
995
+ const hasReturnTopic = /return\s*\{[^}]*topic\s*:/i.test(script);
996
+ const hasReturnContent = /return\s*\{[^}]*content\s*:/i.test(script);
997
+
998
+ if (!hasReturnTopic) {
999
+ errors.push(
1000
+ `[Gap 2] ${prefix}: Transform script must return object with 'topic' property. ` +
1001
+ `Fix: return { topic: "TOPIC_NAME", content: {...} }`
1002
+ );
1003
+ }
1004
+
1005
+ if (!hasReturnContent) {
1006
+ errors.push(
1007
+ `[Gap 2] ${prefix}: Transform script must return object with 'content' property. ` +
1008
+ `Fix: return { topic: "...", content: { data: result } }`
1009
+ );
1010
+ }
1011
+ }
1012
+
1013
+ function validateConductorOperations(agent, hook, prefix, errors) {
1014
+ const targetsClusterOperations =
1015
+ hook.config?.topic === 'CLUSTER_OPERATIONS' ||
1016
+ hook.transform?.script?.includes('CLUSTER_OPERATIONS');
1017
+
1018
+ if (agent.role !== 'conductor' || !targetsClusterOperations) {
1019
+ return;
1020
+ }
1021
+
1022
+ if (!hook.transform?.script) {
1023
+ return;
1024
+ }
1025
+
1026
+ const script = hook.transform.script;
1027
+ const hasOperations = /operations\s*:/i.test(script);
1028
+ if (!hasOperations) {
1029
+ errors.push(
1030
+ `[Gap 7] ${prefix}: CLUSTER_OPERATIONS message must include 'operations' field. ` +
1031
+ `Fix: return { topic: "CLUSTER_OPERATIONS", content: { data: { operations: JSON.stringify([...]) } } }`
1032
+ );
1033
+ }
1034
+ }
1035
+
1036
+ function validateHookLogic(hook, prefix, errors) {
1037
+ if (!hook.logic) {
1038
+ return;
1039
+ }
1040
+
1041
+ if (hook.logic.engine && hook.logic.engine !== 'javascript') {
1042
+ errors.push(`${prefix}: Hook logic engine must be 'javascript', got: '${hook.logic.engine}'`);
1043
+ }
1044
+
1045
+ if (!hook.logic.script) {
1046
+ errors.push(`${prefix}: Hook logic must have a 'script' property`);
1047
+ } else if (typeof hook.logic.script !== 'string') {
1048
+ errors.push(`${prefix}: Hook logic script must be a string`);
1049
+ } else {
1050
+ try {
1051
+ const vm = require('vm');
1052
+ const wrappedScript = `(function() { 'use strict'; ${hook.logic.script} })()`;
1053
+ new vm.Script(wrappedScript);
1054
+ } catch (syntaxError) {
1055
+ errors.push(`${prefix}: Hook logic script has syntax error: ${syntaxError.message}`);
1056
+ }
1057
+ }
1058
+
1059
+ if (!hook.config && !hook.transform) {
1060
+ errors.push(
1061
+ `${prefix}: Hook with logic block must also have 'config' or 'transform'. ` +
1062
+ `Logic provides overrides, not the full message.`
1063
+ );
1064
+ }
1065
+ }
1066
+
822
1067
  function validateHookSemantics(config) {
823
1068
  const errors = [];
824
1069
  const warnings = [];
@@ -844,58 +1089,19 @@ function validateHookSemantics(config) {
844
1089
 
845
1090
  // === GAP 1: Hook action field missing ===
846
1091
  // Causes runtime crash at agent-hook-executor.js:66
847
- if (!hook.action) {
848
- errors.push(
849
- `[Gap 1] ${prefix}: Missing 'action' field. ` +
850
- `Fix: Add "action": "publish_message" or "action": "execute_system_command"`
851
- );
852
- }
1092
+ validateHookAction(hook, prefix, errors);
853
1093
 
854
1094
  // === GAP 2: Transform script output shape validation ===
855
1095
  // Causes runtime crash at agent-hook-executor.js:148
856
- if (hook.transform?.script) {
857
- const script = hook.transform.script;
858
-
859
- // Check if script returns an object with topic and content
860
- // Simple heuristic: look for return statement with object
861
- const hasReturnTopic = /return\s*\{[^}]*topic\s*:/i.test(script);
862
- const hasReturnContent = /return\s*\{[^}]*content\s*:/i.test(script);
863
-
864
- if (!hasReturnTopic) {
865
- errors.push(
866
- `[Gap 2] ${prefix}: Transform script must return object with 'topic' property. ` +
867
- `Fix: return { topic: "TOPIC_NAME", content: {...} }`
868
- );
869
- }
870
-
871
- if (!hasReturnContent) {
872
- errors.push(
873
- `[Gap 2] ${prefix}: Transform script must return object with 'content' property. ` +
874
- `Fix: return { topic: "...", content: { data: result } }`
875
- );
876
- }
877
- }
1096
+ validateTransformScript(hook, prefix, errors);
878
1097
 
879
1098
  // === GAP 7: CLUSTER_OPERATIONS payload validation ===
880
1099
  // Causes runtime crash at orchestrator.js:722
881
- if (
882
- agent.role === 'conductor' &&
883
- (hook.config?.topic === 'CLUSTER_OPERATIONS' ||
884
- hook.transform?.script?.includes('CLUSTER_OPERATIONS'))
885
- ) {
886
- // Check if operations field is valid JSON structure
887
- if (hook.transform?.script) {
888
- const script = hook.transform.script;
889
- // Look for operations field in return statement
890
- const hasOperations = /operations\s*:/i.test(script);
891
- if (!hasOperations) {
892
- errors.push(
893
- `[Gap 7] ${prefix}: CLUSTER_OPERATIONS message must include 'operations' field. ` +
894
- `Fix: return { topic: "CLUSTER_OPERATIONS", content: { data: { operations: JSON.stringify([...]) } } }`
895
- );
896
- }
897
- }
898
- }
1100
+ validateConductorOperations(agent, hook, prefix, errors);
1101
+
1102
+ // === Logic block validation ===
1103
+ // Logic blocks allow conditional config overrides (like trigger logic)
1104
+ validateHookLogic(hook, prefix, errors);
899
1105
  }
900
1106
  }
901
1107
 
@@ -921,6 +1127,66 @@ function validateRuleCoverage(config) {
921
1127
  return { errors, warnings };
922
1128
  }
923
1129
 
1130
+ const applyIterationPattern = (coveredIterations, pattern, maxIterations) => {
1131
+ if (!pattern) return;
1132
+
1133
+ if (pattern === 'all') {
1134
+ for (let i = 1; i <= maxIterations; i++) {
1135
+ coveredIterations.add(i);
1136
+ }
1137
+ return;
1138
+ }
1139
+
1140
+ if (/^\d+$/.test(pattern)) {
1141
+ coveredIterations.add(parseInt(pattern, 10));
1142
+ return;
1143
+ }
1144
+
1145
+ if (/^\d+-\d+$/.test(pattern)) {
1146
+ const [start, end] = pattern.split('-').map((n) => parseInt(n, 10));
1147
+ for (let i = start; i <= end; i++) {
1148
+ coveredIterations.add(i);
1149
+ }
1150
+ return;
1151
+ }
1152
+
1153
+ if (/^\d+\+$/.test(pattern)) {
1154
+ const start = parseInt(pattern, 10);
1155
+ for (let i = start; i <= maxIterations; i++) {
1156
+ coveredIterations.add(i);
1157
+ }
1158
+ }
1159
+ };
1160
+
1161
+ const collectCoverage = (rules, maxIterations) => {
1162
+ const coveredIterations = new Set();
1163
+ for (const rule of rules) {
1164
+ applyIterationPattern(coveredIterations, rule.iterations, maxIterations);
1165
+ }
1166
+ return coveredIterations;
1167
+ };
1168
+
1169
+ const findUncoveredIterations = (coveredIterations, maxIterations) => {
1170
+ const uncoveredIterations = [];
1171
+ for (let i = 1; i <= maxIterations; i++) {
1172
+ if (!coveredIterations.has(i)) {
1173
+ uncoveredIterations.push(i);
1174
+ }
1175
+ }
1176
+ return uncoveredIterations;
1177
+ };
1178
+
1179
+ const reportCoverageGap = (agent, gapNumber, label, fixHint, uncoveredIterations) => {
1180
+ if (uncoveredIterations.length === 0) {
1181
+ return;
1182
+ }
1183
+ const ranges = groupConsecutive(uncoveredIterations);
1184
+ errors.push(
1185
+ `[Gap ${gapNumber}] Agent '${agent.id}': ${label} have gaps at iterations ${ranges.join(', ')}. ` +
1186
+ `Fix: ${fixHint}`
1187
+ );
1188
+ };
1189
+
924
1190
  for (const agent of config.agents) {
925
1191
  if (agent.type === 'subcluster') {
926
1192
  continue;
@@ -931,50 +1197,15 @@ function validateRuleCoverage(config) {
931
1197
  // === GAP 4: Model rule iteration gaps ===
932
1198
  // Causes runtime crash at agent-wrapper.js:154
933
1199
  if (agent.modelRules && Array.isArray(agent.modelRules)) {
934
- const coveredIterations = new Set();
935
-
936
- for (const rule of agent.modelRules) {
937
- const pattern = rule.iterations;
938
-
939
- if (pattern === 'all') {
940
- // Covers all iterations
941
- for (let i = 1; i <= maxIterations; i++) {
942
- coveredIterations.add(i);
943
- }
944
- } else if (/^\d+$/.test(pattern)) {
945
- // Single iteration: "5"
946
- coveredIterations.add(parseInt(pattern));
947
- } else if (/^\d+-\d+$/.test(pattern)) {
948
- // Range: "1-3"
949
- const [start, end] = pattern.split('-').map((n) => parseInt(n));
950
- for (let i = start; i <= end; i++) {
951
- coveredIterations.add(i);
952
- }
953
- } else if (/^\d+\+$/.test(pattern)) {
954
- // Open-ended: "5+"
955
- const start = parseInt(pattern);
956
- for (let i = start; i <= maxIterations; i++) {
957
- coveredIterations.add(i);
958
- }
959
- }
960
- }
961
-
962
- // Find gaps
963
- const uncoveredIterations = [];
964
- for (let i = 1; i <= maxIterations; i++) {
965
- if (!coveredIterations.has(i)) {
966
- uncoveredIterations.push(i);
967
- }
968
- }
969
-
970
- if (uncoveredIterations.length > 0) {
971
- // Group consecutive iterations for readability
972
- const ranges = groupConsecutive(uncoveredIterations);
973
- errors.push(
974
- `[Gap 4] Agent '${agent.id}': Model rules have gaps at iterations ${ranges.join(', ')}. ` +
975
- `Fix: Add catch-all rule { "iterations": "all", "model": "sonnet" } or extend existing ranges.`
976
- );
977
- }
1200
+ const coveredIterations = collectCoverage(agent.modelRules, maxIterations);
1201
+ const uncoveredIterations = findUncoveredIterations(coveredIterations, maxIterations);
1202
+ reportCoverageGap(
1203
+ agent,
1204
+ 4,
1205
+ 'Model rules',
1206
+ 'Add catch-all rule { "iterations": "all", "model": "sonnet" } or extend existing ranges.',
1207
+ uncoveredIterations
1208
+ );
978
1209
  }
979
1210
 
980
1211
  // === GAP 5: Prompt rule iteration gaps ===
@@ -985,44 +1216,15 @@ function validateRuleCoverage(config) {
985
1216
  agent.promptConfig.rules &&
986
1217
  Array.isArray(agent.promptConfig.rules)
987
1218
  ) {
988
- const coveredIterations = new Set();
989
-
990
- for (const rule of agent.promptConfig.rules) {
991
- const pattern = rule.iterations;
992
-
993
- if (pattern === 'all') {
994
- for (let i = 1; i <= maxIterations; i++) {
995
- coveredIterations.add(i);
996
- }
997
- } else if (/^\d+$/.test(pattern)) {
998
- coveredIterations.add(parseInt(pattern));
999
- } else if (/^\d+-\d+$/.test(pattern)) {
1000
- const [start, end] = pattern.split('-').map((n) => parseInt(n));
1001
- for (let i = start; i <= end; i++) {
1002
- coveredIterations.add(i);
1003
- }
1004
- } else if (/^\d+\+$/.test(pattern)) {
1005
- const start = parseInt(pattern);
1006
- for (let i = start; i <= maxIterations; i++) {
1007
- coveredIterations.add(i);
1008
- }
1009
- }
1010
- }
1011
-
1012
- const uncoveredIterations = [];
1013
- for (let i = 1; i <= maxIterations; i++) {
1014
- if (!coveredIterations.has(i)) {
1015
- uncoveredIterations.push(i);
1016
- }
1017
- }
1018
-
1019
- if (uncoveredIterations.length > 0) {
1020
- const ranges = groupConsecutive(uncoveredIterations);
1021
- errors.push(
1022
- `[Gap 5] Agent '${agent.id}': Prompt rules have gaps at iterations ${ranges.join(', ')}. ` +
1023
- `Fix: Add catch-all rule { "iterations": "all", "prompt": "..." } or extend existing ranges.`
1024
- );
1025
- }
1219
+ const coveredIterations = collectCoverage(agent.promptConfig.rules, maxIterations);
1220
+ const uncoveredIterations = findUncoveredIterations(coveredIterations, maxIterations);
1221
+ reportCoverageGap(
1222
+ agent,
1223
+ 5,
1224
+ 'Prompt rules',
1225
+ 'Add catch-all rule { "iterations": "all", "prompt": "..." } or extend existing ranges.',
1226
+ uncoveredIterations
1227
+ );
1026
1228
  }
1027
1229
  }
1028
1230
 
@@ -1076,70 +1278,63 @@ function groupConsecutive(numbers) {
1076
1278
  * @param {Object} config - Cluster configuration
1077
1279
  * @returns {{ errors: string[], warnings: string[] }}
1078
1280
  */
1079
- function detectNAgentCycles(config) {
1080
- const errors = [];
1081
- const warnings = [];
1281
+ function collectAgentDependencies(agent, topicProducers) {
1282
+ const dependencies = new Set();
1082
1283
 
1083
- if (!config.agents || !Array.isArray(config.agents)) {
1084
- return { errors, warnings };
1284
+ for (const trigger of agent.triggers || []) {
1285
+ const topic = trigger.topic;
1286
+ if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue;
1287
+ if (topic.endsWith('*')) continue;
1288
+
1289
+ const producers = topicProducers.get(topic) || [];
1290
+ for (const producer of producers) {
1291
+ if (producer !== agent.id) {
1292
+ dependencies.add(producer);
1293
+ }
1294
+ }
1085
1295
  }
1086
1296
 
1087
- // Build agent dependency graph: agent -> [agents it depends on]
1297
+ return Array.from(dependencies);
1298
+ }
1299
+
1300
+ function buildAgentGraph(config) {
1088
1301
  const agentGraph = new Map();
1089
- const topicProducers = new Map(); // topic -> [agentIds]
1302
+ const topicProducers = new Map();
1090
1303
 
1091
- // Initialize graph
1092
1304
  for (const agent of config.agents) {
1093
1305
  if (agent.type === 'subcluster') continue;
1094
1306
  agentGraph.set(agent.id, []);
1095
1307
 
1096
- // Track what topics this agent produces
1097
1308
  const outputTopic = agent.hooks?.onComplete?.config?.topic;
1098
- if (outputTopic) {
1099
- if (!topicProducers.has(outputTopic)) {
1100
- topicProducers.set(outputTopic, []);
1101
- }
1102
- topicProducers.get(outputTopic).push(agent.id);
1309
+ if (!outputTopic) continue;
1310
+
1311
+ if (!topicProducers.has(outputTopic)) {
1312
+ topicProducers.set(outputTopic, []);
1103
1313
  }
1314
+ topicProducers.get(outputTopic).push(agent.id);
1104
1315
  }
1105
1316
 
1106
- // Build dependencies: agent consumes topic -> depends on agents that produce it
1107
1317
  for (const agent of config.agents) {
1108
1318
  if (agent.type === 'subcluster') continue;
1109
-
1110
- const dependencies = new Set();
1111
-
1112
- for (const trigger of agent.triggers || []) {
1113
- const topic = trigger.topic;
1114
- if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue;
1115
- if (topic.endsWith('*')) continue; // Skip wildcards
1116
-
1117
- const producers = topicProducers.get(topic) || [];
1118
- for (const producer of producers) {
1119
- if (producer !== agent.id) {
1120
- dependencies.add(producer);
1121
- }
1122
- }
1123
- }
1124
-
1125
- agentGraph.set(agent.id, Array.from(dependencies));
1319
+ agentGraph.set(agent.id, collectAgentDependencies(agent, topicProducers));
1126
1320
  }
1127
1321
 
1128
- // === GAP 6: Detect cycles with DFS ===
1322
+ return agentGraph;
1323
+ }
1324
+
1325
+ function findFirstCycle(agentGraph) {
1129
1326
  const visited = new Set();
1130
1327
  const recursionStack = new Set();
1131
1328
 
1132
- function dfs(agentId, path) {
1329
+ const dfs = (agentId, path) => {
1133
1330
  visited.add(agentId);
1134
1331
  recursionStack.add(agentId);
1135
1332
 
1136
1333
  const dependencies = agentGraph.get(agentId) || [];
1137
1334
  for (const nextAgent of dependencies) {
1138
1335
  if (recursionStack.has(nextAgent)) {
1139
- // Cycle detected - return the full cycle path
1140
1336
  const cycleStartIndex = path.indexOf(nextAgent);
1141
- const cyclePath = [...path.slice(cycleStartIndex), nextAgent];
1142
- return cyclePath;
1337
+ return [...path.slice(cycleStartIndex), nextAgent];
1143
1338
  }
1144
1339
 
1145
1340
  if (!visited.has(nextAgent)) {
@@ -1150,38 +1345,255 @@ function detectNAgentCycles(config) {
1150
1345
 
1151
1346
  recursionStack.delete(agentId);
1152
1347
  return null;
1153
- }
1348
+ };
1154
1349
 
1155
- // Check all agents as starting points
1156
1350
  for (const agentId of agentGraph.keys()) {
1157
1351
  if (!visited.has(agentId)) {
1158
1352
  const cycle = dfs(agentId, [agentId]);
1159
- if (cycle) {
1160
- // Check if cycle has escape logic (any agent in cycle has trigger logic)
1161
- const hasEscapeLogic = cycle.some((id) => {
1162
- const agent = config.agents.find((a) => a.id === id);
1163
- return agent?.triggers?.some((t) => t.logic);
1164
- });
1165
-
1166
- const cycleStr = cycle.join(' → ');
1167
- if (!hasEscapeLogic) {
1168
- errors.push(
1169
- `[Gap 6] Circular dependency detected: ${cycleStr}. ` +
1170
- `Fix: Add logic conditions to break the loop, or set maxIterations on involved agents.`
1171
- );
1172
- } else {
1353
+ if (cycle) return cycle;
1354
+ }
1355
+ }
1356
+
1357
+ return null;
1358
+ }
1359
+
1360
+ function detectNAgentCycles(config) {
1361
+ const errors = [];
1362
+ const warnings = [];
1363
+
1364
+ if (!config.agents || !Array.isArray(config.agents)) {
1365
+ return { errors, warnings };
1366
+ }
1367
+
1368
+ const agentGraph = buildAgentGraph(config);
1369
+ const cycle = findFirstCycle(agentGraph);
1370
+
1371
+ if (cycle) {
1372
+ const agentsById = new Map(
1373
+ config.agents.filter((agent) => agent.type !== 'subcluster').map((agent) => [agent.id, agent])
1374
+ );
1375
+ const hasEscapeLogic = cycle.some((id) => agentsById.get(id)?.triggers?.some((t) => t.logic));
1376
+ const cycleStr = cycle.join(' → ');
1377
+
1378
+ if (!hasEscapeLogic) {
1379
+ errors.push(
1380
+ `[Gap 6] Circular dependency detected: ${cycleStr}. ` +
1381
+ `Fix: Add logic conditions to break the loop, or set maxIterations on involved agents.`
1382
+ );
1383
+ } else {
1384
+ warnings.push(
1385
+ `Circular dependency detected: ${cycleStr}. ` +
1386
+ `Has escape logic in triggers, but verify maxIterations is set to prevent infinite loops.`
1387
+ );
1388
+ }
1389
+ }
1390
+
1391
+ return { errors, warnings };
1392
+ }
1393
+
1394
+ function collectAgentIdConflicts(agents, errors) {
1395
+ const allAgentIds = new Map();
1396
+
1397
+ const collectAgentIds = (list, depth = 0) => {
1398
+ if (!list) return;
1399
+
1400
+ for (const agent of list) {
1401
+ if (!agent.id) continue;
1402
+
1403
+ if (allAgentIds.has(agent.id)) {
1404
+ const firstSeenDepth = allAgentIds.get(agent.id);
1405
+ errors.push(
1406
+ `[Gap 11] Duplicate agent ID '${agent.id}' found across cluster hierarchy ` +
1407
+ `(first at depth ${firstSeenDepth}, duplicate at depth ${depth}). ` +
1408
+ `Fix: Ensure all agent IDs are unique across the entire cluster.`
1409
+ );
1410
+ } else {
1411
+ allAgentIds.set(agent.id, depth);
1412
+ }
1413
+
1414
+ if (agent.type === 'subcluster' && agent.config?.agents) {
1415
+ collectAgentIds(agent.config.agents, depth + 1);
1416
+ }
1417
+ }
1418
+ };
1419
+
1420
+ collectAgentIds(agents);
1421
+ }
1422
+
1423
+ function buildTopicProducers(agents) {
1424
+ const topicProducers = new Map();
1425
+
1426
+ for (const agent of agents) {
1427
+ if (agent.type === 'subcluster') continue;
1428
+ const outputTopic = agent.hooks?.onComplete?.config?.topic;
1429
+ if (!outputTopic) continue;
1430
+
1431
+ if (!topicProducers.has(outputTopic)) {
1432
+ topicProducers.set(outputTopic, []);
1433
+ }
1434
+ topicProducers.get(outputTopic).push(agent.id);
1435
+ }
1436
+
1437
+ return topicProducers;
1438
+ }
1439
+
1440
+ function validateJsonSchema(prefix, agent, errors) {
1441
+ if (!agent.jsonSchema) return;
1442
+
1443
+ try {
1444
+ JSON.stringify(agent.jsonSchema);
1445
+
1446
+ if (typeof agent.jsonSchema !== 'object') {
1447
+ errors.push(
1448
+ `[Gap 8] ${prefix}: jsonSchema must be an object, got ${typeof agent.jsonSchema}. ` +
1449
+ `Fix: Use valid JSON Schema format with 'type' and 'properties' fields.`
1450
+ );
1451
+ }
1452
+ } catch (error) {
1453
+ errors.push(
1454
+ `[Gap 8] ${prefix}: jsonSchema is not valid JSON: ${error.message}. ` +
1455
+ `Fix: Ensure schema is a valid JSON object.`
1456
+ );
1457
+ }
1458
+ }
1459
+
1460
+ function validateContextSource(prefix, source, topicProducers, errors, warnings) {
1461
+ const topic = source.topic;
1462
+ if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') return;
1463
+ if (topic.endsWith('*')) return;
1464
+
1465
+ const producers = topicProducers.get(topic) || [];
1466
+ if (producers.length === 0) {
1467
+ warnings.push(
1468
+ `[Gap 9] ${prefix}: Context source topic '${topic}' is never produced. ` +
1469
+ `Agent will get empty context for this source.`
1470
+ );
1471
+ }
1472
+
1473
+ if (source.amount === undefined) {
1474
+ warnings.push(
1475
+ `[Gap 14] ${prefix}: Context source for topic '${topic}' missing 'amount' field. ` +
1476
+ `Defaults may not be what you expect.`
1477
+ );
1478
+ }
1479
+
1480
+ if (source.strategy && !['latest', 'all', 'oldest'].includes(source.strategy)) {
1481
+ errors.push(
1482
+ `[Gap 14] ${prefix}: Context source strategy '${source.strategy}' is invalid. ` +
1483
+ `Fix: Use 'latest', 'all', or 'oldest'.`
1484
+ );
1485
+ }
1486
+ }
1487
+
1488
+ function validateContextSources(prefix, agent, topicProducers, errors, warnings) {
1489
+ if (!agent.contextStrategy?.sources) return;
1490
+
1491
+ for (const source of agent.contextStrategy.sources) {
1492
+ validateContextSource(prefix, source, topicProducers, errors, warnings);
1493
+ }
1494
+ }
1495
+
1496
+ function validateIsolationConfig(prefix, agent, path, errors, warnings) {
1497
+ if (!agent.isolation) return;
1498
+
1499
+ if (agent.isolation.type === 'docker') {
1500
+ if (!agent.isolation.image) {
1501
+ errors.push(
1502
+ `[Gap 10] ${prefix}: Docker isolation requires 'image' field. ` +
1503
+ `Fix: Add "image": "zeroshot-runner" or custom image name.`
1504
+ );
1505
+ }
1506
+
1507
+ if (agent.isolation.mounts) {
1508
+ for (const mount of agent.isolation.mounts) {
1509
+ if (mount.host && !path.isAbsolute(mount.host)) {
1173
1510
  warnings.push(
1174
- `Circular dependency detected: ${cycleStr}. ` +
1175
- `Has escape logic in triggers, but verify maxIterations is set to prevent infinite loops.`
1511
+ `[Gap 10] ${prefix}: Docker mount host path '${mount.host}' is not absolute. ` +
1512
+ `May cause runtime errors.`
1176
1513
  );
1177
1514
  }
1178
- // Only report first cycle to avoid noise
1179
- break;
1180
1515
  }
1181
1516
  }
1517
+
1518
+ return;
1182
1519
  }
1183
1520
 
1184
- return { errors, warnings };
1521
+ if (agent.isolation.type && agent.isolation.type !== 'worktree') {
1522
+ errors.push(
1523
+ `[Gap 10] ${prefix}: Unknown isolation type '${agent.isolation.type}'. ` +
1524
+ `Fix: Use 'docker' or 'worktree'.`
1525
+ );
1526
+ }
1527
+ }
1528
+
1529
+ function validateLoadConfig(prefix, agent, fs, errors) {
1530
+ if (!agent.loadConfig) return;
1531
+ const configPath = agent.loadConfig.path;
1532
+ if (configPath && !fs.existsSync(configPath)) {
1533
+ errors.push(
1534
+ `[Gap 12] ${prefix}: Load config file '${configPath}' does not exist. ` +
1535
+ `Fix: Check file path or remove loadConfig.`
1536
+ );
1537
+ }
1538
+ }
1539
+
1540
+ function validateTaskExecutor(prefix, agent, errors) {
1541
+ if (!agent.taskExecutor) return;
1542
+
1543
+ if (agent.taskExecutor.command === undefined) {
1544
+ errors.push(
1545
+ `[Gap 13] ${prefix}: Task executor missing 'command' field. ` +
1546
+ `Fix: Add "command": "claude" or custom command.`
1547
+ );
1548
+ }
1549
+
1550
+ if (agent.taskExecutor.retries !== undefined) {
1551
+ if (typeof agent.taskExecutor.retries !== 'number' || agent.taskExecutor.retries < 0) {
1552
+ errors.push(
1553
+ `[Gap 13] ${prefix}: Task executor 'retries' must be a non-negative number, got ${agent.taskExecutor.retries}. ` +
1554
+ `Fix: Use a positive integer or 0.`
1555
+ );
1556
+ }
1557
+ }
1558
+
1559
+ if (agent.taskExecutor.timeout !== undefined) {
1560
+ if (typeof agent.taskExecutor.timeout !== 'number' || agent.taskExecutor.timeout <= 0) {
1561
+ errors.push(
1562
+ `[Gap 13] ${prefix}: Task executor 'timeout' must be a positive number, got ${agent.taskExecutor.timeout}. ` +
1563
+ `Fix: Use a positive number in milliseconds.`
1564
+ );
1565
+ }
1566
+ }
1567
+ }
1568
+
1569
+ function validateRoleReferences(prefix, agent, roles, errors) {
1570
+ for (const trigger of agent.triggers || []) {
1571
+ if (!trigger.logic?.script) continue;
1572
+ const script = trigger.logic.script;
1573
+ const roleMatches = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
1574
+
1575
+ if (!roleMatches) continue;
1576
+
1577
+ for (const match of roleMatches) {
1578
+ const role = match.match(/['"](\w+)['"]/)[1];
1579
+ if (roles.has(role)) continue;
1580
+
1581
+ const isCritical =
1582
+ /\.length\s*[><=!]/.test(script) ||
1583
+ /allResponded/.test(script) ||
1584
+ /hasConsensus/.test(script);
1585
+
1586
+ const hasZeroLengthFallback =
1587
+ /\.length\s*===?\s*0\s*\)\s*return/.test(script) || /\.length\s*[<]=\s*0/.test(script);
1588
+
1589
+ if (isCritical && !hasZeroLengthFallback) {
1590
+ errors.push(
1591
+ `[Gap 15] ${prefix}: Logic references role '${role}' which doesn't exist. ` +
1592
+ `This will cause logic to fail. Fix: Add agent with role '${role}' or update logic.`
1593
+ );
1594
+ }
1595
+ }
1596
+ }
1185
1597
  }
1186
1598
 
1187
1599
  /**
@@ -1213,33 +1625,12 @@ function validateConfigSemantics(config) {
1213
1625
  const path = require('path');
1214
1626
 
1215
1627
  // === GAP 11: Agent ID conflicts across subclusters ===
1216
- // Collect all agent IDs recursively (including subclusters)
1217
- const allAgentIds = new Map(); // Map of agentId -> depth where first seen
1628
+ collectAgentIdConflicts(config.agents, errors);
1218
1629
 
1219
- function collectAgentIds(agents, depth = 0) {
1220
- if (!agents) return;
1221
-
1222
- for (const agent of agents) {
1223
- if (!agent.id) continue; // Skip agents without IDs (caught in Phase 1)
1224
-
1225
- if (allAgentIds.has(agent.id)) {
1226
- const firstSeenDepth = allAgentIds.get(agent.id);
1227
- errors.push(
1228
- `[Gap 11] Duplicate agent ID '${agent.id}' found across cluster hierarchy ` +
1229
- `(first at depth ${firstSeenDepth}, duplicate at depth ${depth}). ` +
1230
- `Fix: Ensure all agent IDs are unique across the entire cluster.`
1231
- );
1232
- } else {
1233
- allAgentIds.set(agent.id, depth);
1234
- }
1235
-
1236
- if (agent.type === 'subcluster' && agent.config?.agents) {
1237
- collectAgentIds(agent.config.agents, depth + 1);
1238
- }
1239
- }
1240
- }
1241
-
1242
- collectAgentIds(config.agents);
1630
+ const topicProducers = buildTopicProducers(config.agents);
1631
+ const roles = new Set(
1632
+ config.agents.filter((agent) => agent.type !== 'subcluster').map((agent) => agent.role)
1633
+ );
1243
1634
 
1244
1635
  for (const agent of config.agents) {
1245
1636
  if (agent.type === 'subcluster') continue;
@@ -1247,170 +1638,262 @@ function validateConfigSemantics(config) {
1247
1638
  const prefix = `Agent '${agent.id}'`;
1248
1639
 
1249
1640
  // === GAP 8: JSON schema structurally invalid ===
1250
- if (agent.jsonSchema) {
1251
- try {
1252
- // Check if schema can be stringified (basic structural check)
1253
- JSON.stringify(agent.jsonSchema);
1254
-
1255
- // Check required fields for JSON schema
1256
- if (typeof agent.jsonSchema !== 'object') {
1257
- errors.push(
1258
- `[Gap 8] ${prefix}: jsonSchema must be an object, got ${typeof agent.jsonSchema}. ` +
1259
- `Fix: Use valid JSON Schema format with 'type' and 'properties' fields.`
1260
- );
1261
- }
1262
- } catch (e) {
1263
- errors.push(
1264
- `[Gap 8] ${prefix}: jsonSchema is not valid JSON: ${e.message}. ` +
1265
- `Fix: Ensure schema is a valid JSON object.`
1266
- );
1267
- }
1268
- }
1641
+ validateJsonSchema(prefix, agent, errors);
1269
1642
 
1270
1643
  // === GAP 9: Context sources never produced (enhanced check) ===
1271
1644
  // Already partially covered in Phase 2, but add stricter checks
1272
- if (agent.contextStrategy?.sources) {
1273
- const topicProducers = new Map();
1274
-
1275
- // Build topic producers map
1276
- for (const a of config.agents) {
1277
- if (a.type === 'subcluster') continue;
1278
- const outputTopic = a.hooks?.onComplete?.config?.topic;
1279
- if (outputTopic) {
1280
- if (!topicProducers.has(outputTopic)) {
1281
- topicProducers.set(outputTopic, []);
1282
- }
1283
- topicProducers.get(outputTopic).push(a.id);
1284
- }
1285
- }
1645
+ validateContextSources(prefix, agent, topicProducers, errors, warnings);
1286
1646
 
1287
- for (const source of agent.contextStrategy.sources) {
1288
- const topic = source.topic;
1289
- if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue;
1290
- if (topic.endsWith('*')) continue;
1647
+ // === GAP 10: Isolation config invalid ===
1648
+ validateIsolationConfig(prefix, agent, path, errors, warnings);
1291
1649
 
1292
- const producers = topicProducers.get(topic) || [];
1293
- if (producers.length === 0) {
1294
- warnings.push(
1295
- `[Gap 9] ${prefix}: Context source topic '${topic}' is never produced. ` +
1296
- `Agent will get empty context for this source.`
1297
- );
1298
- }
1650
+ // === GAP 12: Load config file paths don't exist ===
1651
+ validateLoadConfig(prefix, agent, fs, errors);
1299
1652
 
1300
- // === GAP 14: Context source format invalid ===
1301
- if (source.amount === undefined) {
1302
- warnings.push(
1303
- `[Gap 14] ${prefix}: Context source for topic '${topic}' missing 'amount' field. ` +
1304
- `Defaults may not be what you expect.`
1305
- );
1306
- }
1307
- if (source.strategy && !['latest', 'all', 'oldest'].includes(source.strategy)) {
1308
- errors.push(
1309
- `[Gap 14] ${prefix}: Context source strategy '${source.strategy}' is invalid. ` +
1310
- `Fix: Use 'latest', 'all', or 'oldest'.`
1311
- );
1312
- }
1313
- }
1653
+ // === GAP 13: Task executor config invalid ===
1654
+ validateTaskExecutor(prefix, agent, errors);
1655
+
1656
+ // === GAP 15: Stricter role reference validation ===
1657
+ // Upgrade from WARNING to ERROR when role is used in critical logic
1658
+ validateRoleReferences(prefix, agent, roles, errors);
1659
+ }
1660
+
1661
+ return { errors, warnings };
1662
+ }
1663
+
1664
+ function resolveProviderName(agent, config, settings) {
1665
+ const resolved =
1666
+ config.forceProvider ||
1667
+ agent.provider ||
1668
+ config.defaultProvider ||
1669
+ settings.defaultProvider ||
1670
+ 'claude';
1671
+ return normalizeProviderName(resolved) || 'claude';
1672
+ }
1673
+
1674
+ function validateProviderLevel(provider, requestedLevel, minLevel, maxLevel) {
1675
+ const providerModule = getProvider(provider);
1676
+ const levels = providerModule.getLevelMapping();
1677
+ const rank = (level) => levels[level]?.rank;
1678
+
1679
+ if (!levels[requestedLevel]) {
1680
+ throw new Error(`Invalid level "${requestedLevel}" for provider "${provider}"`);
1681
+ }
1682
+
1683
+ if (minLevel && !levels[minLevel]) {
1684
+ throw new Error(`Invalid minLevel "${minLevel}" for provider "${provider}"`);
1685
+ }
1686
+
1687
+ if (maxLevel && !levels[maxLevel]) {
1688
+ throw new Error(`Invalid maxLevel "${maxLevel}" for provider "${provider}"`);
1689
+ }
1690
+
1691
+ if (minLevel && maxLevel && rank(minLevel) > rank(maxLevel)) {
1692
+ throw new Error(
1693
+ `minLevel "${minLevel}" exceeds maxLevel "${maxLevel}" for provider "${provider}"`
1694
+ );
1695
+ }
1696
+
1697
+ if (maxLevel && rank(requestedLevel) > rank(maxLevel)) {
1698
+ throw new Error(
1699
+ `Level "${requestedLevel}" exceeds maxLevel "${maxLevel}" for provider "${provider}"`
1700
+ );
1701
+ }
1702
+
1703
+ if (minLevel && rank(requestedLevel) < rank(minLevel)) {
1704
+ throw new Error(
1705
+ `Level "${requestedLevel}" is below minLevel "${minLevel}" for provider "${provider}"`
1706
+ );
1707
+ }
1708
+
1709
+ return requestedLevel;
1710
+ }
1711
+
1712
+ function validateProviderSettings(provider, providerSettings) {
1713
+ const providerModule = getProvider(provider);
1714
+ const levels = providerModule.getLevelMapping();
1715
+ const settings = providerSettings || {};
1716
+
1717
+ const minLevel = settings.minLevel || providerModule.getDefaultMinLevel?.();
1718
+ const maxLevel = settings.maxLevel || providerModule.getDefaultMaxLevel?.();
1719
+ const defaultLevel = settings.defaultLevel || providerModule.getDefaultLevel();
1720
+
1721
+ validateProviderLevel(provider, defaultLevel, minLevel, maxLevel);
1722
+
1723
+ for (const [level, override] of Object.entries(settings.levelOverrides || {})) {
1724
+ if (!levels[level]) {
1725
+ throw new Error(`Unknown level "${level}" in overrides for provider "${provider}"`);
1726
+ }
1727
+ if (override?.model && (typeof override.model !== 'string' || !override.model.trim())) {
1728
+ throw new Error(
1729
+ `Invalid model override (must be non-empty string) for provider "${provider}"`
1730
+ );
1731
+ }
1732
+ if (override?.reasoningEffort && !['codex', 'opencode'].includes(provider)) {
1733
+ throw new Error(`reasoningEffort overrides are only supported for Codex and Opencode`);
1734
+ }
1735
+ if (
1736
+ override?.reasoningEffort &&
1737
+ !['low', 'medium', 'high', 'xhigh'].includes(override.reasoningEffort)
1738
+ ) {
1739
+ throw new Error(
1740
+ `Invalid reasoningEffort "${override.reasoningEffort}" (low|medium|high|xhigh)`
1741
+ );
1314
1742
  }
1743
+ }
1744
+ }
1315
1745
 
1316
- // === GAP 10: Isolation config invalid ===
1317
- if (agent.isolation) {
1318
- if (agent.isolation.type === 'docker') {
1319
- if (!agent.isolation.image) {
1320
- errors.push(
1321
- `[Gap 10] ${prefix}: Docker isolation requires 'image' field. ` +
1322
- `Fix: Add "image": "zeroshot-runner" or custom image name.`
1323
- );
1324
- }
1746
+ function resolveAgentProvider(agent, config, settings, errors) {
1747
+ const provider = resolveProviderName(agent, config, settings);
1748
+ if (!VALID_PROVIDERS.includes(provider)) {
1749
+ errors.push(`Agent "${agent.id}" references unknown provider "${provider}"`);
1750
+ return null;
1751
+ }
1752
+ return provider;
1753
+ }
1325
1754
 
1326
- // Check mount paths are absolute
1327
- if (agent.isolation.mounts) {
1328
- for (const mount of agent.isolation.mounts) {
1329
- if (mount.host && !path.isAbsolute(mount.host)) {
1330
- warnings.push(
1331
- `[Gap 10] ${prefix}: Docker mount host path '${mount.host}' is not absolute. ` +
1332
- `May cause runtime errors.`
1333
- );
1334
- }
1335
- }
1336
- }
1337
- } else if (agent.isolation.type && agent.isolation.type !== 'worktree') {
1338
- errors.push(
1339
- `[Gap 10] ${prefix}: Unknown isolation type '${agent.isolation.type}'. ` +
1340
- `Fix: Use 'docker' or 'worktree'.`
1341
- );
1342
- }
1755
+ function buildProviderContext(provider, settings) {
1756
+ const providerModule = getProvider(provider);
1757
+ const levels = providerModule.getLevelMapping();
1758
+ const catalog = providerModule.getModelCatalog();
1759
+ const providerSettings = settings.providerSettings?.[provider] || {};
1760
+ const minLevel = providerSettings.minLevel;
1761
+ const maxLevel = providerSettings.maxLevel;
1762
+ const rank = (level) => levels[level]?.rank;
1763
+
1764
+ return {
1765
+ providerModule,
1766
+ levels,
1767
+ catalog,
1768
+ providerSettings,
1769
+ minLevel,
1770
+ maxLevel,
1771
+ rank,
1772
+ };
1773
+ }
1774
+
1775
+ function validateJsonSchemaSupport(agent, provider, warnings) {
1776
+ if (!agent.jsonSchema) return;
1777
+ const cap = CAPABILITIES[provider]?.jsonSchema;
1778
+ if (cap === 'experimental') {
1779
+ warnings.push(
1780
+ `Agent "${agent.id}" uses jsonSchema with ${provider} provider - ` +
1781
+ `this feature is experimental and may not work reliably`
1782
+ );
1783
+ } else if (!cap) {
1784
+ warnings.push(
1785
+ `Agent "${agent.id}" uses jsonSchema but ${provider} provider doesn't support it`
1786
+ );
1787
+ }
1788
+ }
1789
+
1790
+ function validateModelLevelSupport(agent, provider, levels, warnings) {
1791
+ if (agent.modelLevel && !levels[agent.modelLevel]) {
1792
+ warnings.push(
1793
+ `Agent "${agent.id}" uses modelLevel "${agent.modelLevel}" which is not valid for ${provider}`
1794
+ );
1795
+ }
1796
+ }
1797
+
1798
+ function validateModelSelection(agent, context, warnings) {
1799
+ const { provider, catalog, minLevel, maxLevel, rank } = context;
1800
+
1801
+ if (agent.model) {
1802
+ if (!catalog[agent.model]) {
1803
+ warnings.push(
1804
+ `Agent "${agent.id}" uses model "${agent.model}" which is not valid for ${provider}`
1805
+ );
1343
1806
  }
1807
+ return;
1808
+ }
1344
1809
 
1345
- // === GAP 12: Load config file paths don't exist ===
1346
- if (agent.loadConfig) {
1347
- const configPath = agent.loadConfig.path;
1348
- if (configPath && !fs.existsSync(configPath)) {
1349
- errors.push(
1350
- `[Gap 12] ${prefix}: Load config file '${configPath}' does not exist. ` +
1351
- `Fix: Check file path or remove loadConfig.`
1352
- );
1353
- }
1810
+ if (agent.modelLevel && minLevel && maxLevel) {
1811
+ if (rank(minLevel) > rank(maxLevel)) {
1812
+ warnings.push(
1813
+ `Provider "${provider}" has minLevel "${minLevel}" above maxLevel "${maxLevel}"`
1814
+ );
1815
+ } else if (rank(agent.modelLevel) < rank(minLevel)) {
1816
+ warnings.push(
1817
+ `Agent "${agent.id}" uses modelLevel "${agent.modelLevel}" below minLevel "${minLevel}" for ${provider}`
1818
+ );
1819
+ } else if (rank(agent.modelLevel) > rank(maxLevel)) {
1820
+ warnings.push(
1821
+ `Agent "${agent.id}" uses modelLevel "${agent.modelLevel}" above maxLevel "${maxLevel}" for ${provider}`
1822
+ );
1354
1823
  }
1824
+ }
1825
+ }
1355
1826
 
1356
- // === GAP 13: Task executor config invalid ===
1357
- if (agent.taskExecutor) {
1358
- if (agent.taskExecutor.command === undefined) {
1359
- errors.push(
1360
- `[Gap 13] ${prefix}: Task executor missing 'command' field. ` +
1361
- `Fix: Add "command": "claude" or custom command.`
1362
- );
1363
- }
1827
+ function validateModelRulesSupport(agent, provider, catalog, levels, warnings) {
1828
+ if (!agent.modelRules || !Array.isArray(agent.modelRules)) return;
1364
1829
 
1365
- if (agent.taskExecutor.retries !== undefined) {
1366
- if (typeof agent.taskExecutor.retries !== 'number' || agent.taskExecutor.retries < 0) {
1367
- errors.push(
1368
- `[Gap 13] ${prefix}: Task executor 'retries' must be a non-negative number, got ${agent.taskExecutor.retries}. ` +
1369
- `Fix: Use a positive integer or 0.`
1370
- );
1371
- }
1372
- }
1830
+ for (const rule of agent.modelRules) {
1831
+ if (rule.modelLevel && !levels[rule.modelLevel]) {
1832
+ warnings.push(
1833
+ `Agent "${agent.id}" uses modelLevel "${rule.modelLevel}" in modelRules which is not valid for ${provider}`
1834
+ );
1835
+ }
1836
+ if (rule.model && !catalog[rule.model]) {
1837
+ warnings.push(
1838
+ `Agent "${agent.id}" uses model "${rule.model}" in modelRules which is not valid for ${provider}`
1839
+ );
1840
+ }
1841
+ }
1842
+ }
1373
1843
 
1374
- if (agent.taskExecutor.timeout !== undefined) {
1375
- if (typeof agent.taskExecutor.timeout !== 'number' || agent.taskExecutor.timeout <= 0) {
1376
- errors.push(
1377
- `[Gap 13] ${prefix}: Task executor 'timeout' must be a positive number, got ${agent.taskExecutor.timeout}. ` +
1378
- `Fix: Use a positive number in milliseconds.`
1379
- );
1380
- }
1381
- }
1844
+ function validateReasoningEffortSupport(agent, provider, warnings) {
1845
+ if (agent.reasoningEffort && !['codex', 'opencode'].includes(provider)) {
1846
+ warnings.push(`Agent "${agent.id}" sets reasoningEffort but ${provider} does not support it`);
1847
+ } else if (
1848
+ agent.reasoningEffort &&
1849
+ !['low', 'medium', 'high', 'xhigh'].includes(agent.reasoningEffort)
1850
+ ) {
1851
+ warnings.push(
1852
+ `Agent "${agent.id}" has invalid reasoningEffort "${agent.reasoningEffort}" (low|medium|high|xhigh)`
1853
+ );
1854
+ }
1855
+ }
1856
+
1857
+ function validateProviderFeatures(config, settings) {
1858
+ const errors = [];
1859
+ const warnings = [];
1860
+
1861
+ const providersToValidate = VALID_PROVIDERS;
1862
+
1863
+ for (const provider of providersToValidate) {
1864
+ try {
1865
+ validateProviderSettings(provider, settings.providerSettings?.[provider]);
1866
+ } catch (err) {
1867
+ errors.push(err.message);
1382
1868
  }
1869
+ }
1383
1870
 
1384
- // === GAP 15: Stricter role reference validation ===
1385
- // Upgrade from WARNING to ERROR when role is used in critical logic
1386
- const roles = new Set(config.agents.filter((a) => a.type !== 'subcluster').map((a) => a.role));
1871
+ if (!config.agents || !Array.isArray(config.agents)) {
1872
+ return { errors, warnings };
1873
+ }
1387
1874
 
1388
- for (const trigger of agent.triggers || []) {
1389
- if (trigger.logic?.script) {
1390
- const script = trigger.logic.script;
1391
- const roleMatches = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
1392
-
1393
- if (roleMatches) {
1394
- for (const match of roleMatches) {
1395
- const role = match.match(/['"](\w+)['"]/)[1];
1396
- if (!roles.has(role)) {
1397
- // Check if the logic depends on this role for critical decisions
1398
- const isCritical =
1399
- /\.length\s*[><=!]/.test(script) || // Checking count
1400
- /allResponded/.test(script) || // Waiting for responses
1401
- /hasConsensus/.test(script); // Consensus check
1402
-
1403
- if (isCritical) {
1404
- errors.push(
1405
- `[Gap 15] ${prefix}: Logic references role '${role}' which doesn't exist. ` +
1406
- `This will cause logic to fail. Fix: Add agent with role '${role}' or update logic.`
1407
- );
1408
- }
1409
- }
1410
- }
1411
- }
1412
- }
1875
+ for (const agent of config.agents) {
1876
+ if (agent.type === 'subcluster') {
1877
+ continue;
1413
1878
  }
1879
+
1880
+ const provider = resolveAgentProvider(agent, config, settings, errors);
1881
+ if (!provider) continue;
1882
+
1883
+ const { levels, catalog, minLevel, maxLevel, rank } = buildProviderContext(provider, settings);
1884
+ const modelSelectionContext = {
1885
+ provider,
1886
+ catalog,
1887
+ minLevel,
1888
+ maxLevel,
1889
+ rank,
1890
+ };
1891
+
1892
+ validateJsonSchemaSupport(agent, provider, warnings);
1893
+ validateModelLevelSupport(agent, provider, levels, warnings);
1894
+ validateModelSelection(agent, modelSelectionContext, warnings);
1895
+ validateModelRulesSupport(agent, provider, catalog, levels, warnings);
1896
+ validateReasoningEffortSupport(agent, provider, warnings);
1414
1897
  }
1415
1898
 
1416
1899
  return { errors, warnings };
@@ -1435,5 +1918,8 @@ module.exports = {
1435
1918
  validateRuleCoverage,
1436
1919
  detectNAgentCycles,
1437
1920
  validateConfigSemantics,
1921
+ validateProviderLevel,
1922
+ validateProviderSettings,
1923
+ validateProviderFeatures,
1438
1924
  groupConsecutive,
1439
1925
  };