@covibes/zeroshot 5.3.0 → 5.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -385
  3. package/cli/index.js +108 -4615
  4. package/package.json +16 -125
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -150
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -253
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -697
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -152
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -212
  21. package/lib/id-detector.js +0 -53
  22. package/lib/provider-detection.js +0 -59
  23. package/lib/provider-names.js +0 -56
  24. package/lib/settings.js +0 -405
  25. package/lib/stream-json-parser.js +0 -10
  26. package/scripts/fix-node-pty-permissions.js +0 -75
  27. package/scripts/record-demo.sh +0 -279
  28. package/scripts/test-install.sh +0 -40
  29. package/scripts/validate-templates.js +0 -100
  30. package/src/agent/agent-config.js +0 -193
  31. package/src/agent/agent-context-builder.js +0 -335
  32. package/src/agent/agent-hook-executor.js +0 -402
  33. package/src/agent/agent-lifecycle.js +0 -683
  34. package/src/agent/agent-stuck-detector.js +0 -256
  35. package/src/agent/agent-task-executor.js +0 -1431
  36. package/src/agent/agent-trigger-evaluator.js +0 -67
  37. package/src/agent/output-extraction.js +0 -200
  38. package/src/agent/output-reformatter.js +0 -175
  39. package/src/agent/schema-utils.js +0 -111
  40. package/src/agent-wrapper.js +0 -570
  41. package/src/agents/git-pusher-agent.json +0 -20
  42. package/src/attach/attach-client.js +0 -438
  43. package/src/attach/attach-server.js +0 -543
  44. package/src/attach/index.js +0 -35
  45. package/src/attach/protocol.js +0 -220
  46. package/src/attach/ring-buffer.js +0 -121
  47. package/src/attach/socket-discovery.js +0 -267
  48. package/src/claude-task-runner.js +0 -523
  49. package/src/config-router.js +0 -80
  50. package/src/config-validator.js +0 -1660
  51. package/src/copy-worker.js +0 -43
  52. package/src/github.js +0 -139
  53. package/src/isolation-manager.js +0 -1381
  54. package/src/ledger.js +0 -614
  55. package/src/logic-engine.js +0 -223
  56. package/src/message-bus-bridge.js +0 -139
  57. package/src/message-bus.js +0 -249
  58. package/src/name-generator.js +0 -232
  59. package/src/orchestrator.js +0 -2519
  60. package/src/preflight.js +0 -485
  61. package/src/process-metrics.js +0 -566
  62. package/src/providers/anthropic/cli-builder.js +0 -45
  63. package/src/providers/anthropic/index.js +0 -134
  64. package/src/providers/anthropic/models.js +0 -23
  65. package/src/providers/anthropic/output-parser.js +0 -159
  66. package/src/providers/base-provider.js +0 -181
  67. package/src/providers/capabilities.js +0 -51
  68. package/src/providers/google/cli-builder.js +0 -55
  69. package/src/providers/google/index.js +0 -116
  70. package/src/providers/google/models.js +0 -24
  71. package/src/providers/google/output-parser.js +0 -92
  72. package/src/providers/index.js +0 -75
  73. package/src/providers/openai/cli-builder.js +0 -122
  74. package/src/providers/openai/index.js +0 -135
  75. package/src/providers/openai/models.js +0 -21
  76. package/src/providers/openai/output-parser.js +0 -129
  77. package/src/schemas/sub-cluster.js +0 -156
  78. package/src/status-footer.js +0 -980
  79. package/src/sub-cluster-wrapper.js +0 -560
  80. package/src/task-runner.js +0 -30
  81. package/src/template-resolver.js +0 -369
  82. package/src/tui/CHANGES.txt +0 -133
  83. package/src/tui/LAYOUT.md +0 -261
  84. package/src/tui/README.txt +0 -192
  85. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  86. package/src/tui/data-poller.js +0 -325
  87. package/src/tui/demo.js +0 -208
  88. package/src/tui/formatters.js +0 -128
  89. package/src/tui/index.js +0 -193
  90. package/src/tui/keybindings.js +0 -383
  91. package/src/tui/layout.js +0 -334
  92. package/src/tui/renderer.js +0 -194
  93. package/task-lib/attachable-watcher.js +0 -272
  94. package/task-lib/claude-recovery.js +0 -119
  95. package/task-lib/commands/clean.js +0 -50
  96. package/task-lib/commands/get-log-path.js +0 -23
  97. package/task-lib/commands/kill.js +0 -32
  98. package/task-lib/commands/list.js +0 -105
  99. package/task-lib/commands/logs.js +0 -411
  100. package/task-lib/commands/resume.js +0 -42
  101. package/task-lib/commands/run.js +0 -57
  102. package/task-lib/commands/schedule.js +0 -105
  103. package/task-lib/commands/scheduler-cmd.js +0 -96
  104. package/task-lib/commands/schedules.js +0 -98
  105. package/task-lib/commands/status.js +0 -44
  106. package/task-lib/commands/unschedule.js +0 -16
  107. package/task-lib/completion.js +0 -9
  108. package/task-lib/config.js +0 -10
  109. package/task-lib/name-generator.js +0 -230
  110. package/task-lib/package.json +0 -3
  111. package/task-lib/runner.js +0 -144
  112. package/task-lib/scheduler.js +0 -252
  113. package/task-lib/store.js +0 -230
  114. package/task-lib/tui/formatters.js +0 -166
  115. package/task-lib/tui/index.js +0 -197
  116. package/task-lib/tui/layout.js +0 -111
  117. package/task-lib/tui/renderer.js +0 -119
  118. package/task-lib/tui.js +0 -384
  119. package/task-lib/watcher.js +0 -188
@@ -1,1660 +0,0 @@
1
- /**
2
- * Config Validator - Static analysis for zeroshot cluster configurations
3
- *
4
- * Catches logical failures that would cause clusters to:
5
- * - Never start (no bootstrap trigger)
6
- * - Never complete (no path to completion)
7
- * - Loop infinitely (circular dependencies)
8
- * - Deadlock (impossible consensus)
9
- * - Waste compute (orchestrator executing tasks)
10
- *
11
- * Run at config load time to fail fast before spawning agents.
12
- */
13
-
14
- const { loadSettings } = require('../lib/settings');
15
- const { VALID_PROVIDERS, normalizeProviderName } = require('../lib/provider-names');
16
- const { getProvider } = require('./providers');
17
- const { CAPABILITIES } = require('./providers/capabilities');
18
-
19
- /**
20
- * Check if config is a conductor-bootstrap style config
21
- * Conductor configs dynamically spawn agents via CLUSTER_OPERATIONS
22
- * @param {Object} config - Cluster configuration
23
- * @returns {boolean}
24
- */
25
- function isConductorConfig(config) {
26
- return config.agents?.some(
27
- (a) =>
28
- a.role === 'conductor' &&
29
- // Old style: static topic in config
30
- (a.hooks?.onComplete?.config?.topic === 'CLUSTER_OPERATIONS' ||
31
- // New style: topic set in transform script (check for CLUSTER_OPERATIONS in script)
32
- a.hooks?.onComplete?.transform?.script?.includes('CLUSTER_OPERATIONS'))
33
- );
34
- }
35
-
36
- /**
37
- * Validate a cluster configuration for structural correctness
38
- * @param {Object} config - Cluster configuration
39
- * @param {Number} depth - Current nesting depth (for subcluster validation)
40
- * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
41
- */
42
- function validateConfig(config, depth = 0) {
43
- const errors = [];
44
- const warnings = [];
45
- const settings = loadSettings();
46
-
47
- // Max nesting depth check
48
- const MAX_DEPTH = 5;
49
- if (depth > MAX_DEPTH) {
50
- errors.push(`Cluster nesting exceeds max depth (${MAX_DEPTH})`);
51
- return { valid: false, errors, warnings };
52
- }
53
-
54
- // === PHASE 1: Basic structure validation ===
55
- const basicResult = validateBasicStructure(config, depth);
56
- errors.push(...basicResult.errors);
57
- warnings.push(...basicResult.warnings);
58
-
59
- // Note: We continue to other phases even if Phase 1 has errors,
60
- // to collect ALL validation issues (especially semantic checks in Phase 6-9)
61
-
62
- // Conductor configs dynamically spawn agents - skip message flow analysis
63
- // The orchestrator validates the spawned config at CLUSTER_OPERATIONS execution time
64
- const conductorMode = isConductorConfig(config);
65
-
66
- // === PHASE 2: Message flow analysis (skip for conductor configs) ===
67
- if (!conductorMode) {
68
- const flowResult = analyzeMessageFlow(config);
69
- errors.push(...flowResult.errors);
70
- warnings.push(...flowResult.warnings);
71
- }
72
-
73
- // === PHASE 3: Agent-specific validation ===
74
- const agentResult = validateAgents(config);
75
- errors.push(...agentResult.errors);
76
- warnings.push(...agentResult.warnings);
77
-
78
- // === PHASE 4: Logic script validation ===
79
- const logicResult = validateLogicScripts(config);
80
- errors.push(...logicResult.errors);
81
- warnings.push(...logicResult.warnings);
82
-
83
- // === PHASE 5: Template variable validation ===
84
- const templateResult = validateTemplateVariables(config, depth);
85
- errors.push(...templateResult.errors);
86
- warnings.push(...templateResult.warnings);
87
-
88
- // === PHASE 6: Hook semantic validation ===
89
- const hookResult = validateHookSemantics(config);
90
- errors.push(...hookResult.errors);
91
- warnings.push(...hookResult.warnings);
92
-
93
- // === PHASE 7: Rule coverage validation ===
94
- const ruleResult = validateRuleCoverage(config);
95
- errors.push(...ruleResult.errors);
96
- warnings.push(...ruleResult.warnings);
97
-
98
- // === PHASE 8: N-agent cycle detection ===
99
- const cycleResult = detectNAgentCycles(config);
100
- errors.push(...cycleResult.errors);
101
- warnings.push(...cycleResult.warnings);
102
-
103
- // === PHASE 9: Configuration semantic validation ===
104
- const configResult = validateConfigSemantics(config);
105
- errors.push(...configResult.errors);
106
- warnings.push(...configResult.warnings);
107
-
108
- // === PHASE 10: Provider feature validation ===
109
- const providerResult = validateProviderFeatures(config, settings);
110
- errors.push(...providerResult.errors);
111
- warnings.push(...providerResult.warnings);
112
-
113
- return {
114
- valid: errors.length === 0,
115
- errors,
116
- warnings,
117
- };
118
- }
119
-
120
- /**
121
- * Phase 1: Validate basic structure (fields, types, duplicates)
122
- */
123
- function validateBasicStructure(config, depth = 0) {
124
- const errors = [];
125
- const warnings = [];
126
-
127
- if (!config.agents || !Array.isArray(config.agents)) {
128
- errors.push('agents array is required');
129
- return { errors, warnings };
130
- }
131
-
132
- if (config.agents.length === 0) {
133
- errors.push('agents array cannot be empty');
134
- return { errors, warnings };
135
- }
136
-
137
- const seenIds = new Set();
138
-
139
- for (let i = 0; i < config.agents.length; i++) {
140
- const agent = config.agents[i];
141
- const prefix = `agents[${i}]`;
142
-
143
- // Check if this is a subcluster
144
- const isSubCluster = agent.type === 'subcluster';
145
-
146
- // Required fields
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
- }
160
-
161
- // Validate subclusters
162
- if (isSubCluster) {
163
- const subClusterSchema = require('./schemas/sub-cluster');
164
- const subResult = subClusterSchema.validateSubCluster(agent, depth);
165
- errors.push(...subResult.errors);
166
- warnings.push(...subResult.warnings);
167
- continue; // Skip regular agent validation
168
- }
169
-
170
- // Regular agent validation
171
- if (!agent.triggers || !Array.isArray(agent.triggers)) {
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
- }
205
-
206
- // Validate model rules if present
207
- if (agent.modelRules) {
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
- }
227
-
228
- // Note: Detailed coverage gap checking (iteration ranges) is done in Phase 7
229
- }
230
- }
231
- }
232
-
233
- return { errors, warnings };
234
- }
235
-
236
- /**
237
- * Phase 2: Analyze message flow for structural problems
238
- */
239
- function analyzeMessageFlow(config) {
240
- const errors = [];
241
- const warnings = [];
242
-
243
- // Build topic graph
244
- const topicProducers = new Map(); // topic -> [agentIds that produce it]
245
- const topicConsumers = new Map(); // topic -> [agentIds that consume it]
246
- const agentOutputTopics = new Map(); // agentId -> [topics it produces]
247
- const agentInputTopics = new Map(); // agentId -> [topics it consumes]
248
-
249
- // System always produces ISSUE_OPENED
250
- topicProducers.set('ISSUE_OPENED', ['system']);
251
-
252
- for (const agent of config.agents) {
253
- agentInputTopics.set(agent.id, []);
254
- agentOutputTopics.set(agent.id, []);
255
-
256
- // Track what topics this agent consumes (triggers)
257
- for (const trigger of agent.triggers || []) {
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
- }
275
- }
276
-
277
- // === CHECK 1: No bootstrap trigger ===
278
- const issueOpenedConsumers = topicConsumers.get('ISSUE_OPENED') || [];
279
- if (issueOpenedConsumers.length === 0) {
280
- errors.push(
281
- 'No agent triggers on ISSUE_OPENED. Cluster will never start. ' +
282
- 'Add a trigger: { "topic": "ISSUE_OPENED", "action": "execute_task" }'
283
- );
284
- }
285
-
286
- // === CHECK 2: No completion handler ===
287
- const completionHandlers = config.agents.filter(
288
- (a) =>
289
- a.triggers?.some((t) => t.action === 'stop_cluster') ||
290
- a.id === 'completion-detector' ||
291
- a.id === 'git-pusher' ||
292
- a.hooks?.onComplete?.config?.topic === 'CLUSTER_COMPLETE'
293
- );
294
- const isTemplateConfig = config.params && Object.keys(config.params).length > 0;
295
-
296
- if (completionHandlers.length === 0) {
297
- const message =
298
- 'No completion handler found. Cluster will run until idle timeout (2 min). ' +
299
- 'Add an agent with trigger action: "stop_cluster"';
300
- if (isTemplateConfig) {
301
- warnings.push(`${message} (template will rely on orchestrator injection)`);
302
- } else {
303
- errors.push(message);
304
- }
305
- } else if (completionHandlers.length > 1) {
306
- errors.push(
307
- `Multiple completion handlers: [${completionHandlers.map((a) => a.id).join(', ')}]. ` +
308
- 'This causes race conditions. Keep only one.'
309
- );
310
- }
311
-
312
- // === CHECK 3: Orphan topics (produced but never consumed) ===
313
- for (const [topic, producers] of topicProducers) {
314
- if (topic === 'CLUSTER_COMPLETE') continue; // System handles this
315
- const consumers = topicConsumers.get(topic) || [];
316
- if (consumers.length === 0) {
317
- warnings.push(
318
- `Topic '${topic}' is produced by [${producers.join(', ')}] but never consumed. Dead end.`
319
- );
320
- }
321
- }
322
-
323
- // === CHECK 4: Waiting for topics that are never produced ===
324
- for (const [topic, consumers] of topicConsumers) {
325
- if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue; // System produces
326
- if (topic.endsWith('*')) continue; // Wildcard pattern
327
- const producers = topicProducers.get(topic) || [];
328
- if (producers.length === 0) {
329
- errors.push(
330
- `Topic '${topic}' consumed by [${consumers.join(', ')}] but never produced. ` +
331
- 'These agents will never trigger.'
332
- );
333
- }
334
- }
335
-
336
- // === CHECK 5: Self-triggering agents (instant infinite loop) ===
337
- for (const agent of config.agents) {
338
- const inputs = agentInputTopics.get(agent.id) || [];
339
- const outputs = agentOutputTopics.get(agent.id) || [];
340
- const selfTrigger = inputs.find((t) => outputs.includes(t));
341
- if (selfTrigger) {
342
- errors.push(
343
- `Agent '${agent.id}' triggers on '${selfTrigger}' and produces '${selfTrigger}'. ` +
344
- 'Instant infinite loop.'
345
- );
346
- }
347
- }
348
-
349
- // === CHECK 6: Two-agent circular dependency ===
350
- for (const agentA of config.agents) {
351
- const outputsA = agentOutputTopics.get(agentA.id) || [];
352
- for (const agentB of config.agents) {
353
- if (agentA.id === agentB.id) continue;
354
- const inputsB = agentInputTopics.get(agentB.id) || [];
355
- const outputsB = agentOutputTopics.get(agentB.id) || [];
356
- const inputsA = agentInputTopics.get(agentA.id) || [];
357
-
358
- // A produces what B consumes, AND B produces what A consumes
359
- const aToB = outputsA.some((t) => inputsB.includes(t));
360
- const bToA = outputsB.some((t) => inputsA.includes(t));
361
-
362
- if (aToB && bToA) {
363
- // This might be intentional (rejection loop), check if there's an escape
364
- const hasEscapeLogic =
365
- agentA.triggers?.some((t) => t.logic) || agentB.triggers?.some((t) => t.logic);
366
- if (!hasEscapeLogic) {
367
- warnings.push(
368
- `Circular dependency: '${agentA.id}' ↔ '${agentB.id}'. ` +
369
- 'Add logic conditions to prevent infinite loop, or ensure maxIterations is set.'
370
- );
371
- }
372
- }
373
- }
374
- }
375
-
376
- // === CHECK 7: Validator without worker re-trigger ===
377
- const validators = config.agents.filter((a) => a.role === 'validator');
378
- const workers = config.agents.filter((a) => a.role === 'implementation');
379
-
380
- if (validators.length > 0 && workers.length > 0) {
381
- for (const worker of workers) {
382
- const triggersOnValidation = worker.triggers?.some(
383
- (t) => t.topic === 'VALIDATION_RESULT' || t.topic.includes('VALIDATION')
384
- );
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
- }
392
- }
393
-
394
- // === CHECK 8: Context strategy missing trigger topics ===
395
- for (const agent of config.agents) {
396
- if (!agent.contextStrategy?.sources) continue;
397
-
398
- const triggerTopics = (agent.triggers || []).map((t) => t.topic);
399
- const contextTopics = agent.contextStrategy.sources.map((s) => s.topic);
400
-
401
- for (const triggerTopic of triggerTopics) {
402
- if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED') continue;
403
- if (triggerTopic.endsWith('*')) continue;
404
-
405
- if (!contextTopics.includes(triggerTopic)) {
406
- warnings.push(
407
- `Agent '${agent.id}' triggers on '${triggerTopic}' but doesn't include it in contextStrategy. ` +
408
- 'Agent may not see what triggered it.'
409
- );
410
- }
411
- }
412
- }
413
-
414
- return { errors, warnings };
415
- }
416
-
417
- /**
418
- * Phase 3: Validate agent-specific configurations
419
- */
420
- function validateAgents(config) {
421
- const errors = [];
422
- const warnings = [];
423
-
424
- const roles = new Map(); // role -> [agentIds]
425
-
426
- for (const agent of config.agents) {
427
- // Track roles
428
- if (!roles.has(agent.role)) {
429
- roles.set(agent.role, []);
430
- }
431
- roles.get(agent.role).push(agent.id);
432
-
433
- // Orchestrator should not execute tasks
434
- if (agent.role === 'orchestrator') {
435
- const executesTask = agent.triggers?.some(
436
- (t) => t.action === 'execute_task' || (!t.action && !t.logic)
437
- );
438
- if (executesTask) {
439
- warnings.push(
440
- `Orchestrator '${agent.id}' has execute_task triggers. ` +
441
- 'Orchestrators typically use action: "stop_cluster". This may waste API calls.'
442
- );
443
- }
444
- }
445
-
446
- // Check for git operations in validator prompts (unreliable in agents)
447
- if (agent.role === 'validator') {
448
- const prompt = typeof agent.prompt === 'string' ? agent.prompt : agent.prompt?.system;
449
- const gitPatterns = ['git diff', 'git status', 'git log', 'git show'];
450
- for (const pattern of gitPatterns) {
451
- if (prompt?.includes(pattern)) {
452
- errors.push(
453
- `Validator '${agent.id}' uses '${pattern}' - git state is unreliable in agents`
454
- );
455
- }
456
- }
457
- }
458
-
459
- // JSON output without schema
460
- if (agent.outputFormat === 'json' && !agent.jsonSchema) {
461
- warnings.push(
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
- }
474
-
475
- // No maxIterations on implementation agent (unbounded retries)
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
- }
482
-
483
- // FORBIDDEN: Direct model specification in configs
484
- // Use modelLevel (level1/level2/level3) for provider-agnostic model selection
485
- if (agent.model) {
486
- errors.push(
487
- `Agent '${agent.id}' uses 'model: "${agent.model}"'. ` +
488
- `Use 'modelLevel: "level1|level2|level3"' instead for provider-agnostic model selection.`
489
- );
490
- }
491
- }
492
-
493
- // Check for role references in logic scripts
494
- // IMPORTANT: Changed from error to warning because some triggers are designed to be
495
- // no-ops when the referenced role doesn't exist (e.g., worker's VALIDATION_RESULT
496
- // trigger returns false when validators.length === 0)
497
- for (const agent of config.agents) {
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
- }
516
-
517
- return { errors, warnings };
518
- }
519
-
520
- /**
521
- * Phase 4: Validate logic scripts (syntax only, not semantics)
522
- */
523
- function validateLogicScripts(config) {
524
- const errors = [];
525
- const warnings = [];
526
-
527
- const vm = require('vm');
528
-
529
- for (const agent of config.agents) {
530
- for (const trigger of agent.triggers || []) {
531
- if (!trigger.logic?.script) continue;
532
-
533
- const script = trigger.logic.script;
534
-
535
- // Syntax check
536
- try {
537
- const wrappedScript = `(function() { ${script} })()`;
538
- new vm.Script(wrappedScript);
539
- } catch (syntaxError) {
540
- errors.push(`Agent '${agent.id}' has invalid logic script: ${syntaxError.message}`);
541
- continue;
542
- }
543
-
544
- // Check for common mistakes - only flag if script is JUST "return false" or "return true"
545
- // Complex scripts with conditionals should not trigger this
546
- const trimmedScript = script.trim().replace(/\s+/g, ' ');
547
- const isSimpleReturnFalse = /^return\s+false;?$/.test(trimmedScript);
548
- const isSimpleReturnTrue = /^return\s+true;?$/.test(trimmedScript);
549
-
550
- if (isSimpleReturnFalse) {
551
- warnings.push(
552
- `Agent '${agent.id}' logic is just 'return false'. Agent will never trigger.`
553
- );
554
- }
555
-
556
- if (isSimpleReturnTrue) {
557
- warnings.push(
558
- `Agent '${agent.id}' logic is just 'return true'. Consider adding conditions or removing the logic block.`
559
- );
560
- }
561
-
562
- // Check for undefined variable access (common typos)
563
- const knownVars = [
564
- 'ledger',
565
- 'cluster',
566
- 'message',
567
- 'agent',
568
- 'helpers',
569
- 'Set',
570
- 'Map',
571
- 'Array',
572
- 'Object',
573
- 'JSON',
574
- 'Date',
575
- 'Math',
576
- ];
577
- const varPattern = /\b([a-zA-Z_]\w*)\s*\./g;
578
- let match;
579
- while ((match = varPattern.exec(script)) !== null) {
580
- const varName = match[1];
581
- if (
582
- !knownVars.includes(varName) &&
583
- !script.includes(`const ${varName}`) &&
584
- !script.includes(`let ${varName}`)
585
- ) {
586
- warnings.push(
587
- `Agent '${agent.id}' logic uses '${varName}' which may be undefined. ` +
588
- `Available: [${knownVars.join(', ')}]`
589
- );
590
- break; // Only warn once per agent
591
- }
592
- }
593
- }
594
- }
595
-
596
- return { errors, warnings };
597
- }
598
-
599
- /**
600
- * Phase 5: Validate template variables against jsonSchema
601
- * Ensures {{result.*}} references in hooks match defined schema properties
602
- *
603
- * Issue #14 - Gap 3:
604
- * - Gap 3: Template variables don't exist (line 582-656)
605
- *
606
- */
607
- function validateTemplateVariables(config, depth = 0) {
608
- const errors = [];
609
- const warnings = [];
610
-
611
- if (!config.agents || !Array.isArray(config.agents)) {
612
- return { errors, warnings };
613
- }
614
-
615
- const prefix = depth > 0 ? `Sub-cluster (depth ${depth}): ` : '';
616
-
617
- for (const agent of config.agents) {
618
- // Skip subclusters - they have their own validation
619
- if (agent.type === 'subcluster') {
620
- // Recursively validate subcluster config
621
- if (agent.config?.agents) {
622
- const subResult = validateTemplateVariables(agent.config, depth + 1);
623
- // Prefix sub-cluster errors with agent ID
624
- errors.push(...subResult.errors.map((e) => `Sub-cluster '${agent.id}': ${e}`));
625
- warnings.push(...subResult.warnings.map((w) => `Sub-cluster '${agent.id}': ${w}`));
626
- }
627
- continue;
628
- }
629
-
630
- const result = validateAgentTemplateVariables(agent, agent.id);
631
- errors.push(...result.errors.map((e) => `${prefix}${e}`));
632
- warnings.push(...result.warnings.map((w) => `${prefix}${w}`));
633
- }
634
-
635
- return { errors, warnings };
636
- }
637
-
638
- /**
639
- * Validate template variables for a single agent
640
- * @param {Object} agent - Agent configuration
641
- * @param {String} agentId - Agent ID for error messages
642
- * @returns {{ errors: string[], warnings: string[] }}
643
- */
644
- function validateAgentTemplateVariables(agent, agentId) {
645
- const errors = [];
646
- const warnings = [];
647
-
648
- // Extract schema properties (null if non-JSON output or text output)
649
- const schemaProps = extractSchemaProperties(agent);
650
-
651
- // If schemaProps is null, this agent doesn't use JSON output - skip validation
652
- if (schemaProps === null) {
653
- return { errors, warnings };
654
- }
655
-
656
- // Extract template variables from hooks
657
- const templateVars = extractTemplateVariables(agent);
658
-
659
- // Check for undefined references (ERROR)
660
- for (const varName of templateVars) {
661
- if (!schemaProps.has(varName)) {
662
- const availableProps = Array.from(schemaProps).join(', ');
663
- errors.push(
664
- `Agent '${agentId}': Template uses '{{result.${varName}}}' but '${varName}' is not defined in jsonSchema. ` +
665
- `Available properties: [${availableProps}]`
666
- );
667
- }
668
- }
669
-
670
- // Check for unused schema properties (WARNING)
671
- for (const prop of schemaProps) {
672
- if (!templateVars.has(prop)) {
673
- warnings.push(
674
- `Agent '${agentId}': Schema property '${prop}' is defined but never referenced in hooks. ` +
675
- `Consider removing it to save tokens.`
676
- );
677
- }
678
- }
679
-
680
- return { errors, warnings };
681
- }
682
-
683
- /**
684
- * Extract all template variables ({{result.*}}) from agent hooks
685
- * Searches hooks.onComplete.config (recursive) and hooks.onComplete.transform.script
686
- * Also searches triggers[].onComplete patterns
687
- * @param {Object} agent - Agent configuration
688
- * @returns {Set<string>} Set of variable names referenced
689
- */
690
- function extractTemplateVariables(agent) {
691
- const variables = new Set();
692
-
693
- // Regex patterns - reset lastIndex before each use to avoid state pollution
694
- const mustachePattern = /\{\{result\.([^}]+)\}\}/g;
695
- const directPattern = /\bresult\.([a-zA-Z_][a-zA-Z0-9_]*)/g;
696
-
697
- /**
698
- * Recursively traverse an object/array and extract template variables from strings
699
- */
700
- function traverseAndExtract(obj) {
701
- if (obj === null || obj === undefined) {
702
- return;
703
- }
704
-
705
- if (typeof obj === 'string') {
706
- // Extract mustache-style {{result.field}}
707
- mustachePattern.lastIndex = 0;
708
- let match;
709
- while ((match = mustachePattern.exec(obj)) !== null) {
710
- variables.add(match[1]);
711
- }
712
- return;
713
- }
714
-
715
- if (Array.isArray(obj)) {
716
- for (const item of obj) {
717
- traverseAndExtract(item);
718
- }
719
- return;
720
- }
721
-
722
- if (typeof obj === 'object') {
723
- for (const value of Object.values(obj)) {
724
- traverseAndExtract(value);
725
- }
726
- }
727
- }
728
-
729
- /**
730
- * Extract variables from transform script (direct result.field access)
731
- */
732
- function extractFromScript(script) {
733
- if (typeof script !== 'string') {
734
- return;
735
- }
736
-
737
- directPattern.lastIndex = 0;
738
- let match;
739
- while ((match = directPattern.exec(script)) !== null) {
740
- variables.add(match[1]);
741
- }
742
- }
743
-
744
- // Extract from hooks.onComplete.config
745
- if (agent.hooks?.onComplete?.config) {
746
- traverseAndExtract(agent.hooks.onComplete.config);
747
- }
748
-
749
- // Extract from hooks.onComplete.transform.script
750
- if (agent.hooks?.onComplete?.transform?.script) {
751
- extractFromScript(agent.hooks.onComplete.transform.script);
752
- }
753
-
754
- // Extract from triggers[].onComplete (some agents define hooks per-trigger)
755
- if (agent.triggers && Array.isArray(agent.triggers)) {
756
- for (const trigger of agent.triggers) {
757
- if (trigger.onComplete?.config) {
758
- traverseAndExtract(trigger.onComplete.config);
759
- }
760
- if (trigger.onComplete?.transform?.script) {
761
- extractFromScript(trigger.onComplete.transform.script);
762
- }
763
- }
764
- }
765
-
766
- return variables;
767
- }
768
-
769
- /**
770
- * Extract schema properties from agent's jsonSchema
771
- * @param {Object} agent - Agent configuration
772
- * @returns {Set<string>|null} Set of property names, or null if agent doesn't use JSON output
773
- */
774
- function extractSchemaProperties(agent) {
775
- // Non-JSON agents don't need validation
776
- // Both 'json' and 'stream-json' use jsonSchema and need validation
777
- if (!['json', 'stream-json'].includes(agent.outputFormat)) {
778
- return null;
779
- }
780
-
781
- // If explicit schema is provided, use its properties
782
- if (agent.jsonSchema?.properties) {
783
- return new Set(Object.keys(agent.jsonSchema.properties));
784
- }
785
-
786
- // Default schema when outputFormat is 'json' but no explicit schema
787
- // See: agent-config.js:62-69
788
- return new Set(['summary', 'result']);
789
- }
790
-
791
- /**
792
- * Check if iteration pattern is valid
793
- */
794
- function isValidIterationPattern(pattern) {
795
- if (pattern === 'all') return true;
796
- if (/^\d+$/.test(pattern)) return true; // "1"
797
- if (/^\d+-\d+$/.test(pattern)) return true; // "1-3"
798
- if (/^\d+\+$/.test(pattern)) return true; // "5+"
799
- return false;
800
- }
801
-
802
- /**
803
- * Format validation result for CLI output
804
- */
805
- function formatValidationResult(result) {
806
- const lines = [];
807
-
808
- if (result.valid) {
809
- lines.push('✅ Configuration is valid');
810
- } else {
811
- lines.push('❌ Configuration has errors');
812
- }
813
-
814
- if (result.errors.length > 0) {
815
- lines.push('\nErrors:');
816
- for (const error of result.errors) {
817
- lines.push(` ❌ ${error}`);
818
- }
819
- }
820
-
821
- if (result.warnings.length > 0) {
822
- lines.push('\nWarnings:');
823
- for (const warning of result.warnings) {
824
- lines.push(` ⚠️ ${warning}`);
825
- }
826
- }
827
-
828
- return lines.join('\n');
829
- }
830
-
831
- /**
832
- * Phase 6: Hook semantic validation
833
- * Catches runtime failures in hook execution (agent-hook-executor.js)
834
- *
835
- * Issue #14 - Gaps 1, 2, 7:
836
- * - Gap 1: Hook action field missing (line 837-842)
837
- * - Gap 2: Transform script output shape (line 846-866)
838
- * - Gap 7: Conductor CLUSTER_OPERATIONS payload (line 869-888)
839
- *
840
- * @param {Object} config - Cluster configuration
841
- * @returns {{ errors: string[], warnings: string[] }}
842
- */
843
- function validateHookSemantics(config) {
844
- const errors = [];
845
- const warnings = [];
846
-
847
- if (!config.agents || !Array.isArray(config.agents)) {
848
- return { errors, warnings };
849
- }
850
-
851
- for (const agent of config.agents) {
852
- // Skip subclusters - they have their own validation
853
- if (agent.type === 'subcluster') {
854
- continue;
855
- }
856
-
857
- const hooks = agent.hooks || {};
858
- const hookTypes = ['onComplete', 'onFailure', 'onTimeout'];
859
-
860
- for (const hookType of hookTypes) {
861
- const hook = hooks[hookType];
862
- if (!hook) continue;
863
-
864
- const prefix = `Agent '${agent.id}' hooks.${hookType}`;
865
-
866
- // === GAP 1: Hook action field missing ===
867
- // Causes runtime crash at agent-hook-executor.js:66
868
- if (!hook.action) {
869
- errors.push(
870
- `[Gap 1] ${prefix}: Missing 'action' field. ` +
871
- `Fix: Add "action": "publish_message" or "action": "execute_system_command"`
872
- );
873
- }
874
-
875
- // === GAP 2: Transform script output shape validation ===
876
- // Causes runtime crash at agent-hook-executor.js:148
877
- if (hook.transform?.script) {
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
- }
899
-
900
- // === GAP 7: CLUSTER_OPERATIONS payload validation ===
901
- // Causes runtime crash at orchestrator.js:722
902
- if (
903
- agent.role === 'conductor' &&
904
- (hook.config?.topic === 'CLUSTER_OPERATIONS' ||
905
- hook.transform?.script?.includes('CLUSTER_OPERATIONS'))
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
- }
920
- }
921
- }
922
-
923
- return { errors, warnings };
924
- }
925
-
926
- /**
927
- * Phase 7: Rule coverage validation
928
- * Catches gaps in model rules and prompt rules that cause runtime failures
929
- *
930
- * Issue #14 - Gaps 4, 5:
931
- * - Gap 4: Model rule iteration gaps (line 916-963)
932
- * - Gap 5: Prompt rule iteration gaps (line 965-1014)
933
- *
934
- * @param {Object} config - Cluster configuration
935
- * @returns {{ errors: string[], warnings: string[] }}
936
- */
937
- function validateRuleCoverage(config) {
938
- const errors = [];
939
- const warnings = [];
940
-
941
- if (!config.agents || !Array.isArray(config.agents)) {
942
- return { errors, warnings };
943
- }
944
-
945
- for (const agent of config.agents) {
946
- if (agent.type === 'subcluster') {
947
- continue;
948
- }
949
-
950
- const maxIterations = agent.maxIterations || 30;
951
-
952
- // === GAP 4: Model rule iteration gaps ===
953
- // Causes runtime crash at agent-wrapper.js:154
954
- if (agent.modelRules && Array.isArray(agent.modelRules)) {
955
- const coveredIterations = new Set();
956
-
957
- for (const rule of agent.modelRules) {
958
- const pattern = rule.iterations;
959
-
960
- if (pattern === 'all') {
961
- // Covers all iterations
962
- for (let i = 1; i <= maxIterations; i++) {
963
- coveredIterations.add(i);
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
- }
999
- }
1000
-
1001
- // === GAP 5: Prompt rule iteration gaps ===
1002
- // Causes runtime crash at agent-wrapper.js:222
1003
- if (
1004
- agent.promptConfig &&
1005
- agent.promptConfig.type === 'rules' &&
1006
- agent.promptConfig.rules &&
1007
- Array.isArray(agent.promptConfig.rules)
1008
- ) {
1009
- const coveredIterations = new Set();
1010
-
1011
- for (const rule of agent.promptConfig.rules) {
1012
- const pattern = rule.iterations;
1013
-
1014
- if (pattern === 'all') {
1015
- for (let i = 1; i <= maxIterations; i++) {
1016
- coveredIterations.add(i);
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
- }
1047
- }
1048
- }
1049
-
1050
- return { errors, warnings };
1051
- }
1052
-
1053
- /**
1054
- * Helper: Group consecutive numbers into ranges for readable output
1055
- * Example: [1, 2, 3, 5, 7, 8, 9] -> ["1-3", "5", "7-9"]
1056
- */
1057
- function groupConsecutive(numbers) {
1058
- if (numbers.length === 0) return [];
1059
-
1060
- const sorted = [...numbers].sort((a, b) => a - b);
1061
- const ranges = [];
1062
- let rangeStart = sorted[0];
1063
- let rangeEnd = sorted[0];
1064
-
1065
- for (let i = 1; i < sorted.length; i++) {
1066
- if (sorted[i] === rangeEnd + 1) {
1067
- rangeEnd = sorted[i];
1068
- } else {
1069
- // End of range
1070
- if (rangeStart === rangeEnd) {
1071
- ranges.push(`${rangeStart}`);
1072
- } else {
1073
- ranges.push(`${rangeStart}-${rangeEnd}`);
1074
- }
1075
- rangeStart = sorted[i];
1076
- rangeEnd = sorted[i];
1077
- }
1078
- }
1079
-
1080
- // Add final range
1081
- if (rangeStart === rangeEnd) {
1082
- ranges.push(`${rangeStart}`);
1083
- } else {
1084
- ranges.push(`${rangeStart}-${rangeEnd}`);
1085
- }
1086
-
1087
- return ranges;
1088
- }
1089
-
1090
- /**
1091
- * Phase 8: N-agent cycle detection
1092
- * Detects circular dependencies with 3+ agents using DFS
1093
- *
1094
- * Issue #14 - Gap 6:
1095
- * - Gap 6: 3+ agent circular dependencies (line 1060-1165)
1096
- *
1097
- * @param {Object} config - Cluster configuration
1098
- * @returns {{ errors: string[], warnings: string[] }}
1099
- */
1100
- function detectNAgentCycles(config) {
1101
- const errors = [];
1102
- const warnings = [];
1103
-
1104
- if (!config.agents || !Array.isArray(config.agents)) {
1105
- return { errors, warnings };
1106
- }
1107
-
1108
- // Build agent dependency graph: agent -> [agents it depends on]
1109
- const agentGraph = new Map();
1110
- const topicProducers = new Map(); // topic -> [agentIds]
1111
-
1112
- // Initialize graph
1113
- for (const agent of config.agents) {
1114
- if (agent.type === 'subcluster') continue;
1115
- agentGraph.set(agent.id, []);
1116
-
1117
- // Track what topics this agent produces
1118
- const outputTopic = agent.hooks?.onComplete?.config?.topic;
1119
- if (outputTopic) {
1120
- if (!topicProducers.has(outputTopic)) {
1121
- topicProducers.set(outputTopic, []);
1122
- }
1123
- topicProducers.get(outputTopic).push(agent.id);
1124
- }
1125
- }
1126
-
1127
- // Build dependencies: agent consumes topic -> depends on agents that produce it
1128
- for (const agent of config.agents) {
1129
- 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));
1147
- }
1148
-
1149
- // === GAP 6: Detect cycles with DFS ===
1150
- const visited = new Set();
1151
- const recursionStack = new Set();
1152
-
1153
- function dfs(agentId, path) {
1154
- visited.add(agentId);
1155
- recursionStack.add(agentId);
1156
-
1157
- const dependencies = agentGraph.get(agentId) || [];
1158
- for (const nextAgent of dependencies) {
1159
- if (recursionStack.has(nextAgent)) {
1160
- // Cycle detected - return the full cycle path
1161
- const cycleStartIndex = path.indexOf(nextAgent);
1162
- const cyclePath = [...path.slice(cycleStartIndex), nextAgent];
1163
- return cyclePath;
1164
- }
1165
-
1166
- if (!visited.has(nextAgent)) {
1167
- const cycle = dfs(nextAgent, [...path, nextAgent]);
1168
- if (cycle) return cycle;
1169
- }
1170
- }
1171
-
1172
- recursionStack.delete(agentId);
1173
- return null;
1174
- }
1175
-
1176
- // Check all agents as starting points
1177
- for (const agentId of agentGraph.keys()) {
1178
- if (!visited.has(agentId)) {
1179
- 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
- }
1202
- }
1203
- }
1204
-
1205
- return { errors, warnings };
1206
- }
1207
-
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) {
1226
- const errors = [];
1227
- const warnings = [];
1228
-
1229
- if (!config.agents || !Array.isArray(config.agents)) {
1230
- return { errors, warnings };
1231
- }
1232
-
1233
- const fs = require('fs');
1234
- const path = require('path');
1235
-
1236
- // === GAP 11: Agent ID conflicts across subclusters ===
1237
- // Collect all agent IDs recursively (including subclusters)
1238
- const allAgentIds = new Map(); // Map of agentId -> depth where first seen
1239
-
1240
- function collectAgentIds(agents, depth = 0) {
1241
- if (!agents) return;
1242
-
1243
- for (const agent of agents) {
1244
- if (!agent.id) continue; // Skip agents without IDs (caught in Phase 1)
1245
-
1246
- if (allAgentIds.has(agent.id)) {
1247
- const firstSeenDepth = allAgentIds.get(agent.id);
1248
- errors.push(
1249
- `[Gap 11] Duplicate agent ID '${agent.id}' found across cluster hierarchy ` +
1250
- `(first at depth ${firstSeenDepth}, duplicate at depth ${depth}). ` +
1251
- `Fix: Ensure all agent IDs are unique across the entire cluster.`
1252
- );
1253
- } else {
1254
- allAgentIds.set(agent.id, depth);
1255
- }
1256
-
1257
- if (agent.type === 'subcluster' && agent.config?.agents) {
1258
- collectAgentIds(agent.config.agents, depth + 1);
1259
- }
1260
- }
1261
- }
1262
-
1263
- collectAgentIds(config.agents);
1264
-
1265
- for (const agent of config.agents) {
1266
- if (agent.type === 'subcluster') continue;
1267
-
1268
- const prefix = `Agent '${agent.id}'`;
1269
-
1270
- // === GAP 8: JSON schema structurally invalid ===
1271
- if (agent.jsonSchema) {
1272
- try {
1273
- // Check if schema can be stringified (basic structural check)
1274
- JSON.stringify(agent.jsonSchema);
1275
-
1276
- // Check required fields for JSON schema
1277
- if (typeof agent.jsonSchema !== 'object') {
1278
- errors.push(
1279
- `[Gap 8] ${prefix}: jsonSchema must be an object, got ${typeof agent.jsonSchema}. ` +
1280
- `Fix: Use valid JSON Schema format with 'type' and 'properties' fields.`
1281
- );
1282
- }
1283
- } catch (e) {
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
- }
1289
- }
1290
-
1291
- // === GAP 9: Context sources never produced (enhanced check) ===
1292
- // Already partially covered in Phase 2, but add stricter checks
1293
- if (agent.contextStrategy?.sources) {
1294
- const topicProducers = new Map();
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
- }
1307
-
1308
- for (const source of agent.contextStrategy.sources) {
1309
- const topic = source.topic;
1310
- if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue;
1311
- if (topic.endsWith('*')) continue;
1312
-
1313
- const producers = topicProducers.get(topic) || [];
1314
- if (producers.length === 0) {
1315
- warnings.push(
1316
- `[Gap 9] ${prefix}: Context source topic '${topic}' is never produced. ` +
1317
- `Agent will get empty context for this source.`
1318
- );
1319
- }
1320
-
1321
- // === GAP 14: Context source format invalid ===
1322
- if (source.amount === undefined) {
1323
- warnings.push(
1324
- `[Gap 14] ${prefix}: Context source for topic '${topic}' missing 'amount' field. ` +
1325
- `Defaults may not be what you expect.`
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'.`
1332
- );
1333
- }
1334
- }
1335
- }
1336
-
1337
- // === GAP 10: Isolation config invalid ===
1338
- if (agent.isolation) {
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
- }
1346
-
1347
- // Check mount paths are absolute
1348
- if (agent.isolation.mounts) {
1349
- for (const mount of agent.isolation.mounts) {
1350
- if (mount.host && !path.isAbsolute(mount.host)) {
1351
- warnings.push(
1352
- `[Gap 10] ${prefix}: Docker mount host path '${mount.host}' is not absolute. ` +
1353
- `May cause runtime errors.`
1354
- );
1355
- }
1356
- }
1357
- }
1358
- } else if (agent.isolation.type && agent.isolation.type !== 'worktree') {
1359
- errors.push(
1360
- `[Gap 10] ${prefix}: Unknown isolation type '${agent.isolation.type}'. ` +
1361
- `Fix: Use 'docker' or 'worktree'.`
1362
- );
1363
- }
1364
- }
1365
-
1366
- // === GAP 12: Load config file paths don't exist ===
1367
- if (agent.loadConfig) {
1368
- const configPath = agent.loadConfig.path;
1369
- if (configPath && !fs.existsSync(configPath)) {
1370
- errors.push(
1371
- `[Gap 12] ${prefix}: Load config file '${configPath}' does not exist. ` +
1372
- `Fix: Check file path or remove loadConfig.`
1373
- );
1374
- }
1375
- }
1376
-
1377
- // === GAP 13: Task executor config invalid ===
1378
- if (agent.taskExecutor) {
1379
- if (agent.taskExecutor.command === undefined) {
1380
- errors.push(
1381
- `[Gap 13] ${prefix}: Task executor missing 'command' field. ` +
1382
- `Fix: Add "command": "claude" or custom command.`
1383
- );
1384
- }
1385
-
1386
- if (agent.taskExecutor.retries !== undefined) {
1387
- if (typeof agent.taskExecutor.retries !== 'number' || agent.taskExecutor.retries < 0) {
1388
- errors.push(
1389
- `[Gap 13] ${prefix}: Task executor 'retries' must be a non-negative number, got ${agent.taskExecutor.retries}. ` +
1390
- `Fix: Use a positive integer or 0.`
1391
- );
1392
- }
1393
- }
1394
-
1395
- if (agent.taskExecutor.timeout !== undefined) {
1396
- if (typeof agent.taskExecutor.timeout !== 'number' || agent.taskExecutor.timeout <= 0) {
1397
- errors.push(
1398
- `[Gap 13] ${prefix}: Task executor 'timeout' must be a positive number, got ${agent.taskExecutor.timeout}. ` +
1399
- `Fix: Use a positive number in milliseconds.`
1400
- );
1401
- }
1402
- }
1403
- }
1404
-
1405
- // === GAP 15: Stricter role reference validation ===
1406
- // Upgrade from WARNING to ERROR when role is used in critical logic
1407
- const roles = new Set(config.agents.filter((a) => a.type !== 'subcluster').map((a) => a.role));
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
- }
1441
- }
1442
-
1443
- return { errors, warnings };
1444
- }
1445
-
1446
- function resolveProviderName(agent, config, settings) {
1447
- const resolved =
1448
- config.forceProvider ||
1449
- agent.provider ||
1450
- config.defaultProvider ||
1451
- settings.defaultProvider ||
1452
- 'claude';
1453
- return normalizeProviderName(resolved) || 'claude';
1454
- }
1455
-
1456
- function validateProviderLevel(provider, requestedLevel, minLevel, maxLevel) {
1457
- const providerModule = getProvider(provider);
1458
- const levels = providerModule.getLevelMapping();
1459
- const rank = (level) => levels[level]?.rank;
1460
-
1461
- if (!levels[requestedLevel]) {
1462
- throw new Error(`Invalid level "${requestedLevel}" for provider "${provider}"`);
1463
- }
1464
-
1465
- if (minLevel && !levels[minLevel]) {
1466
- throw new Error(`Invalid minLevel "${minLevel}" for provider "${provider}"`);
1467
- }
1468
-
1469
- if (maxLevel && !levels[maxLevel]) {
1470
- throw new Error(`Invalid maxLevel "${maxLevel}" for provider "${provider}"`);
1471
- }
1472
-
1473
- if (minLevel && maxLevel && rank(minLevel) > rank(maxLevel)) {
1474
- throw new Error(
1475
- `minLevel "${minLevel}" exceeds maxLevel "${maxLevel}" for provider "${provider}"`
1476
- );
1477
- }
1478
-
1479
- if (maxLevel && rank(requestedLevel) > rank(maxLevel)) {
1480
- throw new Error(
1481
- `Level "${requestedLevel}" exceeds maxLevel "${maxLevel}" for provider "${provider}"`
1482
- );
1483
- }
1484
-
1485
- if (minLevel && rank(requestedLevel) < rank(minLevel)) {
1486
- throw new Error(
1487
- `Level "${requestedLevel}" is below minLevel "${minLevel}" for provider "${provider}"`
1488
- );
1489
- }
1490
-
1491
- return requestedLevel;
1492
- }
1493
-
1494
- function validateProviderSettings(provider, providerSettings) {
1495
- const providerModule = getProvider(provider);
1496
- const levels = providerModule.getLevelMapping();
1497
- const settings = providerSettings || {};
1498
-
1499
- const minLevel = settings.minLevel || providerModule.getDefaultMinLevel?.();
1500
- const maxLevel = settings.maxLevel || providerModule.getDefaultMaxLevel?.();
1501
- const defaultLevel = settings.defaultLevel || providerModule.getDefaultLevel();
1502
-
1503
- validateProviderLevel(provider, defaultLevel, minLevel, maxLevel);
1504
-
1505
- for (const [level, override] of Object.entries(settings.levelOverrides || {})) {
1506
- if (!levels[level]) {
1507
- throw new Error(`Unknown level "${level}" in overrides for provider "${provider}"`);
1508
- }
1509
- if (override?.model && (typeof override.model !== 'string' || !override.model.trim())) {
1510
- throw new Error(
1511
- `Invalid model override (must be non-empty string) for provider "${provider}"`
1512
- );
1513
- }
1514
- if (override?.reasoningEffort && provider !== 'codex') {
1515
- throw new Error(`reasoningEffort overrides are only supported for Codex`);
1516
- }
1517
- if (
1518
- override?.reasoningEffort &&
1519
- !['low', 'medium', 'high', 'xhigh'].includes(override.reasoningEffort)
1520
- ) {
1521
- throw new Error(
1522
- `Invalid reasoningEffort "${override.reasoningEffort}" (low|medium|high|xhigh)`
1523
- );
1524
- }
1525
- }
1526
- }
1527
-
1528
- function validateProviderFeatures(config, settings) {
1529
- const errors = [];
1530
- const warnings = [];
1531
-
1532
- const providersToValidate = VALID_PROVIDERS;
1533
-
1534
- for (const provider of providersToValidate) {
1535
- try {
1536
- validateProviderSettings(provider, settings.providerSettings?.[provider]);
1537
- } catch (err) {
1538
- errors.push(err.message);
1539
- }
1540
- }
1541
-
1542
- if (!config.agents || !Array.isArray(config.agents)) {
1543
- return { errors, warnings };
1544
- }
1545
-
1546
- for (const agent of config.agents) {
1547
- if (agent.type === 'subcluster') {
1548
- continue;
1549
- }
1550
-
1551
- const provider = resolveProviderName(agent, config, settings);
1552
- if (!VALID_PROVIDERS.includes(provider)) {
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
- }
1606
-
1607
- if (agent.modelRules && Array.isArray(agent.modelRules)) {
1608
- for (const rule of agent.modelRules) {
1609
- if (rule.modelLevel && !levels[rule.modelLevel]) {
1610
- warnings.push(
1611
- `Agent "${agent.id}" uses modelLevel "${rule.modelLevel}" in modelRules which is not valid for ${provider}`
1612
- );
1613
- }
1614
- if (rule.model && !catalog[rule.model]) {
1615
- warnings.push(
1616
- `Agent "${agent.id}" uses model "${rule.model}" in modelRules which is not valid for ${provider}`
1617
- );
1618
- }
1619
- }
1620
- }
1621
-
1622
- if (agent.reasoningEffort && provider !== 'codex') {
1623
- warnings.push(`Agent "${agent.id}" sets reasoningEffort but ${provider} does not support it`);
1624
- } else if (
1625
- agent.reasoningEffort &&
1626
- !['low', 'medium', 'high', 'xhigh'].includes(agent.reasoningEffort)
1627
- ) {
1628
- warnings.push(
1629
- `Agent "${agent.id}" has invalid reasoningEffort "${agent.reasoningEffort}" (low|medium|high|xhigh)`
1630
- );
1631
- }
1632
- }
1633
-
1634
- return { errors, warnings };
1635
- }
1636
-
1637
- module.exports = {
1638
- validateConfig,
1639
- isConductorConfig,
1640
- validateBasicStructure,
1641
- analyzeMessageFlow,
1642
- validateAgents,
1643
- validateLogicScripts,
1644
- isValidIterationPattern,
1645
- formatValidationResult,
1646
- // Phase 5: Template variable validation
1647
- validateTemplateVariables,
1648
- extractTemplateVariables,
1649
- extractSchemaProperties,
1650
- validateAgentTemplateVariables,
1651
- // Phase 6-9: Semantic validation
1652
- validateHookSemantics,
1653
- validateRuleCoverage,
1654
- detectNAgentCycles,
1655
- validateConfigSemantics,
1656
- validateProviderLevel,
1657
- validateProviderSettings,
1658
- validateProviderFeatures,
1659
- groupConsecutive,
1660
- };