@covibes/zeroshot 5.4.0 → 5.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -465
  3. package/cli/index.js +101 -5231
  4. package/package.json +16 -130
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -149
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -275
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -581
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -208
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -220
  21. package/lib/git-remote-utils.js +0 -165
  22. package/lib/id-detector.js +0 -56
  23. package/lib/provider-defaults.js +0 -62
  24. package/lib/provider-detection.js +0 -59
  25. package/lib/provider-names.js +0 -57
  26. package/lib/settings/claude-auth.js +0 -78
  27. package/lib/settings.js +0 -503
  28. package/lib/stream-json-parser.js +0 -10
  29. package/scripts/fix-node-pty-permissions.js +0 -75
  30. package/scripts/record-demo.sh +0 -279
  31. package/scripts/setup-merge-queue.sh +0 -170
  32. package/scripts/test-install.sh +0 -40
  33. package/scripts/validate-templates.js +0 -100
  34. package/src/agent/agent-config.js +0 -246
  35. package/src/agent/agent-context-builder.js +0 -444
  36. package/src/agent/agent-hook-executor.js +0 -599
  37. package/src/agent/agent-lifecycle.js +0 -743
  38. package/src/agent/agent-stuck-detector.js +0 -256
  39. package/src/agent/agent-task-executor.js +0 -1690
  40. package/src/agent/agent-trigger-evaluator.js +0 -67
  41. package/src/agent/output-extraction.js +0 -217
  42. package/src/agent/output-reformatter.js +0 -175
  43. package/src/agent/schema-utils.js +0 -146
  44. package/src/agent-wrapper.js +0 -579
  45. package/src/agents/git-pusher-template.js +0 -285
  46. package/src/attach/attach-client.js +0 -438
  47. package/src/attach/attach-server.js +0 -543
  48. package/src/attach/index.js +0 -35
  49. package/src/attach/protocol.js +0 -220
  50. package/src/attach/ring-buffer.js +0 -121
  51. package/src/attach/socket-discovery.js +0 -267
  52. package/src/claude-task-runner.js +0 -574
  53. package/src/config-router.js +0 -80
  54. package/src/config-validator.js +0 -1925
  55. package/src/copy-worker.js +0 -43
  56. package/src/input-helpers.js +0 -65
  57. package/src/isolation-manager.js +0 -1471
  58. package/src/issue-providers/README.md +0 -305
  59. package/src/issue-providers/azure-devops-provider.js +0 -273
  60. package/src/issue-providers/base-provider.js +0 -232
  61. package/src/issue-providers/github-provider.js +0 -179
  62. package/src/issue-providers/gitlab-provider.js +0 -241
  63. package/src/issue-providers/index.js +0 -196
  64. package/src/issue-providers/jira-provider.js +0 -239
  65. package/src/ledger.js +0 -631
  66. package/src/lib/safe-exec.js +0 -88
  67. package/src/logic-engine.js +0 -223
  68. package/src/message-bus-bridge.js +0 -139
  69. package/src/message-bus.js +0 -249
  70. package/src/name-generator.js +0 -232
  71. package/src/orchestrator.js +0 -2815
  72. package/src/preflight.js +0 -639
  73. package/src/process-metrics.js +0 -608
  74. package/src/providers/anthropic/cli-builder.js +0 -73
  75. package/src/providers/anthropic/index.js +0 -204
  76. package/src/providers/anthropic/models.js +0 -23
  77. package/src/providers/anthropic/output-parser.js +0 -177
  78. package/src/providers/base-provider.js +0 -251
  79. package/src/providers/capabilities.js +0 -60
  80. package/src/providers/google/cli-builder.js +0 -55
  81. package/src/providers/google/index.js +0 -116
  82. package/src/providers/google/models.js +0 -24
  83. package/src/providers/google/output-parser.js +0 -101
  84. package/src/providers/index.js +0 -91
  85. package/src/providers/openai/cli-builder.js +0 -133
  86. package/src/providers/openai/index.js +0 -136
  87. package/src/providers/openai/models.js +0 -21
  88. package/src/providers/openai/output-parser.js +0 -143
  89. package/src/providers/opencode/cli-builder.js +0 -42
  90. package/src/providers/opencode/index.js +0 -103
  91. package/src/providers/opencode/models.js +0 -55
  92. package/src/providers/opencode/output-parser.js +0 -122
  93. package/src/schemas/sub-cluster.js +0 -185
  94. package/src/status-footer.js +0 -1026
  95. package/src/sub-cluster-wrapper.js +0 -601
  96. package/src/task-runner.js +0 -30
  97. package/src/template-resolver.js +0 -372
  98. package/src/tui/CHANGES.txt +0 -133
  99. package/src/tui/LAYOUT.md +0 -261
  100. package/src/tui/README.txt +0 -192
  101. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  102. package/src/tui/data-poller.js +0 -349
  103. package/src/tui/demo.js +0 -208
  104. package/src/tui/formatters.js +0 -129
  105. package/src/tui/index.js +0 -193
  106. package/src/tui/keybindings.js +0 -324
  107. package/src/tui/layout.js +0 -334
  108. package/src/tui/renderer.js +0 -184
  109. package/task-lib/attachable-watcher.js +0 -309
  110. package/task-lib/claude-recovery.js +0 -156
  111. package/task-lib/commands/clean.js +0 -50
  112. package/task-lib/commands/episodes.js +0 -105
  113. package/task-lib/commands/get-log-path.js +0 -23
  114. package/task-lib/commands/kill.js +0 -32
  115. package/task-lib/commands/list.js +0 -105
  116. package/task-lib/commands/logs.js +0 -453
  117. package/task-lib/commands/resume.js +0 -42
  118. package/task-lib/commands/run.js +0 -57
  119. package/task-lib/commands/schedule.js +0 -105
  120. package/task-lib/commands/scheduler-cmd.js +0 -96
  121. package/task-lib/commands/schedules.js +0 -148
  122. package/task-lib/commands/status.js +0 -44
  123. package/task-lib/commands/unschedule.js +0 -16
  124. package/task-lib/completion.js +0 -9
  125. package/task-lib/config.js +0 -8
  126. package/task-lib/name-generator.js +0 -230
  127. package/task-lib/package.json +0 -3
  128. package/task-lib/runner.js +0 -201
  129. package/task-lib/scheduler.js +0 -252
  130. package/task-lib/store.js +0 -529
  131. package/task-lib/tui/formatters.js +0 -215
  132. package/task-lib/tui/index.js +0 -197
  133. package/task-lib/tui/layout.js +0 -111
  134. package/task-lib/tui/renderer.js +0 -129
  135. package/task-lib/tui.js +0 -425
  136. package/task-lib/watcher.js +0 -226
@@ -1,1925 +0,0 @@
1
- /**
2
- * Config Validator - Static analysis for zeroshot cluster configurations
3
- *
4
- * Catches logical failures that would cause clusters to:
5
- * - Never start (no bootstrap trigger)
6
- * - Never complete (no path to completion)
7
- * - Loop infinitely (circular dependencies)
8
- * - Deadlock (impossible consensus)
9
- * - Waste compute (orchestrator executing tasks)
10
- *
11
- * Run at config load time to fail fast before spawning agents.
12
- */
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
-
19
- /**
20
- * Check if config is a conductor-bootstrap style config
21
- * Conductor configs dynamically spawn agents via CLUSTER_OPERATIONS
22
- * @param {Object} config - Cluster configuration
23
- * @returns {boolean}
24
- */
25
- function isConductorConfig(config) {
26
- return config.agents?.some(
27
- (a) =>
28
- a.role === 'conductor' &&
29
- // Old style: static topic in config
30
- (a.hooks?.onComplete?.config?.topic === 'CLUSTER_OPERATIONS' ||
31
- // New style: topic set in transform script (check for CLUSTER_OPERATIONS in script)
32
- a.hooks?.onComplete?.transform?.script?.includes('CLUSTER_OPERATIONS'))
33
- );
34
- }
35
-
36
- /**
37
- * Validate a cluster configuration for structural correctness
38
- * @param {Object} config - Cluster configuration
39
- * @param {Number} depth - Current nesting depth (for subcluster validation)
40
- * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
41
- */
42
- function validateConfig(config, depth = 0) {
43
- const errors = [];
44
- const warnings = [];
45
- const settings = loadSettings();
46
-
47
- // Max nesting depth check
48
- const MAX_DEPTH = 5;
49
- if (depth > MAX_DEPTH) {
50
- errors.push(`Cluster nesting exceeds max depth (${MAX_DEPTH})`);
51
- return { valid: false, errors, warnings };
52
- }
53
-
54
- // === PHASE 1: Basic structure validation ===
55
- const basicResult = validateBasicStructure(config, depth);
56
- errors.push(...basicResult.errors);
57
- warnings.push(...basicResult.warnings);
58
-
59
- // Note: We continue to other phases even if Phase 1 has errors,
60
- // to collect ALL validation issues (especially semantic checks in Phase 6-9)
61
-
62
- // Conductor configs dynamically spawn agents - skip message flow analysis
63
- // The orchestrator validates the spawned config at CLUSTER_OPERATIONS execution time
64
- const conductorMode = isConductorConfig(config);
65
-
66
- // === PHASE 2: Message flow analysis (skip for conductor configs) ===
67
- if (!conductorMode) {
68
- const flowResult = analyzeMessageFlow(config);
69
- errors.push(...flowResult.errors);
70
- warnings.push(...flowResult.warnings);
71
- }
72
-
73
- // === PHASE 3: Agent-specific validation ===
74
- const agentResult = validateAgents(config);
75
- errors.push(...agentResult.errors);
76
- warnings.push(...agentResult.warnings);
77
-
78
- // === PHASE 4: Logic script validation ===
79
- const logicResult = validateLogicScripts(config);
80
- errors.push(...logicResult.errors);
81
- warnings.push(...logicResult.warnings);
82
-
83
- // === PHASE 5: Template variable validation ===
84
- const templateResult = validateTemplateVariables(config, depth);
85
- errors.push(...templateResult.errors);
86
- warnings.push(...templateResult.warnings);
87
-
88
- // === PHASE 6: Hook semantic validation ===
89
- const hookResult = validateHookSemantics(config);
90
- errors.push(...hookResult.errors);
91
- warnings.push(...hookResult.warnings);
92
-
93
- // === PHASE 7: Rule coverage validation ===
94
- const ruleResult = validateRuleCoverage(config);
95
- errors.push(...ruleResult.errors);
96
- warnings.push(...ruleResult.warnings);
97
-
98
- // === PHASE 8: N-agent cycle detection ===
99
- const cycleResult = detectNAgentCycles(config);
100
- errors.push(...cycleResult.errors);
101
- warnings.push(...cycleResult.warnings);
102
-
103
- // === PHASE 9: Configuration semantic validation ===
104
- const configResult = validateConfigSemantics(config);
105
- errors.push(...configResult.errors);
106
- warnings.push(...configResult.warnings);
107
-
108
- // === PHASE 10: Provider feature validation ===
109
- const providerResult = validateProviderFeatures(config, settings);
110
- errors.push(...providerResult.errors);
111
- warnings.push(...providerResult.warnings);
112
-
113
- return {
114
- valid: errors.length === 0,
115
- errors,
116
- warnings,
117
- };
118
- }
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
-
214
- /**
215
- * Phase 1: Validate basic structure (fields, types, duplicates)
216
- */
217
- function validateBasicStructure(config, depth = 0) {
218
- const errors = [];
219
- const warnings = [];
220
-
221
- if (!config.agents || !Array.isArray(config.agents)) {
222
- errors.push('agents array is required');
223
- return { errors, warnings };
224
- }
225
-
226
- if (config.agents.length === 0) {
227
- errors.push('agents array cannot be empty');
228
- return { errors, warnings };
229
- }
230
-
231
- const seenIds = new Set();
232
-
233
- for (let i = 0; i < config.agents.length; i++) {
234
- const agent = config.agents[i];
235
- const prefix = `agents[${i}]`;
236
-
237
- // Check if this is a subcluster
238
- const isSubCluster = agent.type === 'subcluster';
239
-
240
- validateAgentIdentity(agent, prefix, seenIds, errors);
241
-
242
- // Validate subclusters
243
- if (isSubCluster) {
244
- validateSubclusterAgent(agent, depth, errors, warnings);
245
- continue; // Skip regular agent validation
246
- }
247
-
248
- // Regular agent validation
249
- validateAgentTriggers(agent, prefix, errors);
250
-
251
- // Validate model rules if present
252
- validateAgentModelRules(agent, prefix, errors);
253
-
254
- // Note: Detailed coverage gap checking (iteration ranges) is done in Phase 7
255
- }
256
-
257
- return { errors, warnings };
258
- }
259
-
260
- /**
261
- * Phase 2: Analyze message flow for structural problems
262
- */
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
- }
301
-
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();
314
-
315
- topicProducers.set('ISSUE_OPENED', ['system']);
316
-
317
- for (const agent of config.agents) {
318
- agentInputTopics.set(agent.id, []);
319
- agentOutputTopics.set(agent.id, []);
320
-
321
- recordAgentTriggers(agent, topicConsumers, agentInputTopics);
322
- recordAgentOutputs(agent, topicProducers, agentOutputTopics);
323
- }
324
-
325
- return {
326
- topicProducers,
327
- topicConsumers,
328
- agentOutputTopics,
329
- agentInputTopics,
330
- };
331
- }
332
-
333
- function reportMissingBootstrap(topicConsumers, errors) {
334
- const issueOpenedConsumers = topicConsumers.get('ISSUE_OPENED') || [];
335
- if (issueOpenedConsumers.length === 0) {
336
- errors.push(
337
- 'No agent triggers on ISSUE_OPENED. Cluster will never start. ' +
338
- 'Add a trigger: { "topic": "ISSUE_OPENED", "action": "execute_task" }'
339
- );
340
- }
341
- }
342
-
343
- function reportCompletionHandlers(config, errors, warnings) {
344
- const completionHandlers = config.agents.filter(
345
- (a) =>
346
- a.triggers?.some((t) => t.action === 'stop_cluster') ||
347
- a.id === 'completion-detector' ||
348
- a.id === 'git-pusher' ||
349
- a.hooks?.onComplete?.config?.topic === 'CLUSTER_COMPLETE'
350
- );
351
- const isTemplateConfig = config.params && Object.keys(config.params).length > 0;
352
-
353
- if (completionHandlers.length === 0) {
354
- const message =
355
- 'No completion handler found. Cluster will run until idle timeout (2 min). ' +
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
- }
362
- } else if (completionHandlers.length > 1) {
363
- errors.push(
364
- `Multiple completion handlers: [${completionHandlers.map((a) => a.id).join(', ')}]. ` +
365
- 'This causes race conditions. Keep only one.'
366
- );
367
- }
368
- }
369
-
370
- function reportOrphanTopics(topicProducers, topicConsumers, warnings) {
371
- for (const [topic, producers] of topicProducers) {
372
- if (topic === 'CLUSTER_COMPLETE') {
373
- continue;
374
- }
375
-
376
- const consumers = topicConsumers.get(topic) || [];
377
- if (consumers.length === 0) {
378
- warnings.push(
379
- `Topic '${topic}' is produced by [${producers.join(', ')}] but never consumed. Dead end.`
380
- );
381
- }
382
- }
383
- }
384
-
385
- function reportUnproducedTopics(topicConsumers, topicProducers, errors) {
386
- for (const [topic, consumers] of topicConsumers) {
387
- if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') {
388
- continue;
389
- }
390
- if (topic.endsWith('*')) {
391
- continue;
392
- }
393
-
394
- const producers = topicProducers.get(topic) || [];
395
- if (producers.length === 0) {
396
- errors.push(
397
- `Topic '${topic}' consumed by [${consumers.join(', ')}] but never produced. ` +
398
- 'These agents will never trigger.'
399
- );
400
- }
401
- }
402
- }
403
-
404
- function reportSelfTriggeringAgents(config, agentInputTopics, agentOutputTopics, errors) {
405
- for (const agent of config.agents) {
406
- const inputs = agentInputTopics.get(agent.id) || [];
407
- const outputs = agentOutputTopics.get(agent.id) || [];
408
- const selfTrigger = inputs.find((t) => outputs.includes(t));
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) {
417
- errors.push(
418
- `Agent '${agent.id}' triggers on '${selfTrigger}' and produces '${selfTrigger}'. ` +
419
- 'Instant infinite loop.'
420
- );
421
- }
422
- }
423
- }
424
-
425
- function reportTwoAgentCycles(config, agentInputTopics, agentOutputTopics, warnings) {
426
- for (const agentA of config.agents) {
427
- const outputsA = agentOutputTopics.get(agentA.id) || [];
428
- for (const agentB of config.agents) {
429
- if (agentA.id === agentB.id) {
430
- continue;
431
- }
432
-
433
- const inputsB = agentInputTopics.get(agentB.id) || [];
434
- const outputsB = agentOutputTopics.get(agentB.id) || [];
435
- const inputsA = agentInputTopics.get(agentA.id) || [];
436
-
437
- const aToB = outputsA.some((t) => inputsB.includes(t));
438
- const bToA = outputsB.some((t) => inputsA.includes(t));
439
-
440
- if (aToB && bToA) {
441
- const hasEscapeLogic =
442
- agentA.triggers?.some((t) => t.logic) || agentB.triggers?.some((t) => t.logic);
443
- if (!hasEscapeLogic) {
444
- warnings.push(
445
- `Circular dependency: '${agentA.id}' ↔ '${agentB.id}'. ` +
446
- 'Add logic conditions to prevent infinite loop, or ensure maxIterations is set.'
447
- );
448
- }
449
- }
450
- }
451
- }
452
- }
453
-
454
- function reportMissingValidationTriggers(config, errors) {
455
- const validators = config.agents.filter((a) => a.role === 'validator');
456
- const workers = config.agents.filter((a) => a.role === 'implementation');
457
-
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": {...} }'
470
- );
471
- }
472
- }
473
- }
474
-
475
- function reportMissingContextTopics(config, warnings) {
476
- for (const agent of config.agents) {
477
- if (!agent.contextStrategy?.sources) {
478
- continue;
479
- }
480
-
481
- const triggerTopics = (agent.triggers || []).map((t) => t.topic);
482
- const contextTopics = agent.contextStrategy.sources.map((s) => s.topic);
483
-
484
- for (const triggerTopic of triggerTopics) {
485
- if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED') {
486
- continue;
487
- }
488
- if (triggerTopic.endsWith('*')) {
489
- continue;
490
- }
491
-
492
- if (!contextTopics.includes(triggerTopic)) {
493
- warnings.push(
494
- `Agent '${agent.id}' triggers on '${triggerTopic}' but doesn't include it in contextStrategy. ` +
495
- 'Agent may not see what triggered it.'
496
- );
497
- }
498
- }
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);
517
-
518
- return { errors, warnings };
519
- }
520
-
521
- /**
522
- * Phase 3: Validate agent-specific configurations
523
- */
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
- }
530
-
531
- function agentExecutesTask(agent) {
532
- return agent.triggers?.some((t) => t.action === 'execute_task' || (!t.action && !t.logic));
533
- }
534
-
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`);
558
- }
559
- }
560
- }
561
-
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;
618
- }
619
-
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(', ')}]`
625
- );
626
- }
627
- }
628
- }
629
- }
630
- }
631
-
632
- function validateAgents(config) {
633
- const errors = [];
634
- const warnings = [];
635
-
636
- const roles = new Map(); // role -> [agentIds]
637
-
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);
646
- }
647
-
648
- // Check for role references in logic scripts
649
- // IMPORTANT: Changed from error to warning because some triggers are designed to be
650
- // no-ops when the referenced role doesn't exist (e.g., worker's VALIDATION_RESULT
651
- // trigger returns false when validators.length === 0)
652
- warnMissingRoleReferences(config.agents, roles, warnings);
653
-
654
- return { errors, warnings };
655
- }
656
-
657
- /**
658
- * Phase 4: Validate logic scripts (syntax only, not semantics)
659
- */
660
- function validateLogicScripts(config) {
661
- const errors = [];
662
- const warnings = [];
663
-
664
- const vm = require('vm');
665
-
666
- for (const agent of config.agents) {
667
- for (const trigger of agent.triggers || []) {
668
- if (!trigger.logic?.script) continue;
669
-
670
- const script = trigger.logic.script;
671
-
672
- // Syntax check
673
- try {
674
- const wrappedScript = `(function() { ${script} })()`;
675
- new vm.Script(wrappedScript);
676
- } catch (syntaxError) {
677
- errors.push(`Agent '${agent.id}' has invalid logic script: ${syntaxError.message}`);
678
- continue;
679
- }
680
-
681
- // Check for common mistakes - only flag if script is JUST "return false" or "return true"
682
- // Complex scripts with conditionals should not trigger this
683
- const trimmedScript = script.trim().replace(/\s+/g, ' ');
684
- const isSimpleReturnFalse = /^return\s+false;?$/.test(trimmedScript);
685
- const isSimpleReturnTrue = /^return\s+true;?$/.test(trimmedScript);
686
-
687
- if (isSimpleReturnFalse) {
688
- warnings.push(
689
- `Agent '${agent.id}' logic is just 'return false'. Agent will never trigger.`
690
- );
691
- }
692
-
693
- if (isSimpleReturnTrue) {
694
- warnings.push(
695
- `Agent '${agent.id}' logic is just 'return true'. Consider adding conditions or removing the logic block.`
696
- );
697
- }
698
-
699
- // Check for undefined variable access (common typos)
700
- const knownVars = [
701
- 'ledger',
702
- 'cluster',
703
- 'message',
704
- 'agent',
705
- 'helpers',
706
- 'Set',
707
- 'Map',
708
- 'Array',
709
- 'Object',
710
- 'JSON',
711
- 'Date',
712
- 'Math',
713
- ];
714
- const varPattern = /\b([a-zA-Z_]\w*)\s*\./g;
715
- let match;
716
- while ((match = varPattern.exec(script)) !== null) {
717
- const varName = match[1];
718
- if (
719
- !knownVars.includes(varName) &&
720
- !script.includes(`const ${varName}`) &&
721
- !script.includes(`let ${varName}`)
722
- ) {
723
- warnings.push(
724
- `Agent '${agent.id}' logic uses '${varName}' which may be undefined. ` +
725
- `Available: [${knownVars.join(', ')}]`
726
- );
727
- break; // Only warn once per agent
728
- }
729
- }
730
- }
731
- }
732
-
733
- return { errors, warnings };
734
- }
735
-
736
- /**
737
- * Phase 5: Validate template variables against jsonSchema
738
- * Ensures {{result.*}} references in hooks match defined schema properties
739
- *
740
- * Issue #14 - Gap 3:
741
- * - Gap 3: Template variables don't exist (line 582-656)
742
- *
743
- */
744
- function validateTemplateVariables(config, depth = 0) {
745
- const errors = [];
746
- const warnings = [];
747
-
748
- if (!config.agents || !Array.isArray(config.agents)) {
749
- return { errors, warnings };
750
- }
751
-
752
- const prefix = depth > 0 ? `Sub-cluster (depth ${depth}): ` : '';
753
-
754
- for (const agent of config.agents) {
755
- // Skip subclusters - they have their own validation
756
- if (agent.type === 'subcluster') {
757
- // Recursively validate subcluster config
758
- if (agent.config?.agents) {
759
- const subResult = validateTemplateVariables(agent.config, depth + 1);
760
- // Prefix sub-cluster errors with agent ID
761
- errors.push(...subResult.errors.map((e) => `Sub-cluster '${agent.id}': ${e}`));
762
- warnings.push(...subResult.warnings.map((w) => `Sub-cluster '${agent.id}': ${w}`));
763
- }
764
- continue;
765
- }
766
-
767
- const result = validateAgentTemplateVariables(agent, agent.id);
768
- errors.push(...result.errors.map((e) => `${prefix}${e}`));
769
- warnings.push(...result.warnings.map((w) => `${prefix}${w}`));
770
- }
771
-
772
- return { errors, warnings };
773
- }
774
-
775
- /**
776
- * Validate template variables for a single agent
777
- * @param {Object} agent - Agent configuration
778
- * @param {String} agentId - Agent ID for error messages
779
- * @returns {{ errors: string[], warnings: string[] }}
780
- */
781
- function validateAgentTemplateVariables(agent, agentId) {
782
- const errors = [];
783
- const warnings = [];
784
-
785
- // Extract schema properties (null if non-JSON output or text output)
786
- const schemaProps = extractSchemaProperties(agent);
787
-
788
- // If schemaProps is null, this agent doesn't use JSON output - skip validation
789
- if (schemaProps === null) {
790
- return { errors, warnings };
791
- }
792
-
793
- // Extract template variables from hooks
794
- const templateVars = extractTemplateVariables(agent);
795
-
796
- // Check for undefined references (ERROR)
797
- for (const varName of templateVars) {
798
- if (!schemaProps.has(varName)) {
799
- const availableProps = Array.from(schemaProps).join(', ');
800
- errors.push(
801
- `Agent '${agentId}': Template uses '{{result.${varName}}}' but '${varName}' is not defined in jsonSchema. ` +
802
- `Available properties: [${availableProps}]`
803
- );
804
- }
805
- }
806
-
807
- // Check for unused schema properties (WARNING)
808
- for (const prop of schemaProps) {
809
- if (!templateVars.has(prop)) {
810
- warnings.push(
811
- `Agent '${agentId}': Schema property '${prop}' is defined but never referenced in hooks. ` +
812
- `Consider removing it to save tokens.`
813
- );
814
- }
815
- }
816
-
817
- return { errors, warnings };
818
- }
819
-
820
- /**
821
- * Extract all template variables ({{result.*}}) from agent hooks
822
- * Searches hooks.onComplete.config (recursive) and hooks.onComplete.transform.script
823
- * Also searches triggers[].onComplete patterns
824
- * @param {Object} agent - Agent configuration
825
- * @returns {Set<string>} Set of variable names referenced
826
- */
827
- function extractTemplateVariables(agent) {
828
- const variables = new Set();
829
-
830
- // Regex patterns - reset lastIndex before each use to avoid state pollution
831
- const mustachePattern = /\{\{result\.([^}]+)\}\}/g;
832
- const directPattern = /\bresult\.([a-zA-Z_][a-zA-Z0-9_]*)/g;
833
-
834
- /**
835
- * Recursively traverse an object/array and extract template variables from strings
836
- */
837
- function traverseAndExtract(obj) {
838
- if (obj === null || obj === undefined) {
839
- return;
840
- }
841
-
842
- if (typeof obj === 'string') {
843
- // Extract mustache-style {{result.field}}
844
- mustachePattern.lastIndex = 0;
845
- let match;
846
- while ((match = mustachePattern.exec(obj)) !== null) {
847
- variables.add(match[1]);
848
- }
849
- return;
850
- }
851
-
852
- if (Array.isArray(obj)) {
853
- for (const item of obj) {
854
- traverseAndExtract(item);
855
- }
856
- return;
857
- }
858
-
859
- if (typeof obj === 'object') {
860
- for (const value of Object.values(obj)) {
861
- traverseAndExtract(value);
862
- }
863
- }
864
- }
865
-
866
- /**
867
- * Extract variables from transform script (direct result.field access)
868
- */
869
- function extractFromScript(script) {
870
- if (typeof script !== 'string') {
871
- return;
872
- }
873
-
874
- directPattern.lastIndex = 0;
875
- let match;
876
- while ((match = directPattern.exec(script)) !== null) {
877
- variables.add(match[1]);
878
- }
879
- }
880
-
881
- // Extract from hooks.onComplete.config
882
- if (agent.hooks?.onComplete?.config) {
883
- traverseAndExtract(agent.hooks.onComplete.config);
884
- }
885
-
886
- // Extract from hooks.onComplete.transform.script
887
- if (agent.hooks?.onComplete?.transform?.script) {
888
- extractFromScript(agent.hooks.onComplete.transform.script);
889
- }
890
-
891
- // Extract from triggers[].onComplete (some agents define hooks per-trigger)
892
- if (agent.triggers && Array.isArray(agent.triggers)) {
893
- for (const trigger of agent.triggers) {
894
- if (trigger.onComplete?.config) {
895
- traverseAndExtract(trigger.onComplete.config);
896
- }
897
- if (trigger.onComplete?.transform?.script) {
898
- extractFromScript(trigger.onComplete.transform.script);
899
- }
900
- }
901
- }
902
-
903
- return variables;
904
- }
905
-
906
- /**
907
- * Extract schema properties from agent's jsonSchema
908
- * @param {Object} agent - Agent configuration
909
- * @returns {Set<string>|null} Set of property names, or null if agent doesn't use JSON output
910
- */
911
- function extractSchemaProperties(agent) {
912
- // Non-JSON agents don't need validation
913
- // Both 'json' and 'stream-json' use jsonSchema and need validation
914
- if (!['json', 'stream-json'].includes(agent.outputFormat)) {
915
- return null;
916
- }
917
-
918
- // If explicit schema is provided, use its properties
919
- if (agent.jsonSchema?.properties) {
920
- return new Set(Object.keys(agent.jsonSchema.properties));
921
- }
922
-
923
- // Default schema when outputFormat is 'json' but no explicit schema
924
- // See: agent-config.js:62-69
925
- return new Set(['summary', 'result']);
926
- }
927
-
928
- /**
929
- * Check if iteration pattern is valid
930
- */
931
- function isValidIterationPattern(pattern) {
932
- if (pattern === 'all') return true;
933
- if (/^\d+$/.test(pattern)) return true; // "1"
934
- if (/^\d+-\d+$/.test(pattern)) return true; // "1-3"
935
- if (/^\d+\+$/.test(pattern)) return true; // "5+"
936
- return false;
937
- }
938
-
939
- /**
940
- * Format validation result for CLI output
941
- */
942
- function formatValidationResult(result) {
943
- const lines = [];
944
-
945
- if (result.valid) {
946
- lines.push('✅ Configuration is valid');
947
- } else {
948
- lines.push('❌ Configuration has errors');
949
- }
950
-
951
- if (result.errors.length > 0) {
952
- lines.push('\nErrors:');
953
- for (const error of result.errors) {
954
- lines.push(` ❌ ${error}`);
955
- }
956
- }
957
-
958
- if (result.warnings.length > 0) {
959
- lines.push('\nWarnings:');
960
- for (const warning of result.warnings) {
961
- lines.push(` ⚠️ ${warning}`);
962
- }
963
- }
964
-
965
- return lines.join('\n');
966
- }
967
-
968
- /**
969
- * Phase 6: Hook semantic validation
970
- * Catches runtime failures in hook execution (agent-hook-executor.js)
971
- *
972
- * Issue #14 - Gaps 1, 2, 7:
973
- * - Gap 1: Hook action field missing (line 837-842)
974
- * - Gap 2: Transform script output shape (line 846-866)
975
- * - Gap 7: Conductor CLUSTER_OPERATIONS payload (line 869-888)
976
- *
977
- * @param {Object} config - Cluster configuration
978
- * @returns {{ errors: string[], warnings: string[] }}
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
-
1067
- function validateHookSemantics(config) {
1068
- const errors = [];
1069
- const warnings = [];
1070
-
1071
- if (!config.agents || !Array.isArray(config.agents)) {
1072
- return { errors, warnings };
1073
- }
1074
-
1075
- for (const agent of config.agents) {
1076
- // Skip subclusters - they have their own validation
1077
- if (agent.type === 'subcluster') {
1078
- continue;
1079
- }
1080
-
1081
- const hooks = agent.hooks || {};
1082
- const hookTypes = ['onComplete', 'onFailure', 'onTimeout'];
1083
-
1084
- for (const hookType of hookTypes) {
1085
- const hook = hooks[hookType];
1086
- if (!hook) continue;
1087
-
1088
- const prefix = `Agent '${agent.id}' hooks.${hookType}`;
1089
-
1090
- // === GAP 1: Hook action field missing ===
1091
- // Causes runtime crash at agent-hook-executor.js:66
1092
- validateHookAction(hook, prefix, errors);
1093
-
1094
- // === GAP 2: Transform script output shape validation ===
1095
- // Causes runtime crash at agent-hook-executor.js:148
1096
- validateTransformScript(hook, prefix, errors);
1097
-
1098
- // === GAP 7: CLUSTER_OPERATIONS payload validation ===
1099
- // Causes runtime crash at orchestrator.js:722
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);
1105
- }
1106
- }
1107
-
1108
- return { errors, warnings };
1109
- }
1110
-
1111
- /**
1112
- * Phase 7: Rule coverage validation
1113
- * Catches gaps in model rules and prompt rules that cause runtime failures
1114
- *
1115
- * Issue #14 - Gaps 4, 5:
1116
- * - Gap 4: Model rule iteration gaps (line 916-963)
1117
- * - Gap 5: Prompt rule iteration gaps (line 965-1014)
1118
- *
1119
- * @param {Object} config - Cluster configuration
1120
- * @returns {{ errors: string[], warnings: string[] }}
1121
- */
1122
- function validateRuleCoverage(config) {
1123
- const errors = [];
1124
- const warnings = [];
1125
-
1126
- if (!config.agents || !Array.isArray(config.agents)) {
1127
- return { errors, warnings };
1128
- }
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
-
1190
- for (const agent of config.agents) {
1191
- if (agent.type === 'subcluster') {
1192
- continue;
1193
- }
1194
-
1195
- const maxIterations = agent.maxIterations || 30;
1196
-
1197
- // === GAP 4: Model rule iteration gaps ===
1198
- // Causes runtime crash at agent-wrapper.js:154
1199
- if (agent.modelRules && Array.isArray(agent.modelRules)) {
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
- );
1209
- }
1210
-
1211
- // === GAP 5: Prompt rule iteration gaps ===
1212
- // Causes runtime crash at agent-wrapper.js:222
1213
- if (
1214
- agent.promptConfig &&
1215
- agent.promptConfig.type === 'rules' &&
1216
- agent.promptConfig.rules &&
1217
- Array.isArray(agent.promptConfig.rules)
1218
- ) {
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
- );
1228
- }
1229
- }
1230
-
1231
- return { errors, warnings };
1232
- }
1233
-
1234
- /**
1235
- * Helper: Group consecutive numbers into ranges for readable output
1236
- * Example: [1, 2, 3, 5, 7, 8, 9] -> ["1-3", "5", "7-9"]
1237
- */
1238
- function groupConsecutive(numbers) {
1239
- if (numbers.length === 0) return [];
1240
-
1241
- const sorted = [...numbers].sort((a, b) => a - b);
1242
- const ranges = [];
1243
- let rangeStart = sorted[0];
1244
- let rangeEnd = sorted[0];
1245
-
1246
- for (let i = 1; i < sorted.length; i++) {
1247
- if (sorted[i] === rangeEnd + 1) {
1248
- rangeEnd = sorted[i];
1249
- } else {
1250
- // End of range
1251
- if (rangeStart === rangeEnd) {
1252
- ranges.push(`${rangeStart}`);
1253
- } else {
1254
- ranges.push(`${rangeStart}-${rangeEnd}`);
1255
- }
1256
- rangeStart = sorted[i];
1257
- rangeEnd = sorted[i];
1258
- }
1259
- }
1260
-
1261
- // Add final range
1262
- if (rangeStart === rangeEnd) {
1263
- ranges.push(`${rangeStart}`);
1264
- } else {
1265
- ranges.push(`${rangeStart}-${rangeEnd}`);
1266
- }
1267
-
1268
- return ranges;
1269
- }
1270
-
1271
- /**
1272
- * Phase 8: N-agent cycle detection
1273
- * Detects circular dependencies with 3+ agents using DFS
1274
- *
1275
- * Issue #14 - Gap 6:
1276
- * - Gap 6: 3+ agent circular dependencies (line 1060-1165)
1277
- *
1278
- * @param {Object} config - Cluster configuration
1279
- * @returns {{ errors: string[], warnings: string[] }}
1280
- */
1281
- function collectAgentDependencies(agent, topicProducers) {
1282
- const dependencies = new Set();
1283
-
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
- }
1295
- }
1296
-
1297
- return Array.from(dependencies);
1298
- }
1299
-
1300
- function buildAgentGraph(config) {
1301
- const agentGraph = new Map();
1302
- const topicProducers = new Map();
1303
-
1304
- for (const agent of config.agents) {
1305
- if (agent.type === 'subcluster') continue;
1306
- agentGraph.set(agent.id, []);
1307
-
1308
- const outputTopic = agent.hooks?.onComplete?.config?.topic;
1309
- if (!outputTopic) continue;
1310
-
1311
- if (!topicProducers.has(outputTopic)) {
1312
- topicProducers.set(outputTopic, []);
1313
- }
1314
- topicProducers.get(outputTopic).push(agent.id);
1315
- }
1316
-
1317
- for (const agent of config.agents) {
1318
- if (agent.type === 'subcluster') continue;
1319
- agentGraph.set(agent.id, collectAgentDependencies(agent, topicProducers));
1320
- }
1321
-
1322
- return agentGraph;
1323
- }
1324
-
1325
- function findFirstCycle(agentGraph) {
1326
- const visited = new Set();
1327
- const recursionStack = new Set();
1328
-
1329
- const dfs = (agentId, path) => {
1330
- visited.add(agentId);
1331
- recursionStack.add(agentId);
1332
-
1333
- const dependencies = agentGraph.get(agentId) || [];
1334
- for (const nextAgent of dependencies) {
1335
- if (recursionStack.has(nextAgent)) {
1336
- const cycleStartIndex = path.indexOf(nextAgent);
1337
- return [...path.slice(cycleStartIndex), nextAgent];
1338
- }
1339
-
1340
- if (!visited.has(nextAgent)) {
1341
- const cycle = dfs(nextAgent, [...path, nextAgent]);
1342
- if (cycle) return cycle;
1343
- }
1344
- }
1345
-
1346
- recursionStack.delete(agentId);
1347
- return null;
1348
- };
1349
-
1350
- for (const agentId of agentGraph.keys()) {
1351
- if (!visited.has(agentId)) {
1352
- const cycle = dfs(agentId, [agentId]);
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)) {
1510
- warnings.push(
1511
- `[Gap 10] ${prefix}: Docker mount host path '${mount.host}' is not absolute. ` +
1512
- `May cause runtime errors.`
1513
- );
1514
- }
1515
- }
1516
- }
1517
-
1518
- return;
1519
- }
1520
-
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
- }
1597
- }
1598
-
1599
- /**
1600
- * Phase 9: Configuration semantic validation
1601
- * Validates configuration fields that can cause runtime failures
1602
- *
1603
- * Issue #14 - Gaps 8-15:
1604
- * - Gap 8: JSON schema structurally invalid (line 1219-1237)
1605
- * - Gap 9: Context sources never produced (line 1240-1283)
1606
- * - Gap 10: Isolation config invalid (line 1286-1312)
1607
- * - Gap 11: Agent ID conflicts across subclusters (line 1185-1212)
1608
- * - Gap 12: Load config file paths don't exist (line 1315-1323)
1609
- * - Gap 13: Task executor config invalid (line 1326-1352)
1610
- * - Gap 14: Context source format invalid (line 1270-1282)
1611
- * - Gap 15: Role references in logic (stricter) (line 1354-1383)
1612
- *
1613
- * @param {Object} config - Cluster configuration
1614
- * @returns {{ errors: string[], warnings: string[] }}
1615
- */
1616
- function validateConfigSemantics(config) {
1617
- const errors = [];
1618
- const warnings = [];
1619
-
1620
- if (!config.agents || !Array.isArray(config.agents)) {
1621
- return { errors, warnings };
1622
- }
1623
-
1624
- const fs = require('fs');
1625
- const path = require('path');
1626
-
1627
- // === GAP 11: Agent ID conflicts across subclusters ===
1628
- collectAgentIdConflicts(config.agents, errors);
1629
-
1630
- const topicProducers = buildTopicProducers(config.agents);
1631
- const roles = new Set(
1632
- config.agents.filter((agent) => agent.type !== 'subcluster').map((agent) => agent.role)
1633
- );
1634
-
1635
- for (const agent of config.agents) {
1636
- if (agent.type === 'subcluster') continue;
1637
-
1638
- const prefix = `Agent '${agent.id}'`;
1639
-
1640
- // === GAP 8: JSON schema structurally invalid ===
1641
- validateJsonSchema(prefix, agent, errors);
1642
-
1643
- // === GAP 9: Context sources never produced (enhanced check) ===
1644
- // Already partially covered in Phase 2, but add stricter checks
1645
- validateContextSources(prefix, agent, topicProducers, errors, warnings);
1646
-
1647
- // === GAP 10: Isolation config invalid ===
1648
- validateIsolationConfig(prefix, agent, path, errors, warnings);
1649
-
1650
- // === GAP 12: Load config file paths don't exist ===
1651
- validateLoadConfig(prefix, agent, fs, errors);
1652
-
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
- );
1742
- }
1743
- }
1744
- }
1745
-
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
- }
1754
-
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
- );
1806
- }
1807
- return;
1808
- }
1809
-
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
- );
1823
- }
1824
- }
1825
- }
1826
-
1827
- function validateModelRulesSupport(agent, provider, catalog, levels, warnings) {
1828
- if (!agent.modelRules || !Array.isArray(agent.modelRules)) return;
1829
-
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
- }
1843
-
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);
1868
- }
1869
- }
1870
-
1871
- if (!config.agents || !Array.isArray(config.agents)) {
1872
- return { errors, warnings };
1873
- }
1874
-
1875
- for (const agent of config.agents) {
1876
- if (agent.type === 'subcluster') {
1877
- continue;
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);
1897
- }
1898
-
1899
- return { errors, warnings };
1900
- }
1901
-
1902
- module.exports = {
1903
- validateConfig,
1904
- isConductorConfig,
1905
- validateBasicStructure,
1906
- analyzeMessageFlow,
1907
- validateAgents,
1908
- validateLogicScripts,
1909
- isValidIterationPattern,
1910
- formatValidationResult,
1911
- // Phase 5: Template variable validation
1912
- validateTemplateVariables,
1913
- extractTemplateVariables,
1914
- extractSchemaProperties,
1915
- validateAgentTemplateVariables,
1916
- // Phase 6-9: Semantic validation
1917
- validateHookSemantics,
1918
- validateRuleCoverage,
1919
- detectNAgentCycles,
1920
- validateConfigSemantics,
1921
- validateProviderLevel,
1922
- validateProviderSettings,
1923
- validateProviderFeatures,
1924
- groupConsecutive,
1925
- };