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