@covibes/zeroshot 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -14
- package/cli/commands/providers.js +8 -9
- package/cli/index.js +3032 -2409
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +1 -1
- package/cluster-templates/base-templates/full-workflow.json +72 -188
- package/cluster-templates/base-templates/worker-validator.json +59 -3
- package/cluster-templates/conductor-bootstrap.json +4 -4
- package/lib/docker-config.js +8 -0
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-names.js +2 -1
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +161 -63
- package/package.json +7 -2
- package/scripts/setup-merge-queue.sh +170 -0
- package/src/agent/agent-config.js +135 -82
- package/src/agent/agent-context-builder.js +297 -188
- package/src/agent/agent-hook-executor.js +310 -113
- package/src/agent/agent-lifecycle.js +385 -325
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +824 -565
- package/src/agent/output-extraction.js +41 -24
- package/src/agent/output-reformatter.js +1 -1
- package/src/agent/schema-utils.js +108 -73
- package/src/agent-wrapper.js +10 -1
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +85 -34
- package/src/config-validator.js +922 -657
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +289 -199
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +22 -5
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1107 -811
- package/src/preflight.js +313 -159
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +53 -25
- package/src/providers/anthropic/index.js +72 -2
- package/src/providers/anthropic/output-parser.js +32 -14
- package/src/providers/base-provider.js +70 -0
- package/src/providers/capabilities.js +9 -0
- package/src/providers/google/output-parser.js +47 -38
- package/src/providers/index.js +19 -3
- package/src/providers/openai/cli-builder.js +14 -3
- package/src/providers/openai/index.js +1 -0
- package/src/providers/openai/output-parser.js +44 -30
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +76 -35
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +118 -81
- package/task-lib/claude-recovery.js +61 -24
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +2 -2
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +84 -27
- package/task-lib/scheduler.js +1 -1
- package/task-lib/store.js +467 -168
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +128 -90
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -139
package/src/config-validator.js
CHANGED
|
@@ -117,6 +117,100 @@ function validateConfig(config, depth = 0) {
|
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
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
|
+
|
|
120
214
|
/**
|
|
121
215
|
* Phase 1: Validate basic structure (fields, types, duplicates)
|
|
122
216
|
*/
|
|
@@ -143,91 +237,21 @@ function validateBasicStructure(config, depth = 0) {
|
|
|
143
237
|
// Check if this is a subcluster
|
|
144
238
|
const isSubCluster = agent.type === 'subcluster';
|
|
145
239
|
|
|
146
|
-
|
|
147
|
-
if (!agent.id) {
|
|
148
|
-
errors.push(`${prefix}.id is required`);
|
|
149
|
-
} else if (typeof agent.id !== 'string') {
|
|
150
|
-
errors.push(`${prefix}.id must be a string`);
|
|
151
|
-
} else if (seenIds.has(agent.id)) {
|
|
152
|
-
errors.push(`Duplicate agent id: "${agent.id}"`);
|
|
153
|
-
} else {
|
|
154
|
-
seenIds.add(agent.id);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!agent.role) {
|
|
158
|
-
errors.push(`${prefix}.role is required`);
|
|
159
|
-
}
|
|
240
|
+
validateAgentIdentity(agent, prefix, seenIds, errors);
|
|
160
241
|
|
|
161
242
|
// Validate subclusters
|
|
162
243
|
if (isSubCluster) {
|
|
163
|
-
|
|
164
|
-
const subResult = subClusterSchema.validateSubCluster(agent, depth);
|
|
165
|
-
errors.push(...subResult.errors);
|
|
166
|
-
warnings.push(...subResult.warnings);
|
|
244
|
+
validateSubclusterAgent(agent, depth, errors, warnings);
|
|
167
245
|
continue; // Skip regular agent validation
|
|
168
246
|
}
|
|
169
247
|
|
|
170
248
|
// Regular agent validation
|
|
171
|
-
|
|
172
|
-
errors.push(`${prefix}.triggers array is required`);
|
|
173
|
-
} else if (agent.triggers.length === 0) {
|
|
174
|
-
errors.push(`${prefix}.triggers cannot be empty (agent would never activate)`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Validate triggers structure
|
|
178
|
-
if (agent.triggers) {
|
|
179
|
-
for (let j = 0; j < agent.triggers.length; j++) {
|
|
180
|
-
const trigger = agent.triggers[j];
|
|
181
|
-
const triggerPrefix = `${prefix}.triggers[${j}]`;
|
|
182
|
-
|
|
183
|
-
if (!trigger.topic) {
|
|
184
|
-
errors.push(`${triggerPrefix}.topic is required`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (trigger.action && !['execute_task', 'stop_cluster'].includes(trigger.action)) {
|
|
188
|
-
errors.push(
|
|
189
|
-
`${triggerPrefix}.action must be 'execute_task' or 'stop_cluster', got '${trigger.action}'`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (trigger.logic) {
|
|
194
|
-
if (!trigger.logic.script) {
|
|
195
|
-
errors.push(`${triggerPrefix}.logic.script is required when logic is specified`);
|
|
196
|
-
}
|
|
197
|
-
if (trigger.logic.engine && trigger.logic.engine !== 'javascript') {
|
|
198
|
-
errors.push(
|
|
199
|
-
`${triggerPrefix}.logic.engine must be 'javascript', got '${trigger.logic.engine}'`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
249
|
+
validateAgentTriggers(agent, prefix, errors);
|
|
205
250
|
|
|
206
251
|
// Validate model rules if present
|
|
207
|
-
|
|
208
|
-
if (!Array.isArray(agent.modelRules)) {
|
|
209
|
-
errors.push(`${prefix}.modelRules must be an array`);
|
|
210
|
-
} else {
|
|
211
|
-
for (let j = 0; j < agent.modelRules.length; j++) {
|
|
212
|
-
const rule = agent.modelRules[j];
|
|
213
|
-
const rulePrefix = `${prefix}.modelRules[${j}]`;
|
|
214
|
-
|
|
215
|
-
if (!rule.iterations) {
|
|
216
|
-
errors.push(`${rulePrefix}.iterations is required`);
|
|
217
|
-
} else if (!isValidIterationPattern(rule.iterations)) {
|
|
218
|
-
errors.push(
|
|
219
|
-
`${rulePrefix}.iterations '${rule.iterations}' is invalid. Valid: "1", "1-3", "5+", "all"`
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!rule.model && !rule.modelLevel) {
|
|
224
|
-
errors.push(`${rulePrefix}.model or modelLevel is required`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
252
|
+
validateAgentModelRules(agent, prefix, errors);
|
|
227
253
|
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
}
|
|
254
|
+
// Note: Detailed coverage gap checking (iteration ranges) is done in Phase 7
|
|
231
255
|
}
|
|
232
256
|
|
|
233
257
|
return { errors, warnings };
|
|
@@ -236,45 +260,77 @@ function validateBasicStructure(config, depth = 0) {
|
|
|
236
260
|
/**
|
|
237
261
|
* Phase 2: Analyze message flow for structural problems
|
|
238
262
|
*/
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
|
242
308
|
|
|
243
|
-
|
|
244
|
-
const topicProducers = new Map();
|
|
245
|
-
const topicConsumers = new Map();
|
|
246
|
-
const agentOutputTopics = new Map();
|
|
247
|
-
const agentInputTopics = new Map();
|
|
309
|
+
function buildMessageFlowGraph(config) {
|
|
310
|
+
const topicProducers = new Map();
|
|
311
|
+
const topicConsumers = new Map();
|
|
312
|
+
const agentOutputTopics = new Map();
|
|
313
|
+
const agentInputTopics = new Map();
|
|
248
314
|
|
|
249
|
-
// System always produces ISSUE_OPENED
|
|
250
315
|
topicProducers.set('ISSUE_OPENED', ['system']);
|
|
251
316
|
|
|
252
317
|
for (const agent of config.agents) {
|
|
253
318
|
agentInputTopics.set(agent.id, []);
|
|
254
319
|
agentOutputTopics.set(agent.id, []);
|
|
255
320
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const topic = trigger.topic;
|
|
259
|
-
if (!topicConsumers.has(topic)) {
|
|
260
|
-
topicConsumers.set(topic, []);
|
|
261
|
-
}
|
|
262
|
-
topicConsumers.get(topic).push(agent.id);
|
|
263
|
-
agentInputTopics.get(agent.id).push(topic);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Track what topics this agent produces (hooks)
|
|
267
|
-
const outputTopic = agent.hooks?.onComplete?.config?.topic;
|
|
268
|
-
if (outputTopic) {
|
|
269
|
-
if (!topicProducers.has(outputTopic)) {
|
|
270
|
-
topicProducers.set(outputTopic, []);
|
|
271
|
-
}
|
|
272
|
-
topicProducers.get(outputTopic).push(agent.id);
|
|
273
|
-
agentOutputTopics.get(agent.id).push(outputTopic);
|
|
274
|
-
}
|
|
321
|
+
recordAgentTriggers(agent, topicConsumers, agentInputTopics);
|
|
322
|
+
recordAgentOutputs(agent, topicProducers, agentOutputTopics);
|
|
275
323
|
}
|
|
276
324
|
|
|
277
|
-
|
|
325
|
+
return {
|
|
326
|
+
topicProducers,
|
|
327
|
+
topicConsumers,
|
|
328
|
+
agentOutputTopics,
|
|
329
|
+
agentInputTopics,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function reportMissingBootstrap(topicConsumers, errors) {
|
|
278
334
|
const issueOpenedConsumers = topicConsumers.get('ISSUE_OPENED') || [];
|
|
279
335
|
if (issueOpenedConsumers.length === 0) {
|
|
280
336
|
errors.push(
|
|
@@ -282,8 +338,9 @@ function analyzeMessageFlow(config) {
|
|
|
282
338
|
'Add a trigger: { "topic": "ISSUE_OPENED", "action": "execute_task" }'
|
|
283
339
|
);
|
|
284
340
|
}
|
|
341
|
+
}
|
|
285
342
|
|
|
286
|
-
|
|
343
|
+
function reportCompletionHandlers(config, errors, warnings) {
|
|
287
344
|
const completionHandlers = config.agents.filter(
|
|
288
345
|
(a) =>
|
|
289
346
|
a.triggers?.some((t) => t.action === 'stop_cluster') ||
|
|
@@ -308,10 +365,14 @@ function analyzeMessageFlow(config) {
|
|
|
308
365
|
'This causes race conditions. Keep only one.'
|
|
309
366
|
);
|
|
310
367
|
}
|
|
368
|
+
}
|
|
311
369
|
|
|
312
|
-
|
|
370
|
+
function reportOrphanTopics(topicProducers, topicConsumers, warnings) {
|
|
313
371
|
for (const [topic, producers] of topicProducers) {
|
|
314
|
-
if (topic === 'CLUSTER_COMPLETE')
|
|
372
|
+
if (topic === 'CLUSTER_COMPLETE') {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
315
376
|
const consumers = topicConsumers.get(topic) || [];
|
|
316
377
|
if (consumers.length === 0) {
|
|
317
378
|
warnings.push(
|
|
@@ -319,11 +380,17 @@ function analyzeMessageFlow(config) {
|
|
|
319
380
|
);
|
|
320
381
|
}
|
|
321
382
|
}
|
|
383
|
+
}
|
|
322
384
|
|
|
323
|
-
|
|
385
|
+
function reportUnproducedTopics(topicConsumers, topicProducers, errors) {
|
|
324
386
|
for (const [topic, consumers] of topicConsumers) {
|
|
325
|
-
if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED')
|
|
326
|
-
|
|
387
|
+
if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (topic.endsWith('*')) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
327
394
|
const producers = topicProducers.get(topic) || [];
|
|
328
395
|
if (producers.length === 0) {
|
|
329
396
|
errors.push(
|
|
@@ -332,35 +399,45 @@ function analyzeMessageFlow(config) {
|
|
|
332
399
|
);
|
|
333
400
|
}
|
|
334
401
|
}
|
|
402
|
+
}
|
|
335
403
|
|
|
336
|
-
|
|
404
|
+
function reportSelfTriggeringAgents(config, agentInputTopics, agentOutputTopics, errors) {
|
|
337
405
|
for (const agent of config.agents) {
|
|
338
406
|
const inputs = agentInputTopics.get(agent.id) || [];
|
|
339
407
|
const outputs = agentOutputTopics.get(agent.id) || [];
|
|
340
408
|
const selfTrigger = inputs.find((t) => outputs.includes(t));
|
|
341
|
-
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) {
|
|
342
417
|
errors.push(
|
|
343
418
|
`Agent '${agent.id}' triggers on '${selfTrigger}' and produces '${selfTrigger}'. ` +
|
|
344
419
|
'Instant infinite loop.'
|
|
345
420
|
);
|
|
346
421
|
}
|
|
347
422
|
}
|
|
423
|
+
}
|
|
348
424
|
|
|
349
|
-
|
|
425
|
+
function reportTwoAgentCycles(config, agentInputTopics, agentOutputTopics, warnings) {
|
|
350
426
|
for (const agentA of config.agents) {
|
|
351
427
|
const outputsA = agentOutputTopics.get(agentA.id) || [];
|
|
352
428
|
for (const agentB of config.agents) {
|
|
353
|
-
if (agentA.id === agentB.id)
|
|
429
|
+
if (agentA.id === agentB.id) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
354
433
|
const inputsB = agentInputTopics.get(agentB.id) || [];
|
|
355
434
|
const outputsB = agentOutputTopics.get(agentB.id) || [];
|
|
356
435
|
const inputsA = agentInputTopics.get(agentA.id) || [];
|
|
357
436
|
|
|
358
|
-
// A produces what B consumes, AND B produces what A consumes
|
|
359
437
|
const aToB = outputsA.some((t) => inputsB.includes(t));
|
|
360
438
|
const bToA = outputsB.some((t) => inputsA.includes(t));
|
|
361
439
|
|
|
362
440
|
if (aToB && bToA) {
|
|
363
|
-
// This might be intentional (rejection loop), check if there's an escape
|
|
364
441
|
const hasEscapeLogic =
|
|
365
442
|
agentA.triggers?.some((t) => t.logic) || agentB.triggers?.some((t) => t.logic);
|
|
366
443
|
if (!hasEscapeLogic) {
|
|
@@ -372,35 +449,45 @@ function analyzeMessageFlow(config) {
|
|
|
372
449
|
}
|
|
373
450
|
}
|
|
374
451
|
}
|
|
452
|
+
}
|
|
375
453
|
|
|
376
|
-
|
|
454
|
+
function reportMissingValidationTriggers(config, errors) {
|
|
377
455
|
const validators = config.agents.filter((a) => a.role === 'validator');
|
|
378
456
|
const workers = config.agents.filter((a) => a.role === 'implementation');
|
|
379
457
|
|
|
380
|
-
if (validators.length
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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": {...} }'
|
|
384
470
|
);
|
|
385
|
-
if (!triggersOnValidation) {
|
|
386
|
-
errors.push(
|
|
387
|
-
`Worker '${worker.id}' has validators but doesn't trigger on VALIDATION_RESULT. ` +
|
|
388
|
-
'Rejections will be ignored. Add trigger: { "topic": "VALIDATION_RESULT", "logic": {...} }'
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
471
|
}
|
|
392
472
|
}
|
|
473
|
+
}
|
|
393
474
|
|
|
394
|
-
|
|
475
|
+
function reportMissingContextTopics(config, warnings) {
|
|
395
476
|
for (const agent of config.agents) {
|
|
396
|
-
if (!agent.contextStrategy?.sources)
|
|
477
|
+
if (!agent.contextStrategy?.sources) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
397
480
|
|
|
398
481
|
const triggerTopics = (agent.triggers || []).map((t) => t.topic);
|
|
399
482
|
const contextTopics = agent.contextStrategy.sources.map((s) => s.topic);
|
|
400
483
|
|
|
401
484
|
for (const triggerTopic of triggerTopics) {
|
|
402
|
-
if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED')
|
|
403
|
-
|
|
485
|
+
if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED') {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (triggerTopic.endsWith('*')) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
404
491
|
|
|
405
492
|
if (!contextTopics.includes(triggerTopic)) {
|
|
406
493
|
warnings.push(
|
|
@@ -410,6 +497,23 @@ function analyzeMessageFlow(config) {
|
|
|
410
497
|
}
|
|
411
498
|
}
|
|
412
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);
|
|
413
517
|
|
|
414
518
|
return { errors, warnings };
|
|
415
519
|
}
|
|
@@ -417,102 +521,135 @@ function analyzeMessageFlow(config) {
|
|
|
417
521
|
/**
|
|
418
522
|
* Phase 3: Validate agent-specific configurations
|
|
419
523
|
*/
|
|
420
|
-
function
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
}
|
|
423
530
|
|
|
424
|
-
|
|
531
|
+
function agentExecutesTask(agent) {
|
|
532
|
+
return agent.triggers?.some((t) => t.action === 'execute_task' || (!t.action && !t.logic));
|
|
533
|
+
}
|
|
425
534
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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`);
|
|
430
558
|
}
|
|
431
|
-
|
|
559
|
+
}
|
|
560
|
+
}
|
|
432
561
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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;
|
|
443
618
|
}
|
|
444
|
-
}
|
|
445
619
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (prompt?.includes(pattern)) {
|
|
452
|
-
errors.push(
|
|
453
|
-
`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(', ')}]`
|
|
454
625
|
);
|
|
455
626
|
}
|
|
456
627
|
}
|
|
457
628
|
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
458
631
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
`Agent '${agent.id}' has outputFormat: 'json' but no jsonSchema. ` +
|
|
463
|
-
'Output parsing may be unreliable.'
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Very high maxIterations
|
|
468
|
-
if (agent.maxIterations && agent.maxIterations > 50) {
|
|
469
|
-
warnings.push(
|
|
470
|
-
`Agent '${agent.id}' has maxIterations: ${agent.maxIterations}. ` +
|
|
471
|
-
'This may consume significant API credits if stuck in a loop.'
|
|
472
|
-
);
|
|
473
|
-
}
|
|
632
|
+
function validateAgents(config) {
|
|
633
|
+
const errors = [];
|
|
634
|
+
const warnings = [];
|
|
474
635
|
|
|
475
|
-
|
|
476
|
-
if (agent.role === 'implementation' && !agent.maxIterations) {
|
|
477
|
-
warnings.push(
|
|
478
|
-
`Implementation agent '${agent.id}' has no maxIterations. ` +
|
|
479
|
-
'Defaults to 30, but consider setting explicitly.'
|
|
480
|
-
);
|
|
481
|
-
}
|
|
636
|
+
const roles = new Map(); // role -> [agentIds]
|
|
482
637
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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);
|
|
491
646
|
}
|
|
492
647
|
|
|
493
648
|
// Check for role references in logic scripts
|
|
494
649
|
// IMPORTANT: Changed from error to warning because some triggers are designed to be
|
|
495
650
|
// no-ops when the referenced role doesn't exist (e.g., worker's VALIDATION_RESULT
|
|
496
651
|
// trigger returns false when validators.length === 0)
|
|
497
|
-
|
|
498
|
-
for (const trigger of agent.triggers || []) {
|
|
499
|
-
if (trigger.logic?.script) {
|
|
500
|
-
const script = trigger.logic.script;
|
|
501
|
-
const roleMatch = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
|
|
502
|
-
if (roleMatch) {
|
|
503
|
-
for (const match of roleMatch) {
|
|
504
|
-
const role = match.match(/['"](\w+)['"]/)[1];
|
|
505
|
-
if (!roles.has(role)) {
|
|
506
|
-
warnings.push(
|
|
507
|
-
`Agent '${agent.id}' logic references role '${role}' but no agent has that role. ` +
|
|
508
|
-
`Trigger may be a no-op. Available roles: [${Array.from(roles.keys()).join(', ')}]`
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
652
|
+
warnMissingRoleReferences(config.agents, roles, warnings);
|
|
516
653
|
|
|
517
654
|
return { errors, warnings };
|
|
518
655
|
}
|
|
@@ -840,6 +977,93 @@ function formatValidationResult(result) {
|
|
|
840
977
|
* @param {Object} config - Cluster configuration
|
|
841
978
|
* @returns {{ errors: string[], warnings: string[] }}
|
|
842
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
|
+
|
|
843
1067
|
function validateHookSemantics(config) {
|
|
844
1068
|
const errors = [];
|
|
845
1069
|
const warnings = [];
|
|
@@ -865,58 +1089,19 @@ function validateHookSemantics(config) {
|
|
|
865
1089
|
|
|
866
1090
|
// === GAP 1: Hook action field missing ===
|
|
867
1091
|
// Causes runtime crash at agent-hook-executor.js:66
|
|
868
|
-
|
|
869
|
-
errors.push(
|
|
870
|
-
`[Gap 1] ${prefix}: Missing 'action' field. ` +
|
|
871
|
-
`Fix: Add "action": "publish_message" or "action": "execute_system_command"`
|
|
872
|
-
);
|
|
873
|
-
}
|
|
1092
|
+
validateHookAction(hook, prefix, errors);
|
|
874
1093
|
|
|
875
1094
|
// === GAP 2: Transform script output shape validation ===
|
|
876
1095
|
// Causes runtime crash at agent-hook-executor.js:148
|
|
877
|
-
|
|
878
|
-
const script = hook.transform.script;
|
|
879
|
-
|
|
880
|
-
// Check if script returns an object with topic and content
|
|
881
|
-
// Simple heuristic: look for return statement with object
|
|
882
|
-
const hasReturnTopic = /return\s*\{[^}]*topic\s*:/i.test(script);
|
|
883
|
-
const hasReturnContent = /return\s*\{[^}]*content\s*:/i.test(script);
|
|
884
|
-
|
|
885
|
-
if (!hasReturnTopic) {
|
|
886
|
-
errors.push(
|
|
887
|
-
`[Gap 2] ${prefix}: Transform script must return object with 'topic' property. ` +
|
|
888
|
-
`Fix: return { topic: "TOPIC_NAME", content: {...} }`
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (!hasReturnContent) {
|
|
893
|
-
errors.push(
|
|
894
|
-
`[Gap 2] ${prefix}: Transform script must return object with 'content' property. ` +
|
|
895
|
-
`Fix: return { topic: "...", content: { data: result } }`
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
1096
|
+
validateTransformScript(hook, prefix, errors);
|
|
899
1097
|
|
|
900
1098
|
// === GAP 7: CLUSTER_OPERATIONS payload validation ===
|
|
901
1099
|
// Causes runtime crash at orchestrator.js:722
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
)
|
|
907
|
-
// Check if operations field is valid JSON structure
|
|
908
|
-
if (hook.transform?.script) {
|
|
909
|
-
const script = hook.transform.script;
|
|
910
|
-
// Look for operations field in return statement
|
|
911
|
-
const hasOperations = /operations\s*:/i.test(script);
|
|
912
|
-
if (!hasOperations) {
|
|
913
|
-
errors.push(
|
|
914
|
-
`[Gap 7] ${prefix}: CLUSTER_OPERATIONS message must include 'operations' field. ` +
|
|
915
|
-
`Fix: return { topic: "CLUSTER_OPERATIONS", content: { data: { operations: JSON.stringify([...]) } } }`
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
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);
|
|
920
1105
|
}
|
|
921
1106
|
}
|
|
922
1107
|
|
|
@@ -942,6 +1127,66 @@ function validateRuleCoverage(config) {
|
|
|
942
1127
|
return { errors, warnings };
|
|
943
1128
|
}
|
|
944
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
|
+
|
|
945
1190
|
for (const agent of config.agents) {
|
|
946
1191
|
if (agent.type === 'subcluster') {
|
|
947
1192
|
continue;
|
|
@@ -952,50 +1197,15 @@ function validateRuleCoverage(config) {
|
|
|
952
1197
|
// === GAP 4: Model rule iteration gaps ===
|
|
953
1198
|
// Causes runtime crash at agent-wrapper.js:154
|
|
954
1199
|
if (agent.modelRules && Array.isArray(agent.modelRules)) {
|
|
955
|
-
const coveredIterations =
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
} else if (/^\d+$/.test(pattern)) {
|
|
966
|
-
// Single iteration: "5"
|
|
967
|
-
coveredIterations.add(parseInt(pattern));
|
|
968
|
-
} else if (/^\d+-\d+$/.test(pattern)) {
|
|
969
|
-
// Range: "1-3"
|
|
970
|
-
const [start, end] = pattern.split('-').map((n) => parseInt(n));
|
|
971
|
-
for (let i = start; i <= end; i++) {
|
|
972
|
-
coveredIterations.add(i);
|
|
973
|
-
}
|
|
974
|
-
} else if (/^\d+\+$/.test(pattern)) {
|
|
975
|
-
// Open-ended: "5+"
|
|
976
|
-
const start = parseInt(pattern);
|
|
977
|
-
for (let i = start; i <= maxIterations; i++) {
|
|
978
|
-
coveredIterations.add(i);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Find gaps
|
|
984
|
-
const uncoveredIterations = [];
|
|
985
|
-
for (let i = 1; i <= maxIterations; i++) {
|
|
986
|
-
if (!coveredIterations.has(i)) {
|
|
987
|
-
uncoveredIterations.push(i);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
if (uncoveredIterations.length > 0) {
|
|
992
|
-
// Group consecutive iterations for readability
|
|
993
|
-
const ranges = groupConsecutive(uncoveredIterations);
|
|
994
|
-
errors.push(
|
|
995
|
-
`[Gap 4] Agent '${agent.id}': Model rules have gaps at iterations ${ranges.join(', ')}. ` +
|
|
996
|
-
`Fix: Add catch-all rule { "iterations": "all", "model": "sonnet" } or extend existing ranges.`
|
|
997
|
-
);
|
|
998
|
-
}
|
|
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
|
+
);
|
|
999
1209
|
}
|
|
1000
1210
|
|
|
1001
1211
|
// === GAP 5: Prompt rule iteration gaps ===
|
|
@@ -1006,44 +1216,15 @@ function validateRuleCoverage(config) {
|
|
|
1006
1216
|
agent.promptConfig.rules &&
|
|
1007
1217
|
Array.isArray(agent.promptConfig.rules)
|
|
1008
1218
|
) {
|
|
1009
|
-
const coveredIterations =
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
} else if (/^\d+$/.test(pattern)) {
|
|
1019
|
-
coveredIterations.add(parseInt(pattern));
|
|
1020
|
-
} else if (/^\d+-\d+$/.test(pattern)) {
|
|
1021
|
-
const [start, end] = pattern.split('-').map((n) => parseInt(n));
|
|
1022
|
-
for (let i = start; i <= end; i++) {
|
|
1023
|
-
coveredIterations.add(i);
|
|
1024
|
-
}
|
|
1025
|
-
} else if (/^\d+\+$/.test(pattern)) {
|
|
1026
|
-
const start = parseInt(pattern);
|
|
1027
|
-
for (let i = start; i <= maxIterations; i++) {
|
|
1028
|
-
coveredIterations.add(i);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const uncoveredIterations = [];
|
|
1034
|
-
for (let i = 1; i <= maxIterations; i++) {
|
|
1035
|
-
if (!coveredIterations.has(i)) {
|
|
1036
|
-
uncoveredIterations.push(i);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
if (uncoveredIterations.length > 0) {
|
|
1041
|
-
const ranges = groupConsecutive(uncoveredIterations);
|
|
1042
|
-
errors.push(
|
|
1043
|
-
`[Gap 5] Agent '${agent.id}': Prompt rules have gaps at iterations ${ranges.join(', ')}. ` +
|
|
1044
|
-
`Fix: Add catch-all rule { "iterations": "all", "prompt": "..." } or extend existing ranges.`
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
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
|
+
);
|
|
1047
1228
|
}
|
|
1048
1229
|
}
|
|
1049
1230
|
|
|
@@ -1097,70 +1278,63 @@ function groupConsecutive(numbers) {
|
|
|
1097
1278
|
* @param {Object} config - Cluster configuration
|
|
1098
1279
|
* @returns {{ errors: string[], warnings: string[] }}
|
|
1099
1280
|
*/
|
|
1100
|
-
function
|
|
1101
|
-
const
|
|
1102
|
-
const warnings = [];
|
|
1281
|
+
function collectAgentDependencies(agent, topicProducers) {
|
|
1282
|
+
const dependencies = new Set();
|
|
1103
1283
|
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
+
}
|
|
1106
1295
|
}
|
|
1107
1296
|
|
|
1108
|
-
|
|
1297
|
+
return Array.from(dependencies);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function buildAgentGraph(config) {
|
|
1109
1301
|
const agentGraph = new Map();
|
|
1110
|
-
const topicProducers = new Map();
|
|
1302
|
+
const topicProducers = new Map();
|
|
1111
1303
|
|
|
1112
|
-
// Initialize graph
|
|
1113
1304
|
for (const agent of config.agents) {
|
|
1114
1305
|
if (agent.type === 'subcluster') continue;
|
|
1115
1306
|
agentGraph.set(agent.id, []);
|
|
1116
1307
|
|
|
1117
|
-
// Track what topics this agent produces
|
|
1118
1308
|
const outputTopic = agent.hooks?.onComplete?.config?.topic;
|
|
1119
|
-
if (outputTopic)
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
topicProducers.get(outputTopic).push(agent.id);
|
|
1309
|
+
if (!outputTopic) continue;
|
|
1310
|
+
|
|
1311
|
+
if (!topicProducers.has(outputTopic)) {
|
|
1312
|
+
topicProducers.set(outputTopic, []);
|
|
1124
1313
|
}
|
|
1314
|
+
topicProducers.get(outputTopic).push(agent.id);
|
|
1125
1315
|
}
|
|
1126
1316
|
|
|
1127
|
-
// Build dependencies: agent consumes topic -> depends on agents that produce it
|
|
1128
1317
|
for (const agent of config.agents) {
|
|
1129
1318
|
if (agent.type === 'subcluster') continue;
|
|
1130
|
-
|
|
1131
|
-
const dependencies = new Set();
|
|
1132
|
-
|
|
1133
|
-
for (const trigger of agent.triggers || []) {
|
|
1134
|
-
const topic = trigger.topic;
|
|
1135
|
-
if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue;
|
|
1136
|
-
if (topic.endsWith('*')) continue; // Skip wildcards
|
|
1137
|
-
|
|
1138
|
-
const producers = topicProducers.get(topic) || [];
|
|
1139
|
-
for (const producer of producers) {
|
|
1140
|
-
if (producer !== agent.id) {
|
|
1141
|
-
dependencies.add(producer);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
agentGraph.set(agent.id, Array.from(dependencies));
|
|
1319
|
+
agentGraph.set(agent.id, collectAgentDependencies(agent, topicProducers));
|
|
1147
1320
|
}
|
|
1148
1321
|
|
|
1149
|
-
|
|
1322
|
+
return agentGraph;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function findFirstCycle(agentGraph) {
|
|
1150
1326
|
const visited = new Set();
|
|
1151
1327
|
const recursionStack = new Set();
|
|
1152
1328
|
|
|
1153
|
-
|
|
1329
|
+
const dfs = (agentId, path) => {
|
|
1154
1330
|
visited.add(agentId);
|
|
1155
1331
|
recursionStack.add(agentId);
|
|
1156
1332
|
|
|
1157
1333
|
const dependencies = agentGraph.get(agentId) || [];
|
|
1158
1334
|
for (const nextAgent of dependencies) {
|
|
1159
1335
|
if (recursionStack.has(nextAgent)) {
|
|
1160
|
-
// Cycle detected - return the full cycle path
|
|
1161
1336
|
const cycleStartIndex = path.indexOf(nextAgent);
|
|
1162
|
-
|
|
1163
|
-
return cyclePath;
|
|
1337
|
+
return [...path.slice(cycleStartIndex), nextAgent];
|
|
1164
1338
|
}
|
|
1165
1339
|
|
|
1166
1340
|
if (!visited.has(nextAgent)) {
|
|
@@ -1171,58 +1345,19 @@ function detectNAgentCycles(config) {
|
|
|
1171
1345
|
|
|
1172
1346
|
recursionStack.delete(agentId);
|
|
1173
1347
|
return null;
|
|
1174
|
-
}
|
|
1348
|
+
};
|
|
1175
1349
|
|
|
1176
|
-
// Check all agents as starting points
|
|
1177
1350
|
for (const agentId of agentGraph.keys()) {
|
|
1178
1351
|
if (!visited.has(agentId)) {
|
|
1179
1352
|
const cycle = dfs(agentId, [agentId]);
|
|
1180
|
-
if (cycle)
|
|
1181
|
-
// Check if cycle has escape logic (any agent in cycle has trigger logic)
|
|
1182
|
-
const hasEscapeLogic = cycle.some((id) => {
|
|
1183
|
-
const agent = config.agents.find((a) => a.id === id);
|
|
1184
|
-
return agent?.triggers?.some((t) => t.logic);
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
const cycleStr = cycle.join(' → ');
|
|
1188
|
-
if (!hasEscapeLogic) {
|
|
1189
|
-
errors.push(
|
|
1190
|
-
`[Gap 6] Circular dependency detected: ${cycleStr}. ` +
|
|
1191
|
-
`Fix: Add logic conditions to break the loop, or set maxIterations on involved agents.`
|
|
1192
|
-
);
|
|
1193
|
-
} else {
|
|
1194
|
-
warnings.push(
|
|
1195
|
-
`Circular dependency detected: ${cycleStr}. ` +
|
|
1196
|
-
`Has escape logic in triggers, but verify maxIterations is set to prevent infinite loops.`
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
// Only report first cycle to avoid noise
|
|
1200
|
-
break;
|
|
1201
|
-
}
|
|
1353
|
+
if (cycle) return cycle;
|
|
1202
1354
|
}
|
|
1203
1355
|
}
|
|
1204
1356
|
|
|
1205
|
-
return
|
|
1357
|
+
return null;
|
|
1206
1358
|
}
|
|
1207
1359
|
|
|
1208
|
-
|
|
1209
|
-
* Phase 9: Configuration semantic validation
|
|
1210
|
-
* Validates configuration fields that can cause runtime failures
|
|
1211
|
-
*
|
|
1212
|
-
* Issue #14 - Gaps 8-15:
|
|
1213
|
-
* - Gap 8: JSON schema structurally invalid (line 1219-1237)
|
|
1214
|
-
* - Gap 9: Context sources never produced (line 1240-1283)
|
|
1215
|
-
* - Gap 10: Isolation config invalid (line 1286-1312)
|
|
1216
|
-
* - Gap 11: Agent ID conflicts across subclusters (line 1185-1212)
|
|
1217
|
-
* - Gap 12: Load config file paths don't exist (line 1315-1323)
|
|
1218
|
-
* - Gap 13: Task executor config invalid (line 1326-1352)
|
|
1219
|
-
* - Gap 14: Context source format invalid (line 1270-1282)
|
|
1220
|
-
* - Gap 15: Role references in logic (stricter) (line 1354-1383)
|
|
1221
|
-
*
|
|
1222
|
-
* @param {Object} config - Cluster configuration
|
|
1223
|
-
* @returns {{ errors: string[], warnings: string[] }}
|
|
1224
|
-
*/
|
|
1225
|
-
function validateConfigSemantics(config) {
|
|
1360
|
+
function detectNAgentCycles(config) {
|
|
1226
1361
|
const errors = [];
|
|
1227
1362
|
const warnings = [];
|
|
1228
1363
|
|
|
@@ -1230,18 +1365,40 @@ function validateConfigSemantics(config) {
|
|
|
1230
1365
|
return { errors, warnings };
|
|
1231
1366
|
}
|
|
1232
1367
|
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1368
|
+
const agentGraph = buildAgentGraph(config);
|
|
1369
|
+
const cycle = findFirstCycle(agentGraph);
|
|
1235
1370
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
+
}
|
|
1239
1390
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1391
|
+
return { errors, warnings };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function collectAgentIdConflicts(agents, errors) {
|
|
1395
|
+
const allAgentIds = new Map();
|
|
1242
1396
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1397
|
+
const collectAgentIds = (list, depth = 0) => {
|
|
1398
|
+
if (!list) return;
|
|
1399
|
+
|
|
1400
|
+
for (const agent of list) {
|
|
1401
|
+
if (!agent.id) continue;
|
|
1245
1402
|
|
|
1246
1403
|
if (allAgentIds.has(agent.id)) {
|
|
1247
1404
|
const firstSeenDepth = allAgentIds.get(agent.id);
|
|
@@ -1258,186 +1415,247 @@ function validateConfigSemantics(config) {
|
|
|
1258
1415
|
collectAgentIds(agent.config.agents, depth + 1);
|
|
1259
1416
|
}
|
|
1260
1417
|
}
|
|
1261
|
-
}
|
|
1418
|
+
};
|
|
1262
1419
|
|
|
1263
|
-
collectAgentIds(
|
|
1420
|
+
collectAgentIds(agents);
|
|
1421
|
+
}
|
|
1264
1422
|
|
|
1265
|
-
|
|
1423
|
+
function buildTopicProducers(agents) {
|
|
1424
|
+
const topicProducers = new Map();
|
|
1425
|
+
|
|
1426
|
+
for (const agent of agents) {
|
|
1266
1427
|
if (agent.type === 'subcluster') continue;
|
|
1428
|
+
const outputTopic = agent.hooks?.onComplete?.config?.topic;
|
|
1429
|
+
if (!outputTopic) continue;
|
|
1267
1430
|
|
|
1268
|
-
|
|
1431
|
+
if (!topicProducers.has(outputTopic)) {
|
|
1432
|
+
topicProducers.set(outputTopic, []);
|
|
1433
|
+
}
|
|
1434
|
+
topicProducers.get(outputTopic).push(agent.id);
|
|
1435
|
+
}
|
|
1269
1436
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
errors.push(
|
|
1285
|
-
`[Gap 8] ${prefix}: jsonSchema is not valid JSON: ${e.message}. ` +
|
|
1286
|
-
`Fix: Ensure schema is a valid JSON object.`
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
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
|
+
);
|
|
1289
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
|
+
}
|
|
1290
1459
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
// Build topic producers map
|
|
1297
|
-
for (const a of config.agents) {
|
|
1298
|
-
if (a.type === 'subcluster') continue;
|
|
1299
|
-
const outputTopic = a.hooks?.onComplete?.config?.topic;
|
|
1300
|
-
if (outputTopic) {
|
|
1301
|
-
if (!topicProducers.has(outputTopic)) {
|
|
1302
|
-
topicProducers.set(outputTopic, []);
|
|
1303
|
-
}
|
|
1304
|
-
topicProducers.get(outputTopic).push(a.id);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
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;
|
|
1307
1464
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
+
}
|
|
1312
1472
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
+
}
|
|
1320
1487
|
|
|
1321
|
-
|
|
1322
|
-
|
|
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)) {
|
|
1323
1510
|
warnings.push(
|
|
1324
|
-
`[Gap
|
|
1325
|
-
`
|
|
1326
|
-
);
|
|
1327
|
-
}
|
|
1328
|
-
if (source.strategy && !['latest', 'all', 'oldest'].includes(source.strategy)) {
|
|
1329
|
-
errors.push(
|
|
1330
|
-
`[Gap 14] ${prefix}: Context source strategy '${source.strategy}' is invalid. ` +
|
|
1331
|
-
`Fix: Use 'latest', 'all', or 'oldest'.`
|
|
1511
|
+
`[Gap 10] ${prefix}: Docker mount host path '${mount.host}' is not absolute. ` +
|
|
1512
|
+
`May cause runtime errors.`
|
|
1332
1513
|
);
|
|
1333
1514
|
}
|
|
1334
1515
|
}
|
|
1335
1516
|
}
|
|
1336
1517
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (agent.isolation.type === 'docker') {
|
|
1340
|
-
if (!agent.isolation.image) {
|
|
1341
|
-
errors.push(
|
|
1342
|
-
`[Gap 10] ${prefix}: Docker isolation requires 'image' field. ` +
|
|
1343
|
-
`Fix: Add "image": "zeroshot-runner" or custom image name.`
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1346
1520
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
+
);
|
|
1364
1556
|
}
|
|
1557
|
+
}
|
|
1365
1558
|
|
|
1366
|
-
|
|
1367
|
-
if (agent.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
`Fix: Check file path or remove loadConfig.`
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
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
|
+
);
|
|
1375
1565
|
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1376
1568
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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) {
|
|
1380
1590
|
errors.push(
|
|
1381
|
-
`[Gap
|
|
1382
|
-
`Fix: Add
|
|
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.`
|
|
1383
1593
|
);
|
|
1384
1594
|
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1385
1598
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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 = [];
|
|
1394
1619
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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);
|
|
1404
1655
|
|
|
1405
1656
|
// === GAP 15: Stricter role reference validation ===
|
|
1406
1657
|
// Upgrade from WARNING to ERROR when role is used in critical logic
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
for (const trigger of agent.triggers || []) {
|
|
1410
|
-
if (trigger.logic?.script) {
|
|
1411
|
-
const script = trigger.logic.script;
|
|
1412
|
-
const roleMatches = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
|
|
1413
|
-
|
|
1414
|
-
if (roleMatches) {
|
|
1415
|
-
for (const match of roleMatches) {
|
|
1416
|
-
const role = match.match(/['"](\w+)['"]/)[1];
|
|
1417
|
-
if (!roles.has(role)) {
|
|
1418
|
-
// Check if the logic depends on this role for critical decisions
|
|
1419
|
-
const isCritical =
|
|
1420
|
-
/\.length\s*[><=!]/.test(script) || // Checking count
|
|
1421
|
-
/allResponded/.test(script) || // Waiting for responses
|
|
1422
|
-
/hasConsensus/.test(script); // Consensus check
|
|
1423
|
-
|
|
1424
|
-
// Check if logic has a valid "zero length" fallback pattern
|
|
1425
|
-
// e.g., "if (validators.length === 0) return true" handles missing role gracefully
|
|
1426
|
-
const hasZeroLengthFallback =
|
|
1427
|
-
/\.length\s*===?\s*0\s*\)\s*return/.test(script) || // length === 0) return
|
|
1428
|
-
/\.length\s*[<]=\s*0/.test(script); // length <= 0 or length < 1
|
|
1429
|
-
|
|
1430
|
-
if (isCritical && !hasZeroLengthFallback) {
|
|
1431
|
-
errors.push(
|
|
1432
|
-
`[Gap 15] ${prefix}: Logic references role '${role}' which doesn't exist. ` +
|
|
1433
|
-
`This will cause logic to fail. Fix: Add agent with role '${role}' or update logic.`
|
|
1434
|
-
);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1658
|
+
validateRoleReferences(prefix, agent, roles, errors);
|
|
1441
1659
|
}
|
|
1442
1660
|
|
|
1443
1661
|
return { errors, warnings };
|
|
@@ -1511,8 +1729,8 @@ function validateProviderSettings(provider, providerSettings) {
|
|
|
1511
1729
|
`Invalid model override (must be non-empty string) for provider "${provider}"`
|
|
1512
1730
|
);
|
|
1513
1731
|
}
|
|
1514
|
-
if (override?.reasoningEffort &&
|
|
1515
|
-
throw new Error(`reasoningEffort overrides are only supported for Codex`);
|
|
1732
|
+
if (override?.reasoningEffort && !['codex', 'opencode'].includes(provider)) {
|
|
1733
|
+
throw new Error(`reasoningEffort overrides are only supported for Codex and Opencode`);
|
|
1516
1734
|
}
|
|
1517
1735
|
if (
|
|
1518
1736
|
override?.reasoningEffort &&
|
|
@@ -1525,6 +1743,117 @@ function validateProviderSettings(provider, providerSettings) {
|
|
|
1525
1743
|
}
|
|
1526
1744
|
}
|
|
1527
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
|
+
|
|
1528
1857
|
function validateProviderFeatures(config, settings) {
|
|
1529
1858
|
const errors = [];
|
|
1530
1859
|
const warnings = [];
|
|
@@ -1548,87 +1877,23 @@ function validateProviderFeatures(config, settings) {
|
|
|
1548
1877
|
continue;
|
|
1549
1878
|
}
|
|
1550
1879
|
|
|
1551
|
-
const provider =
|
|
1552
|
-
if (!
|
|
1553
|
-
errors.push(`Agent "${agent.id}" references unknown provider "${provider}"`);
|
|
1554
|
-
continue;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
const providerModule = getProvider(provider);
|
|
1558
|
-
const levels = providerModule.getLevelMapping();
|
|
1559
|
-
const catalog = providerModule.getModelCatalog();
|
|
1560
|
-
const providerSettings = settings.providerSettings?.[provider] || {};
|
|
1561
|
-
const minLevel = providerSettings.minLevel;
|
|
1562
|
-
const maxLevel = providerSettings.maxLevel;
|
|
1563
|
-
const rank = (level) => levels[level]?.rank;
|
|
1564
|
-
|
|
1565
|
-
if (agent.jsonSchema) {
|
|
1566
|
-
const cap = CAPABILITIES[provider]?.jsonSchema;
|
|
1567
|
-
if (cap === 'experimental') {
|
|
1568
|
-
warnings.push(
|
|
1569
|
-
`Agent "${agent.id}" uses jsonSchema with ${provider} provider - ` +
|
|
1570
|
-
`this feature is experimental and may not work reliably`
|
|
1571
|
-
);
|
|
1572
|
-
} else if (!cap) {
|
|
1573
|
-
warnings.push(
|
|
1574
|
-
`Agent "${agent.id}" uses jsonSchema but ${provider} provider doesn't support it`
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
if (agent.modelLevel && !levels[agent.modelLevel]) {
|
|
1580
|
-
warnings.push(
|
|
1581
|
-
`Agent "${agent.id}" uses modelLevel "${agent.modelLevel}" which is not valid for ${provider}`
|
|
1582
|
-
);
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
if (agent.model) {
|
|
1586
|
-
if (!catalog[agent.model]) {
|
|
1587
|
-
warnings.push(
|
|
1588
|
-
`Agent "${agent.id}" uses model "${agent.model}" which is not valid for ${provider}`
|
|
1589
|
-
);
|
|
1590
|
-
}
|
|
1591
|
-
} else if (agent.modelLevel && minLevel && maxLevel) {
|
|
1592
|
-
if (rank(minLevel) > rank(maxLevel)) {
|
|
1593
|
-
warnings.push(
|
|
1594
|
-
`Provider "${provider}" has minLevel "${minLevel}" above maxLevel "${maxLevel}"`
|
|
1595
|
-
);
|
|
1596
|
-
} else if (rank(agent.modelLevel) < rank(minLevel)) {
|
|
1597
|
-
warnings.push(
|
|
1598
|
-
`Agent "${agent.id}" uses modelLevel "${agent.modelLevel}" below minLevel "${minLevel}" for ${provider}`
|
|
1599
|
-
);
|
|
1600
|
-
} else if (rank(agent.modelLevel) > rank(maxLevel)) {
|
|
1601
|
-
warnings.push(
|
|
1602
|
-
`Agent "${agent.id}" uses modelLevel "${agent.modelLevel}" above maxLevel "${maxLevel}" for ${provider}`
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1880
|
+
const provider = resolveAgentProvider(agent, config, settings, errors);
|
|
1881
|
+
if (!provider) continue;
|
|
1606
1882
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
warnings.push(
|
|
1616
|
-
`Agent "${agent.id}" uses model "${rule.model}" in modelRules which is not valid for ${provider}`
|
|
1617
|
-
);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1883
|
+
const { levels, catalog, minLevel, maxLevel, rank } = buildProviderContext(provider, settings);
|
|
1884
|
+
const modelSelectionContext = {
|
|
1885
|
+
provider,
|
|
1886
|
+
catalog,
|
|
1887
|
+
minLevel,
|
|
1888
|
+
maxLevel,
|
|
1889
|
+
rank,
|
|
1890
|
+
};
|
|
1621
1891
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
) {
|
|
1628
|
-
warnings.push(
|
|
1629
|
-
`Agent "${agent.id}" has invalid reasoningEffort "${agent.reasoningEffort}" (low|medium|high|xhigh)`
|
|
1630
|
-
);
|
|
1631
|
-
}
|
|
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);
|
|
1632
1897
|
}
|
|
1633
1898
|
|
|
1634
1899
|
return { errors, warnings };
|