network-ai 5.0.0 → 5.1.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/INTEGRATION_GUIDE.md +4 -4
- package/QUICKSTART.md +4 -4
- package/README.md +15 -12
- package/dist/adapters/custom-adapter.d.ts +1 -1
- package/dist/adapters/custom-adapter.js +1 -1
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +4 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/langchain-adapter.js +1 -1
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/mcp-adapter.d.ts +1 -1
- package/dist/adapters/mcp-adapter.js +3 -3
- package/dist/adapters/mcp-adapter.js.map +1 -1
- package/dist/adapters/openclaw-adapter.d.ts +1 -1
- package/dist/adapters/openclaw-adapter.js +3 -3
- package/dist/adapters/openclaw-adapter.js.map +1 -1
- package/dist/adapters/orchestrator-adapter.d.ts +118 -0
- package/dist/adapters/orchestrator-adapter.d.ts.map +1 -0
- package/dist/adapters/orchestrator-adapter.js +218 -0
- package/dist/adapters/orchestrator-adapter.js.map +1 -0
- package/dist/demo-control-plane.d.ts +12 -0
- package/dist/demo-control-plane.d.ts.map +1 -0
- package/dist/demo-control-plane.js +147 -0
- package/dist/demo-control-plane.js.map +1 -0
- package/dist/demo-worktree-dashboard.d.ts +2 -0
- package/dist/demo-worktree-dashboard.d.ts.map +1 -0
- package/dist/demo-worktree-dashboard.js +131 -0
- package/dist/demo-worktree-dashboard.js.map +1 -0
- package/dist/examples/01-hello-swarm.d.ts +13 -0
- package/dist/examples/01-hello-swarm.d.ts.map +1 -0
- package/dist/examples/01-hello-swarm.js +165 -0
- package/dist/examples/01-hello-swarm.js.map +1 -0
- package/dist/examples/02-fsm-pipeline.d.ts +20 -0
- package/dist/examples/02-fsm-pipeline.d.ts.map +1 -0
- package/dist/examples/02-fsm-pipeline.js +189 -0
- package/dist/examples/02-fsm-pipeline.js.map +1 -0
- package/dist/examples/03-parallel-agents.d.ts +21 -0
- package/dist/examples/03-parallel-agents.d.ts.map +1 -0
- package/dist/examples/03-parallel-agents.js +192 -0
- package/dist/examples/03-parallel-agents.js.map +1 -0
- package/dist/examples/05-code-review-swarm.d.ts +21 -0
- package/dist/examples/05-code-review-swarm.d.ts.map +1 -0
- package/dist/examples/05-code-review-swarm.js +1177 -0
- package/dist/examples/05-code-review-swarm.js.map +1 -0
- package/dist/examples/06-ai-pipeline-demo.d.ts +24 -0
- package/dist/examples/06-ai-pipeline-demo.d.ts.map +1 -0
- package/dist/examples/06-ai-pipeline-demo.js +263 -0
- package/dist/examples/06-ai-pipeline-demo.js.map +1 -0
- package/dist/examples/07-full-showcase.d.ts +27 -0
- package/dist/examples/07-full-showcase.d.ts.map +1 -0
- package/dist/examples/07-full-showcase.js +946 -0
- package/dist/examples/07-full-showcase.js.map +1 -0
- package/dist/examples/08-control-plane-stress-demo.d.ts +19 -0
- package/dist/examples/08-control-plane-stress-demo.d.ts.map +1 -0
- package/dist/examples/08-control-plane-stress-demo.js +186 -0
- package/dist/examples/08-control-plane-stress-demo.js.map +1 -0
- package/dist/examples/09-real-langchain.d.ts +19 -0
- package/dist/examples/09-real-langchain.d.ts.map +1 -0
- package/dist/examples/09-real-langchain.js +231 -0
- package/dist/examples/09-real-langchain.js.map +1 -0
- package/dist/examples/10-nemoclaw-sandbox-swarm.d.ts +16 -0
- package/dist/examples/10-nemoclaw-sandbox-swarm.d.ts.map +1 -0
- package/dist/examples/10-nemoclaw-sandbox-swarm.js +270 -0
- package/dist/examples/10-nemoclaw-sandbox-swarm.js.map +1 -0
- package/dist/examples/demo-runner.d.ts +2 -0
- package/dist/examples/demo-runner.d.ts.map +1 -0
- package/dist/examples/demo-runner.js +119 -0
- package/dist/examples/demo-runner.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-guardian.js.map +1 -1
- package/dist/lib/control-plane.d.ts +128 -0
- package/dist/lib/control-plane.d.ts.map +1 -0
- package/dist/lib/control-plane.js +527 -0
- package/dist/lib/control-plane.js.map +1 -0
- package/dist/lib/coverage-reporter.js.map +1 -1
- package/dist/lib/goal-dsl.d.ts.map +1 -1
- package/dist/lib/goal-dsl.js +0 -1
- package/dist/lib/goal-dsl.js.map +1 -1
- package/dist/lib/work-tree-dashboard.d.ts +130 -0
- package/dist/lib/work-tree-dashboard.d.ts.map +1 -0
- package/dist/lib/work-tree-dashboard.js +583 -0
- package/dist/lib/work-tree-dashboard.js.map +1 -0
- package/dist/lib/work-tree-ui.d.ts +107 -0
- package/dist/lib/work-tree-ui.d.ts.map +1 -0
- package/dist/lib/work-tree-ui.js +333 -0
- package/dist/lib/work-tree-ui.js.map +1 -0
- package/dist/lib/work-tree.d.ts +184 -0
- package/dist/lib/work-tree.d.ts.map +1 -0
- package/dist/lib/work-tree.js +480 -0
- package/dist/lib/work-tree.js.map +1 -0
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +3 -2
- package/dist/security.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 05-code-review-swarm.ts
|
|
4
|
+
* ───────────────────────
|
|
5
|
+
* 6 real LLM agents do a parallel code review of a buggy auth service.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* Wave (parallel): 5 specialist reviewers — security, performance,
|
|
9
|
+
* reliability, testing, architecture
|
|
10
|
+
* Final : 1 coordinator — "top 3 blockers before you ship"
|
|
11
|
+
*
|
|
12
|
+
* Each agent makes a real gpt-5.2 call.
|
|
13
|
+
* Findings are coordinated through SharedBlackboard.
|
|
14
|
+
*
|
|
15
|
+
* Run:
|
|
16
|
+
* npx ts-node examples/05-code-review-swarm.ts
|
|
17
|
+
*
|
|
18
|
+
* API key:
|
|
19
|
+
* $env:OPENAI_API_KEY = Read-Host -MaskInput "API Key"
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
55
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
56
|
+
};
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
const openai_1 = __importDefault(require("openai"));
|
|
59
|
+
const readline = __importStar(require("readline"));
|
|
60
|
+
const __1 = require("..");
|
|
61
|
+
// ─── Interactive input prompt (4 modes) ──────────────────────────────────────
|
|
62
|
+
function promptForInput(c) {
|
|
63
|
+
return new Promise(resolve => {
|
|
64
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
65
|
+
console.log(`\n ${c.bold}What should the swarm review?${c.reset}`);
|
|
66
|
+
console.log(` ${c.dim}[1] Built-in example (auth-service.ts — 5 deliberate bugs)${c.reset}`);
|
|
67
|
+
console.log(` ${c.dim}[2] Paste your own code ${c.reset}${c.dim}(TypeScript, Python, Go, any language)${c.reset}`);
|
|
68
|
+
console.log(` ${c.dim}[3] Paste a system design doc ${c.reset}${c.dim}(architecture, API spec, DB schema, deployment plan)${c.reset}`);
|
|
69
|
+
console.log(` ${c.dim}[4] Custom role ${c.reset}${c.dim}(proposal, policy, email, report — any content, any reviewer)${c.reset}`);
|
|
70
|
+
console.log();
|
|
71
|
+
/** Collect lines until user types "end", then resolve. */
|
|
72
|
+
const pasteLines = (mode, label, customRole) => {
|
|
73
|
+
const lines = [];
|
|
74
|
+
rl.on('line', line => {
|
|
75
|
+
if (line.trim().toUpperCase() === 'END') {
|
|
76
|
+
rl.close();
|
|
77
|
+
resolve({ content: lines.join('\n'), label, mode, customRole });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
lines.push(line);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
rl.question(` ${c.bold}Choice [1/2/3/4]:${c.reset} `, choice => {
|
|
85
|
+
const ch = choice.trim();
|
|
86
|
+
if (ch !== '2' && ch !== '3' && ch !== '4') {
|
|
87
|
+
// Default: built-in example
|
|
88
|
+
rl.close();
|
|
89
|
+
resolve({ content: CODE_UNDER_REVIEW, label: 'auth-service.ts (built-in)', mode: 'code' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (ch === '3') {
|
|
93
|
+
console.log(`\n ${c.dim}Paste your system design doc or API spec below.`);
|
|
94
|
+
console.log(` ${c.dim}(architecture overview, API spec, DB schema, deployment plan, infra doc, etc.)`);
|
|
95
|
+
console.log(` ${c.reset}${c.bold}When finished, type end on its own new line and press Enter:${c.reset}`);
|
|
96
|
+
console.log(` ${c.dim} ┌─ example ──────────────────────┐`);
|
|
97
|
+
console.log(` ${c.dim} │ ... last line of your doc ... │`);
|
|
98
|
+
console.log(` ${c.dim} │ end │`);
|
|
99
|
+
console.log(` ${c.dim} └────────────────────────────────┘${c.reset}\n`);
|
|
100
|
+
pasteLines('design', 'system design doc');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '4') {
|
|
104
|
+
rl.question(`\n ${c.bold}Your reviewer role (e.g. "VP of Sales reviewing a junior SDR's proposal"):${c.reset} `, roleDesc => {
|
|
105
|
+
console.log(`\n ${c.dim}Paste your content below.`);
|
|
106
|
+
console.log(` ${c.dim}(proposal, business plan, policy doc, email, report, job description, marketing copy, etc.)`);
|
|
107
|
+
console.log(` ${c.reset}${c.bold}When finished, type end on its own new line and press Enter:${c.reset}`);
|
|
108
|
+
console.log(` ${c.dim} ┌─ example ──────────────────────┐`);
|
|
109
|
+
console.log(` ${c.dim} │ ... last line of content ... │`);
|
|
110
|
+
console.log(` ${c.dim} │ end │`);
|
|
111
|
+
console.log(` ${c.dim} └────────────────────────────────┘${c.reset}\n`);
|
|
112
|
+
pasteLines('custom', 'custom content', roleDesc.trim());
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// [2] Own code
|
|
117
|
+
console.log(`\n ${c.dim}Paste your code below.`);
|
|
118
|
+
console.log(` ${c.dim}(TypeScript, JavaScript, Python, Go, Java, SQL, shell script, etc.)`);
|
|
119
|
+
console.log(` ${c.reset}${c.bold}When finished, type end on its own new line and press Enter:${c.reset}`);
|
|
120
|
+
console.log(` ${c.dim} ┌─ example ──────────────────────┐`);
|
|
121
|
+
console.log(` ${c.dim} │ ... last line of your code .. │`);
|
|
122
|
+
console.log(` ${c.dim} │ end │`);
|
|
123
|
+
console.log(` ${c.dim} └────────────────────────────────┘${c.reset}\n`);
|
|
124
|
+
pasteLines('code', 'custom code');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// ─── Content / mode mismatch guard ─────────────────────────────────────────
|
|
129
|
+
async function warnIfMismatch(mode, content, colors) {
|
|
130
|
+
const lower = content.toLowerCase();
|
|
131
|
+
const looksLikeCode =
|
|
132
|
+
// keywords that are unambiguous on their own
|
|
133
|
+
/\b(function\s*\(|function\s+\w+|class\s+\w+|import\s+[\w{*]|export\s+(default|function|class|const|async))\b/.test(lower) ||
|
|
134
|
+
// assignment keywords — must be followed by identifier + =
|
|
135
|
+
/\b(const|let|var)\s+\w+[\w\s,]*\s*=/.test(lower) ||
|
|
136
|
+
// function-def patterns across languages
|
|
137
|
+
/\b(def|fn|func)\s+\w+\s*\(/.test(lower) ||
|
|
138
|
+
// async/await in code context (await expression, not "await your reply")
|
|
139
|
+
/\bawait\s+\w+\s*[\.(]/.test(lower) ||
|
|
140
|
+
// interface / struct with body
|
|
141
|
+
/\b(interface|struct)\s+\w+\s*\{/.test(lower) ||
|
|
142
|
+
// lots of braces = almost certainly code
|
|
143
|
+
(content.match(/\{/g)?.length ?? 0) > 5;
|
|
144
|
+
const looksLikeTechnical = /\b(service|api|endpoint|database|schema|component|architecture|deploy|infrastructure|scalab|queue|cache|auth|load.?balanc|microservice|latency|throughput|replica|partition|shard)\b/.test(lower);
|
|
145
|
+
let message = '';
|
|
146
|
+
let suggestion = '';
|
|
147
|
+
if (mode === 'code' && !looksLikeCode) {
|
|
148
|
+
message = `⚠ This doesn't look like code.`;
|
|
149
|
+
suggestion = `For documents, proposals, or non-technical content use [3] or [4].`;
|
|
150
|
+
}
|
|
151
|
+
else if (mode === 'design' && looksLikeCode) {
|
|
152
|
+
message = `⚠ This looks like code, not a design doc.`;
|
|
153
|
+
suggestion = `For code review use [1] or [2] — they have security, performance, and architecture angles built in.`;
|
|
154
|
+
}
|
|
155
|
+
else if (mode === 'design' && !looksLikeTechnical) {
|
|
156
|
+
message = `⚠ This doesn't look like a technical design doc.`;
|
|
157
|
+
suggestion = `For non-technical content (proposals, plans, emails) use [4] with a matching reviewer role.`;
|
|
158
|
+
}
|
|
159
|
+
else if (mode === 'custom' && looksLikeCode) {
|
|
160
|
+
message = `⚠ This looks like code.`;
|
|
161
|
+
suggestion = `For specialist code review use [1] or [2] — they have security, performance, and architecture angles built in.`;
|
|
162
|
+
}
|
|
163
|
+
if (!message)
|
|
164
|
+
return;
|
|
165
|
+
console.log(`\n ${colors.yellow}${colors.bold}${message}${colors.reset}`);
|
|
166
|
+
console.log(` ${colors.dim} ${suggestion}${colors.reset}\n`);
|
|
167
|
+
await new Promise(resolvePrompt => {
|
|
168
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
169
|
+
rl.question(` ${colors.bold}Continue anyway? [y/N]:${colors.reset} `, answer => {
|
|
170
|
+
rl.close();
|
|
171
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
172
|
+
console.log(`\n ${colors.dim}Cancelled — re-run and choose the right mode.${colors.reset}\n`);
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
resolvePrompt();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// ─── Load .env file if present (no dotenv dependency) ────────────────────────
|
|
180
|
+
;
|
|
181
|
+
(function loadDotEnv() {
|
|
182
|
+
const fs = require('fs');
|
|
183
|
+
const path = require('path');
|
|
184
|
+
const envPath = path.resolve(__dirname, '..', '.env');
|
|
185
|
+
if (!fs.existsSync(envPath))
|
|
186
|
+
return;
|
|
187
|
+
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
188
|
+
for (const raw of lines) {
|
|
189
|
+
const line = raw.trim();
|
|
190
|
+
if (!line || line.startsWith('#'))
|
|
191
|
+
continue;
|
|
192
|
+
const eq = line.indexOf('=');
|
|
193
|
+
if (eq < 1)
|
|
194
|
+
continue;
|
|
195
|
+
const key = line.slice(0, eq).trim();
|
|
196
|
+
const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
197
|
+
if (key && val && !process.env[key])
|
|
198
|
+
process.env[key] = val;
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
// ─── Guard: require API key upfront ──────────────────────────────────────────
|
|
202
|
+
const API_KEY = process.env.OPENAI_API_KEY ?? '';
|
|
203
|
+
if (!API_KEY) {
|
|
204
|
+
console.error('\n[ERROR] OPENAI_API_KEY is not set.\n' +
|
|
205
|
+
' PowerShell: $env:OPENAI_API_KEY = Read-Host -MaskInput "API Key"\n' +
|
|
206
|
+
' bash/zsh: export OPENAI_API_KEY=sk-...\n' +
|
|
207
|
+
' .env file: copy .env.example to .env and add your key\n');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const openai = new openai_1.default({ apiKey: API_KEY });
|
|
211
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
212
|
+
const c = {
|
|
213
|
+
reset: '\x1b[0m',
|
|
214
|
+
bold: '\x1b[1m',
|
|
215
|
+
dim: '\x1b[2m',
|
|
216
|
+
cyan: '\x1b[36m',
|
|
217
|
+
green: '\x1b[32m',
|
|
218
|
+
yellow: '\x1b[33m',
|
|
219
|
+
red: '\x1b[31m',
|
|
220
|
+
blue: '\x1b[34m',
|
|
221
|
+
magenta: '\x1b[35m',
|
|
222
|
+
white: '\x1b[37m',
|
|
223
|
+
};
|
|
224
|
+
const SEV = {
|
|
225
|
+
CRITICAL: `${c.bold}${c.red}CRITICAL${c.reset}`,
|
|
226
|
+
HIGH: `${c.bold}${c.yellow}HIGH ${c.reset}`,
|
|
227
|
+
MEDIUM: `${c.bold}${c.cyan}MEDIUM ${c.reset}`,
|
|
228
|
+
LOW: `${c.bold}${c.dim}LOW ${c.reset}`,
|
|
229
|
+
};
|
|
230
|
+
const banner = (msg) => console.log(`\n${c.bold}${c.cyan}=== ${msg} ===${c.reset}`);
|
|
231
|
+
// ─── Stage pipeline indicator ────────────────────────────────────────────────
|
|
232
|
+
const STAGES = ['Input', 'Reviewing', 'Fixing', 'Merging'];
|
|
233
|
+
function stageBar(active) {
|
|
234
|
+
const parts = STAGES.map((name, i) => {
|
|
235
|
+
if (i < active)
|
|
236
|
+
return `${c.green}✓ ${name}${c.reset}`;
|
|
237
|
+
if (i === active)
|
|
238
|
+
return `${c.bold}${c.yellow}● ${name}${c.reset}`;
|
|
239
|
+
return `${c.dim}○ ${name}${c.reset}`;
|
|
240
|
+
});
|
|
241
|
+
console.log(`\n ${parts.join(` ${c.dim}→${c.reset} `)}\n`);
|
|
242
|
+
}
|
|
243
|
+
const tag = (id, color) => `${color}[${id.padEnd(12)}]${c.reset}`;
|
|
244
|
+
const agent = (id, msg) => console.log(` ${tag(id, c.blue)} ${msg}`);
|
|
245
|
+
const divider = () => console.log(` ${c.dim}${'─'.repeat(64)}${c.reset}`);
|
|
246
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
247
|
+
function decodeHtml(s) {
|
|
248
|
+
// Single regex pass with a lookup table.
|
|
249
|
+
// Using one .replace() call avoids the chained double-unescaping pattern
|
|
250
|
+
// that CodeQL js/double-escaping flags in fluent .replace() chains.
|
|
251
|
+
const map = {
|
|
252
|
+
'&#x27;': "'", '&#39;': "'",
|
|
253
|
+
'&quot;': '"', '&apos;': "'",
|
|
254
|
+
'&gt;': '>', '&lt;': '<',
|
|
255
|
+
'&amp;': '&', '&': '&',
|
|
256
|
+
'"': '"', ''': "'",
|
|
257
|
+
''': "'", ''': "'",
|
|
258
|
+
'`': '`', '<': '<',
|
|
259
|
+
'>': '>', '|': '|',
|
|
260
|
+
'|': '|', '?': '?',
|
|
261
|
+
'?': '?',
|
|
262
|
+
};
|
|
263
|
+
return s.replace(/&(?:#x27|#39|quot|apos|gt|lt|amp);|&|&(?:quot|apos|lt|gt|vert|quest);|&#(?:x27|x60|x7C|x3F|39);/g, (m) => map[m] ?? m);
|
|
264
|
+
}
|
|
265
|
+
function extractContent(resp) {
|
|
266
|
+
const msg = resp?.choices?.[0]?.message;
|
|
267
|
+
if (!msg)
|
|
268
|
+
return '';
|
|
269
|
+
if (typeof msg.content === 'string' && msg.content.length > 0)
|
|
270
|
+
return msg.content;
|
|
271
|
+
if (Array.isArray(msg.content)) {
|
|
272
|
+
const parts = msg.content
|
|
273
|
+
.filter((p) => p?.type === 'text' || p?.type === 'output_text')
|
|
274
|
+
.map((p) => p.text ?? '')
|
|
275
|
+
.join('\n');
|
|
276
|
+
if (parts.length > 0)
|
|
277
|
+
return parts;
|
|
278
|
+
}
|
|
279
|
+
if (typeof msg.refusal === 'string' && msg.refusal.length > 0)
|
|
280
|
+
return `[refused] ${msg.refusal}`;
|
|
281
|
+
return '';
|
|
282
|
+
}
|
|
283
|
+
// ─── The code under review ────────────────────────────────────────────────────
|
|
284
|
+
// A realistic but deliberately buggy TypeScript auth service (condensed).
|
|
285
|
+
const CODE_UNDER_REVIEW = `
|
|
286
|
+
// auth-service.ts
|
|
287
|
+
import { db } from './database';
|
|
288
|
+
import jwt from 'jsonwebtoken';
|
|
289
|
+
|
|
290
|
+
const JWT_SECRET = 'supersecret123'; // hardcoded
|
|
291
|
+
const SESSIONS: Record<string, string> = {}; // unbounded in-memory store
|
|
292
|
+
|
|
293
|
+
export async function login(username: string, password: string) {
|
|
294
|
+
const user = await db.query(
|
|
295
|
+
\`SELECT * FROM users WHERE username = '\${username}' AND password = '\${password}'\`
|
|
296
|
+
);
|
|
297
|
+
if (!user.rows.length) return { error: 'Invalid credentials' };
|
|
298
|
+
const token = jwt.sign({ id: user.rows[0].id, role: user.rows[0].role }, JWT_SECRET);
|
|
299
|
+
SESSIONS[user.rows[0].id] = token;
|
|
300
|
+
return { token };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function getUserData(userId: string, requesterId: string) {
|
|
304
|
+
const result = await db.query(\`SELECT * FROM users WHERE id = \${userId}\`);
|
|
305
|
+
return result.rows[0]; // no authz check — any user can read any user
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function resetPassword(email: string) {
|
|
309
|
+
const token = Math.random().toString(36).slice(2); // weak token, no expiry
|
|
310
|
+
await db.query(\`UPDATE users SET reset_token = '\${token}' WHERE email = '\${email}'\`);
|
|
311
|
+
sendEmail(email, \`https://app.example.com/reset?token=\${token}\`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function changePassword(userId: string, newPassword: string) {
|
|
315
|
+
// no old password check, no complexity, stored plaintext
|
|
316
|
+
await db.query(\`UPDATE users SET password = '\${newPassword}' WHERE id = '\${userId}'\`);
|
|
317
|
+
}
|
|
318
|
+
`.trim();
|
|
319
|
+
// ─── 5 reviewer agents ────────────────────────────────────────────────────────
|
|
320
|
+
const REVIEWERS = [
|
|
321
|
+
{
|
|
322
|
+
id: 'sec_review',
|
|
323
|
+
label: 'Security',
|
|
324
|
+
angle: 'security vulnerabilities: SQL injection, auth bypass, token weaknesses, insecure crypto, privilege escalation',
|
|
325
|
+
color: c.red,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 'perf_review',
|
|
329
|
+
label: 'Performance',
|
|
330
|
+
angle: 'performance issues: N+1 queries, missing indexes, unbounded queries, synchronous blocking, memory leaks',
|
|
331
|
+
color: c.yellow,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'rel_review',
|
|
335
|
+
label: 'Reliability',
|
|
336
|
+
angle: 'reliability issues: missing error handling, no timeouts, unhandled rejections, race conditions, data loss risks',
|
|
337
|
+
color: c.cyan,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 'test_review',
|
|
341
|
+
label: 'Testing',
|
|
342
|
+
angle: 'testability and test coverage gaps: untestable design, missing edge case tests, side effects, no mocks',
|
|
343
|
+
color: c.magenta,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: 'arch_review',
|
|
347
|
+
label: 'Architecture',
|
|
348
|
+
angle: 'architecture and design: separation of concerns, coupling, scalability limits, maintainability, tech debt',
|
|
349
|
+
color: c.green,
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
// ─── 5 generic reviewer agents (custom role mode — any content type) ───────────
|
|
353
|
+
const CUSTOM_REVIEWERS = [
|
|
354
|
+
{
|
|
355
|
+
id: 'clarity_review',
|
|
356
|
+
label: 'Clarity',
|
|
357
|
+
angle: 'clarity and communication: is it easy to understand, well-structured, free of ambiguity, jargon, or confusing phrasing',
|
|
358
|
+
color: c.cyan,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: 'completeness_review',
|
|
362
|
+
label: 'Completeness',
|
|
363
|
+
angle: 'completeness: missing sections, unanswered questions, gaps in reasoning, unstated assumptions, or missing context',
|
|
364
|
+
color: c.yellow,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: 'accuracy_review',
|
|
368
|
+
label: 'Accuracy',
|
|
369
|
+
angle: 'accuracy: incorrect facts, unsupported claims, logical inconsistencies, contradictions, or outdated information',
|
|
370
|
+
color: c.red,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: 'risk_review',
|
|
374
|
+
label: 'Risk',
|
|
375
|
+
angle: 'risks and downsides: what could go wrong, unintended consequences, blind spots, or decisions that may backfire',
|
|
376
|
+
color: c.magenta,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: 'improvement_review',
|
|
380
|
+
label: 'Improvement',
|
|
381
|
+
angle: 'overall effectiveness: what could be stronger, more persuasive, better evidenced, or more actionable',
|
|
382
|
+
color: c.green,
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
// ─── 5 design-doc reviewer agents ────────────────────────────────────────────
|
|
386
|
+
const DESIGN_REVIEWERS = [
|
|
387
|
+
{
|
|
388
|
+
id: 'scale_review',
|
|
389
|
+
label: 'Scalability',
|
|
390
|
+
angle: 'scalability: throughput bottlenecks, horizontal scaling limits, stateful components, data partitioning, hot-spots',
|
|
391
|
+
color: c.yellow,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: 'sec_review',
|
|
395
|
+
label: 'Security',
|
|
396
|
+
angle: 'security: attack surface, trust boundary violations, sensitive data flows, auth model weaknesses, privilege escalation paths',
|
|
397
|
+
color: c.red,
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
id: 'ops_review',
|
|
401
|
+
label: 'Operability',
|
|
402
|
+
angle: 'operability: observability gaps, deployment complexity, failure modes, missing runbook steps, on-call burden',
|
|
403
|
+
color: c.cyan,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: 'con_review',
|
|
407
|
+
label: 'Consistency',
|
|
408
|
+
angle: 'data consistency: race conditions, distributed state hazards, eventual vs strong consistency trade-offs, dual-write risks',
|
|
409
|
+
color: c.magenta,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: 'sim_review',
|
|
413
|
+
label: 'Simplicity',
|
|
414
|
+
angle: 'simplicity: accidental complexity, over-engineering, maintainability burden, cognitive load for new engineers',
|
|
415
|
+
color: c.green,
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
// ─── Format a reviewer's raw output into structured finding lines ─────────────
|
|
419
|
+
function printFindings(label, color, raw) {
|
|
420
|
+
const decoded = decodeHtml(raw);
|
|
421
|
+
console.log(`\n ${color}${c.bold}[${label}]${c.reset}`);
|
|
422
|
+
const lines = decoded.split('\n').filter(l => l.trim().length > 0);
|
|
423
|
+
for (const line of lines) {
|
|
424
|
+
const trimmed = line.trim();
|
|
425
|
+
const sevMatch = trimmed.match(/^\[?(CRITICAL|HIGH|MEDIUM|LOW)\]?[:\s-]*/i);
|
|
426
|
+
if (sevMatch) {
|
|
427
|
+
const sev = sevMatch[1].toUpperCase();
|
|
428
|
+
const rest = trimmed.slice(sevMatch[0].length).trim();
|
|
429
|
+
// Split on | Fix: into two lines
|
|
430
|
+
const fixIdx = rest.indexOf('|');
|
|
431
|
+
if (fixIdx !== -1) {
|
|
432
|
+
const issue = rest.slice(0, fixIdx).trim();
|
|
433
|
+
const fix = rest.slice(fixIdx + 1).trim();
|
|
434
|
+
console.log(` ${SEV[sev] ?? sev} ${issue}`);
|
|
435
|
+
console.log(` ${c.dim} ${fix}${c.reset}`);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
console.log(` ${SEV[sev] ?? sev} ${rest}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
console.log(` ${c.dim}${trimmed}${c.reset}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// ─── MAIN ─────────────────────────────────────────────────────────────────────
|
|
447
|
+
async function main() {
|
|
448
|
+
banner('network-ai -- Code Review Swarm');
|
|
449
|
+
console.log();
|
|
450
|
+
console.log(` ${c.yellow}${c.bold}DEMO${c.reset} ${c.dim}This is a demonstration of the Network-AI multi-agent framework.${c.reset}`);
|
|
451
|
+
console.log(` ${c.dim} Results are generated by LLMs and are for illustrative purposes only.${c.reset}`);
|
|
452
|
+
console.log(` ${c.dim} Do not rely on output for production, legal, medical, or financial decisions.${c.reset}`);
|
|
453
|
+
console.log(` ${c.dim} Source: https://github.com/agentience/network-ai${c.reset}`);
|
|
454
|
+
const { content, label: codeLabel, mode, customRole } = await promptForInput(c);
|
|
455
|
+
await warnIfMismatch(mode, content, c);
|
|
456
|
+
const lineCount = content.split('\n').length;
|
|
457
|
+
const activeReviewers = mode === 'design' ? DESIGN_REVIEWERS : mode === 'custom' ? CUSTOM_REVIEWERS : REVIEWERS;
|
|
458
|
+
const reviewMode = mode === 'design' ? 'Design review' :
|
|
459
|
+
mode === 'custom' ? `Custom review — ${customRole ?? 'custom role'}` :
|
|
460
|
+
'Code review';
|
|
461
|
+
console.log();
|
|
462
|
+
console.log(` ${c.dim}File : ${codeLabel} (${lineCount} lines)${c.reset}`);
|
|
463
|
+
console.log(` ${c.dim}Mode : ${reviewMode}${c.reset}`);
|
|
464
|
+
const agentCount = mode !== 'design'
|
|
465
|
+
? (activeReviewers.length + ' reviewers + ' + activeReviewers.length + ' fixers + 1 merger')
|
|
466
|
+
: (activeReviewers.length + ' reviewers + 1 coordinator');
|
|
467
|
+
console.log(` ${c.dim}Model: gpt-5.2 | Agents: ${agentCount} | sequential${c.reset}`);
|
|
468
|
+
stageBar(1); // ✓ Input → ● Reviewing → ○ Fixing → ○ Merging
|
|
469
|
+
// ─── Blackboard + adapter ────────────────────────────────────────────────
|
|
470
|
+
const blackboard = new __1.SharedBlackboard(process.cwd());
|
|
471
|
+
blackboard.registerAgent('orchestrator', 'tok-orch', ['*']);
|
|
472
|
+
blackboard.registerAgent('coordinator', 'tok-coord', ['review:', 'verdict:']);
|
|
473
|
+
blackboard.registerAgent('merger', 'tok-merger', ['review:', 'fix:', 'verdict:']);
|
|
474
|
+
for (const r of activeReviewers) {
|
|
475
|
+
blackboard.registerAgent(r.id, `tok-${r.id}`, ['review:']);
|
|
476
|
+
if (mode !== 'design') {
|
|
477
|
+
const fid = r.id.replace('_review', '_fixer');
|
|
478
|
+
blackboard.registerAgent(fid, 'tok-' + fid, ['fix:']);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const adapter = new __1.CustomAdapter();
|
|
482
|
+
// ─── Spinner state (shared between handlers and main loop) ───────────────
|
|
483
|
+
const FRAMES = ['⠋', '⠙', '⠸', '⢰', '⣠', '⣄', '⡆', '⠇'];
|
|
484
|
+
let spinFrame = 0;
|
|
485
|
+
const agentState = new Map(activeReviewers.map(r => [r.id, { status: 'waiting', findings: '', ms: 0 }]));
|
|
486
|
+
// Direct findings store — bypasses blackboard sanitization which strips security-relevant content
|
|
487
|
+
const reviewFindings = new Map();
|
|
488
|
+
function countFindings(raw) {
|
|
489
|
+
return raw.split('\n').filter(l => /^\[?(CRITICAL|HIGH|MEDIUM|LOW)/i.test(l.trim())).length;
|
|
490
|
+
}
|
|
491
|
+
function renderBoard() {
|
|
492
|
+
process.stdout.write(`\x1b[${activeReviewers.length}A`); // move up
|
|
493
|
+
for (const r of activeReviewers) {
|
|
494
|
+
const st = agentState.get(r.id);
|
|
495
|
+
const icon = st.status === 'waiting' ? `${c.dim}·${c.reset}` :
|
|
496
|
+
st.status === 'running' ? `${c.cyan}${FRAMES[spinFrame % FRAMES.length]}${c.reset}` :
|
|
497
|
+
`${c.green}✓${c.reset}`;
|
|
498
|
+
const detail = st.status === 'waiting' ? `${c.dim}waiting${c.reset}` :
|
|
499
|
+
st.status === 'running' ? `${c.dim}analyzing...${c.reset}` :
|
|
500
|
+
st.findings
|
|
501
|
+
? `${c.green}${countFindings(st.findings)} findings${c.reset} ${c.dim}${(st.ms / 1000).toFixed(1)}s${c.reset}`
|
|
502
|
+
: `${c.dim}(no findings)${c.reset}`;
|
|
503
|
+
process.stdout.write(`\x1b[2K ${icon} ${r.color}${r.label.padEnd(14)}${c.reset} ${detail}\n`);
|
|
504
|
+
}
|
|
505
|
+
spinFrame++;
|
|
506
|
+
}
|
|
507
|
+
const fixerState = new Map();
|
|
508
|
+
function renderFixerBoard(fixers) {
|
|
509
|
+
process.stdout.write(`\x1b[${fixers.length}A`);
|
|
510
|
+
for (const fr of fixers) {
|
|
511
|
+
const fid = fr.id.replace('_review', '_fixer');
|
|
512
|
+
const st = fixerState.get(fid) ?? { status: 'waiting', nChanges: 0, ms: 0 };
|
|
513
|
+
const icon = st.status === 'waiting' ? `${c.dim}·${c.reset}` :
|
|
514
|
+
st.status === 'running' ? `${c.cyan}${FRAMES[spinFrame % FRAMES.length]}${c.reset}` :
|
|
515
|
+
`${c.green}✓${c.reset}`;
|
|
516
|
+
const detail = st.status === 'waiting' ? `${c.dim}waiting${c.reset}` :
|
|
517
|
+
st.status === 'running' ? `${c.dim}patching...${c.reset}` :
|
|
518
|
+
st.nChanges > 0
|
|
519
|
+
? `${c.green}${st.nChanges} patch${st.nChanges !== 1 ? 'es' : ''}${c.reset} ${c.dim}${(st.ms / 1000).toFixed(1)}s${c.reset}`
|
|
520
|
+
: `${c.dim}(no changes)${c.reset}`;
|
|
521
|
+
process.stdout.write(`\x1b[2K ${icon} ${fr.color}${fr.label.padEnd(14)}${c.reset} ${detail}\n`);
|
|
522
|
+
}
|
|
523
|
+
spinFrame++;
|
|
524
|
+
}
|
|
525
|
+
// Shared rate-limit state: set by each handler from response headers,
|
|
526
|
+
// read by the sequential dispatch loop to wait exactly as long as needed.
|
|
527
|
+
let nextCallAfterMs = 0;
|
|
528
|
+
/** Parse OpenAI reset header strings like "1s", "500ms", "1m30s" into ms. */
|
|
529
|
+
function parseResetMs(header) {
|
|
530
|
+
if (!header)
|
|
531
|
+
return 0;
|
|
532
|
+
let ms = 0;
|
|
533
|
+
const mMatch = header.match(/(\d+)m/);
|
|
534
|
+
if (mMatch)
|
|
535
|
+
ms += parseInt(mMatch[1]) * 60_000;
|
|
536
|
+
const sMatch = header.match(/(\d+(?:\.\d+)?)s/);
|
|
537
|
+
if (sMatch)
|
|
538
|
+
ms += parseFloat(sMatch[1]) * 1_000;
|
|
539
|
+
const msMatch = header.match(/(\d+)ms/);
|
|
540
|
+
if (msMatch)
|
|
541
|
+
ms += parseInt(msMatch[1]);
|
|
542
|
+
return Math.ceil(ms);
|
|
543
|
+
}
|
|
544
|
+
// ─── Prompt builders (mode-aware) ────────────────────────────────────────
|
|
545
|
+
function buildPrompts(angle) {
|
|
546
|
+
if (mode === 'design') {
|
|
547
|
+
return {
|
|
548
|
+
SYSTEM: 'You are a senior software architect reviewing a system design document.\n' +
|
|
549
|
+
'For each concern you identify, output one line in this exact format:\n' +
|
|
550
|
+
'[SEVERITY] Short description of concern | Fix: one-line recommendation\n' +
|
|
551
|
+
'SEVERITY is one of: CRITICAL, HIGH, MEDIUM, LOW\n' +
|
|
552
|
+
'Identify 3-5 concerns. Be specific about which component or decision.\n' +
|
|
553
|
+
'Focus only on real architectural risks — do not invent problems.',
|
|
554
|
+
USER: `Review the following system design document from a ${angle} perspective.\n\n` +
|
|
555
|
+
`\`\`\`\n${content}\n\`\`\``,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const rolePrefix = mode === 'custom' && customRole
|
|
559
|
+
? `You are ${customRole}.\n`
|
|
560
|
+
: 'You are a senior code reviewer.\n';
|
|
561
|
+
return {
|
|
562
|
+
SYSTEM: rolePrefix +
|
|
563
|
+
'For each issue you find, output one line in this exact format:\n' +
|
|
564
|
+
'[SEVERITY] Short description of issue | Fix: one-line fix\n' +
|
|
565
|
+
'SEVERITY is one of: CRITICAL, HIGH, MEDIUM, LOW\n' +
|
|
566
|
+
'Find 3-5 issues. Be specific about line numbers when relevant.\n' +
|
|
567
|
+
'Focus only on real issues — do not invent problems.',
|
|
568
|
+
USER: `Review the following content from a ${angle} perspective.\n\n` +
|
|
569
|
+
`\`\`\`\n${content}\n\`\`\``,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// ─── Register reviewer handlers ──────────────────────────────────────────
|
|
573
|
+
for (const { id, label, angle } of activeReviewers) {
|
|
574
|
+
adapter.registerHandler(id, async (_payload) => {
|
|
575
|
+
agentState.set(id, { status: 'running', findings: '', ms: 0 });
|
|
576
|
+
const t0 = Date.now();
|
|
577
|
+
const { SYSTEM, USER } = buildPrompts(angle);
|
|
578
|
+
try {
|
|
579
|
+
const { data: resp, response: httpResp } = await openai.chat.completions.create({
|
|
580
|
+
model: 'gpt-5.2',
|
|
581
|
+
messages: [{ role: 'system', content: SYSTEM }, { role: 'user', content: USER }],
|
|
582
|
+
max_completion_tokens: 400,
|
|
583
|
+
temperature: 0.4,
|
|
584
|
+
}).withResponse();
|
|
585
|
+
const remaining = parseInt(httpResp.headers.get('x-ratelimit-remaining-requests') ?? '99');
|
|
586
|
+
if (remaining <= 1) {
|
|
587
|
+
nextCallAfterMs = parseResetMs(httpResp.headers.get('x-ratelimit-reset-requests')) + 500;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
nextCallAfterMs = 0;
|
|
591
|
+
}
|
|
592
|
+
const finish = resp?.choices?.[0]?.finish_reason ?? 'none';
|
|
593
|
+
const findings = decodeHtml(extractContent(resp));
|
|
594
|
+
const ms = Date.now() - t0;
|
|
595
|
+
if (!findings) {
|
|
596
|
+
agentState.set(id, { status: 'done', findings: `[empty — finish_reason: ${finish}]`, ms });
|
|
597
|
+
return { label, findings: '', ms };
|
|
598
|
+
}
|
|
599
|
+
agentState.set(id, { status: 'done', findings, ms });
|
|
600
|
+
reviewFindings.set(id, { label, angle, findings, ms });
|
|
601
|
+
try {
|
|
602
|
+
blackboard.write(`review:${id}`, { label, angle, findings, ms }, id, 3600, `tok-${id}`);
|
|
603
|
+
}
|
|
604
|
+
catch { /* sanitizer may strip security content — reviewFindings is the source of truth */ }
|
|
605
|
+
return { label, findings, ms };
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
const ms = Date.now() - t0;
|
|
609
|
+
const msg = err?.message ?? String(err);
|
|
610
|
+
agentState.set(id, { status: 'done', findings: `[error: ${msg}]`, ms });
|
|
611
|
+
return { label, findings: '', ms };
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// ─── Shared display config ────────────────────────────────────────────────
|
|
616
|
+
const isDesign = mode === 'design';
|
|
617
|
+
const _blockersHeader = isDesign ? '=== ARCHITECTURAL RISKS ===' : '=== SHIP BLOCKERS ===';
|
|
618
|
+
const _fixedHeader = isDesign ? '=== REVISED DESIGN ===' : '=== FIXED CODE ===';
|
|
619
|
+
void _blockersHeader;
|
|
620
|
+
void _fixedHeader;
|
|
621
|
+
// ─── Design mode: single coordinator call ────────────────────────────────
|
|
622
|
+
let coordinatorResult = null;
|
|
623
|
+
if (isDesign) {
|
|
624
|
+
adapter.registerHandler('coordinator', async (payload) => {
|
|
625
|
+
agent('coordinator', 'Synthesizing risks + rewriting design...');
|
|
626
|
+
const allReviews = payload.handoff?.context?.reviews ?? [];
|
|
627
|
+
const combined = allReviews.map(r => '=== ' + r.label + ' Review ===\n' + r.findings).join('\n\n');
|
|
628
|
+
const t0 = Date.now();
|
|
629
|
+
try {
|
|
630
|
+
const resp = await openai.chat.completions.create({
|
|
631
|
+
model: 'gpt-5.2',
|
|
632
|
+
messages: [
|
|
633
|
+
{ role: 'system', content: 'You are a senior editor producing a revised version of the exact document the user submitted.\n' +
|
|
634
|
+
'CRITICAL RULES — violating any of these is an error:\n' +
|
|
635
|
+
' 1. Your output MUST be a rewritten version of the original document text below — nothing else.\n' +
|
|
636
|
+
' 2. DO NOT invent a new document, a new example, or a replacement subject.\n' +
|
|
637
|
+
' 3. DO NOT output code, TypeScript, JavaScript, Python, SQL, or any programming language.\n' +
|
|
638
|
+
' 4. DO NOT output import statements, class definitions, function bodies, or variable declarations.\n' +
|
|
639
|
+
' 5. Output plain prose or Markdown only — the same kind of document the user gave you, improved.' },
|
|
640
|
+
{ role: 'user', content: '5 specialist reviewers analysed this document:\n\n' +
|
|
641
|
+
'=== ORIGINAL DOCUMENT (rewrite THIS — do not replace it) ===\n' + content + '\n\n' +
|
|
642
|
+
'=== SPECIALIST FINDINGS ===\n' + combined + '\n\n' +
|
|
643
|
+
'Return JSON with two string keys:\n' +
|
|
644
|
+
' "blockers" — exactly three lines, each: #N [SEVERITY] one-line issue description\n' +
|
|
645
|
+
' "fixed" — the complete rewritten document in plain prose / Markdown. ' +
|
|
646
|
+
'Keep every section from the original. Improve weak sections based on the findings. ' +
|
|
647
|
+
'After each improved section add a line: > **Change:** what was improved and why. ' +
|
|
648
|
+
'PLAIN TEXT / MARKDOWN ONLY — absolutely no code, no import statements, no function definitions.' },
|
|
649
|
+
],
|
|
650
|
+
max_completion_tokens: 32000,
|
|
651
|
+
temperature: 0.2,
|
|
652
|
+
response_format: { type: 'json_object' },
|
|
653
|
+
});
|
|
654
|
+
const finishReason = resp?.choices?.[0]?.finish_reason ?? 'unknown';
|
|
655
|
+
const ms = Date.now() - t0;
|
|
656
|
+
let verdict = '', fixed = '';
|
|
657
|
+
try {
|
|
658
|
+
const parsed = JSON.parse(extractContent(resp));
|
|
659
|
+
verdict = decodeHtml(String(parsed.blockers ?? '').trim());
|
|
660
|
+
fixed = decodeHtml(String(parsed.fixed ?? '').trim());
|
|
661
|
+
}
|
|
662
|
+
catch { /* keep empty */ }
|
|
663
|
+
coordinatorResult = { verdict, fixed, finishReason, reviewCount: allReviews.length, ms };
|
|
664
|
+
try {
|
|
665
|
+
blackboard.write('verdict:final', { verdict, fixed, finishReason, reviewCount: allReviews.length, ms, generatedAt: new Date().toISOString() }, 'coordinator', 3600, 'tok-coord');
|
|
666
|
+
}
|
|
667
|
+
catch { /* blackboard sanitization may strip content — coordinatorResult is the source of truth */ }
|
|
668
|
+
return { verdict, fixed, finishReason, ms };
|
|
669
|
+
}
|
|
670
|
+
catch (coordErr) {
|
|
671
|
+
const ms = Date.now() - t0;
|
|
672
|
+
const errMsg = String(coordErr?.message ?? coordErr).slice(0, 200);
|
|
673
|
+
process.stderr.write('\n[coordinator] API error: ' + errMsg + '\n');
|
|
674
|
+
coordinatorResult = { verdict: '#1 [CRITICAL] Coordinator API call failed: ' + errMsg, fixed: '', finishReason: 'error', reviewCount: allReviews.length, ms };
|
|
675
|
+
return { verdict: coordinatorResult.verdict, fixed: '', finishReason: 'error', ms };
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
// ─── Code / custom mode: Wave 2 fixers + Wave 3 merger ───────────────────
|
|
680
|
+
// Direct result store — bypasses blackboard sanitization for large/code-heavy outputs
|
|
681
|
+
let mergerResult = null;
|
|
682
|
+
const fixerPatches = new Map();
|
|
683
|
+
if (!isDesign) {
|
|
684
|
+
// 5 specialist fixers — each reads its reviewer's findings and applies targeted patches
|
|
685
|
+
for (const { id, label, angle } of activeReviewers) {
|
|
686
|
+
const fixId = id.replace('_review', '_fixer');
|
|
687
|
+
adapter.registerHandler(fixId, async (_payload) => {
|
|
688
|
+
fixerState.set(fixId, { status: 'running', nChanges: 0, ms: 0 });
|
|
689
|
+
const reviewEntry = reviewFindings.get(id);
|
|
690
|
+
const findings = reviewEntry?.findings ?? '';
|
|
691
|
+
const t0 = Date.now();
|
|
692
|
+
try {
|
|
693
|
+
const resp = await openai.chat.completions.create({
|
|
694
|
+
model: 'gpt-5.2',
|
|
695
|
+
messages: [
|
|
696
|
+
{
|
|
697
|
+
role: 'system',
|
|
698
|
+
content: mode === 'custom'
|
|
699
|
+
? 'You are a specialist editor focused only on ' + angle.split(':')[0] + ' improvements.\n' +
|
|
700
|
+
'Return a JSON object with key "changes": an array where each object has:\n' +
|
|
701
|
+
' "fn" — section or heading name\n' +
|
|
702
|
+
' "issue" — one sentence: what was wrong or weak\n' +
|
|
703
|
+
' "fixed" — complete revised version of that section.'
|
|
704
|
+
: 'You are a specialist code fixer focused only on ' + angle.split(':')[0] + ' issues.\n' +
|
|
705
|
+
'Return a JSON object with key "changes": an array where each object has:\n' +
|
|
706
|
+
' "fn" — function or block name\n' +
|
|
707
|
+
' "issue" — one sentence: what was wrong\n' +
|
|
708
|
+
' "fixed" — complete corrected function/block. Rules: single-quoted strings, no template literals, ' +
|
|
709
|
+
'always write the constructor keyword in TypeScript classes, ' +
|
|
710
|
+
'write || and && and ?? as literal characters (never omit or encode them), ' +
|
|
711
|
+
'write | for TypeScript union types (e.g. string | null), ' +
|
|
712
|
+
'semicolons between object/type literal members.',
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
role: 'user',
|
|
716
|
+
content: mode === 'custom'
|
|
717
|
+
? 'Original content:\n' + content + '\n\n' +
|
|
718
|
+
label + ' issues to improve:\n' + (findings || 'No issues — return { "changes": [] }')
|
|
719
|
+
: 'Original code:\n' + content + '\n\n' +
|
|
720
|
+
label + ' issues to fix:\n' + (findings || 'No issues — return { "changes": [] }'),
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
max_completion_tokens: 16000,
|
|
724
|
+
temperature: 0.1,
|
|
725
|
+
response_format: { type: 'json_object' },
|
|
726
|
+
});
|
|
727
|
+
const ms = Date.now() - t0;
|
|
728
|
+
let changes = [];
|
|
729
|
+
try {
|
|
730
|
+
const parsed = JSON.parse(extractContent(resp));
|
|
731
|
+
changes = Array.isArray(parsed.changes) ? parsed.changes : [];
|
|
732
|
+
}
|
|
733
|
+
catch { /* keep empty */ }
|
|
734
|
+
fixerState.set(fixId, { status: 'done', nChanges: changes.length, ms });
|
|
735
|
+
fixerPatches.set(id, { domain: label, changes, ms });
|
|
736
|
+
try {
|
|
737
|
+
blackboard.write('fix:' + id, { domain: label, changes, ms }, fixId, 3600, 'tok-' + fixId);
|
|
738
|
+
}
|
|
739
|
+
catch { /* sanitizer may strip code content */ }
|
|
740
|
+
return { domain: label, changes, ms };
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
const ms = Date.now() - t0;
|
|
744
|
+
fixerState.set(fixId, { status: 'done', nChanges: 0, ms });
|
|
745
|
+
fixerPatches.set(id, { domain: label, changes: [], ms });
|
|
746
|
+
try {
|
|
747
|
+
blackboard.write('fix:' + id, { domain: label, changes: [], ms }, fixId, 3600, 'tok-' + fixId);
|
|
748
|
+
}
|
|
749
|
+
catch { /* sanitizer may strip code content */ }
|
|
750
|
+
return { domain: label, changes: [], ms };
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
// Merger: reads all 5 fix patches from blackboard, produces unified clean output
|
|
755
|
+
adapter.registerHandler('merger', async (_payload) => {
|
|
756
|
+
agent('merger', 'Merging all targeted patches into final unified output...');
|
|
757
|
+
const allFixes = activeReviewers.map(r => {
|
|
758
|
+
return fixerPatches.get(r.id) ?? { domain: r.label, changes: [] };
|
|
759
|
+
});
|
|
760
|
+
const t0 = Date.now();
|
|
761
|
+
// Budget-aware payload truncation: keep ALL patches but cap "fixed" body size
|
|
762
|
+
// dynamically so total patch chars stay under ~40 000 (safe for 32k-token merger).
|
|
763
|
+
// Mode 4 can produce 10+ patches/domain (37 total); mode 2 produces 2–6 per domain.
|
|
764
|
+
// Normalize shape defensively so malformed fixer payloads never crash merger prep.
|
|
765
|
+
const normalizedFixes = allFixes.map((f) => ({
|
|
766
|
+
domain: String(f?.domain ?? ''),
|
|
767
|
+
changes: Array.isArray(f?.changes)
|
|
768
|
+
? f.changes.map((c) => ({
|
|
769
|
+
fn: String(c?.fn ?? ''),
|
|
770
|
+
issue: String(c?.issue ?? ''),
|
|
771
|
+
fixed: String(c?.fixed ?? ''),
|
|
772
|
+
}))
|
|
773
|
+
: [],
|
|
774
|
+
}));
|
|
775
|
+
const totalPatchCount = normalizedFixes.reduce((s, f) => s + f.changes.length, 0);
|
|
776
|
+
const PAYLOAD_BUDGET = 40000; // chars
|
|
777
|
+
const originLen = content.length;
|
|
778
|
+
const patchBudget = Math.max(PAYLOAD_BUDGET - originLen, 8000);
|
|
779
|
+
// min 400 chars per patch so instructions remain meaningful
|
|
780
|
+
const maxPerPatch = totalPatchCount > 0
|
|
781
|
+
? Math.max(400, Math.floor(patchBudget / totalPatchCount))
|
|
782
|
+
: 2000;
|
|
783
|
+
const truncatedFixes = normalizedFixes.map(f => ({
|
|
784
|
+
domain: f.domain,
|
|
785
|
+
changes: f.changes.map((c) => ({
|
|
786
|
+
fn: c.fn,
|
|
787
|
+
issue: c.issue,
|
|
788
|
+
fixed: c.fixed.length > maxPerPatch
|
|
789
|
+
? c.fixed.slice(0, maxPerPatch) + '\n// ... (continue applying the fix intent described above)'
|
|
790
|
+
: c.fixed,
|
|
791
|
+
})),
|
|
792
|
+
}));
|
|
793
|
+
try {
|
|
794
|
+
const resp = await openai.chat.completions.create({
|
|
795
|
+
model: 'gpt-5.2',
|
|
796
|
+
messages: [
|
|
797
|
+
{
|
|
798
|
+
role: 'system',
|
|
799
|
+
content: mode === 'custom'
|
|
800
|
+
? 'You are a lead editor incorporating 5 sets of targeted improvements into one final revised version.\n' +
|
|
801
|
+
'Apply ALL improvements. Where two improvements touch the same section, combine both.\n' +
|
|
802
|
+
'CRITICAL: Output plain prose or Markdown ONLY. DO NOT output code, TypeScript, JavaScript, SQL, ' +
|
|
803
|
+
'import statements, class definitions, function bodies, or any programming language.\n' +
|
|
804
|
+
'Return JSON with exactly two string keys:\n' +
|
|
805
|
+
' "blockers" — top 3 most important improvements made, each: #N [SEVERITY] What was improved (impact in 5 words)\n' +
|
|
806
|
+
' "fixed" — complete final revised version of the content in plain prose / Markdown. ' +
|
|
807
|
+
'After each changed section add a line: > **Change:** what was improved and why. ' +
|
|
808
|
+
'NO CODE — plain text / Markdown only.'
|
|
809
|
+
: 'You are the lead engineer merging 5 targeted patch sets into one clean final file.\n' +
|
|
810
|
+
'Apply ALL patches. Where two patches touch the same function, combine both fixes.\n' +
|
|
811
|
+
'Return JSON with exactly two string keys:\n' +
|
|
812
|
+
' "blockers" — top 3 most important fixes, each: #N [SEVERITY] What was fixed (impact in 5 words)\n' +
|
|
813
|
+
' "fixed" — complete unified source file. Rules: no markdown fences, single-quoted strings, ' +
|
|
814
|
+
'no template literals, always write the constructor keyword in TypeScript classes, ' +
|
|
815
|
+
'write || and && and ?? as literal characters (never omit or encode them), ' +
|
|
816
|
+
'write | for TypeScript union types (e.g. string | null — never omit the pipe), ' +
|
|
817
|
+
'semicolons between object/type literal members, one // FIX: comment per changed block.',
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
role: 'user',
|
|
821
|
+
content: mode === 'custom'
|
|
822
|
+
? 'Original content:\n' + content + '\n\nImprovement sets to merge:\n' + JSON.stringify(truncatedFixes, null, 2)
|
|
823
|
+
: 'Original code:\n' + content + '\n\nPatch sets to merge:\n' + JSON.stringify(truncatedFixes, null, 2),
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
max_completion_tokens: 32000,
|
|
827
|
+
temperature: 0.1,
|
|
828
|
+
response_format: { type: 'json_object' },
|
|
829
|
+
});
|
|
830
|
+
const finishReason = resp?.choices?.[0]?.finish_reason ?? 'unknown';
|
|
831
|
+
const ms = Date.now() - t0;
|
|
832
|
+
let verdict = '', fixed = '';
|
|
833
|
+
try {
|
|
834
|
+
const parsed = JSON.parse(extractContent(resp));
|
|
835
|
+
verdict = decodeHtml(String(parsed.blockers ?? '').trim());
|
|
836
|
+
fixed = decodeHtml(String(parsed.fixed ?? '').trim());
|
|
837
|
+
}
|
|
838
|
+
catch { /* keep empty */ }
|
|
839
|
+
mergerResult = { verdict, fixed, finishReason, reviewCount: activeReviewers.length, ms };
|
|
840
|
+
try {
|
|
841
|
+
blackboard.write('verdict:final', { verdict, fixed, finishReason, reviewCount: activeReviewers.length, ms, generatedAt: new Date().toISOString() }, 'merger', 3600, 'tok-merger');
|
|
842
|
+
}
|
|
843
|
+
catch { /* blackboard sanitization may strip code content — mergerResult is the source of truth */ }
|
|
844
|
+
return { verdict, fixed, finishReason, ms };
|
|
845
|
+
}
|
|
846
|
+
catch (mergerErr) {
|
|
847
|
+
const ms = Date.now() - t0;
|
|
848
|
+
const errMsg = String(mergerErr?.message ?? mergerErr).slice(0, 200);
|
|
849
|
+
process.stderr.write('\n[merger] API error: ' + errMsg + '\n');
|
|
850
|
+
mergerResult = { verdict: '#1 [CRITICAL] Merger API call failed: ' + errMsg, fixed: '', finishReason: 'error', reviewCount: activeReviewers.length, ms };
|
|
851
|
+
return { verdict: mergerResult.verdict, fixed: '', finishReason: 'error', ms };
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
// ─── Orchestrator ─────────────────────────────────────────────────────────
|
|
856
|
+
const orchestrator = (0, __1.createSwarmOrchestrator)({
|
|
857
|
+
qualityThreshold: 0,
|
|
858
|
+
trustLevels: [
|
|
859
|
+
{ agentId: 'orchestrator', trustLevel: 0.9, allowedNamespaces: ['*'], allowedResources: ['*'] },
|
|
860
|
+
{ agentId: 'coordinator', trustLevel: 0.9, allowedNamespaces: ['*'], allowedResources: ['*'] },
|
|
861
|
+
{ agentId: 'merger', trustLevel: 0.9, allowedNamespaces: ['*'], allowedResources: ['*'] },
|
|
862
|
+
...activeReviewers.map(r => ({
|
|
863
|
+
agentId: r.id,
|
|
864
|
+
trustLevel: 0.8,
|
|
865
|
+
allowedNamespaces: ['task:', 'review:'],
|
|
866
|
+
allowedResources: ['EXTERNAL_SERVICE'],
|
|
867
|
+
})),
|
|
868
|
+
...(!isDesign ? activeReviewers.map(r => ({
|
|
869
|
+
agentId: r.id.replace('_review', '_fixer'),
|
|
870
|
+
trustLevel: 0.8,
|
|
871
|
+
allowedNamespaces: ['review:', 'fix:'],
|
|
872
|
+
allowedResources: ['EXTERNAL_SERVICE'],
|
|
873
|
+
})) : []),
|
|
874
|
+
],
|
|
875
|
+
});
|
|
876
|
+
await orchestrator.addAdapter(adapter);
|
|
877
|
+
const ctx = { agentId: 'orchestrator', taskId: 'code-review-001', sessionId: 'cr-001' };
|
|
878
|
+
const totalStart = Date.now();
|
|
879
|
+
const collectedReviews = [];
|
|
880
|
+
banner(`${activeReviewers.length} Reviewers`);
|
|
881
|
+
console.log();
|
|
882
|
+
console.log(` ${c.yellow}⚡ Single API key — dispatching sequentially (RPM-limited)${c.reset}`);
|
|
883
|
+
console.log(` ${c.dim} Speed depends on your provider, model tier, and API architecture.${c.reset}`);
|
|
884
|
+
console.log(` ${c.dim} Multiple keys, a faster provider (Groq, gpt-4o-mini), or a local GPU${c.reset}`);
|
|
885
|
+
console.log(` ${c.dim} enables true parallel dispatch and cuts this to ~8s.${c.reset}`);
|
|
886
|
+
console.log();
|
|
887
|
+
// Print initial status board (all waiting)
|
|
888
|
+
for (const r of activeReviewers) {
|
|
889
|
+
process.stdout.write(` ${c.dim}·${c.reset} ${r.color}${r.label.padEnd(14)}${c.reset} ${c.dim}waiting${c.reset}\n`);
|
|
890
|
+
}
|
|
891
|
+
// Spinner interval — refreshes status board every 120ms
|
|
892
|
+
const spinInterval = setInterval(renderBoard, 120);
|
|
893
|
+
// Sequential dispatch with adaptive rate-limit gap
|
|
894
|
+
for (let i = 0; i < activeReviewers.length; i++) {
|
|
895
|
+
if (i > 0) {
|
|
896
|
+
const wait = nextCallAfterMs > 0 ? nextCallAfterMs : 1000;
|
|
897
|
+
await sleep(wait);
|
|
898
|
+
nextCallAfterMs = 0;
|
|
899
|
+
}
|
|
900
|
+
const r = activeReviewers[i];
|
|
901
|
+
await orchestrator.execute('delegate_task', {
|
|
902
|
+
targetAgent: `custom:${r.id}`,
|
|
903
|
+
taskPayload: {
|
|
904
|
+
_rid: totalStart,
|
|
905
|
+
instruction: r.angle,
|
|
906
|
+
context: { content },
|
|
907
|
+
expectedOutput: '[SEVERITY] Issue | Fix: remedy',
|
|
908
|
+
},
|
|
909
|
+
}, ctx);
|
|
910
|
+
const rf = reviewFindings.get(r.id);
|
|
911
|
+
if (rf?.findings)
|
|
912
|
+
collectedReviews.push({ label: rf.label, findings: rf.findings, color: r.color });
|
|
913
|
+
}
|
|
914
|
+
// Silent retry for any blank agents (rate-limit hit on first call)
|
|
915
|
+
const failed = activeReviewers.filter(r => !collectedReviews.find(cr => cr.label === r.label));
|
|
916
|
+
if (failed.length > 0) {
|
|
917
|
+
await sleep(12000);
|
|
918
|
+
for (const r of failed) {
|
|
919
|
+
await orchestrator.execute('delegate_task', {
|
|
920
|
+
targetAgent: `custom:${r.id}`,
|
|
921
|
+
taskPayload: {
|
|
922
|
+
_rid: totalStart,
|
|
923
|
+
instruction: r.angle,
|
|
924
|
+
context: { content },
|
|
925
|
+
expectedOutput: '[SEVERITY] Issue | Fix: remedy',
|
|
926
|
+
},
|
|
927
|
+
}, ctx);
|
|
928
|
+
const rf2 = reviewFindings.get(r.id);
|
|
929
|
+
if (rf2?.findings)
|
|
930
|
+
collectedReviews.push({ label: rf2.label, findings: rf2.findings, color: r.color });
|
|
931
|
+
// Update agentState for board if retry succeeded
|
|
932
|
+
const st = agentState.get(r.id);
|
|
933
|
+
if (!st.findings && rf2?.findings) {
|
|
934
|
+
agentState.set(r.id, { ...st, findings: rf2.findings });
|
|
935
|
+
}
|
|
936
|
+
await sleep(3000);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
clearInterval(spinInterval);
|
|
940
|
+
renderBoard(); // final repaint — all ✓
|
|
941
|
+
// ─── Print full findings ──────────────────────────────────────────────────
|
|
942
|
+
for (const review of collectedReviews) {
|
|
943
|
+
printFindings(review.label, review.color, review.findings);
|
|
944
|
+
}
|
|
945
|
+
divider();
|
|
946
|
+
console.log(` ${c.dim}${collectedReviews.length}/${activeReviewers.length} reviewers returned findings${c.reset}`);
|
|
947
|
+
// ─── Wave 2: Targeted fixers (code / custom mode) ───────────────────────
|
|
948
|
+
const finalBanner = isDesign ? 'ARCHITECTURAL RISKS' : mode === 'custom' ? 'KEY ISSUES' : 'SHIP BLOCKERS';
|
|
949
|
+
const fixedBanner = isDesign ? 'REVISED DESIGN' : mode === 'custom' ? 'REVISED CONTENT' : 'FIXED CODE';
|
|
950
|
+
if (!isDesign) {
|
|
951
|
+
stageBar(2); // ✓ Reviewing → ● Fixing → ○ Merging
|
|
952
|
+
banner(activeReviewers.length + ' Fixers');
|
|
953
|
+
console.log();
|
|
954
|
+
for (const r of activeReviewers) {
|
|
955
|
+
const fid = r.id.replace('_review', '_fixer');
|
|
956
|
+
fixerState.set(fid, { status: 'waiting', nChanges: 0, ms: 0 });
|
|
957
|
+
process.stdout.write(` ${c.dim}·${c.reset} ${r.color}${r.label.padEnd(14)}${c.reset} ${c.dim}waiting${c.reset}\n`);
|
|
958
|
+
}
|
|
959
|
+
const fixerSpinInterval = setInterval(() => renderFixerBoard(activeReviewers), 120);
|
|
960
|
+
for (let i = 0; i < activeReviewers.length; i++) {
|
|
961
|
+
if (i > 0) {
|
|
962
|
+
const wait = nextCallAfterMs > 0 ? nextCallAfterMs : 1000;
|
|
963
|
+
await sleep(wait);
|
|
964
|
+
nextCallAfterMs = 0;
|
|
965
|
+
}
|
|
966
|
+
const r = activeReviewers[i];
|
|
967
|
+
const fid = r.id.replace('_review', '_fixer');
|
|
968
|
+
await orchestrator.execute('delegate_task', {
|
|
969
|
+
targetAgent: 'custom:' + fid,
|
|
970
|
+
taskPayload: { _rid: totalStart, instruction: r.angle, context: { content }, expectedOutput: '{ "changes": [] }' },
|
|
971
|
+
}, ctx);
|
|
972
|
+
}
|
|
973
|
+
clearInterval(fixerSpinInterval);
|
|
974
|
+
renderFixerBoard(activeReviewers);
|
|
975
|
+
}
|
|
976
|
+
// ─── Wave 3: Merger / Coordinator ────────────────────────────────────────
|
|
977
|
+
stageBar(3); // ● Merging
|
|
978
|
+
const _mergeTarget = isDesign ? 'custom:coordinator' : 'custom:merger';
|
|
979
|
+
void _mergeTarget;
|
|
980
|
+
const mergeLabel = isDesign ? 'Synthesizing risks + rewriting design...' : 'Merging patches into unified output...';
|
|
981
|
+
banner(isDesign ? 'Coordinator' : 'Merger');
|
|
982
|
+
console.log();
|
|
983
|
+
let coordFrame = 0;
|
|
984
|
+
const coordSpin = setInterval(() => {
|
|
985
|
+
process.stdout.write(`\r ${c.cyan}${FRAMES[coordFrame % FRAMES.length]}${c.reset} ${c.dim}${mergeLabel}${c.reset} `);
|
|
986
|
+
coordFrame++;
|
|
987
|
+
}, 120);
|
|
988
|
+
// Execute merger/coordinator directly through the adapter.
|
|
989
|
+
// This avoids orchestrator task-cache collisions and sanitizer interference
|
|
990
|
+
// on large final outputs.
|
|
991
|
+
const mergeAgentId = isDesign ? 'coordinator' : 'merger';
|
|
992
|
+
const mergeExec = await adapter.executeAgent(mergeAgentId, {
|
|
993
|
+
action: 'execute',
|
|
994
|
+
params: {},
|
|
995
|
+
handoff: {
|
|
996
|
+
handoffId: 'merge-direct-' + totalStart,
|
|
997
|
+
sourceAgent: 'orchestrator',
|
|
998
|
+
targetAgent: 'custom:' + mergeAgentId,
|
|
999
|
+
taskType: 'delegate',
|
|
1000
|
+
instruction: mergeLabel,
|
|
1001
|
+
context: { reviews: collectedReviews },
|
|
1002
|
+
expectedOutput: '{ "blockers": "...", "fixed": "..." }',
|
|
1003
|
+
},
|
|
1004
|
+
blackboardSnapshot: {},
|
|
1005
|
+
}, {
|
|
1006
|
+
agentId: 'orchestrator',
|
|
1007
|
+
taskId: 'merge-direct-' + totalStart,
|
|
1008
|
+
sessionId: 'demo-' + totalStart,
|
|
1009
|
+
});
|
|
1010
|
+
if (!mergeExec.success) {
|
|
1011
|
+
const errMsg = String(mergeExec.error?.message ?? 'unknown merge execution error');
|
|
1012
|
+
process.stderr.write('\n[' + mergeAgentId + '] adapter execution error: ' + errMsg + '\n');
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
const data = mergeExec.data;
|
|
1016
|
+
if (!isDesign && !mergerResult && data && typeof data === 'object') {
|
|
1017
|
+
mergerResult = {
|
|
1018
|
+
verdict: String(data.verdict ?? ''),
|
|
1019
|
+
fixed: String(data.fixed ?? ''),
|
|
1020
|
+
finishReason: String(data.finishReason ?? 'unknown'),
|
|
1021
|
+
reviewCount: activeReviewers.length,
|
|
1022
|
+
ms: Number(data.ms ?? 0),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
if (isDesign && !coordinatorResult && data && typeof data === 'object') {
|
|
1026
|
+
coordinatorResult = {
|
|
1027
|
+
verdict: String(data.verdict ?? ''),
|
|
1028
|
+
fixed: String(data.fixed ?? ''),
|
|
1029
|
+
finishReason: String(data.finishReason ?? 'unknown'),
|
|
1030
|
+
reviewCount: activeReviewers.length,
|
|
1031
|
+
ms: Number(data.ms ?? 0),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
clearInterval(coordSpin);
|
|
1036
|
+
process.stdout.write('\r\x1b[2K');
|
|
1037
|
+
const val = (mergerResult ?? coordinatorResult);
|
|
1038
|
+
if (val) {
|
|
1039
|
+
const reviewType = isDesign ? 'architecture reviews' : 'specialist reviews';
|
|
1040
|
+
// ── Blockers
|
|
1041
|
+
banner(finalBanner);
|
|
1042
|
+
console.log(` ${c.dim}Ready in ${val.ms} ms${c.reset}\n`);
|
|
1043
|
+
for (const line of val.verdict.split('\n').filter(Boolean)) {
|
|
1044
|
+
await sleep(400);
|
|
1045
|
+
const trimmed = line.trim();
|
|
1046
|
+
const sevMatch = trimmed.match(/\[?(CRITICAL|HIGH|MEDIUM|LOW)\]?/i);
|
|
1047
|
+
if (sevMatch) {
|
|
1048
|
+
const sev = sevMatch[1].toUpperCase();
|
|
1049
|
+
const rest = trimmed.replace(/\[?(CRITICAL|HIGH|MEDIUM|LOW)\]?/i, SEV[sev] ?? sev);
|
|
1050
|
+
console.log(` ${c.bold}${rest}${c.reset}`);
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
console.log(` ${c.bold}${c.white}${trimmed}${c.reset}`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
console.log();
|
|
1057
|
+
console.log(` ${c.dim}Based on ${val.reviewCount}/${activeReviewers.length} ${reviewType}${c.reset}`);
|
|
1058
|
+
// ── Fixed output
|
|
1059
|
+
if (val.fixed && val.fixed.trim().length > 10) {
|
|
1060
|
+
banner(fixedBanner);
|
|
1061
|
+
const ext = isDesign || mode === 'custom' ? 'md' : 'ts';
|
|
1062
|
+
const slug = codeLabel.replace(/[^a-z0-9]/gi, '-').toLowerCase().replace(/-+/g, '-').slice(0, 30);
|
|
1063
|
+
const path = require('path');
|
|
1064
|
+
const fs = require('fs');
|
|
1065
|
+
const { execSync } = require('child_process');
|
|
1066
|
+
const outDir = path.join(__dirname, 'output');
|
|
1067
|
+
if (!fs.existsSync(outDir))
|
|
1068
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
1069
|
+
const outFile = path.join(outDir, `fixed-${slug}-${Date.now()}.${ext}`);
|
|
1070
|
+
if (!path.resolve(outFile).startsWith(path.resolve(outDir) + path.sep))
|
|
1071
|
+
throw new Error('Output path outside output directory');
|
|
1072
|
+
// ── Syntax feedback loop (TS files only, max 2 correction passes) ───
|
|
1073
|
+
let currentCode = val.fixed;
|
|
1074
|
+
let syntaxPasses = 0;
|
|
1075
|
+
const MAX_SYNTAX_PASSES = 2;
|
|
1076
|
+
async function runSyntaxCheck(code) {
|
|
1077
|
+
if (ext !== 'ts')
|
|
1078
|
+
return [];
|
|
1079
|
+
const tmpFile = path.join(outDir, `_syntax_tmp_${Date.now()}.ts`);
|
|
1080
|
+
if (!path.resolve(tmpFile).startsWith(path.resolve(outDir) + path.sep))
|
|
1081
|
+
throw new Error('Temp path outside output directory');
|
|
1082
|
+
fs.writeFileSync(tmpFile, code, 'utf8');
|
|
1083
|
+
try {
|
|
1084
|
+
execSync('npx tsc --noEmit --skipLibCheck --noResolve --allowSyntheticDefaultImports ' +
|
|
1085
|
+
'--target ES2020 --module commonjs --strict false "' + tmpFile + '"', { stdio: 'pipe', cwd: process.cwd() });
|
|
1086
|
+
return [];
|
|
1087
|
+
}
|
|
1088
|
+
catch (e) {
|
|
1089
|
+
const out = String(e.stdout ?? '') + String(e.stderr ?? '');
|
|
1090
|
+
return out.split('\n')
|
|
1091
|
+
// Filter import-resolution noise — we only want real syntax errors
|
|
1092
|
+
.filter(l => l.includes('error TS') && !l.includes('TS2307') && !l.includes('TS2304') && !l.includes('TS2305') && !l.includes('TS2339'))
|
|
1093
|
+
.map(l => l.replace(tmpFile, '<file>').trim())
|
|
1094
|
+
.filter(Boolean);
|
|
1095
|
+
}
|
|
1096
|
+
finally {
|
|
1097
|
+
try {
|
|
1098
|
+
fs.unlinkSync(tmpFile);
|
|
1099
|
+
}
|
|
1100
|
+
catch { /* ignore */ }
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
let syntaxErrors = await runSyntaxCheck(currentCode);
|
|
1104
|
+
while (syntaxErrors.length > 0 && syntaxPasses < MAX_SYNTAX_PASSES) {
|
|
1105
|
+
syntaxPasses++;
|
|
1106
|
+
console.log(` ${c.yellow}⟳ ${syntaxErrors.length} syntax error${syntaxErrors.length !== 1 ? 's' : ''} — correction pass ${syntaxPasses}/${MAX_SYNTAX_PASSES}...${c.reset}`);
|
|
1107
|
+
let corrFrame = 0;
|
|
1108
|
+
const corrSpin = setInterval(() => {
|
|
1109
|
+
process.stdout.write(`\r ${c.cyan}${FRAMES[corrFrame++ % FRAMES.length]}${c.reset} ${c.dim}Fixing syntax...${c.reset} `);
|
|
1110
|
+
}, 120);
|
|
1111
|
+
try {
|
|
1112
|
+
const corrResp = await openai.chat.completions.create({
|
|
1113
|
+
model: 'gpt-5.2',
|
|
1114
|
+
max_completion_tokens: 32000,
|
|
1115
|
+
temperature: 0.1,
|
|
1116
|
+
response_format: { type: 'json_object' },
|
|
1117
|
+
messages: [
|
|
1118
|
+
{
|
|
1119
|
+
role: 'system',
|
|
1120
|
+
content: 'You are a TypeScript syntax corrector. Fix ONLY the reported errors — do not change logic.\n' +
|
|
1121
|
+
'Common causes: missing | in union types (string | null), missing constructor keyword, ' +
|
|
1122
|
+
'missing || or && or ?? operators.\n' +
|
|
1123
|
+
'Return JSON with one key "fixed": the complete corrected source file. ' +
|
|
1124
|
+
'Single-quoted strings only. No template literals. No markdown fences.',
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
role: 'user',
|
|
1128
|
+
content: 'TypeScript errors to fix:\n' + syntaxErrors.join('\n') + '\n\n' +
|
|
1129
|
+
'Current code:\n' + currentCode,
|
|
1130
|
+
},
|
|
1131
|
+
],
|
|
1132
|
+
});
|
|
1133
|
+
clearInterval(corrSpin);
|
|
1134
|
+
process.stdout.write('\r\x1b[2K');
|
|
1135
|
+
const parsed = JSON.parse(extractContent(corrResp));
|
|
1136
|
+
currentCode = decodeHtml(String(parsed.fixed ?? currentCode).trim());
|
|
1137
|
+
syntaxErrors = await runSyntaxCheck(currentCode);
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
clearInterval(corrSpin);
|
|
1141
|
+
process.stdout.write('\r\x1b[2K');
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const fixedLines = currentCode.split('\n');
|
|
1146
|
+
const fixCount = fixedLines.filter((l) => l.includes('// FIX:') || l.includes('## Changes Made')).length;
|
|
1147
|
+
const truncated = val.finishReason === 'length';
|
|
1148
|
+
if (syntaxErrors.length === 0) {
|
|
1149
|
+
console.log(` ${c.green}✓ ${fixedLines.length} lines · ${fixCount} changes · syntax clean${truncated ? ` ${c.yellow}⚠ output truncated` : ''}${c.reset}\n`);
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
console.log(` ${c.yellow}⚠ ${fixedLines.length} lines · ${fixCount} changes · ${syntaxErrors.length} syntax error${syntaxErrors.length !== 1 ? 's' : ''} remain${truncated ? ' ⚠ output truncated' : ''}${c.reset}\n`);
|
|
1153
|
+
}
|
|
1154
|
+
for (let i = 0; i < fixedLines.length; i++) {
|
|
1155
|
+
const lineNum = String(i + 1).padStart(4, ' ');
|
|
1156
|
+
const line = decodeHtml(fixedLines[i]);
|
|
1157
|
+
const isFix = line.includes('// FIX:') || line.includes('## Changes Made') || line.includes('> **Change:');
|
|
1158
|
+
console.log(` ${c.dim}${lineNum}${c.reset} ${isFix ? c.green : c.dim}${line}${c.reset}`);
|
|
1159
|
+
}
|
|
1160
|
+
console.log();
|
|
1161
|
+
const outputContent = ext === 'ts' ? `// @ts-nocheck\n${currentCode}` : currentCode;
|
|
1162
|
+
fs.writeFileSync(outFile, outputContent, 'utf8');
|
|
1163
|
+
console.log(` ${c.green}✓ Saved → ${outFile}${c.reset} ${c.dim}(open to see the full file)${c.reset}`);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
console.log(` ${c.yellow}⚠ Fixed output empty (token limit hit) — increase max_completion_tokens or use a model with higher context${c.reset}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const totalMs = Date.now() - totalStart;
|
|
1170
|
+
const llmCalls = isDesign ? activeReviewers.length + 1 : activeReviewers.length * 2 + 1;
|
|
1171
|
+
console.log(`\n ${c.dim}Total: ${totalMs} ms (${(totalMs / 1000).toFixed(1)}s) — ${llmCalls} LLM calls via network-ai${c.reset}\n`);
|
|
1172
|
+
}
|
|
1173
|
+
main().catch(err => {
|
|
1174
|
+
console.error('\n[ERROR]', err.message);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
});
|
|
1177
|
+
//# sourceMappingURL=05-code-review-swarm.js.map
|