tribunal-kit 5.7.0 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.agent/ARCHITECTURE.md +6 -7
  2. package/.agent/agents/frontend-reviewer.md +13 -0
  3. package/.agent/agents/frontend-specialist.md +14 -0
  4. package/.agent/agents/logic-reviewer.md +11 -0
  5. package/.agent/agents/orchestrator.md +15 -0
  6. package/.agent/agents/security-auditor.md +13 -0
  7. package/.agent/agents/ui-ux-auditor.md +7 -31
  8. package/.agent/history/memory/.memory.idx +766 -0
  9. package/.agent/history/memory/MEMORY.md +62 -0
  10. package/.agent/routing_index.json +694 -714
  11. package/.agent/rules/GEMINI.md +58 -8
  12. package/.agent/scripts/_colors.js +131 -89
  13. package/.agent/scripts/_utils.js +163 -128
  14. package/.agent/scripts/auto_preview.js +207 -197
  15. package/.agent/scripts/bundle_analyzer.js +227 -192
  16. package/.agent/scripts/case_law_manager.js +991 -689
  17. package/.agent/scripts/checklist.js +233 -190
  18. package/.agent/scripts/context_broker.js +930 -605
  19. package/.agent/scripts/dependency_analyzer.js +275 -184
  20. package/.agent/scripts/graph_builder.js +412 -341
  21. package/.agent/scripts/graph_visualizer.js +392 -390
  22. package/.agent/scripts/graph_zoom.js +198 -156
  23. package/.agent/scripts/inner_loop_validator.js +523 -445
  24. package/.agent/scripts/lint_runner.js +199 -157
  25. package/.agent/scripts/marathon_harness.js +819 -661
  26. package/.agent/scripts/minify_context.js +115 -100
  27. package/.agent/scripts/mutation_runner.js +321 -280
  28. package/.agent/scripts/prompt_compiler.js +62 -42
  29. package/.agent/scripts/schema_validator.js +373 -280
  30. package/.agent/scripts/security_scan.js +333 -190
  31. package/.agent/scripts/session_manager.js +306 -270
  32. package/.agent/scripts/skill_evolution.js +810 -637
  33. package/.agent/scripts/skill_integrator.js +327 -307
  34. package/.agent/scripts/strengthen_skills.js +203 -193
  35. package/.agent/scripts/swarm_dispatcher.js +558 -457
  36. package/.agent/scripts/test_runner.js +178 -152
  37. package/.agent/scripts/verify_all.js +200 -168
  38. package/.agent/skills/fabel-protocol/SKILL.md +235 -0
  39. package/.agent/skills/thinking-protocol/SKILL.md +27 -0
  40. package/.agent/workflows/generate.md +1 -1
  41. package/.agent/workflows/tribunal-speed.md +1 -1
  42. package/README.md +53 -53
  43. package/bin/mcp-server.js +460 -175
  44. package/bin/tribunal-kit.js +1245 -987
  45. package/bin/wrapper.js +104 -74
  46. package/dist/cli.js +31 -0
  47. package/dist/commands/case.js +23 -0
  48. package/dist/commands/compile.js +84 -0
  49. package/dist/commands/init.js +42 -0
  50. package/dist/commands/learn.js +57 -0
  51. package/dist/commands/memory.js +456 -0
  52. package/package.json +2 -2
  53. package/scripts/benchmark.js +162 -125
  54. package/scripts/changelog.js +196 -168
  55. package/scripts/sync-version.js +94 -81
  56. package/scripts/validate-payload.js +85 -78
@@ -1,457 +1,558 @@
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
- // ─── ANSI TUI Renderer ────────────────────────────────────────────────────────
14
- class SwarmDashboard {
15
- constructor(workers) {
16
- this.workers = workers.map(w => ({
17
- name: w.target_agent || w.agent || 'Worker',
18
- task: (w.task_description || w.goal || '').slice(0, 40) + '...',
19
- status: '⏳ Pending',
20
- color: '\x1b[33m' // Yellow
21
- }));
22
- this.spinnerFrames = ['', '', '', '', '', '', '', '', '', ''];
23
- this.frameIdx = 0;
24
- this.linesRendered = 0;
25
- this.timer = null;
26
- }
27
-
28
- render() {
29
- if (this.linesRendered > 0) {
30
- process.stdout.write(`\x1b[${this.linesRendered}A`);
31
- }
32
-
33
- let output = '\n\x1b[1m\x1b[36m━━━ Tribunal Swarm Dispatcher ━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n\n';
34
- const frame = this.spinnerFrames[this.frameIdx];
35
-
36
- this.workers.forEach((w, i) => {
37
- const icon = w.status.includes('Pending') ? `\x1b[36m${frame}\x1b[0m` :
38
- w.status.includes('Done') ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✖\x1b[0m';
39
- output += ` ${icon} \x1b[1m${w.name.padEnd(25)}\x1b[0m \x1b[2m|\x1b[0m ${w.color}${w.status.padEnd(12)}\x1b[0m \x1b[2m|\x1b[0m \x1b[3m${w.task}\x1b[0m\n`;
40
- });
41
-
42
- output += '\n\x1b[1m\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n';
43
-
44
- process.stdout.write(output);
45
- this.linesRendered = this.workers.length + 5;
46
- this.frameIdx = (this.frameIdx + 1) % this.spinnerFrames.length;
47
- }
48
-
49
- start() {
50
- console.clear();
51
- this.timer = setInterval(() => this.render(), 80);
52
- }
53
-
54
- stop() {
55
- if (this.timer) clearInterval(this.timer);
56
- this.render(); // Final render
57
- }
58
-
59
- updateStatus(index, status, color) {
60
- if (this.workers[index]) {
61
- this.workers[index].status = status;
62
- this.workers[index].color = color;
63
- }
64
- }
65
- }
66
- // ─────────────────────────────────────────────────────────────────────────────
67
-
68
- const VALID_WORKER_TYPES = new Set([
69
- "research", "generate_code", "review_code", "debug",
70
- "plan", "design_schema", "write_docs", "security_audit",
71
- "optimize", "test"
72
- ]);
73
-
74
- const VALID_RESULT_STATUSES = new Set(["success", "failure", "escalate"]);
75
-
76
- const MAX_GOAL_LENGTH = 200;
77
- const MAX_CONTEXT_LENGTH = 800;
78
- const MAX_WORKERS_PER_SWARM = 5;
79
-
80
- function findAgentDir(startPath) {
81
- let current = path.resolve(startPath);
82
- const root = path.parse(current).root;
83
- while (current !== root) {
84
- const agentDir = path.join(current, '.agent');
85
- if (fs.existsSync(agentDir) && fs.statSync(agentDir).isDirectory()) {
86
- return agentDir;
87
- }
88
- current = path.dirname(current);
89
- }
90
- return null;
91
- }
92
-
93
- // ─── Legacy mode: validate orchestrator micro-worker payloads ──────────────────
94
-
95
- function validatePayload(payloadData, workspaceRoot, agentsDir) {
96
- if (!payloadData.dispatch_micro_workers) {
97
- console.error("ERROR: Payload missing required 'dispatch_micro_workers' array.");
98
- return false;
99
- }
100
-
101
- const workers = payloadData.dispatch_micro_workers;
102
- if (!Array.isArray(workers)) {
103
- console.error("ERROR: 'dispatch_micro_workers' must be a list.");
104
- return false;
105
- }
106
-
107
- let allValid = true;
108
- for (let i = 0; i < workers.length; i++) {
109
- const worker = workers[i];
110
- const agentName = worker.target_agent;
111
- if (!agentName) {
112
- console.error(`ERROR: Worker ${i}: missing 'target_agent'.`);
113
- allValid = false;
114
- continue;
115
- }
116
-
117
- const agentFile = path.join(agentsDir, `${agentName}.md`);
118
- if (!fs.existsSync(agentFile)) {
119
- console.error(`ERROR: Worker ${i}: target_agent '${agentName}' not found at ${agentFile}.`);
120
- allValid = false;
121
- }
122
-
123
- const filesAttached = worker.files_attached || [];
124
- if (!Array.isArray(filesAttached)) {
125
- console.error(`ERROR: Worker ${i}: 'files_attached' must be a list.`);
126
- allValid = false;
127
- continue;
128
- }
129
-
130
- for (const f of filesAttached) {
131
- const filePath = path.resolve(workspaceRoot, f);
132
- if (!fs.existsSync(filePath)) {
133
- console.warn(`WARN: Worker ${i}: attached file '${f}' does not exist (might be a new file to create).`);
134
- }
135
- }
136
- }
137
-
138
- return allValid;
139
- }
140
-
141
- function buildWorkerPrompts(payloadData, workspaceRoot) {
142
- const prompts = [];
143
- let astContext = "";
144
-
145
- try {
146
- const res = execSync(`python -m code_review_graph review-delta`, { cwd: workspaceRoot, stdio: 'pipe' }).toString().trim();
147
- if (res) {
148
- astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
149
- }
150
- } catch {
151
- // ignore warning
152
- }
153
-
154
- const workers = payloadData.dispatch_micro_workers || [];
155
- for (const worker of workers) {
156
- const agent = worker.target_agent;
157
- const ctx = worker.context_summary || "";
158
- const task = worker.task_description || "";
159
- const files = worker.files_attached || [];
160
-
161
- let prompt = `--- MICRO-WORKER DISPATCH ---\n`;
162
- prompt += `Agent: ${agent}\n`;
163
- prompt += `Context: ${ctx}${astContext}\n`;
164
- prompt += `Task: ${task}\n`;
165
- prompt += `Attached Files: ${files.length ? files.join(', ') : 'None'}\n`;
166
- prompt += `-----------------------------`;
167
- prompts.push(prompt);
168
- }
169
- return prompts;
170
- }
171
-
172
- // ─── Swarm mode: validate WorkerRequest / WorkerResult payloads ───────────────
173
-
174
- function validateWorkerRequest(req, index, agentsDir) {
175
- const errors = [];
176
-
177
- const taskId = req.task_id;
178
- if (!taskId || typeof taskId !== 'string') {
179
- errors.push(`WorkerRequest[${index}]: 'task_id' must be a non-empty string.`);
180
- }
181
-
182
- const reqType = req.type;
183
- if (!VALID_WORKER_TYPES.has(reqType)) {
184
- errors.push(`WorkerRequest[${index}]: 'type' must be one of ${[...VALID_WORKER_TYPES].sort()}, got '${reqType}'.`);
185
- }
186
-
187
- const agent = req.agent;
188
- if (!agent || typeof agent !== 'string') {
189
- errors.push(`WorkerRequest[${index}]: 'agent' must be a non-empty string.`);
190
- } else {
191
- const agentFile = path.join(agentsDir, `${agent}.md`);
192
- if (!fs.existsSync(agentFile)) {
193
- errors.push(`WorkerRequest[${index}]: agent '${agent}' not found at ${agentFile}. Only agents that exist in .agent/agents/ are valid.`);
194
- }
195
- }
196
-
197
- const goal = req.goal;
198
- if (!goal || typeof goal !== 'string') {
199
- errors.push(`WorkerRequest[${index}]: 'goal' must be a non-empty string.`);
200
- } else if (goal.length > MAX_GOAL_LENGTH) {
201
- errors.push(`WorkerRequest[${index}]: 'goal' exceeds ${MAX_GOAL_LENGTH} characters (${goal.length} chars). Keep it to a single, focused sentence.`);
202
- }
203
-
204
- const context = req.context;
205
- if (!context || typeof context !== 'string') {
206
- errors.push(`WorkerRequest[${index}]: 'context' must be a non-empty string.`);
207
- } else if (context.length > MAX_CONTEXT_LENGTH) {
208
- errors.push(`WorkerRequest[${index}]: 'context' exceeds ${MAX_CONTEXT_LENGTH} characters (${context.length} chars). Trim to minimal required context only.`);
209
- }
210
-
211
- const maxRetries = req.max_retries;
212
- if (maxRetries !== undefined) {
213
- if (typeof maxRetries !== 'number' || !Number.isInteger(maxRetries) || maxRetries < 1 || maxRetries > 3) {
214
- errors.push(`WorkerRequest[${index}]: 'max_retries' must be an integer between 1 and 3, got '${maxRetries}'.`);
215
- }
216
- }
217
-
218
- return errors;
219
- }
220
-
221
- function validateWorkerResult(res, index) {
222
- const errors = [];
223
-
224
- const taskId = res.task_id;
225
- if (!taskId || typeof taskId !== 'string') {
226
- errors.push(`WorkerResult[${index}]: 'task_id' must be a non-empty string.`);
227
- }
228
-
229
- const agent = res.agent;
230
- if (!agent || typeof agent !== 'string') {
231
- errors.push(`WorkerResult[${index}]: 'agent' must be a non-empty string.`);
232
- }
233
-
234
- const status = res.status;
235
- if (!VALID_RESULT_STATUSES.has(status)) {
236
- errors.push(`WorkerResult[${index}]: 'status' must be one of ${[...VALID_RESULT_STATUSES].sort()}, got '${status}'.`);
237
- }
238
-
239
- const output = res.output;
240
- const error = res.error;
241
- if (status === "success" && !output) {
242
- errors.push(`WorkerResult[${index}]: 'output' is required when status is 'success'.`);
243
- }
244
- if ((status === "failure" || status === "escalate") && !error) {
245
- errors.push(`WorkerResult[${index}]: 'error' is required when status is '${status}'. Be specific — 'Something went wrong' is not acceptable.`);
246
- }
247
-
248
- const attempts = res.attempts;
249
- if (attempts !== undefined) {
250
- if (typeof attempts !== 'number' || !Number.isInteger(attempts) || attempts < 1) {
251
- errors.push(`WorkerResult[${index}]: 'attempts' must be an integer >= 1, got '${attempts}'.`);
252
- }
253
- }
254
-
255
- return errors;
256
- }
257
-
258
- function validateSwarmPayload(payloadData, agentsDir) {
259
- let items;
260
- if (typeof payloadData === 'object' && payloadData !== null) {
261
- if (Array.isArray(payloadData)) {
262
- items = payloadData;
263
- } else if (payloadData.workers && Array.isArray(payloadData.workers)) {
264
- items = payloadData.workers;
265
- } else {
266
- items = [payloadData];
267
- }
268
- } else {
269
- console.error("ERROR: Swarm payload must be a JSON object or array.");
270
- return false;
271
- }
272
-
273
- if (items.length > MAX_WORKERS_PER_SWARM) {
274
- console.error(`ERROR: Swarm payload contains ${items.length} workers, exceeding the maximum of ${MAX_WORKERS_PER_SWARM}.`);
275
- return false;
276
- }
277
-
278
- const allErrors = [];
279
- for (let i = 0; i < items.length; i++) {
280
- const item = items[i];
281
- if (typeof item !== 'object' || item === null) {
282
- allErrors.push(`Item[${i}]: must be a JSON object.`);
283
- continue;
284
- }
285
-
286
- let errors;
287
- if ("status" in item && "output" in item) {
288
- errors = validateWorkerResult(item, i);
289
- } else {
290
- errors = validateWorkerRequest(item, i, agentsDir);
291
- }
292
-
293
- allErrors.push(...errors);
294
- }
295
-
296
- if (allErrors.length > 0) {
297
- for (const err of allErrors) {
298
- console.error(`ERROR: ${err}`);
299
- }
300
- return false;
301
- }
302
-
303
- return true;
304
- }
305
-
306
- // ─── Main ─────────────────────────────────────────────────────────────────────
307
-
308
- function main() {
309
- const args = process.argv.slice(2);
310
- let payload = null;
311
- let file = null;
312
- let workspace = ".";
313
- let mode = "legacy";
314
- let useTui = false;
315
-
316
- for (let i = 0; i < args.length; i++) {
317
- const arg = args[i];
318
- if (arg === '--payload' && i + 1 < args.length) {
319
- payload = args[++i];
320
- } else if (arg === '--file' && i + 1 < args.length) {
321
- file = args[++i];
322
- } else if (arg === '--workspace' && i + 1 < args.length) {
323
- workspace = args[++i];
324
- } else if (arg === '--mode' && i + 1 < args.length) {
325
- mode = args[++i];
326
- } else if (arg === '--tui') {
327
- useTui = true;
328
- } else if (arg === '-h' || arg === '--help') {
329
- console.log("Usage: swarm_dispatcher.js [--payload <json>] [--file <path>] [--workspace <dir>] [--mode legacy|swarm] [--tui]");
330
- process.exit(0);
331
- }
332
- }
333
-
334
- if (!payload && !file) {
335
- console.error("ERROR: Must provide either --payload or --file");
336
- process.exit(1);
337
- }
338
-
339
- const workspaceRoot = path.resolve(workspace);
340
- const agentDir = findAgentDir(workspaceRoot);
341
-
342
- if (!agentDir) {
343
- console.error(`ERROR: Could not find .agent directory starting from ${workspaceRoot}`);
344
- process.exit(1);
345
- }
346
-
347
- const agentsDir = path.join(agentDir, "agents");
348
- if (!fs.existsSync(agentsDir)) {
349
- console.error(`ERROR: Could not find 'agents' directory inside ${agentDir}`);
350
- process.exit(1);
351
- }
352
-
353
- let payloadData;
354
- try {
355
- if (file) {
356
- payloadData = JSON.parse(fs.readFileSync(file, 'utf8'));
357
- } else {
358
- payloadData = JSON.parse(payload);
359
- }
360
- } catch (e) {
361
- console.error(`ERROR: Failed to parse payload as JSON: ${e.message}`);
362
- process.exit(1);
363
- }
364
-
365
- if (mode === "swarm") {
366
- if (!validateSwarmPayload(payloadData, agentsDir)) {
367
- console.error("ERROR: Swarm payload validation failed.");
368
- process.exit(1);
369
- }
370
-
371
- let astContext = "";
372
- try {
373
- const res = execSync(`python -m code_review_graph review-delta`, { cwd: workspaceRoot, stdio: 'pipe' }).toString().trim();
374
- if (res) {
375
- astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
376
- }
377
- } catch {
378
- // ignore
379
- }
380
-
381
- if (astContext) {
382
- const items = (typeof payloadData === 'object' && payloadData !== null && payloadData.workers)
383
- ? payloadData.workers
384
- : (Array.isArray(payloadData) ? payloadData : [payloadData]);
385
-
386
- for (const item of items) {
387
- if (item && "context" in item) {
388
- item.context += astContext;
389
- }
390
- }
391
- }
392
-
393
- if (useTui) {
394
- const workers = (typeof payloadData === 'object' && payloadData !== null && payloadData.workers)
395
- ? payloadData.workers
396
- : (Array.isArray(payloadData) ? payloadData : [payloadData]);
397
-
398
- const dashboard = new SwarmDashboard(workers);
399
- dashboard.start();
400
-
401
- // Simulate parallel execution for demo/UX purposes
402
- setTimeout(() => dashboard.updateStatus(0, 'Researching', '\x1b[36m'), 1000);
403
- setTimeout(() => {
404
- if (workers.length > 1) dashboard.updateStatus(1, 'Generating', '\x1b[35m');
405
- }, 1500);
406
-
407
- setTimeout(() => {
408
- workers.forEach((w, i) => dashboard.updateStatus(i, '✔ Done', '\x1b[32m'));
409
- dashboard.stop();
410
- console.log("\n\x1b[32m✔ Swarm validation complete. Ready for dispatch.\x1b[0m\n");
411
- }, 3000);
412
- } else {
413
- console.log("INFO: Swarm payload validation successful.");
414
- if (astContext) {
415
- console.log("--- ENRICHED SWARM PAYLOAD ---");
416
- console.log(JSON.stringify(payloadData, null, 2));
417
- }
418
- }
419
- } else {
420
- if (!validatePayload(payloadData, workspaceRoot, agentsDir)) {
421
- console.error("ERROR: Payload validation failed.");
422
- process.exit(1);
423
- }
424
-
425
- if (useTui) {
426
- const workers = payloadData.dispatch_micro_workers || [];
427
- const dashboard = new SwarmDashboard(workers);
428
- dashboard.start();
429
-
430
- // Simulate parallel execution for demo/UX purposes
431
- setTimeout(() => dashboard.updateStatus(0, 'Researching', '\x1b[36m'), 1000);
432
- setTimeout(() => {
433
- if (workers.length > 1) dashboard.updateStatus(1, 'Generating', '\x1b[35m');
434
- }, 1500);
435
-
436
- setTimeout(() => {
437
- workers.forEach((w, i) => dashboard.updateStatus(i, '✔ Done', '\x1b[32m'));
438
- dashboard.stop();
439
- console.log("\n\x1b[32m✔ All workers successfully dispatched.\x1b[0m\n");
440
- }, 3000);
441
- } else {
442
- console.log("INFO: Payload validation successful.");
443
- const prompts = buildWorkerPrompts(payloadData, workspaceRoot);
444
-
445
- for (let i = 0; i < prompts.length; i++) {
446
- console.log(`\n[Worker ${i + 1} Ready]`);
447
- console.log(prompts[i]);
448
- }
449
- }
450
- }
451
- }
452
-
453
- module.exports = { validateWorkerRequest, validateWorkerResult, validateSwarmPayload, validatePayload, findAgentDir };
454
-
455
- if (require.main === module) {
456
- main();
457
- }
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
+ // ─── ANSI TUI Renderer ────────────────────────────────────────────────────────
14
+ class SwarmDashboard {
15
+ constructor(workers) {
16
+ this.workers = workers.map((w) => ({
17
+ name: w.target_agent || w.agent || "Worker",
18
+ task: (w.task_description || w.goal || "").slice(0, 40) + "...",
19
+ status: "⏳ Pending",
20
+ color: "\x1b[33m", // Yellow
21
+ }));
22
+ this.spinnerFrames = ["", "", "", "", "", "", "", "", "", ""];
23
+ this.frameIdx = 0;
24
+ this.linesRendered = 0;
25
+ this.timer = null;
26
+ }
27
+
28
+ render() {
29
+ if (this.linesRendered > 0) {
30
+ process.stdout.write(`\x1b[${this.linesRendered}A`);
31
+ }
32
+
33
+ let output =
34
+ "\n\x1b[1m\x1b[36m━━━ Tribunal Swarm Dispatcher ━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n\n";
35
+ const frame = this.spinnerFrames[this.frameIdx];
36
+
37
+ this.workers.forEach((w) => {
38
+ const icon = w.status.includes("Pending")
39
+ ? `\x1b[36m${frame}\x1b[0m`
40
+ : w.status.includes("Done")
41
+ ? "\x1b[32m✔\x1b[0m"
42
+ : "\x1b[31m✖\x1b[0m";
43
+ output += ` ${icon} \x1b[1m${w.name.padEnd(25)}\x1b[0m \x1b[2m|\x1b[0m ${w.color}${w.status.padEnd(12)}\x1b[0m \x1b[2m|\x1b[0m \x1b[3m${w.task}\x1b[0m\n`;
44
+ });
45
+
46
+ output +=
47
+ "\n\x1b[1m\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n";
48
+
49
+ process.stdout.write(output);
50
+ this.linesRendered = this.workers.length + 5;
51
+ this.frameIdx = (this.frameIdx + 1) % this.spinnerFrames.length;
52
+ }
53
+
54
+ start() {
55
+ console.clear();
56
+ this.timer = setInterval(() => this.render(), 80);
57
+ }
58
+
59
+ stop() {
60
+ if (this.timer) clearInterval(this.timer);
61
+ this.render(); // Final render
62
+ }
63
+
64
+ updateStatus(index, status, color) {
65
+ if (this.workers[index]) {
66
+ this.workers[index].status = status;
67
+ this.workers[index].color = color;
68
+ }
69
+ }
70
+ }
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+
73
+ const VALID_WORKER_TYPES = new Set([
74
+ "research",
75
+ "generate_code",
76
+ "review_code",
77
+ "debug",
78
+ "plan",
79
+ "design_schema",
80
+ "write_docs",
81
+ "security_audit",
82
+ "optimize",
83
+ "test",
84
+ ]);
85
+
86
+ const VALID_RESULT_STATUSES = new Set(["success", "failure", "escalate"]);
87
+
88
+ const MAX_GOAL_LENGTH = 200;
89
+ const MAX_CONTEXT_LENGTH = 800;
90
+ const MAX_WORKERS_PER_SWARM = 5;
91
+
92
+ function findAgentDir(startPath) {
93
+ let current = path.resolve(startPath);
94
+ const root = path.parse(current).root;
95
+ while (current !== root) {
96
+ const agentDir = path.join(current, ".agent");
97
+ if (fs.existsSync(agentDir) && fs.statSync(agentDir).isDirectory()) {
98
+ return agentDir;
99
+ }
100
+ current = path.dirname(current);
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // ─── Legacy mode: validate orchestrator micro-worker payloads ──────────────────
106
+
107
+ function validatePayload(payloadData, workspaceRoot, agentsDir) {
108
+ if (!payloadData.dispatch_micro_workers) {
109
+ console.error(
110
+ "ERROR: Payload missing required 'dispatch_micro_workers' array.",
111
+ );
112
+ return false;
113
+ }
114
+
115
+ const workers = payloadData.dispatch_micro_workers;
116
+ if (!Array.isArray(workers)) {
117
+ console.error("ERROR: 'dispatch_micro_workers' must be a list.");
118
+ return false;
119
+ }
120
+
121
+ let allValid = true;
122
+ for (let i = 0; i < workers.length; i++) {
123
+ const worker = workers[i];
124
+ const agentName = worker.target_agent;
125
+ if (!agentName) {
126
+ console.error(`ERROR: Worker ${i}: missing 'target_agent'.`);
127
+ allValid = false;
128
+ continue;
129
+ }
130
+
131
+ const agentFile = path.join(agentsDir, `${agentName}.md`);
132
+ if (!fs.existsSync(agentFile)) {
133
+ console.error(
134
+ `ERROR: Worker ${i}: target_agent '${agentName}' not found at ${agentFile}.`,
135
+ );
136
+ allValid = false;
137
+ }
138
+
139
+ const filesAttached = worker.files_attached || [];
140
+ if (!Array.isArray(filesAttached)) {
141
+ console.error(`ERROR: Worker ${i}: 'files_attached' must be a list.`);
142
+ allValid = false;
143
+ continue;
144
+ }
145
+
146
+ for (const f of filesAttached) {
147
+ const filePath = path.resolve(workspaceRoot, f);
148
+ if (!fs.existsSync(filePath)) {
149
+ console.warn(
150
+ `WARN: Worker ${i}: attached file '${f}' does not exist (might be a new file to create).`,
151
+ );
152
+ }
153
+ }
154
+ }
155
+
156
+ return allValid;
157
+ }
158
+
159
+ function buildWorkerPrompts(payloadData, workspaceRoot) {
160
+ const prompts = [];
161
+ let astContext = "";
162
+
163
+ try {
164
+ const res = execSync(`python -m code_review_graph review-delta`, {
165
+ cwd: workspaceRoot,
166
+ stdio: "pipe",
167
+ })
168
+ .toString()
169
+ .trim();
170
+ if (res) {
171
+ astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
172
+ }
173
+ } catch {
174
+ // ignore warning
175
+ }
176
+
177
+ const workers = payloadData.dispatch_micro_workers || [];
178
+ for (const worker of workers) {
179
+ const agent = worker.target_agent;
180
+ const ctx = worker.context_summary || "";
181
+ const task = worker.task_description || "";
182
+ const files = worker.files_attached || [];
183
+
184
+ let prompt = `--- MICRO-WORKER DISPATCH ---\n`;
185
+ prompt += `Agent: ${agent}\n`;
186
+ prompt += `Context: ${ctx}${astContext}\n`;
187
+ prompt += `Task: ${task}\n`;
188
+ prompt += `Attached Files: ${files.length ? files.join(", ") : "None"}\n`;
189
+ prompt += `-----------------------------`;
190
+ prompts.push(prompt);
191
+ }
192
+ return prompts;
193
+ }
194
+
195
+ // ─── Swarm mode: validate WorkerRequest / WorkerResult payloads ───────────────
196
+
197
+ function validateWorkerRequest(req, index, agentsDir) {
198
+ const errors = [];
199
+
200
+ const taskId = req.task_id;
201
+ if (!taskId || typeof taskId !== "string") {
202
+ errors.push(
203
+ `WorkerRequest[${index}]: 'task_id' must be a non-empty string.`,
204
+ );
205
+ }
206
+
207
+ const reqType = req.type;
208
+ if (!VALID_WORKER_TYPES.has(reqType)) {
209
+ errors.push(
210
+ `WorkerRequest[${index}]: 'type' must be one of ${[...VALID_WORKER_TYPES].sort()}, got '${reqType}'.`,
211
+ );
212
+ }
213
+
214
+ const agent = req.agent;
215
+ if (!agent || typeof agent !== "string") {
216
+ errors.push(`WorkerRequest[${index}]: 'agent' must be a non-empty string.`);
217
+ } else {
218
+ const agentFile = path.join(agentsDir, `${agent}.md`);
219
+ if (!fs.existsSync(agentFile)) {
220
+ errors.push(
221
+ `WorkerRequest[${index}]: agent '${agent}' not found at ${agentFile}. Only agents that exist in .agent/agents/ are valid.`,
222
+ );
223
+ }
224
+ }
225
+
226
+ const goal = req.goal;
227
+ if (!goal || typeof goal !== "string") {
228
+ errors.push(`WorkerRequest[${index}]: 'goal' must be a non-empty string.`);
229
+ } else if (goal.length > MAX_GOAL_LENGTH) {
230
+ errors.push(
231
+ `WorkerRequest[${index}]: 'goal' exceeds ${MAX_GOAL_LENGTH} characters (${goal.length} chars). Keep it to a single, focused sentence.`,
232
+ );
233
+ }
234
+
235
+ const context = req.context;
236
+ if (!context || typeof context !== "string") {
237
+ errors.push(
238
+ `WorkerRequest[${index}]: 'context' must be a non-empty string.`,
239
+ );
240
+ } else if (context.length > MAX_CONTEXT_LENGTH) {
241
+ errors.push(
242
+ `WorkerRequest[${index}]: 'context' exceeds ${MAX_CONTEXT_LENGTH} characters (${context.length} chars). Trim to minimal required context only.`,
243
+ );
244
+ }
245
+
246
+ const maxRetries = req.max_retries;
247
+ if (maxRetries !== undefined) {
248
+ if (
249
+ typeof maxRetries !== "number" ||
250
+ !Number.isInteger(maxRetries) ||
251
+ maxRetries < 1 ||
252
+ maxRetries > 3
253
+ ) {
254
+ errors.push(
255
+ `WorkerRequest[${index}]: 'max_retries' must be an integer between 1 and 3, got '${maxRetries}'.`,
256
+ );
257
+ }
258
+ }
259
+
260
+ return errors;
261
+ }
262
+
263
+ function validateWorkerResult(res, index) {
264
+ const errors = [];
265
+
266
+ const taskId = res.task_id;
267
+ if (!taskId || typeof taskId !== "string") {
268
+ errors.push(
269
+ `WorkerResult[${index}]: 'task_id' must be a non-empty string.`,
270
+ );
271
+ }
272
+
273
+ const agent = res.agent;
274
+ if (!agent || typeof agent !== "string") {
275
+ errors.push(`WorkerResult[${index}]: 'agent' must be a non-empty string.`);
276
+ }
277
+
278
+ const status = res.status;
279
+ if (!VALID_RESULT_STATUSES.has(status)) {
280
+ errors.push(
281
+ `WorkerResult[${index}]: 'status' must be one of ${[...VALID_RESULT_STATUSES].sort()}, got '${status}'.`,
282
+ );
283
+ }
284
+
285
+ const output = res.output;
286
+ const error = res.error;
287
+ if (status === "success" && !output) {
288
+ errors.push(
289
+ `WorkerResult[${index}]: 'output' is required when status is 'success'.`,
290
+ );
291
+ }
292
+ if ((status === "failure" || status === "escalate") && !error) {
293
+ errors.push(
294
+ `WorkerResult[${index}]: 'error' is required when status is '${status}'. Be specific — 'Something went wrong' is not acceptable.`,
295
+ );
296
+ }
297
+
298
+ const attempts = res.attempts;
299
+ if (attempts !== undefined) {
300
+ if (
301
+ typeof attempts !== "number" ||
302
+ !Number.isInteger(attempts) ||
303
+ attempts < 1
304
+ ) {
305
+ errors.push(
306
+ `WorkerResult[${index}]: 'attempts' must be an integer >= 1, got '${attempts}'.`,
307
+ );
308
+ }
309
+ }
310
+
311
+ return errors;
312
+ }
313
+
314
+ function validateSwarmPayload(payloadData, agentsDir) {
315
+ let items;
316
+ if (typeof payloadData === "object" && payloadData !== null) {
317
+ if (Array.isArray(payloadData)) {
318
+ items = payloadData;
319
+ } else if (payloadData.workers && Array.isArray(payloadData.workers)) {
320
+ items = payloadData.workers;
321
+ } else {
322
+ items = [payloadData];
323
+ }
324
+ } else {
325
+ console.error("ERROR: Swarm payload must be a JSON object or array.");
326
+ return false;
327
+ }
328
+
329
+ if (items.length > MAX_WORKERS_PER_SWARM) {
330
+ console.error(
331
+ `ERROR: Swarm payload contains ${items.length} workers, exceeding the maximum of ${MAX_WORKERS_PER_SWARM}.`,
332
+ );
333
+ return false;
334
+ }
335
+
336
+ const allErrors = [];
337
+ for (let i = 0; i < items.length; i++) {
338
+ const item = items[i];
339
+ if (typeof item !== "object" || item === null) {
340
+ allErrors.push(`Item[${i}]: must be a JSON object.`);
341
+ continue;
342
+ }
343
+
344
+ let errors;
345
+ if ("status" in item && "output" in item) {
346
+ errors = validateWorkerResult(item, i);
347
+ } else {
348
+ errors = validateWorkerRequest(item, i, agentsDir);
349
+ }
350
+
351
+ allErrors.push(...errors);
352
+ }
353
+
354
+ if (allErrors.length > 0) {
355
+ for (const err of allErrors) {
356
+ console.error(`ERROR: ${err}`);
357
+ }
358
+ return false;
359
+ }
360
+
361
+ return true;
362
+ }
363
+
364
+ // ─── Main ─────────────────────────────────────────────────────────────────────
365
+
366
+ function main() {
367
+ const args = process.argv.slice(2);
368
+ let payload = null;
369
+ let file = null;
370
+ let workspace = ".";
371
+ let mode = "legacy";
372
+ let useTui = false;
373
+
374
+ for (let i = 0; i < args.length; i++) {
375
+ const arg = args[i];
376
+ if (arg === "--payload" && i + 1 < args.length) {
377
+ payload = args[++i];
378
+ } else if (arg === "--file" && i + 1 < args.length) {
379
+ file = args[++i];
380
+ } else if (arg === "--workspace" && i + 1 < args.length) {
381
+ workspace = args[++i];
382
+ } else if (arg === "--mode" && i + 1 < args.length) {
383
+ mode = args[++i];
384
+ } else if (arg === "--tui") {
385
+ useTui = true;
386
+ } else if (arg === "-h" || arg === "--help") {
387
+ console.log(
388
+ "Usage: swarm_dispatcher.js [--payload <json>] [--file <path>] [--workspace <dir>] [--mode legacy|swarm] [--tui]",
389
+ );
390
+ process.exit(0);
391
+ }
392
+ }
393
+
394
+ if (!payload && !file) {
395
+ console.error("ERROR: Must provide either --payload or --file");
396
+ process.exit(1);
397
+ }
398
+
399
+ const workspaceRoot = path.resolve(workspace);
400
+ const agentDir = findAgentDir(workspaceRoot);
401
+
402
+ if (!agentDir) {
403
+ console.error(
404
+ `ERROR: Could not find .agent directory starting from ${workspaceRoot}`,
405
+ );
406
+ process.exit(1);
407
+ }
408
+
409
+ const agentsDir = path.join(agentDir, "agents");
410
+ if (!fs.existsSync(agentsDir)) {
411
+ console.error(
412
+ `ERROR: Could not find 'agents' directory inside ${agentDir}`,
413
+ );
414
+ process.exit(1);
415
+ }
416
+
417
+ let payloadData;
418
+ try {
419
+ if (file) {
420
+ payloadData = JSON.parse(fs.readFileSync(file, "utf8"));
421
+ } else {
422
+ payloadData = JSON.parse(payload);
423
+ }
424
+ } catch (e) {
425
+ console.error(`ERROR: Failed to parse payload as JSON: ${e.message}`);
426
+ process.exit(1);
427
+ }
428
+
429
+ if (mode === "swarm") {
430
+ if (!validateSwarmPayload(payloadData, agentsDir)) {
431
+ console.error("ERROR: Swarm payload validation failed.");
432
+ process.exit(1);
433
+ }
434
+
435
+ let astContext = "";
436
+ try {
437
+ const res = execSync(`python -m code_review_graph review-delta`, {
438
+ cwd: workspaceRoot,
439
+ stdio: "pipe",
440
+ })
441
+ .toString()
442
+ .trim();
443
+ if (res) {
444
+ astContext = `\n\n[AST Blast Radius Context]:\n${res}`;
445
+ }
446
+ } catch {
447
+ // ignore
448
+ }
449
+
450
+ if (astContext) {
451
+ const items =
452
+ typeof payloadData === "object" &&
453
+ payloadData !== null &&
454
+ payloadData.workers
455
+ ? payloadData.workers
456
+ : Array.isArray(payloadData)
457
+ ? payloadData
458
+ : [payloadData];
459
+
460
+ for (const item of items) {
461
+ if (item && "context" in item) {
462
+ item.context += astContext;
463
+ }
464
+ }
465
+ }
466
+
467
+ if (useTui) {
468
+ const workers =
469
+ typeof payloadData === "object" &&
470
+ payloadData !== null &&
471
+ payloadData.workers
472
+ ? payloadData.workers
473
+ : Array.isArray(payloadData)
474
+ ? payloadData
475
+ : [payloadData];
476
+
477
+ const dashboard = new SwarmDashboard(workers);
478
+ dashboard.start();
479
+
480
+ // Simulate parallel execution for demo/UX purposes
481
+ setTimeout(
482
+ () => dashboard.updateStatus(0, "Researching", "\x1b[36m"),
483
+ 1000,
484
+ );
485
+ setTimeout(() => {
486
+ if (workers.length > 1)
487
+ dashboard.updateStatus(1, "Generating", "\x1b[35m");
488
+ }, 1500);
489
+
490
+ setTimeout(() => {
491
+ workers.forEach((w, i) =>
492
+ dashboard.updateStatus(i, "✔ Done", "\x1b[32m"),
493
+ );
494
+ dashboard.stop();
495
+ console.log(
496
+ "\n\x1b[32m✔ Swarm validation complete. Ready for dispatch.\x1b[0m\n",
497
+ );
498
+ }, 3000);
499
+ } else {
500
+ console.log("INFO: Swarm payload validation successful.");
501
+ if (astContext) {
502
+ console.log("--- ENRICHED SWARM PAYLOAD ---");
503
+ console.log(JSON.stringify(payloadData, null, 2));
504
+ }
505
+ }
506
+ } else {
507
+ if (!validatePayload(payloadData, workspaceRoot, agentsDir)) {
508
+ console.error("ERROR: Payload validation failed.");
509
+ process.exit(1);
510
+ }
511
+
512
+ if (useTui) {
513
+ const workers = payloadData.dispatch_micro_workers || [];
514
+ const dashboard = new SwarmDashboard(workers);
515
+ dashboard.start();
516
+
517
+ // Simulate parallel execution for demo/UX purposes
518
+ setTimeout(
519
+ () => dashboard.updateStatus(0, "Researching", "\x1b[36m"),
520
+ 1000,
521
+ );
522
+ setTimeout(() => {
523
+ if (workers.length > 1)
524
+ dashboard.updateStatus(1, "Generating", "\x1b[35m");
525
+ }, 1500);
526
+
527
+ setTimeout(() => {
528
+ workers.forEach((w, i) =>
529
+ dashboard.updateStatus(i, "✔ Done", "\x1b[32m"),
530
+ );
531
+ dashboard.stop();
532
+ console.log(
533
+ "\n\x1b[32m✔ All workers successfully dispatched.\x1b[0m\n",
534
+ );
535
+ }, 3000);
536
+ } else {
537
+ console.log("INFO: Payload validation successful.");
538
+ const prompts = buildWorkerPrompts(payloadData, workspaceRoot);
539
+
540
+ for (let i = 0; i < prompts.length; i++) {
541
+ console.log(`\n[Worker ${i + 1} Ready]`);
542
+ console.log(prompts[i]);
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ module.exports = {
549
+ validateWorkerRequest,
550
+ validateWorkerResult,
551
+ validateSwarmPayload,
552
+ validatePayload,
553
+ findAgentDir,
554
+ };
555
+
556
+ if (require.main === module) {
557
+ main();
558
+ }