@covibes/zeroshot 1.0.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.
- package/CHANGELOG.md +167 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/index.js +3990 -0
- package/cluster-templates/base-templates/debug-workflow.json +181 -0
- package/cluster-templates/base-templates/full-workflow.json +455 -0
- package/cluster-templates/base-templates/single-worker.json +48 -0
- package/cluster-templates/base-templates/worker-validator.json +131 -0
- package/cluster-templates/conductor-bootstrap.json +122 -0
- package/cluster-templates/conductor-junior-bootstrap.json +69 -0
- package/docker/zeroshot-cluster/Dockerfile +132 -0
- package/lib/completion.js +174 -0
- package/lib/id-detector.js +53 -0
- package/lib/settings.js +97 -0
- package/lib/stream-json-parser.js +236 -0
- package/package.json +121 -0
- package/src/agent/agent-config.js +121 -0
- package/src/agent/agent-context-builder.js +241 -0
- package/src/agent/agent-hook-executor.js +329 -0
- package/src/agent/agent-lifecycle.js +555 -0
- package/src/agent/agent-stuck-detector.js +256 -0
- package/src/agent/agent-task-executor.js +1034 -0
- package/src/agent/agent-trigger-evaluator.js +67 -0
- package/src/agent-wrapper.js +459 -0
- package/src/agents/git-pusher-agent.json +20 -0
- package/src/attach/attach-client.js +438 -0
- package/src/attach/attach-server.js +543 -0
- package/src/attach/index.js +35 -0
- package/src/attach/protocol.js +220 -0
- package/src/attach/ring-buffer.js +121 -0
- package/src/attach/socket-discovery.js +242 -0
- package/src/claude-task-runner.js +468 -0
- package/src/config-router.js +80 -0
- package/src/config-validator.js +598 -0
- package/src/github.js +103 -0
- package/src/isolation-manager.js +1042 -0
- package/src/ledger.js +429 -0
- package/src/logic-engine.js +223 -0
- package/src/message-bus-bridge.js +139 -0
- package/src/message-bus.js +202 -0
- package/src/name-generator.js +232 -0
- package/src/orchestrator.js +1938 -0
- package/src/schemas/sub-cluster.js +156 -0
- package/src/sub-cluster-wrapper.js +545 -0
- package/src/task-runner.js +28 -0
- package/src/template-resolver.js +347 -0
- package/src/tui/CHANGES.txt +133 -0
- package/src/tui/LAYOUT.md +261 -0
- package/src/tui/README.txt +192 -0
- package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
- package/src/tui/data-poller.js +325 -0
- package/src/tui/demo.js +208 -0
- package/src/tui/formatters.js +123 -0
- package/src/tui/index.js +193 -0
- package/src/tui/keybindings.js +383 -0
- package/src/tui/layout.js +317 -0
- package/src/tui/renderer.js +194 -0
|
@@ -0,0 +1,598 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* Check if config is a conductor-bootstrap style config
|
|
16
|
+
* Conductor configs dynamically spawn agents via CLUSTER_OPERATIONS
|
|
17
|
+
* @param {Object} config - Cluster configuration
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isConductorConfig(config) {
|
|
21
|
+
return config.agents?.some(
|
|
22
|
+
(a) =>
|
|
23
|
+
a.role === 'conductor' &&
|
|
24
|
+
// Old style: static topic in config
|
|
25
|
+
(a.hooks?.onComplete?.config?.topic === 'CLUSTER_OPERATIONS' ||
|
|
26
|
+
// New style: topic set in transform script (check for CLUSTER_OPERATIONS in script)
|
|
27
|
+
a.hooks?.onComplete?.transform?.script?.includes('CLUSTER_OPERATIONS'))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate a cluster configuration for structural correctness
|
|
33
|
+
* @param {Object} config - Cluster configuration
|
|
34
|
+
* @param {Number} depth - Current nesting depth (for subcluster validation)
|
|
35
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
36
|
+
*/
|
|
37
|
+
function validateConfig(config, depth = 0) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
const warnings = [];
|
|
40
|
+
|
|
41
|
+
// Max nesting depth check
|
|
42
|
+
const MAX_DEPTH = 5;
|
|
43
|
+
if (depth > MAX_DEPTH) {
|
|
44
|
+
errors.push(`Cluster nesting exceeds max depth (${MAX_DEPTH})`);
|
|
45
|
+
return { valid: false, errors, warnings };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// === PHASE 1: Basic structure validation ===
|
|
49
|
+
const basicResult = validateBasicStructure(config, depth);
|
|
50
|
+
errors.push(...basicResult.errors);
|
|
51
|
+
warnings.push(...basicResult.warnings);
|
|
52
|
+
if (basicResult.errors.length > 0) {
|
|
53
|
+
// Can't proceed with flow analysis if basic structure is broken
|
|
54
|
+
return { valid: false, errors, warnings };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Conductor configs dynamically spawn agents - skip message flow analysis
|
|
58
|
+
// The orchestrator validates the spawned config at CLUSTER_OPERATIONS execution time
|
|
59
|
+
const conductorMode = isConductorConfig(config);
|
|
60
|
+
|
|
61
|
+
// === PHASE 2: Message flow analysis (skip for conductor configs) ===
|
|
62
|
+
if (!conductorMode) {
|
|
63
|
+
const flowResult = analyzeMessageFlow(config);
|
|
64
|
+
errors.push(...flowResult.errors);
|
|
65
|
+
warnings.push(...flowResult.warnings);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// === PHASE 3: Agent-specific validation ===
|
|
69
|
+
const agentResult = validateAgents(config);
|
|
70
|
+
errors.push(...agentResult.errors);
|
|
71
|
+
warnings.push(...agentResult.warnings);
|
|
72
|
+
|
|
73
|
+
// === PHASE 4: Logic script validation ===
|
|
74
|
+
const logicResult = validateLogicScripts(config);
|
|
75
|
+
errors.push(...logicResult.errors);
|
|
76
|
+
warnings.push(...logicResult.warnings);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
valid: errors.length === 0,
|
|
80
|
+
errors,
|
|
81
|
+
warnings,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Phase 1: Validate basic structure (fields, types, duplicates)
|
|
87
|
+
*/
|
|
88
|
+
function validateBasicStructure(config, depth = 0) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
|
|
92
|
+
if (!config.agents || !Array.isArray(config.agents)) {
|
|
93
|
+
errors.push('agents array is required');
|
|
94
|
+
return { errors, warnings };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.agents.length === 0) {
|
|
98
|
+
errors.push('agents array cannot be empty');
|
|
99
|
+
return { errors, warnings };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const seenIds = new Set();
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < config.agents.length; i++) {
|
|
105
|
+
const agent = config.agents[i];
|
|
106
|
+
const prefix = `agents[${i}]`;
|
|
107
|
+
|
|
108
|
+
// Check if this is a subcluster
|
|
109
|
+
const isSubCluster = agent.type === 'subcluster';
|
|
110
|
+
|
|
111
|
+
// Required fields
|
|
112
|
+
if (!agent.id) {
|
|
113
|
+
errors.push(`${prefix}.id is required`);
|
|
114
|
+
} else if (typeof agent.id !== 'string') {
|
|
115
|
+
errors.push(`${prefix}.id must be a string`);
|
|
116
|
+
} else if (seenIds.has(agent.id)) {
|
|
117
|
+
errors.push(`Duplicate agent id: "${agent.id}"`);
|
|
118
|
+
} else {
|
|
119
|
+
seenIds.add(agent.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!agent.role) {
|
|
123
|
+
errors.push(`${prefix}.role is required`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate subclusters
|
|
127
|
+
if (isSubCluster) {
|
|
128
|
+
const subClusterSchema = require('./schemas/sub-cluster');
|
|
129
|
+
const subResult = subClusterSchema.validateSubCluster(agent, depth);
|
|
130
|
+
errors.push(...subResult.errors);
|
|
131
|
+
warnings.push(...subResult.warnings);
|
|
132
|
+
continue; // Skip regular agent validation
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Regular agent validation
|
|
136
|
+
if (!agent.triggers || !Array.isArray(agent.triggers)) {
|
|
137
|
+
errors.push(`${prefix}.triggers array is required`);
|
|
138
|
+
} else if (agent.triggers.length === 0) {
|
|
139
|
+
errors.push(`${prefix}.triggers cannot be empty (agent would never activate)`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate triggers structure
|
|
143
|
+
if (agent.triggers) {
|
|
144
|
+
for (let j = 0; j < agent.triggers.length; j++) {
|
|
145
|
+
const trigger = agent.triggers[j];
|
|
146
|
+
const triggerPrefix = `${prefix}.triggers[${j}]`;
|
|
147
|
+
|
|
148
|
+
if (!trigger.topic) {
|
|
149
|
+
errors.push(`${triggerPrefix}.topic is required`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (trigger.action && !['execute_task', 'stop_cluster'].includes(trigger.action)) {
|
|
153
|
+
errors.push(
|
|
154
|
+
`${triggerPrefix}.action must be 'execute_task' or 'stop_cluster', got '${trigger.action}'`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (trigger.logic) {
|
|
159
|
+
if (!trigger.logic.script) {
|
|
160
|
+
errors.push(`${triggerPrefix}.logic.script is required when logic is specified`);
|
|
161
|
+
}
|
|
162
|
+
if (trigger.logic.engine && trigger.logic.engine !== 'javascript') {
|
|
163
|
+
errors.push(
|
|
164
|
+
`${triggerPrefix}.logic.engine must be 'javascript', got '${trigger.logic.engine}'`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Validate model rules if present
|
|
172
|
+
if (agent.modelRules) {
|
|
173
|
+
if (!Array.isArray(agent.modelRules)) {
|
|
174
|
+
errors.push(`${prefix}.modelRules must be an array`);
|
|
175
|
+
} else {
|
|
176
|
+
for (let j = 0; j < agent.modelRules.length; j++) {
|
|
177
|
+
const rule = agent.modelRules[j];
|
|
178
|
+
const rulePrefix = `${prefix}.modelRules[${j}]`;
|
|
179
|
+
|
|
180
|
+
if (!rule.iterations) {
|
|
181
|
+
errors.push(`${rulePrefix}.iterations is required`);
|
|
182
|
+
} else if (!isValidIterationPattern(rule.iterations)) {
|
|
183
|
+
errors.push(
|
|
184
|
+
`${rulePrefix}.iterations '${rule.iterations}' is invalid. Valid: "1", "1-3", "5+", "all"`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!rule.model) {
|
|
189
|
+
errors.push(`${rulePrefix}.model is required`);
|
|
190
|
+
} else if (!['opus', 'sonnet', 'haiku'].includes(rule.model)) {
|
|
191
|
+
errors.push(
|
|
192
|
+
`${rulePrefix}.model must be 'opus', 'sonnet', or 'haiku', got '${rule.model}'`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for coverage gap (no catch-all rule)
|
|
198
|
+
const hasCatchAll = agent.modelRules.some(
|
|
199
|
+
(r) => r.iterations === 'all' || /^\d+\+$/.test(r.iterations)
|
|
200
|
+
);
|
|
201
|
+
if (!hasCatchAll) {
|
|
202
|
+
errors.push(
|
|
203
|
+
`${prefix}.modelRules has no catch-all rule (e.g., "all" or "5+"). High iterations will fail.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { errors, warnings };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Phase 2: Analyze message flow for structural problems
|
|
215
|
+
*/
|
|
216
|
+
function analyzeMessageFlow(config) {
|
|
217
|
+
const errors = [];
|
|
218
|
+
const warnings = [];
|
|
219
|
+
|
|
220
|
+
// Build topic graph
|
|
221
|
+
const topicProducers = new Map(); // topic -> [agentIds that produce it]
|
|
222
|
+
const topicConsumers = new Map(); // topic -> [agentIds that consume it]
|
|
223
|
+
const agentOutputTopics = new Map(); // agentId -> [topics it produces]
|
|
224
|
+
const agentInputTopics = new Map(); // agentId -> [topics it consumes]
|
|
225
|
+
|
|
226
|
+
// System always produces ISSUE_OPENED
|
|
227
|
+
topicProducers.set('ISSUE_OPENED', ['system']);
|
|
228
|
+
|
|
229
|
+
for (const agent of config.agents) {
|
|
230
|
+
agentInputTopics.set(agent.id, []);
|
|
231
|
+
agentOutputTopics.set(agent.id, []);
|
|
232
|
+
|
|
233
|
+
// Track what topics this agent consumes (triggers)
|
|
234
|
+
for (const trigger of agent.triggers || []) {
|
|
235
|
+
const topic = trigger.topic;
|
|
236
|
+
if (!topicConsumers.has(topic)) {
|
|
237
|
+
topicConsumers.set(topic, []);
|
|
238
|
+
}
|
|
239
|
+
topicConsumers.get(topic).push(agent.id);
|
|
240
|
+
agentInputTopics.get(agent.id).push(topic);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Track what topics this agent produces (hooks)
|
|
244
|
+
const outputTopic = agent.hooks?.onComplete?.config?.topic;
|
|
245
|
+
if (outputTopic) {
|
|
246
|
+
if (!topicProducers.has(outputTopic)) {
|
|
247
|
+
topicProducers.set(outputTopic, []);
|
|
248
|
+
}
|
|
249
|
+
topicProducers.get(outputTopic).push(agent.id);
|
|
250
|
+
agentOutputTopics.get(agent.id).push(outputTopic);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// === CHECK 1: No bootstrap trigger ===
|
|
255
|
+
const issueOpenedConsumers = topicConsumers.get('ISSUE_OPENED') || [];
|
|
256
|
+
if (issueOpenedConsumers.length === 0) {
|
|
257
|
+
errors.push(
|
|
258
|
+
'No agent triggers on ISSUE_OPENED. Cluster will never start. ' +
|
|
259
|
+
'Add a trigger: { "topic": "ISSUE_OPENED", "action": "execute_task" }'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// === CHECK 2: No completion handler ===
|
|
264
|
+
const completionHandlers = config.agents.filter(
|
|
265
|
+
(a) =>
|
|
266
|
+
a.triggers?.some((t) => t.action === 'stop_cluster') ||
|
|
267
|
+
a.id === 'completion-detector' ||
|
|
268
|
+
a.id === 'git-pusher' ||
|
|
269
|
+
a.hooks?.onComplete?.config?.topic === 'CLUSTER_COMPLETE'
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (completionHandlers.length === 0) {
|
|
273
|
+
errors.push(
|
|
274
|
+
'No completion handler found. Cluster will run until idle timeout (2 min). ' +
|
|
275
|
+
'Add an agent with trigger action: "stop_cluster"'
|
|
276
|
+
);
|
|
277
|
+
} else if (completionHandlers.length > 1) {
|
|
278
|
+
errors.push(
|
|
279
|
+
`Multiple completion handlers: [${completionHandlers.map((a) => a.id).join(', ')}]. ` +
|
|
280
|
+
'This causes race conditions. Keep only one.'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// === CHECK 3: Orphan topics (produced but never consumed) ===
|
|
285
|
+
for (const [topic, producers] of topicProducers) {
|
|
286
|
+
if (topic === 'CLUSTER_COMPLETE') continue; // System handles this
|
|
287
|
+
const consumers = topicConsumers.get(topic) || [];
|
|
288
|
+
if (consumers.length === 0) {
|
|
289
|
+
warnings.push(
|
|
290
|
+
`Topic '${topic}' is produced by [${producers.join(', ')}] but never consumed. Dead end.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// === CHECK 4: Waiting for topics that are never produced ===
|
|
296
|
+
for (const [topic, consumers] of topicConsumers) {
|
|
297
|
+
if (topic === 'ISSUE_OPENED' || topic === 'CLUSTER_RESUMED') continue; // System produces
|
|
298
|
+
if (topic.endsWith('*')) continue; // Wildcard pattern
|
|
299
|
+
const producers = topicProducers.get(topic) || [];
|
|
300
|
+
if (producers.length === 0) {
|
|
301
|
+
errors.push(
|
|
302
|
+
`Topic '${topic}' consumed by [${consumers.join(', ')}] but never produced. ` +
|
|
303
|
+
'These agents will never trigger.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// === CHECK 5: Self-triggering agents (instant infinite loop) ===
|
|
309
|
+
for (const agent of config.agents) {
|
|
310
|
+
const inputs = agentInputTopics.get(agent.id) || [];
|
|
311
|
+
const outputs = agentOutputTopics.get(agent.id) || [];
|
|
312
|
+
const selfTrigger = inputs.find((t) => outputs.includes(t));
|
|
313
|
+
if (selfTrigger) {
|
|
314
|
+
errors.push(
|
|
315
|
+
`Agent '${agent.id}' triggers on '${selfTrigger}' and produces '${selfTrigger}'. ` +
|
|
316
|
+
'Instant infinite loop.'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// === CHECK 6: Two-agent circular dependency ===
|
|
322
|
+
for (const agentA of config.agents) {
|
|
323
|
+
const outputsA = agentOutputTopics.get(agentA.id) || [];
|
|
324
|
+
for (const agentB of config.agents) {
|
|
325
|
+
if (agentA.id === agentB.id) continue;
|
|
326
|
+
const inputsB = agentInputTopics.get(agentB.id) || [];
|
|
327
|
+
const outputsB = agentOutputTopics.get(agentB.id) || [];
|
|
328
|
+
const inputsA = agentInputTopics.get(agentA.id) || [];
|
|
329
|
+
|
|
330
|
+
// A produces what B consumes, AND B produces what A consumes
|
|
331
|
+
const aToB = outputsA.some((t) => inputsB.includes(t));
|
|
332
|
+
const bToA = outputsB.some((t) => inputsA.includes(t));
|
|
333
|
+
|
|
334
|
+
if (aToB && bToA) {
|
|
335
|
+
// This might be intentional (rejection loop), check if there's an escape
|
|
336
|
+
const hasEscapeLogic =
|
|
337
|
+
agentA.triggers?.some((t) => t.logic) || agentB.triggers?.some((t) => t.logic);
|
|
338
|
+
if (!hasEscapeLogic) {
|
|
339
|
+
warnings.push(
|
|
340
|
+
`Circular dependency: '${agentA.id}' ↔ '${agentB.id}'. ` +
|
|
341
|
+
'Add logic conditions to prevent infinite loop, or ensure maxIterations is set.'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// === CHECK 7: Validator without worker re-trigger ===
|
|
349
|
+
const validators = config.agents.filter((a) => a.role === 'validator');
|
|
350
|
+
const workers = config.agents.filter((a) => a.role === 'implementation');
|
|
351
|
+
|
|
352
|
+
if (validators.length > 0 && workers.length > 0) {
|
|
353
|
+
for (const worker of workers) {
|
|
354
|
+
const triggersOnValidation = worker.triggers?.some(
|
|
355
|
+
(t) => t.topic === 'VALIDATION_RESULT' || t.topic.includes('VALIDATION')
|
|
356
|
+
);
|
|
357
|
+
if (!triggersOnValidation) {
|
|
358
|
+
errors.push(
|
|
359
|
+
`Worker '${worker.id}' has validators but doesn't trigger on VALIDATION_RESULT. ` +
|
|
360
|
+
'Rejections will be ignored. Add trigger: { "topic": "VALIDATION_RESULT", "logic": {...} }'
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// === CHECK 8: Context strategy missing trigger topics ===
|
|
367
|
+
for (const agent of config.agents) {
|
|
368
|
+
if (!agent.contextStrategy?.sources) continue;
|
|
369
|
+
|
|
370
|
+
const triggerTopics = (agent.triggers || []).map((t) => t.topic);
|
|
371
|
+
const contextTopics = agent.contextStrategy.sources.map((s) => s.topic);
|
|
372
|
+
|
|
373
|
+
for (const triggerTopic of triggerTopics) {
|
|
374
|
+
if (triggerTopic === 'ISSUE_OPENED' || triggerTopic === 'CLUSTER_RESUMED') continue;
|
|
375
|
+
if (triggerTopic.endsWith('*')) continue;
|
|
376
|
+
|
|
377
|
+
if (!contextTopics.includes(triggerTopic)) {
|
|
378
|
+
warnings.push(
|
|
379
|
+
`Agent '${agent.id}' triggers on '${triggerTopic}' but doesn't include it in contextStrategy. ` +
|
|
380
|
+
'Agent may not see what triggered it.'
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { errors, warnings };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Phase 3: Validate agent-specific configurations
|
|
391
|
+
*/
|
|
392
|
+
function validateAgents(config) {
|
|
393
|
+
const errors = [];
|
|
394
|
+
const warnings = [];
|
|
395
|
+
|
|
396
|
+
const roles = new Map(); // role -> [agentIds]
|
|
397
|
+
|
|
398
|
+
for (const agent of config.agents) {
|
|
399
|
+
// Track roles
|
|
400
|
+
if (!roles.has(agent.role)) {
|
|
401
|
+
roles.set(agent.role, []);
|
|
402
|
+
}
|
|
403
|
+
roles.get(agent.role).push(agent.id);
|
|
404
|
+
|
|
405
|
+
// Orchestrator should not execute tasks
|
|
406
|
+
if (agent.role === 'orchestrator') {
|
|
407
|
+
const executesTask = agent.triggers?.some(
|
|
408
|
+
(t) => t.action === 'execute_task' || (!t.action && !t.logic)
|
|
409
|
+
);
|
|
410
|
+
if (executesTask) {
|
|
411
|
+
warnings.push(
|
|
412
|
+
`Orchestrator '${agent.id}' has execute_task triggers. ` +
|
|
413
|
+
'Orchestrators typically use action: "stop_cluster". This may waste API calls.'
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// JSON output without schema
|
|
419
|
+
if (agent.outputFormat === 'json' && !agent.jsonSchema) {
|
|
420
|
+
warnings.push(
|
|
421
|
+
`Agent '${agent.id}' has outputFormat: 'json' but no jsonSchema. ` +
|
|
422
|
+
'Output parsing may be unreliable.'
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Very high maxIterations
|
|
427
|
+
if (agent.maxIterations && agent.maxIterations > 50) {
|
|
428
|
+
warnings.push(
|
|
429
|
+
`Agent '${agent.id}' has maxIterations: ${agent.maxIterations}. ` +
|
|
430
|
+
'This may consume significant API credits if stuck in a loop.'
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// No maxIterations on implementation agent (unbounded retries)
|
|
435
|
+
if (agent.role === 'implementation' && !agent.maxIterations) {
|
|
436
|
+
warnings.push(
|
|
437
|
+
`Implementation agent '${agent.id}' has no maxIterations. ` +
|
|
438
|
+
'Defaults to 30, but consider setting explicitly.'
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check for role references in logic scripts
|
|
444
|
+
// IMPORTANT: Changed from error to warning because some triggers are designed to be
|
|
445
|
+
// no-ops when the referenced role doesn't exist (e.g., worker's VALIDATION_RESULT
|
|
446
|
+
// trigger returns false when validators.length === 0)
|
|
447
|
+
for (const agent of config.agents) {
|
|
448
|
+
for (const trigger of agent.triggers || []) {
|
|
449
|
+
if (trigger.logic?.script) {
|
|
450
|
+
const script = trigger.logic.script;
|
|
451
|
+
const roleMatch = script.match(/getAgentsByRole\(['"](\w+)['"]\)/g);
|
|
452
|
+
if (roleMatch) {
|
|
453
|
+
for (const match of roleMatch) {
|
|
454
|
+
const role = match.match(/['"](\w+)['"]/)[1];
|
|
455
|
+
if (!roles.has(role)) {
|
|
456
|
+
warnings.push(
|
|
457
|
+
`Agent '${agent.id}' logic references role '${role}' but no agent has that role. ` +
|
|
458
|
+
`Trigger may be a no-op. Available roles: [${Array.from(roles.keys()).join(', ')}]`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return { errors, warnings };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Phase 4: Validate logic scripts (syntax only, not semantics)
|
|
472
|
+
*/
|
|
473
|
+
function validateLogicScripts(config) {
|
|
474
|
+
const errors = [];
|
|
475
|
+
const warnings = [];
|
|
476
|
+
|
|
477
|
+
const vm = require('vm');
|
|
478
|
+
|
|
479
|
+
for (const agent of config.agents) {
|
|
480
|
+
for (const trigger of agent.triggers || []) {
|
|
481
|
+
if (!trigger.logic?.script) continue;
|
|
482
|
+
|
|
483
|
+
const script = trigger.logic.script;
|
|
484
|
+
|
|
485
|
+
// Syntax check
|
|
486
|
+
try {
|
|
487
|
+
const wrappedScript = `(function() { ${script} })()`;
|
|
488
|
+
new vm.Script(wrappedScript);
|
|
489
|
+
} catch (syntaxError) {
|
|
490
|
+
errors.push(`Agent '${agent.id}' has invalid logic script: ${syntaxError.message}`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Check for common mistakes - only flag if script is JUST "return false" or "return true"
|
|
495
|
+
// Complex scripts with conditionals should not trigger this
|
|
496
|
+
const trimmedScript = script.trim().replace(/\s+/g, ' ');
|
|
497
|
+
const isSimpleReturnFalse = /^return\s+false;?$/.test(trimmedScript);
|
|
498
|
+
const isSimpleReturnTrue = /^return\s+true;?$/.test(trimmedScript);
|
|
499
|
+
|
|
500
|
+
if (isSimpleReturnFalse) {
|
|
501
|
+
warnings.push(
|
|
502
|
+
`Agent '${agent.id}' logic is just 'return false'. Agent will never trigger.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (isSimpleReturnTrue) {
|
|
507
|
+
warnings.push(
|
|
508
|
+
`Agent '${agent.id}' logic is just 'return true'. Consider adding conditions or removing the logic block.`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check for undefined variable access (common typos)
|
|
513
|
+
const knownVars = [
|
|
514
|
+
'ledger',
|
|
515
|
+
'cluster',
|
|
516
|
+
'message',
|
|
517
|
+
'agent',
|
|
518
|
+
'helpers',
|
|
519
|
+
'Set',
|
|
520
|
+
'Map',
|
|
521
|
+
'Array',
|
|
522
|
+
'Object',
|
|
523
|
+
'JSON',
|
|
524
|
+
'Date',
|
|
525
|
+
'Math',
|
|
526
|
+
];
|
|
527
|
+
const varPattern = /\b([a-zA-Z_]\w*)\s*\./g;
|
|
528
|
+
let match;
|
|
529
|
+
while ((match = varPattern.exec(script)) !== null) {
|
|
530
|
+
const varName = match[1];
|
|
531
|
+
if (
|
|
532
|
+
!knownVars.includes(varName) &&
|
|
533
|
+
!script.includes(`const ${varName}`) &&
|
|
534
|
+
!script.includes(`let ${varName}`)
|
|
535
|
+
) {
|
|
536
|
+
warnings.push(
|
|
537
|
+
`Agent '${agent.id}' logic uses '${varName}' which may be undefined. ` +
|
|
538
|
+
`Available: [${knownVars.join(', ')}]`
|
|
539
|
+
);
|
|
540
|
+
break; // Only warn once per agent
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { errors, warnings };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Check if iteration pattern is valid
|
|
551
|
+
*/
|
|
552
|
+
function isValidIterationPattern(pattern) {
|
|
553
|
+
if (pattern === 'all') return true;
|
|
554
|
+
if (/^\d+$/.test(pattern)) return true; // "1"
|
|
555
|
+
if (/^\d+-\d+$/.test(pattern)) return true; // "1-3"
|
|
556
|
+
if (/^\d+\+$/.test(pattern)) return true; // "5+"
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Format validation result for CLI output
|
|
562
|
+
*/
|
|
563
|
+
function formatValidationResult(result) {
|
|
564
|
+
const lines = [];
|
|
565
|
+
|
|
566
|
+
if (result.valid) {
|
|
567
|
+
lines.push('✅ Configuration is valid');
|
|
568
|
+
} else {
|
|
569
|
+
lines.push('❌ Configuration has errors');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (result.errors.length > 0) {
|
|
573
|
+
lines.push('\nErrors:');
|
|
574
|
+
for (const error of result.errors) {
|
|
575
|
+
lines.push(` ❌ ${error}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (result.warnings.length > 0) {
|
|
580
|
+
lines.push('\nWarnings:');
|
|
581
|
+
for (const warning of result.warnings) {
|
|
582
|
+
lines.push(` ⚠️ ${warning}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return lines.join('\n');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
validateConfig,
|
|
591
|
+
isConductorConfig,
|
|
592
|
+
validateBasicStructure,
|
|
593
|
+
analyzeMessageFlow,
|
|
594
|
+
validateAgents,
|
|
595
|
+
validateLogicScripts,
|
|
596
|
+
isValidIterationPattern,
|
|
597
|
+
formatValidationResult,
|
|
598
|
+
};
|
package/src/github.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub - Fetch and parse GitHub issues
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Issue fetching via gh CLI
|
|
6
|
+
* - Parsing of issue data into context
|
|
7
|
+
* - Fallback to plain text input
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
class GitHub {
|
|
13
|
+
/**
|
|
14
|
+
* Fetch GitHub issue by URL or number
|
|
15
|
+
* @param {String} issueRef - Issue URL or number
|
|
16
|
+
* @returns {Object} Parsed issue data
|
|
17
|
+
*/
|
|
18
|
+
static fetchIssue(issueRef) {
|
|
19
|
+
try {
|
|
20
|
+
// Extract issue number from URL if needed
|
|
21
|
+
const issueNumber = this._extractIssueNumber(issueRef);
|
|
22
|
+
|
|
23
|
+
// Fetch issue using gh CLI
|
|
24
|
+
const cmd = `gh issue view ${issueNumber} --json number,title,body,labels,assignees,comments,url`;
|
|
25
|
+
const output = execSync(cmd, { encoding: 'utf8' });
|
|
26
|
+
const issue = JSON.parse(output);
|
|
27
|
+
|
|
28
|
+
return this._parseIssue(issue);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to fetch GitHub issue: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract issue number from URL or return as-is
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
static _extractIssueNumber(issueRef) {
|
|
39
|
+
// If it's a URL, extract the number
|
|
40
|
+
const urlMatch = issueRef.match(/\/issues\/(\d+)/);
|
|
41
|
+
if (urlMatch) {
|
|
42
|
+
return urlMatch[1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Otherwise assume it's already a number
|
|
46
|
+
return issueRef;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse issue into structured context
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
static _parseIssue(issue) {
|
|
54
|
+
let context = `# GitHub Issue #${issue.number}\n\n`;
|
|
55
|
+
context += `## Title\n${issue.title}\n\n`;
|
|
56
|
+
|
|
57
|
+
if (issue.body) {
|
|
58
|
+
context += `## Description\n${issue.body}\n\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (issue.labels && issue.labels.length > 0) {
|
|
62
|
+
context += `## Labels\n`;
|
|
63
|
+
context += issue.labels.map((l) => `- ${l.name}`).join('\n');
|
|
64
|
+
context += '\n\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (issue.comments && issue.comments.length > 0) {
|
|
68
|
+
context += `## Comments\n\n`;
|
|
69
|
+
for (const comment of issue.comments) {
|
|
70
|
+
context += `### ${comment.author.login} (${new Date(comment.createdAt).toISOString()})\n`;
|
|
71
|
+
context += `${comment.body}\n\n`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
number: issue.number,
|
|
77
|
+
title: issue.title,
|
|
78
|
+
body: issue.body,
|
|
79
|
+
labels: issue.labels || [],
|
|
80
|
+
comments: issue.comments || [],
|
|
81
|
+
url: issue.url || null,
|
|
82
|
+
context,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a plain text input wrapper
|
|
88
|
+
* @param {String} text - Plain text input
|
|
89
|
+
* @returns {Object} Structured context
|
|
90
|
+
*/
|
|
91
|
+
static createTextInput(text) {
|
|
92
|
+
return {
|
|
93
|
+
number: null,
|
|
94
|
+
title: 'Manual Input',
|
|
95
|
+
body: text,
|
|
96
|
+
labels: [],
|
|
97
|
+
comments: [],
|
|
98
|
+
context: `# Manual Input\n\n${text}\n`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = GitHub;
|