tribunal-kit 4.3.1 → 4.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.
- package/.agent/agents/api-architect.md +66 -66
- package/.agent/agents/db-latency-auditor.md +216 -216
- package/.agent/agents/precedence-reviewer.md +250 -250
- package/.agent/agents/resilience-reviewer.md +88 -88
- package/.agent/agents/schema-reviewer.md +67 -67
- package/.agent/agents/throughput-optimizer.md +299 -299
- package/.agent/agents/ui-ux-auditor.md +292 -292
- package/.agent/agents/vitals-reviewer.md +223 -223
- package/.agent/scripts/_colors.js +18 -18
- package/.agent/scripts/_utils.js +42 -42
- package/.agent/scripts/append_flow.js +72 -72
- package/.agent/scripts/auto_preview.js +197 -197
- package/.agent/scripts/bundle_analyzer.js +290 -290
- package/.agent/scripts/case_law_manager.js +17 -6
- package/.agent/scripts/checklist.js +266 -266
- package/.agent/scripts/colors.js +17 -17
- package/.agent/scripts/compress_skills.js +141 -141
- package/.agent/scripts/consolidate_skills.js +149 -149
- package/.agent/scripts/context_broker.js +611 -609
- package/.agent/scripts/deep_compress.js +150 -150
- package/.agent/scripts/dependency_analyzer.js +272 -272
- package/.agent/scripts/graph_builder.js +151 -37
- package/.agent/scripts/graph_visualizer.js +384 -0
- package/.agent/scripts/inner_loop_validator.js +451 -465
- package/.agent/scripts/lint_runner.js +187 -187
- package/.agent/scripts/minify_context.js +100 -100
- package/.agent/scripts/mutation_runner.js +280 -0
- package/.agent/scripts/patch_skills_meta.js +156 -156
- package/.agent/scripts/patch_skills_output.js +244 -244
- package/.agent/scripts/schema_validator.js +297 -297
- package/.agent/scripts/security_scan.js +303 -303
- package/.agent/scripts/session_manager.js +276 -276
- package/.agent/scripts/skill_evolution.js +644 -644
- package/.agent/scripts/skill_integrator.js +313 -313
- package/.agent/scripts/strengthen_skills.js +193 -193
- package/.agent/scripts/strip_tribunal.js +47 -47
- package/.agent/scripts/swarm_dispatcher.js +360 -360
- package/.agent/scripts/test_runner.js +193 -193
- package/.agent/scripts/utils.js +32 -32
- package/.agent/scripts/verify_all.js +257 -256
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
- package/.agent/skills/doc.md +1 -1
- package/.agent/skills/knowledge-graph/SKILL.md +32 -16
- package/.agent/skills/testing-patterns/SKILL.md +19 -2
- package/.agent/skills/ui-ux-pro-max/SKILL.md +480 -43
- package/.agent/workflows/generate.md +183 -183
- package/.agent/workflows/tribunal-speed.md +183 -183
- package/README.md +1 -1
- package/bin/tribunal-kit.js +134 -17
- package/package.json +6 -3
- package/scripts/changelog.js +167 -167
- package/scripts/sync-version.js +81 -81
- package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
|
@@ -1,360 +1,360 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* swarm_dispatcher.js
|
|
4
|
-
* Validate Orchestrator micro-worker payloads (legacy) and Swarm payloads.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const { execSync } = require('child_process');
|
|
12
|
-
|
|
13
|
-
const VALID_WORKER_TYPES = new Set([
|
|
14
|
-
"research", "generate_code", "review_code", "debug",
|
|
15
|
-
"plan", "design_schema", "write_docs", "security_audit",
|
|
16
|
-
"optimize", "test"
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
const VALID_RESULT_STATUSES = new Set(["success", "failure", "escalate"]);
|
|
20
|
-
|
|
21
|
-
const MAX_GOAL_LENGTH = 200;
|
|
22
|
-
const MAX_CONTEXT_LENGTH = 800;
|
|
23
|
-
const MAX_WORKERS_PER_SWARM = 5;
|
|
24
|
-
|
|
25
|
-
function findAgentDir(startPath) {
|
|
26
|
-
let current = path.resolve(startPath);
|
|
27
|
-
const root = path.parse(current).root;
|
|
28
|
-
while (current !== root) {
|
|
29
|
-
const agentDir = path.join(current, '.agent');
|
|
30
|
-
if (fs.existsSync(agentDir) && fs.statSync(agentDir).isDirectory()) {
|
|
31
|
-
return agentDir;
|
|
32
|
-
}
|
|
33
|
-
current = path.dirname(current);
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ─── Legacy mode: validate orchestrator micro-worker payloads ──────────────────
|
|
39
|
-
|
|
40
|
-
function validatePayload(payloadData, workspaceRoot, agentsDir) {
|
|
41
|
-
if (!payloadData.dispatch_micro_workers) {
|
|
42
|
-
console.error("ERROR: Payload missing required 'dispatch_micro_workers' array.");
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const workers = payloadData.dispatch_micro_workers;
|
|
47
|
-
if (!Array.isArray(workers)) {
|
|
48
|
-
console.error("ERROR: 'dispatch_micro_workers' must be a list.");
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let allValid = true;
|
|
53
|
-
for (let i = 0; i < workers.length; i++) {
|
|
54
|
-
const worker = workers[i];
|
|
55
|
-
const agentName = worker.target_agent;
|
|
56
|
-
if (!agentName) {
|
|
57
|
-
console.error(`ERROR: Worker ${i}: missing 'target_agent'.`);
|
|
58
|
-
allValid = false;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const agentFile = path.join(agentsDir, `${agentName}.md`);
|
|
63
|
-
if (!fs.existsSync(agentFile)) {
|
|
64
|
-
console.error(`ERROR: Worker ${i}: target_agent '${agentName}' not found at ${agentFile}.`);
|
|
65
|
-
allValid = false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const filesAttached = worker.files_attached || [];
|
|
69
|
-
if (!Array.isArray(filesAttached)) {
|
|
70
|
-
console.error(`ERROR: Worker ${i}: 'files_attached' must be a list.`);
|
|
71
|
-
allValid = false;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const f of filesAttached) {
|
|
76
|
-
const filePath = path.resolve(workspaceRoot, f);
|
|
77
|
-
if (!fs.existsSync(filePath)) {
|
|
78
|
-
console.warn(`WARN: Worker ${i}: attached file '${f}' does not exist (might be a new file to create).`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return allValid;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildWorkerPrompts(payloadData, workspaceRoot) {
|
|
87
|
-
const prompts = [];
|
|
88
|
-
let astContext = "";
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const res = execSync(`python -m code_review_graph review-delta`, { cwd: workspaceRoot, stdio: 'pipe' }).toString().trim();
|
|
92
|
-
if (res) {
|
|
93
|
-
astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
|
|
94
|
-
}
|
|
95
|
-
} catch
|
|
96
|
-
// ignore warning
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const workers = payloadData.dispatch_micro_workers || [];
|
|
100
|
-
for (const worker of workers) {
|
|
101
|
-
const agent = worker.target_agent;
|
|
102
|
-
const ctx = worker.context_summary || "";
|
|
103
|
-
const task = worker.task_description || "";
|
|
104
|
-
const files = worker.files_attached || [];
|
|
105
|
-
|
|
106
|
-
let prompt = `--- MICRO-WORKER DISPATCH ---\n`;
|
|
107
|
-
prompt += `Agent: ${agent}\n`;
|
|
108
|
-
prompt += `Context: ${ctx}${astContext}\n`;
|
|
109
|
-
prompt += `Task: ${task}\n`;
|
|
110
|
-
prompt += `Attached Files: ${files.length ? files.join(', ') : 'None'}\n`;
|
|
111
|
-
prompt += `-----------------------------`;
|
|
112
|
-
prompts.push(prompt);
|
|
113
|
-
}
|
|
114
|
-
return prompts;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── Swarm mode: validate WorkerRequest / WorkerResult payloads ───────────────
|
|
118
|
-
|
|
119
|
-
function validateWorkerRequest(req, index, agentsDir) {
|
|
120
|
-
const errors = [];
|
|
121
|
-
|
|
122
|
-
const taskId = req.task_id;
|
|
123
|
-
if (!taskId || typeof taskId !== 'string') {
|
|
124
|
-
errors.push(`WorkerRequest[${index}]: 'task_id' must be a non-empty string.`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const reqType = req.type;
|
|
128
|
-
if (!VALID_WORKER_TYPES.has(reqType)) {
|
|
129
|
-
errors.push(`WorkerRequest[${index}]: 'type' must be one of ${[...VALID_WORKER_TYPES].sort()}, got '${reqType}'.`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const agent = req.agent;
|
|
133
|
-
if (!agent || typeof agent !== 'string') {
|
|
134
|
-
errors.push(`WorkerRequest[${index}]: 'agent' must be a non-empty string.`);
|
|
135
|
-
} else {
|
|
136
|
-
const agentFile = path.join(agentsDir, `${agent}.md`);
|
|
137
|
-
if (!fs.existsSync(agentFile)) {
|
|
138
|
-
errors.push(`WorkerRequest[${index}]: agent '${agent}' not found at ${agentFile}. Only agents that exist in .agent/agents/ are valid.`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const goal = req.goal;
|
|
143
|
-
if (!goal || typeof goal !== 'string') {
|
|
144
|
-
errors.push(`WorkerRequest[${index}]: 'goal' must be a non-empty string.`);
|
|
145
|
-
} else if (goal.length > MAX_GOAL_LENGTH) {
|
|
146
|
-
errors.push(`WorkerRequest[${index}]: 'goal' exceeds ${MAX_GOAL_LENGTH} characters (${goal.length} chars). Keep it to a single, focused sentence.`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const context = req.context;
|
|
150
|
-
if (!context || typeof context !== 'string') {
|
|
151
|
-
errors.push(`WorkerRequest[${index}]: 'context' must be a non-empty string.`);
|
|
152
|
-
} else if (context.length > MAX_CONTEXT_LENGTH) {
|
|
153
|
-
errors.push(`WorkerRequest[${index}]: 'context' exceeds ${MAX_CONTEXT_LENGTH} characters (${context.length} chars). Trim to minimal required context only.`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const maxRetries = req.max_retries;
|
|
157
|
-
if (maxRetries !== undefined) {
|
|
158
|
-
if (typeof maxRetries !== 'number' || !Number.isInteger(maxRetries) || maxRetries < 1 || maxRetries > 3) {
|
|
159
|
-
errors.push(`WorkerRequest[${index}]: 'max_retries' must be an integer between 1 and 3, got '${maxRetries}'.`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return errors;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function validateWorkerResult(res, index) {
|
|
167
|
-
const errors = [];
|
|
168
|
-
|
|
169
|
-
const taskId = res.task_id;
|
|
170
|
-
if (!taskId || typeof taskId !== 'string') {
|
|
171
|
-
errors.push(`WorkerResult[${index}]: 'task_id' must be a non-empty string.`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const agent = res.agent;
|
|
175
|
-
if (!agent || typeof agent !== 'string') {
|
|
176
|
-
errors.push(`WorkerResult[${index}]: 'agent' must be a non-empty string.`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const status = res.status;
|
|
180
|
-
if (!VALID_RESULT_STATUSES.has(status)) {
|
|
181
|
-
errors.push(`WorkerResult[${index}]: 'status' must be one of ${[...VALID_RESULT_STATUSES].sort()}, got '${status}'.`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const output = res.output;
|
|
185
|
-
const error = res.error;
|
|
186
|
-
if (status === "success" && !output) {
|
|
187
|
-
errors.push(`WorkerResult[${index}]: 'output' is required when status is 'success'.`);
|
|
188
|
-
}
|
|
189
|
-
if ((status === "failure" || status === "escalate") && !error) {
|
|
190
|
-
errors.push(`WorkerResult[${index}]: 'error' is required when status is '${status}'. Be specific — 'Something went wrong' is not acceptable.`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const attempts = res.attempts;
|
|
194
|
-
if (attempts !== undefined) {
|
|
195
|
-
if (typeof attempts !== 'number' || !Number.isInteger(attempts) || attempts < 1) {
|
|
196
|
-
errors.push(`WorkerResult[${index}]: 'attempts' must be an integer >= 1, got '${attempts}'.`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return errors;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function validateSwarmPayload(payloadData, agentsDir) {
|
|
204
|
-
let items;
|
|
205
|
-
if (typeof payloadData === 'object' && payloadData !== null) {
|
|
206
|
-
if (Array.isArray(payloadData)) {
|
|
207
|
-
items = payloadData;
|
|
208
|
-
} else if (payloadData.workers && Array.isArray(payloadData.workers)) {
|
|
209
|
-
items = payloadData.workers;
|
|
210
|
-
} else {
|
|
211
|
-
items = [payloadData];
|
|
212
|
-
}
|
|
213
|
-
} else {
|
|
214
|
-
console.error("ERROR: Swarm payload must be a JSON object or array.");
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (items.length > MAX_WORKERS_PER_SWARM) {
|
|
219
|
-
console.error(`ERROR: Swarm payload contains ${items.length} workers, exceeding the maximum of ${MAX_WORKERS_PER_SWARM}.`);
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const allErrors = [];
|
|
224
|
-
for (let i = 0; i < items.length; i++) {
|
|
225
|
-
const item = items[i];
|
|
226
|
-
if (typeof item !== 'object' || item === null) {
|
|
227
|
-
allErrors.push(`Item[${i}]: must be a JSON object.`);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
let errors;
|
|
232
|
-
if ("status" in item && "output" in item) {
|
|
233
|
-
errors = validateWorkerResult(item, i);
|
|
234
|
-
} else {
|
|
235
|
-
errors = validateWorkerRequest(item, i, agentsDir);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
allErrors.push(...errors);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (allErrors.length > 0) {
|
|
242
|
-
for (const err of allErrors) {
|
|
243
|
-
console.error(`ERROR: ${err}`);
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
252
|
-
|
|
253
|
-
function main() {
|
|
254
|
-
const args = process.argv.slice(2);
|
|
255
|
-
let payload = null;
|
|
256
|
-
let file = null;
|
|
257
|
-
let workspace = ".";
|
|
258
|
-
let mode = "legacy";
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < args.length; i++) {
|
|
261
|
-
const arg = args[i];
|
|
262
|
-
if (arg === '--payload' && i + 1 < args.length) {
|
|
263
|
-
payload = args[++i];
|
|
264
|
-
} else if (arg === '--file' && i + 1 < args.length) {
|
|
265
|
-
file = args[++i];
|
|
266
|
-
} else if (arg === '--workspace' && i + 1 < args.length) {
|
|
267
|
-
workspace = args[++i];
|
|
268
|
-
} else if (arg === '--mode' && i + 1 < args.length) {
|
|
269
|
-
mode = args[++i];
|
|
270
|
-
} else if (arg === '-h' || arg === '--help') {
|
|
271
|
-
console.log("Usage: swarm_dispatcher.js [--payload <json>] [--file <path>] [--workspace <dir>] [--mode legacy|swarm]");
|
|
272
|
-
process.exit(0);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (!payload && !file) {
|
|
277
|
-
console.error("ERROR: Must provide either --payload or --file");
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const workspaceRoot = path.resolve(workspace);
|
|
282
|
-
const agentDir = findAgentDir(workspaceRoot);
|
|
283
|
-
|
|
284
|
-
if (!agentDir) {
|
|
285
|
-
console.error(`ERROR: Could not find .agent directory starting from ${workspaceRoot}`);
|
|
286
|
-
process.exit(1);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const agentsDir = path.join(agentDir, "agents");
|
|
290
|
-
if (!fs.existsSync(agentsDir)) {
|
|
291
|
-
console.error(`ERROR: Could not find 'agents' directory inside ${agentDir}`);
|
|
292
|
-
process.exit(1);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
let payloadData;
|
|
296
|
-
try {
|
|
297
|
-
if (file) {
|
|
298
|
-
payloadData = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
299
|
-
} else {
|
|
300
|
-
payloadData = JSON.parse(payload);
|
|
301
|
-
}
|
|
302
|
-
} catch (e) {
|
|
303
|
-
console.error(`ERROR: Failed to parse payload as JSON: ${e.message}`);
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (mode === "swarm") {
|
|
308
|
-
if (!validateSwarmPayload(payloadData, agentsDir)) {
|
|
309
|
-
console.error("ERROR: Swarm payload validation failed.");
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
let astContext = "";
|
|
314
|
-
try {
|
|
315
|
-
const res = execSync(`python -m code_review_graph review-delta`, { cwd: workspaceRoot, stdio: 'pipe' }).toString().trim();
|
|
316
|
-
if (res) {
|
|
317
|
-
astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
|
|
318
|
-
}
|
|
319
|
-
} catch
|
|
320
|
-
// ignore
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (astContext) {
|
|
324
|
-
const items = (typeof payloadData === 'object' && payloadData !== null && payloadData.workers)
|
|
325
|
-
? payloadData.workers
|
|
326
|
-
: (Array.isArray(payloadData) ? payloadData : [payloadData]);
|
|
327
|
-
|
|
328
|
-
for (const item of items) {
|
|
329
|
-
if (item && "context" in item) {
|
|
330
|
-
item.context += astContext;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
console.log("INFO: Swarm payload validation successful.");
|
|
336
|
-
if (astContext) {
|
|
337
|
-
console.log("--- ENRICHED SWARM PAYLOAD ---");
|
|
338
|
-
console.log(JSON.stringify(payloadData, null, 2));
|
|
339
|
-
}
|
|
340
|
-
} else {
|
|
341
|
-
if (!validatePayload(payloadData, workspaceRoot, agentsDir)) {
|
|
342
|
-
console.error("ERROR: Payload validation failed.");
|
|
343
|
-
process.exit(1);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
console.log("INFO: Payload validation successful.");
|
|
347
|
-
const prompts = buildWorkerPrompts(payloadData, workspaceRoot);
|
|
348
|
-
|
|
349
|
-
for (let i = 0; i < prompts.length; i++) {
|
|
350
|
-
console.log(`\n[Worker ${i + 1} Ready]`);
|
|
351
|
-
console.log(prompts[i]);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
module.exports = { validateWorkerRequest, validateWorkerResult, validateSwarmPayload, validatePayload, findAgentDir };
|
|
357
|
-
|
|
358
|
-
if (require.main === module) {
|
|
359
|
-
main();
|
|
360
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* swarm_dispatcher.js
|
|
4
|
+
* Validate Orchestrator micro-worker payloads (legacy) and Swarm payloads.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const VALID_WORKER_TYPES = new Set([
|
|
14
|
+
"research", "generate_code", "review_code", "debug",
|
|
15
|
+
"plan", "design_schema", "write_docs", "security_audit",
|
|
16
|
+
"optimize", "test"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const VALID_RESULT_STATUSES = new Set(["success", "failure", "escalate"]);
|
|
20
|
+
|
|
21
|
+
const MAX_GOAL_LENGTH = 200;
|
|
22
|
+
const MAX_CONTEXT_LENGTH = 800;
|
|
23
|
+
const MAX_WORKERS_PER_SWARM = 5;
|
|
24
|
+
|
|
25
|
+
function findAgentDir(startPath) {
|
|
26
|
+
let current = path.resolve(startPath);
|
|
27
|
+
const root = path.parse(current).root;
|
|
28
|
+
while (current !== root) {
|
|
29
|
+
const agentDir = path.join(current, '.agent');
|
|
30
|
+
if (fs.existsSync(agentDir) && fs.statSync(agentDir).isDirectory()) {
|
|
31
|
+
return agentDir;
|
|
32
|
+
}
|
|
33
|
+
current = path.dirname(current);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Legacy mode: validate orchestrator micro-worker payloads ──────────────────
|
|
39
|
+
|
|
40
|
+
function validatePayload(payloadData, workspaceRoot, agentsDir) {
|
|
41
|
+
if (!payloadData.dispatch_micro_workers) {
|
|
42
|
+
console.error("ERROR: Payload missing required 'dispatch_micro_workers' array.");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const workers = payloadData.dispatch_micro_workers;
|
|
47
|
+
if (!Array.isArray(workers)) {
|
|
48
|
+
console.error("ERROR: 'dispatch_micro_workers' must be a list.");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let allValid = true;
|
|
53
|
+
for (let i = 0; i < workers.length; i++) {
|
|
54
|
+
const worker = workers[i];
|
|
55
|
+
const agentName = worker.target_agent;
|
|
56
|
+
if (!agentName) {
|
|
57
|
+
console.error(`ERROR: Worker ${i}: missing 'target_agent'.`);
|
|
58
|
+
allValid = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const agentFile = path.join(agentsDir, `${agentName}.md`);
|
|
63
|
+
if (!fs.existsSync(agentFile)) {
|
|
64
|
+
console.error(`ERROR: Worker ${i}: target_agent '${agentName}' not found at ${agentFile}.`);
|
|
65
|
+
allValid = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const filesAttached = worker.files_attached || [];
|
|
69
|
+
if (!Array.isArray(filesAttached)) {
|
|
70
|
+
console.error(`ERROR: Worker ${i}: 'files_attached' must be a list.`);
|
|
71
|
+
allValid = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const f of filesAttached) {
|
|
76
|
+
const filePath = path.resolve(workspaceRoot, f);
|
|
77
|
+
if (!fs.existsSync(filePath)) {
|
|
78
|
+
console.warn(`WARN: Worker ${i}: attached file '${f}' does not exist (might be a new file to create).`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return allValid;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildWorkerPrompts(payloadData, workspaceRoot) {
|
|
87
|
+
const prompts = [];
|
|
88
|
+
let astContext = "";
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const res = execSync(`python -m code_review_graph review-delta`, { cwd: workspaceRoot, stdio: 'pipe' }).toString().trim();
|
|
92
|
+
if (res) {
|
|
93
|
+
astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore warning
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const workers = payloadData.dispatch_micro_workers || [];
|
|
100
|
+
for (const worker of workers) {
|
|
101
|
+
const agent = worker.target_agent;
|
|
102
|
+
const ctx = worker.context_summary || "";
|
|
103
|
+
const task = worker.task_description || "";
|
|
104
|
+
const files = worker.files_attached || [];
|
|
105
|
+
|
|
106
|
+
let prompt = `--- MICRO-WORKER DISPATCH ---\n`;
|
|
107
|
+
prompt += `Agent: ${agent}\n`;
|
|
108
|
+
prompt += `Context: ${ctx}${astContext}\n`;
|
|
109
|
+
prompt += `Task: ${task}\n`;
|
|
110
|
+
prompt += `Attached Files: ${files.length ? files.join(', ') : 'None'}\n`;
|
|
111
|
+
prompt += `-----------------------------`;
|
|
112
|
+
prompts.push(prompt);
|
|
113
|
+
}
|
|
114
|
+
return prompts;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Swarm mode: validate WorkerRequest / WorkerResult payloads ───────────────
|
|
118
|
+
|
|
119
|
+
function validateWorkerRequest(req, index, agentsDir) {
|
|
120
|
+
const errors = [];
|
|
121
|
+
|
|
122
|
+
const taskId = req.task_id;
|
|
123
|
+
if (!taskId || typeof taskId !== 'string') {
|
|
124
|
+
errors.push(`WorkerRequest[${index}]: 'task_id' must be a non-empty string.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reqType = req.type;
|
|
128
|
+
if (!VALID_WORKER_TYPES.has(reqType)) {
|
|
129
|
+
errors.push(`WorkerRequest[${index}]: 'type' must be one of ${[...VALID_WORKER_TYPES].sort()}, got '${reqType}'.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const agent = req.agent;
|
|
133
|
+
if (!agent || typeof agent !== 'string') {
|
|
134
|
+
errors.push(`WorkerRequest[${index}]: 'agent' must be a non-empty string.`);
|
|
135
|
+
} else {
|
|
136
|
+
const agentFile = path.join(agentsDir, `${agent}.md`);
|
|
137
|
+
if (!fs.existsSync(agentFile)) {
|
|
138
|
+
errors.push(`WorkerRequest[${index}]: agent '${agent}' not found at ${agentFile}. Only agents that exist in .agent/agents/ are valid.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const goal = req.goal;
|
|
143
|
+
if (!goal || typeof goal !== 'string') {
|
|
144
|
+
errors.push(`WorkerRequest[${index}]: 'goal' must be a non-empty string.`);
|
|
145
|
+
} else if (goal.length > MAX_GOAL_LENGTH) {
|
|
146
|
+
errors.push(`WorkerRequest[${index}]: 'goal' exceeds ${MAX_GOAL_LENGTH} characters (${goal.length} chars). Keep it to a single, focused sentence.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const context = req.context;
|
|
150
|
+
if (!context || typeof context !== 'string') {
|
|
151
|
+
errors.push(`WorkerRequest[${index}]: 'context' must be a non-empty string.`);
|
|
152
|
+
} else if (context.length > MAX_CONTEXT_LENGTH) {
|
|
153
|
+
errors.push(`WorkerRequest[${index}]: 'context' exceeds ${MAX_CONTEXT_LENGTH} characters (${context.length} chars). Trim to minimal required context only.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const maxRetries = req.max_retries;
|
|
157
|
+
if (maxRetries !== undefined) {
|
|
158
|
+
if (typeof maxRetries !== 'number' || !Number.isInteger(maxRetries) || maxRetries < 1 || maxRetries > 3) {
|
|
159
|
+
errors.push(`WorkerRequest[${index}]: 'max_retries' must be an integer between 1 and 3, got '${maxRetries}'.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return errors;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateWorkerResult(res, index) {
|
|
167
|
+
const errors = [];
|
|
168
|
+
|
|
169
|
+
const taskId = res.task_id;
|
|
170
|
+
if (!taskId || typeof taskId !== 'string') {
|
|
171
|
+
errors.push(`WorkerResult[${index}]: 'task_id' must be a non-empty string.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const agent = res.agent;
|
|
175
|
+
if (!agent || typeof agent !== 'string') {
|
|
176
|
+
errors.push(`WorkerResult[${index}]: 'agent' must be a non-empty string.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const status = res.status;
|
|
180
|
+
if (!VALID_RESULT_STATUSES.has(status)) {
|
|
181
|
+
errors.push(`WorkerResult[${index}]: 'status' must be one of ${[...VALID_RESULT_STATUSES].sort()}, got '${status}'.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const output = res.output;
|
|
185
|
+
const error = res.error;
|
|
186
|
+
if (status === "success" && !output) {
|
|
187
|
+
errors.push(`WorkerResult[${index}]: 'output' is required when status is 'success'.`);
|
|
188
|
+
}
|
|
189
|
+
if ((status === "failure" || status === "escalate") && !error) {
|
|
190
|
+
errors.push(`WorkerResult[${index}]: 'error' is required when status is '${status}'. Be specific — 'Something went wrong' is not acceptable.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const attempts = res.attempts;
|
|
194
|
+
if (attempts !== undefined) {
|
|
195
|
+
if (typeof attempts !== 'number' || !Number.isInteger(attempts) || attempts < 1) {
|
|
196
|
+
errors.push(`WorkerResult[${index}]: 'attempts' must be an integer >= 1, got '${attempts}'.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return errors;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateSwarmPayload(payloadData, agentsDir) {
|
|
204
|
+
let items;
|
|
205
|
+
if (typeof payloadData === 'object' && payloadData !== null) {
|
|
206
|
+
if (Array.isArray(payloadData)) {
|
|
207
|
+
items = payloadData;
|
|
208
|
+
} else if (payloadData.workers && Array.isArray(payloadData.workers)) {
|
|
209
|
+
items = payloadData.workers;
|
|
210
|
+
} else {
|
|
211
|
+
items = [payloadData];
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
console.error("ERROR: Swarm payload must be a JSON object or array.");
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (items.length > MAX_WORKERS_PER_SWARM) {
|
|
219
|
+
console.error(`ERROR: Swarm payload contains ${items.length} workers, exceeding the maximum of ${MAX_WORKERS_PER_SWARM}.`);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const allErrors = [];
|
|
224
|
+
for (let i = 0; i < items.length; i++) {
|
|
225
|
+
const item = items[i];
|
|
226
|
+
if (typeof item !== 'object' || item === null) {
|
|
227
|
+
allErrors.push(`Item[${i}]: must be a JSON object.`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let errors;
|
|
232
|
+
if ("status" in item && "output" in item) {
|
|
233
|
+
errors = validateWorkerResult(item, i);
|
|
234
|
+
} else {
|
|
235
|
+
errors = validateWorkerRequest(item, i, agentsDir);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
allErrors.push(...errors);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (allErrors.length > 0) {
|
|
242
|
+
for (const err of allErrors) {
|
|
243
|
+
console.error(`ERROR: ${err}`);
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
function main() {
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
let payload = null;
|
|
256
|
+
let file = null;
|
|
257
|
+
let workspace = ".";
|
|
258
|
+
let mode = "legacy";
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < args.length; i++) {
|
|
261
|
+
const arg = args[i];
|
|
262
|
+
if (arg === '--payload' && i + 1 < args.length) {
|
|
263
|
+
payload = args[++i];
|
|
264
|
+
} else if (arg === '--file' && i + 1 < args.length) {
|
|
265
|
+
file = args[++i];
|
|
266
|
+
} else if (arg === '--workspace' && i + 1 < args.length) {
|
|
267
|
+
workspace = args[++i];
|
|
268
|
+
} else if (arg === '--mode' && i + 1 < args.length) {
|
|
269
|
+
mode = args[++i];
|
|
270
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
271
|
+
console.log("Usage: swarm_dispatcher.js [--payload <json>] [--file <path>] [--workspace <dir>] [--mode legacy|swarm]");
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!payload && !file) {
|
|
277
|
+
console.error("ERROR: Must provide either --payload or --file");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const workspaceRoot = path.resolve(workspace);
|
|
282
|
+
const agentDir = findAgentDir(workspaceRoot);
|
|
283
|
+
|
|
284
|
+
if (!agentDir) {
|
|
285
|
+
console.error(`ERROR: Could not find .agent directory starting from ${workspaceRoot}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const agentsDir = path.join(agentDir, "agents");
|
|
290
|
+
if (!fs.existsSync(agentsDir)) {
|
|
291
|
+
console.error(`ERROR: Could not find 'agents' directory inside ${agentDir}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let payloadData;
|
|
296
|
+
try {
|
|
297
|
+
if (file) {
|
|
298
|
+
payloadData = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
299
|
+
} else {
|
|
300
|
+
payloadData = JSON.parse(payload);
|
|
301
|
+
}
|
|
302
|
+
} catch (e) {
|
|
303
|
+
console.error(`ERROR: Failed to parse payload as JSON: ${e.message}`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (mode === "swarm") {
|
|
308
|
+
if (!validateSwarmPayload(payloadData, agentsDir)) {
|
|
309
|
+
console.error("ERROR: Swarm payload validation failed.");
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let astContext = "";
|
|
314
|
+
try {
|
|
315
|
+
const res = execSync(`python -m code_review_graph review-delta`, { cwd: workspaceRoot, stdio: 'pipe' }).toString().trim();
|
|
316
|
+
if (res) {
|
|
317
|
+
astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (astContext) {
|
|
324
|
+
const items = (typeof payloadData === 'object' && payloadData !== null && payloadData.workers)
|
|
325
|
+
? payloadData.workers
|
|
326
|
+
: (Array.isArray(payloadData) ? payloadData : [payloadData]);
|
|
327
|
+
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
if (item && "context" in item) {
|
|
330
|
+
item.context += astContext;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log("INFO: Swarm payload validation successful.");
|
|
336
|
+
if (astContext) {
|
|
337
|
+
console.log("--- ENRICHED SWARM PAYLOAD ---");
|
|
338
|
+
console.log(JSON.stringify(payloadData, null, 2));
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
if (!validatePayload(payloadData, workspaceRoot, agentsDir)) {
|
|
342
|
+
console.error("ERROR: Payload validation failed.");
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log("INFO: Payload validation successful.");
|
|
347
|
+
const prompts = buildWorkerPrompts(payloadData, workspaceRoot);
|
|
348
|
+
|
|
349
|
+
for (let i = 0; i < prompts.length; i++) {
|
|
350
|
+
console.log(`\n[Worker ${i + 1} Ready]`);
|
|
351
|
+
console.log(prompts[i]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { validateWorkerRequest, validateWorkerResult, validateSwarmPayload, validatePayload, findAgentDir };
|
|
357
|
+
|
|
358
|
+
if (require.main === module) {
|
|
359
|
+
main();
|
|
360
|
+
}
|