multi-agents-custom 2.0.0 → 2.1.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.
package/dist/index.mjs CHANGED
@@ -1,21 +1,707 @@
1
1
  import {
2
+ AgentStatus,
2
3
  AntigravityWriter,
4
+ BaseAgent,
3
5
  ConfigGenerator,
4
6
  CopilotWriter,
5
7
  CursorWriter,
6
8
  DEFAULT_PERSONAS,
7
9
  Logger,
10
+ MasterAgent,
8
11
  QwenWriter,
9
12
  buildPersonas,
10
13
  resolveWriters
11
- } from "./chunk-C62CM2KR.mjs";
14
+ } from "./chunk-PDPF4T5Z.mjs";
15
+
16
+ // src/agents/devlead.ts
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+
20
+ // src/agents/reviewers/design.checker.ts
21
+ var DesignChecker = class {
22
+ /**
23
+ * @param diff Set of changed files.
24
+ * @param implDoc Content of docs/ai/implementation/feature-{name}.md.
25
+ * @param designDoc Content of docs/ai/design/feature-{name}.md.
26
+ */
27
+ check(diff, implDoc, designDoc) {
28
+ const findings = [];
29
+ const expectedComponents = this.extractComponents(designDoc);
30
+ const expectedInterfaces = this.extractInterfaces(designDoc);
31
+ for (const file of diff.files) {
32
+ findings.push(
33
+ ...this.checkComponentPresence(file.filePath, file.after, expectedComponents),
34
+ ...this.checkInterfaceConformance(file.filePath, file.after, expectedInterfaces),
35
+ ...this.checkNoNewUnlistedExports(file.filePath, file.before, file.after, expectedComponents)
36
+ );
37
+ }
38
+ findings.push(...this.checkImplDocCoverage(implDoc, expectedComponents));
39
+ return findings;
40
+ }
41
+ /**
42
+ * Extract bold component names from the design doc Components table.
43
+ * Matches `**ComponentName**` patterns.
44
+ */
45
+ extractComponents(designDoc) {
46
+ const matches = [...designDoc.matchAll(/\*\*([A-Z][A-Za-z]+(?:Agent|Checker|Builder|Gate|Registry|Router|Store)?)\*\*/g)];
47
+ return [...new Set(matches.map((m) => m[1]))];
48
+ }
49
+ /**
50
+ * Extract interface/type names from the design doc API/Data-model sections.
51
+ * Matches `interface FooBar` or `type FooBar` patterns.
52
+ */
53
+ extractInterfaces(designDoc) {
54
+ const matches = [...designDoc.matchAll(/(?:interface|type)\s+([A-Z][A-Za-z]+)/g)];
55
+ return [...new Set(matches.map((m) => m[1]))];
56
+ }
57
+ checkComponentPresence(filePath, after, components) {
58
+ const findings = [];
59
+ const isComponentFile = /\.(ts|tsx)$/.test(filePath) && !filePath.includes(".test.");
60
+ if (!isComponentFile || components.length === 0) return findings;
61
+ const matchedAny = components.some((c) => after.includes(c));
62
+ if (!matchedAny) {
63
+ findings.push({
64
+ file: filePath,
65
+ line: null,
66
+ severity: "MAJOR",
67
+ category: "DESIGN",
68
+ owaspCategory: null,
69
+ message: `None of the expected design components (${components.slice(0, 3).join(", ")}\u2026) are referenced in this file. Verify this file is aligned with the approved design.`,
70
+ suggestion: "Cross-reference docs/ai/design/*.md and ensure this file implements a documented component."
71
+ });
72
+ }
73
+ return findings;
74
+ }
75
+ checkInterfaceConformance(filePath, after, interfaces) {
76
+ const findings = [];
77
+ if (!filePath.endsWith(".ts") || filePath.includes(".test.")) return findings;
78
+ const importedTypes = [...after.matchAll(/import\s+(?:type\s+)?{([^}]+)}/g)].flatMap((m) => m[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim())).filter(Boolean);
79
+ for (const imported of importedTypes) {
80
+ if (imported && /^[A-Z]/.test(imported) && imported.length > 1) {
81
+ const inDesign = interfaces.some(
82
+ (i) => i === imported || imported.startsWith(i) || i.startsWith(imported)
83
+ );
84
+ if (!inDesign && interfaces.length > 0) {
85
+ findings.push({
86
+ file: filePath,
87
+ line: null,
88
+ severity: "MINOR",
89
+ category: "DESIGN",
90
+ owaspCategory: null,
91
+ message: `Type "${imported}" is imported but not listed in the design doc interfaces. Confirm it is intentional or update the design doc.`,
92
+ suggestion: "Add the type to the Data Models section of the design doc, or remove it if unused."
93
+ });
94
+ }
95
+ }
96
+ }
97
+ return findings;
98
+ }
99
+ checkNoNewUnlistedExports(filePath, before, after, components) {
100
+ const findings = [];
101
+ if (!filePath.endsWith(".ts") || filePath.includes(".test.")) return findings;
102
+ const exportPattern = /export\s+(?:class|function|const|interface|type)\s+([A-Za-z]+)/g;
103
+ const beforeExports = new Set([...before.matchAll(exportPattern)].map((m) => m[1]));
104
+ const afterExports = [...after.matchAll(exportPattern)].map((m) => m[1]);
105
+ for (const exp of afterExports) {
106
+ if (!beforeExports.has(exp)) {
107
+ const inDesign = components.some(
108
+ (c) => c === exp || exp.includes(c) || c.includes(exp)
109
+ );
110
+ if (!inDesign && components.length > 0) {
111
+ findings.push({
112
+ file: filePath,
113
+ line: null,
114
+ severity: "MINOR",
115
+ category: "DESIGN",
116
+ owaspCategory: null,
117
+ message: `New export "${exp}" was not listed in the approved design doc. If this is intentional, update the design doc first.`,
118
+ suggestion: "Add this component/type to the Components or Data Models section of the design doc."
119
+ });
120
+ }
121
+ }
122
+ }
123
+ return findings;
124
+ }
125
+ checkImplDocCoverage(implDoc, components) {
126
+ const findings = [];
127
+ if (!implDoc.trim() || components.length === 0) return findings;
128
+ const missing = components.filter((c) => !implDoc.includes(c));
129
+ if (missing.length > 0) {
130
+ findings.push({
131
+ file: "docs/ai/implementation/",
132
+ line: null,
133
+ severity: "MINOR",
134
+ category: "DESIGN",
135
+ owaspCategory: null,
136
+ message: `Implementation doc does not mention design component(s): ${missing.join(", ")}.`,
137
+ suggestion: "Update docs/ai/implementation/feature-{name}.md to cover all design components."
138
+ });
139
+ }
140
+ return findings;
141
+ }
142
+ };
143
+
144
+ // src/agents/reviewers/quality.checker.ts
145
+ var _QualityChecker = class _QualityChecker {
146
+ /**
147
+ * @param diff Set of changed files to analyse.
148
+ */
149
+ check(diff) {
150
+ const findings = [];
151
+ for (const file of diff.files) {
152
+ if (!file.filePath.endsWith(".ts") && !file.filePath.endsWith(".tsx")) continue;
153
+ if (file.filePath.includes(".test.") || file.filePath.includes(".spec.")) continue;
154
+ findings.push(
155
+ ...this.checkAnyUsage(file.filePath, file.after),
156
+ ...this.checkFunctionLength(file.filePath, file.after),
157
+ ...this.checkMissingReturnTypes(file.filePath, file.after),
158
+ ...this.checkTodoComments(file.filePath, file.after),
159
+ ...this.checkConsoleStatements(file.filePath, file.after),
160
+ ...this.checkNonNullAssertions(file.filePath, file.after)
161
+ );
162
+ }
163
+ return findings;
164
+ }
165
+ checkAnyUsage(filePath, content) {
166
+ const findings = [];
167
+ const lines = content.split("\n");
168
+ lines.forEach((line, idx) => {
169
+ if (/(?::\s*any\b|as\s+any\b)/.test(line) && !line.trimStart().startsWith("//")) {
170
+ findings.push({
171
+ file: filePath,
172
+ line: idx + 1,
173
+ severity: "MAJOR",
174
+ category: "QUALITY",
175
+ owaspCategory: null,
176
+ message: "`any` type used \u2014 disables TypeScript type safety.",
177
+ suggestion: "Replace `any` with a specific type or `unknown`. Use type guards where needed."
178
+ });
179
+ }
180
+ });
181
+ return findings;
182
+ }
183
+ checkFunctionLength(filePath, content) {
184
+ const findings = [];
185
+ const lines = content.split("\n");
186
+ let functionStart = -1;
187
+ let braceDepth = 0;
188
+ let inFunction = false;
189
+ for (let i = 0; i < lines.length; i++) {
190
+ const line = lines[i];
191
+ const isFunctionDecl = /(?:function\s+\w+|(?:public|private|protected|async)\s+\w+\s*\(|(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\()/.test(line) && line.includes("{");
192
+ if (isFunctionDecl && !inFunction) {
193
+ functionStart = i;
194
+ braceDepth = (line.match(/{/g) ?? []).length - (line.match(/}/g) ?? []).length;
195
+ inFunction = braceDepth > 0;
196
+ continue;
197
+ }
198
+ if (inFunction) {
199
+ braceDepth += (line.match(/{/g) ?? []).length - (line.match(/}/g) ?? []).length;
200
+ if (braceDepth <= 0) {
201
+ const length = i - functionStart + 1;
202
+ if (length > _QualityChecker.MAX_FUNCTION_LINES) {
203
+ findings.push({
204
+ file: filePath,
205
+ line: functionStart + 1,
206
+ severity: "MINOR",
207
+ category: "QUALITY",
208
+ owaspCategory: null,
209
+ message: `Function starting at line ${functionStart + 1} is ${length} lines long (limit: ${_QualityChecker.MAX_FUNCTION_LINES}).`,
210
+ suggestion: "Extract sub-logic into smaller, single-responsibility helper functions."
211
+ });
212
+ }
213
+ inFunction = false;
214
+ functionStart = -1;
215
+ }
216
+ }
217
+ }
218
+ return findings;
219
+ }
220
+ checkMissingReturnTypes(filePath, content) {
221
+ const findings = [];
222
+ const lines = content.split("\n");
223
+ lines.forEach((line, idx) => {
224
+ if (/(?:export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*{|(?:public|private|protected)\s+(?:async\s+)?\w+\s*\([^)]*\)\s*{)/.test(line) && !line.includes(":")) {
225
+ findings.push({
226
+ file: filePath,
227
+ line: idx + 1,
228
+ severity: "MINOR",
229
+ category: "QUALITY",
230
+ owaspCategory: null,
231
+ message: "Function/method appears to be missing an explicit return type annotation.",
232
+ suggestion: "Add an explicit return type (e.g., `: void`, `: string`, `: Promise<T>`) for clarity and type-safety."
233
+ });
234
+ }
235
+ });
236
+ return findings;
237
+ }
238
+ checkTodoComments(filePath, content) {
239
+ const findings = [];
240
+ const lines = content.split("\n");
241
+ lines.forEach((line, idx) => {
242
+ if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line)) {
243
+ findings.push({
244
+ file: filePath,
245
+ line: idx + 1,
246
+ severity: "MINOR",
247
+ category: "QUALITY",
248
+ owaspCategory: null,
249
+ message: `Unresolved ${(line.match(/\b(TODO|FIXME|HACK|XXX)\b/) ?? [])[0]} comment left in code.`,
250
+ suggestion: "Resolve or track this in the issue tracker, then remove the inline comment."
251
+ });
252
+ }
253
+ });
254
+ return findings;
255
+ }
256
+ checkConsoleStatements(filePath, content) {
257
+ const findings = [];
258
+ const lines = content.split("\n");
259
+ lines.forEach((line, idx) => {
260
+ if (/\bconsole\.(log|debug|warn|error|info)\b/.test(line) && !line.trimStart().startsWith("//")) {
261
+ findings.push({
262
+ file: filePath,
263
+ line: idx + 1,
264
+ severity: "MINOR",
265
+ category: "QUALITY",
266
+ owaspCategory: null,
267
+ message: "`console.*` statement found in production code.",
268
+ suggestion: "Use the project Logger utility instead of console.* for structured, level-aware logging."
269
+ });
270
+ }
271
+ });
272
+ return findings;
273
+ }
274
+ checkNonNullAssertions(filePath, content) {
275
+ const findings = [];
276
+ const lines = content.split("\n");
277
+ lines.forEach((line, idx) => {
278
+ if (/[A-Za-z0-9_)}\]]\s*!\s*[.[(]/.test(line) && !line.trimStart().startsWith("//")) {
279
+ findings.push({
280
+ file: filePath,
281
+ line: idx + 1,
282
+ severity: "MINOR",
283
+ category: "QUALITY",
284
+ owaspCategory: null,
285
+ message: "Non-null assertion operator (`!`) used \u2014 may cause runtime errors if value is null/undefined.",
286
+ suggestion: "Add an explicit null-check or use optional chaining (`?.`) instead."
287
+ });
288
+ }
289
+ });
290
+ return findings;
291
+ }
292
+ };
293
+ _QualityChecker.MAX_FUNCTION_LINES = 50;
294
+ var QualityChecker = _QualityChecker;
295
+
296
+ // src/agents/reviewers/owasp.checker.ts
297
+ var OWASPChecker = class {
298
+ /**
299
+ * @param diff Set of changed files to analyse.
300
+ */
301
+ check(diff) {
302
+ const findings = [];
303
+ for (const file of diff.files) {
304
+ if (!file.filePath.endsWith(".ts") && !file.filePath.endsWith(".tsx") && !file.filePath.endsWith(".js")) continue;
305
+ findings.push(
306
+ ...this.checkHardcodedSecrets(file.filePath, file.after),
307
+ ...this.checkWeakCrypto(file.filePath, file.after),
308
+ ...this.checkInjection(file.filePath, file.after),
309
+ ...this.checkMissingAuthChecks(file.filePath, file.after),
310
+ ...this.checkSSRF(file.filePath, file.after),
311
+ ...this.checkInsecureConfigs(file.filePath, file.after)
312
+ );
313
+ }
314
+ return findings;
315
+ }
316
+ // ── A02 / A07 — Hardcoded secrets ────────────────────────────────────────
317
+ checkHardcodedSecrets(filePath, content) {
318
+ const findings = [];
319
+ const lines = content.split("\n");
320
+ const secretPatterns = [
321
+ { pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{4,}/i, label: "hardcoded password" },
322
+ { pattern: /(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*['"`][A-Za-z0-9+/=]{8,}/i, label: "hardcoded API key" },
323
+ { pattern: /(?:token|bearer)\s*[:=]\s*['"`][A-Za-z0-9._-]{16,}/i, label: "hardcoded token" },
324
+ { pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/, label: "embedded private key" }
325
+ ];
326
+ lines.forEach((line, idx) => {
327
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("*")) return;
328
+ for (const { pattern, label } of secretPatterns) {
329
+ if (pattern.test(line)) {
330
+ findings.push({
331
+ file: filePath,
332
+ line: idx + 1,
333
+ severity: "BLOCKER",
334
+ category: "OWASP",
335
+ owaspCategory: "A02 Cryptographic Failures / A07 Authentication Failures",
336
+ message: `Possible ${label} detected. Hardcoded secrets must never appear in source code.`,
337
+ suggestion: "Move secrets to environment variables and access them via `process.env`. Never commit credentials."
338
+ });
339
+ }
340
+ }
341
+ });
342
+ return findings;
343
+ }
344
+ // ── A02 — Weak cryptography ───────────────────────────────────────────────
345
+ checkWeakCrypto(filePath, content) {
346
+ const findings = [];
347
+ const lines = content.split("\n");
348
+ const weakAlgos = [
349
+ { pattern: /\bMD5\b/i, label: "MD5" },
350
+ { pattern: /\bSHA1\b|['"`]sha1['"`]/i, label: "SHA-1" },
351
+ { pattern: /createCipher\b/, label: "createCipher (deprecated \u2014 use createCipheriv)" },
352
+ { pattern: /Math\.random\(\)/, label: "Math.random() for security-sensitive context" }
353
+ ];
354
+ lines.forEach((line, idx) => {
355
+ if (line.trimStart().startsWith("//")) return;
356
+ for (const { pattern, label } of weakAlgos) {
357
+ if (pattern.test(line)) {
358
+ findings.push({
359
+ file: filePath,
360
+ line: idx + 1,
361
+ severity: "BLOCKER",
362
+ category: "OWASP",
363
+ owaspCategory: "A02 Cryptographic Failures",
364
+ message: `Weak or deprecated cryptographic function/algorithm used: ${label}.`,
365
+ suggestion: "Use crypto.randomBytes() for randomness, SHA-256+ for hashing, and AES-GCM for encryption."
366
+ });
367
+ }
368
+ }
369
+ });
370
+ return findings;
371
+ }
372
+ // ── A03 — Injection ───────────────────────────────────────────────────────
373
+ checkInjection(filePath, content) {
374
+ const findings = [];
375
+ const lines = content.split("\n");
376
+ lines.forEach((line, idx) => {
377
+ if (line.trimStart().startsWith("//")) return;
378
+ if (/\beval\s*\(/.test(line)) {
379
+ findings.push({
380
+ file: filePath,
381
+ line: idx + 1,
382
+ severity: "BLOCKER",
383
+ category: "OWASP",
384
+ owaspCategory: "A03 Injection",
385
+ message: "`eval()` detected \u2014 executes arbitrary code and is a severe injection risk.",
386
+ suggestion: "Remove eval(). Use JSON.parse() for JSON, or restructure the logic to avoid dynamic code execution."
387
+ });
388
+ }
389
+ if (/new\s+Function\s*\(/.test(line)) {
390
+ findings.push({
391
+ file: filePath,
392
+ line: idx + 1,
393
+ severity: "BLOCKER",
394
+ category: "OWASP",
395
+ owaspCategory: "A03 Injection",
396
+ message: "`new Function(...)` constructs and executes code from a string \u2014 injection risk.",
397
+ suggestion: "Refactor to avoid dynamic code construction."
398
+ });
399
+ }
400
+ if (/(?:SELECT|INSERT|UPDATE|DELETE|DROP)\s+.+\s*\+\s*(?:req|input|params|body|query)/i.test(line)) {
401
+ findings.push({
402
+ file: filePath,
403
+ line: idx + 1,
404
+ severity: "BLOCKER",
405
+ category: "OWASP",
406
+ owaspCategory: "A03 Injection",
407
+ message: "Possible SQL injection: user-controlled input appears to be concatenated into an SQL string.",
408
+ suggestion: "Use parameterised queries or a prepared statement library. Never interpolate user input into SQL."
409
+ });
410
+ }
411
+ if (/\.innerHTML\s*=/.test(line)) {
412
+ findings.push({
413
+ file: filePath,
414
+ line: idx + 1,
415
+ severity: "MAJOR",
416
+ category: "OWASP",
417
+ owaspCategory: "A03 Injection",
418
+ message: "`innerHTML` assignment may enable cross-site scripting (XSS) if the value contains user input.",
419
+ suggestion: "Use `textContent` for plain text, or sanitise HTML with a trusted library (e.g., DOMPurify)."
420
+ });
421
+ }
422
+ if (/(?:exec|spawn|execSync|spawnSync)\s*\([^)]*(?:req|input|params|body|query)/.test(line)) {
423
+ findings.push({
424
+ file: filePath,
425
+ line: idx + 1,
426
+ severity: "BLOCKER",
427
+ category: "OWASP",
428
+ owaspCategory: "A03 Injection",
429
+ message: "Possible command injection: user-controlled input passed to a shell execution function.",
430
+ suggestion: "Never pass user input to exec/spawn. Use an allowlist of accepted commands. Prefer child_process.execFile with explicit args."
431
+ });
432
+ }
433
+ });
434
+ return findings;
435
+ }
436
+ // ── A07 — Authentication failures ────────────────────────────────────────
437
+ checkMissingAuthChecks(filePath, content) {
438
+ const findings = [];
439
+ const routePattern = /(?:app|router)\s*\.(?:get|post|put|patch|delete)\s*\(/g;
440
+ const hasAuth = /(?:authenticate|authorize|isAuth|verifyToken|requireAuth|middleware)/i.test(content);
441
+ if (routePattern.test(content) && !hasAuth) {
442
+ findings.push({
443
+ file: filePath,
444
+ line: null,
445
+ severity: "MAJOR",
446
+ category: "OWASP",
447
+ owaspCategory: "A07 Identification and Authentication Failures",
448
+ message: "HTTP route handlers found with no apparent authentication middleware. Public routes may expose protected resources.",
449
+ suggestion: "Add authentication/authorisation middleware to all routes that handle sensitive data or privileged operations."
450
+ });
451
+ }
452
+ return findings;
453
+ }
454
+ // ── A10 — SSRF ────────────────────────────────────────────────────────────
455
+ checkSSRF(filePath, content) {
456
+ const findings = [];
457
+ const lines = content.split("\n");
458
+ lines.forEach((line, idx) => {
459
+ if (line.trimStart().startsWith("//")) return;
460
+ if (/(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\s*\(\s*(?:[A-Za-z_$][A-Za-z0-9_$]*(?:\s*\+|\s*`|\[))/.test(line)) {
461
+ findings.push({
462
+ file: filePath,
463
+ line: idx + 1,
464
+ severity: "MAJOR",
465
+ category: "OWASP",
466
+ owaspCategory: "A10 Server-Side Request Forgery (SSRF)",
467
+ message: "HTTP request to a dynamic URL \u2014 if the URL is derived from user input, this may be a SSRF vector.",
468
+ suggestion: "Validate and restrict allowed URL targets via an allowlist. Never forward raw user-supplied URLs to server-side HTTP calls."
469
+ });
470
+ }
471
+ });
472
+ return findings;
473
+ }
474
+ // ── A05 — Security misconfiguration ──────────────────────────────────────
475
+ checkInsecureConfigs(filePath, content) {
476
+ const findings = [];
477
+ const lines = content.split("\n");
478
+ lines.forEach((line, idx) => {
479
+ if (line.trimStart().startsWith("//")) return;
480
+ if (/cors\s*\(\s*\{\s*origin\s*:\s*['"`]\*['"`]/.test(line)) {
481
+ findings.push({
482
+ file: filePath,
483
+ line: idx + 1,
484
+ severity: "MAJOR",
485
+ category: "OWASP",
486
+ owaspCategory: "A05 Security Misconfiguration",
487
+ message: "CORS configured with a wildcard origin (`*`) \u2014 allows any domain to access the API.",
488
+ suggestion: "Restrict CORS origins to a specific list of trusted domains."
489
+ });
490
+ }
491
+ if (/rejectUnauthorized\s*:\s*false/.test(line)) {
492
+ findings.push({
493
+ file: filePath,
494
+ line: idx + 1,
495
+ severity: "BLOCKER",
496
+ category: "OWASP",
497
+ owaspCategory: "A05 Security Misconfiguration",
498
+ message: "`rejectUnauthorized: false` disables TLS certificate validation \u2014 enables MITM attacks.",
499
+ suggestion: "Remove `rejectUnauthorized: false`. Use a properly trusted certificate chain."
500
+ });
501
+ }
502
+ });
503
+ return findings;
504
+ }
505
+ };
506
+
507
+ // src/agents/reviewers/report.builder.ts
508
+ var ReportBuilder = class {
509
+ /**
510
+ * @param featureName Kebab-case feature name.
511
+ * @param findings All findings from all checkers.
512
+ */
513
+ build(featureName, findings) {
514
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
515
+ const hasBlocker = findings.some((f) => f.severity === "BLOCKER");
516
+ const outcome = hasBlocker ? "CHANGES_REQUESTED" : "PASS";
517
+ return {
518
+ featureName,
519
+ timestamp,
520
+ outcome,
521
+ findings,
522
+ summary: this.buildSummary(featureName, findings, outcome)
523
+ };
524
+ }
525
+ buildSummary(featureName, findings, outcome) {
526
+ const blockers = findings.filter((f) => f.severity === "BLOCKER").length;
527
+ const majors = findings.filter((f) => f.severity === "MAJOR").length;
528
+ const minors = findings.filter((f) => f.severity === "MINOR").length;
529
+ const total = findings.length;
530
+ const byCategory = {
531
+ DESIGN: findings.filter((f) => f.category === "DESIGN").length,
532
+ QUALITY: findings.filter((f) => f.category === "QUALITY").length,
533
+ OWASP: findings.filter((f) => f.category === "OWASP").length
534
+ };
535
+ const lines = [
536
+ `## Dev Lead Review \u2014 ${featureName}`,
537
+ "",
538
+ `**Outcome**: ${outcome === "PASS" ? "\u2705 PASS" : "\u274C CHANGES REQUESTED"}`,
539
+ `**Timestamp**: ${(/* @__PURE__ */ new Date()).toUTCString()}`,
540
+ "",
541
+ "### Finding Summary",
542
+ `| Severity | Count |`,
543
+ `|---|---|`,
544
+ `| BLOCKER | ${blockers} |`,
545
+ `| MAJOR | ${majors} |`,
546
+ `| MINOR | ${minors} |`,
547
+ `| **Total**| **${total}** |`,
548
+ "",
549
+ "### By Category",
550
+ `| Category | Count |`,
551
+ `|---|---|`,
552
+ `| Design Adherence | ${byCategory.DESIGN} |`,
553
+ `| Code Quality | ${byCategory.QUALITY} |`,
554
+ `| OWASP Security | ${byCategory.OWASP} |`
555
+ ];
556
+ if (outcome === "CHANGES_REQUESTED") {
557
+ lines.push(
558
+ "",
559
+ "### \u26D4 Blockers \u2014 must be resolved before advancing to Tester",
560
+ ...findings.filter((f) => f.severity === "BLOCKER").map((f) => `- **${f.file}${f.line ? `:${f.line}` : ""}** [${f.owaspCategory ?? f.category}]: ${f.message}`)
561
+ );
562
+ }
563
+ if (outcome === "PASS" && total === 0) {
564
+ lines.push("", "> All checks passed with no findings. Ready for Tester.");
565
+ }
566
+ return lines.join("\n");
567
+ }
568
+ };
569
+
570
+ // src/agents/reviewers/confirmation.gate.ts
571
+ import * as readline from "readline";
572
+ var ConfirmationGate = class {
573
+ /**
574
+ * Display the report and wait for human confirmation.
575
+ *
576
+ * @param report The completed ReviewReport from the ReportBuilder.
577
+ * @returns The human's decision plus any rejection note.
578
+ */
579
+ async wait(report) {
580
+ this.printReport(report);
581
+ return this.promptUser();
582
+ }
583
+ printReport(report) {
584
+ const divider = "\u2500".repeat(72);
585
+ console.log(`
586
+ ${divider}`);
587
+ console.log(report.summary);
588
+ console.log(`
589
+ ${divider}`);
590
+ if (report.findings.length > 0) {
591
+ console.log("\n### All Findings\n");
592
+ for (const f of report.findings) {
593
+ const loc = f.line ? `:${f.line}` : "";
594
+ const owasp = f.owaspCategory ? ` [${f.owaspCategory}]` : "";
595
+ console.log(`[${f.severity}] ${f.category}${owasp}`);
596
+ console.log(` File: ${f.file}${loc}`);
597
+ console.log(` ${f.message}`);
598
+ if (f.suggestion) {
599
+ console.log(` \u2192 ${f.suggestion}`);
600
+ }
601
+ console.log();
602
+ }
603
+ }
604
+ console.log(`${divider}
605
+ `);
606
+ console.log("Please review the findings above and respond:");
607
+ console.log(" /approve \u2014 advance pipeline to Tester");
608
+ console.log(" /reject [note] \u2014 send back to Developer with this note\n");
609
+ }
610
+ promptUser() {
611
+ return new Promise((resolve) => {
612
+ const rl = readline.createInterface({
613
+ input: process.stdin,
614
+ output: process.stdout,
615
+ terminal: false
616
+ });
617
+ const ask = () => {
618
+ rl.question("> ", (answer) => {
619
+ const trimmed = answer.trim();
620
+ if (trimmed === "/approve") {
621
+ rl.close();
622
+ resolve({ decision: "approved" });
623
+ return;
624
+ }
625
+ if (trimmed.startsWith("/reject")) {
626
+ const note = trimmed.slice("/reject".length).trim();
627
+ rl.close();
628
+ resolve({ decision: "rejected", note });
629
+ return;
630
+ }
631
+ console.log("Unknown command. Use `/approve` or `/reject [note]`.");
632
+ ask();
633
+ });
634
+ };
635
+ ask();
636
+ });
637
+ }
638
+ };
639
+
640
+ // src/agents/devlead.ts
641
+ var DevLeadAgent = class {
642
+ constructor(designChecker, qualityChecker, owaspChecker, reportBuilder, confirmationGate) {
643
+ this.designChecker = designChecker ?? new DesignChecker();
644
+ this.qualityChecker = qualityChecker ?? new QualityChecker();
645
+ this.owaspChecker = owaspChecker ?? new OWASPChecker();
646
+ this.reportBuilder = reportBuilder ?? new ReportBuilder();
647
+ this.confirmationGate = confirmationGate ?? new ConfirmationGate();
648
+ }
649
+ /**
650
+ * Run a full review cycle for a feature's code changes.
651
+ *
652
+ * 1. Loads the design doc and implementation doc from disk.
653
+ * 2. Runs all three checkers against the diff.
654
+ * 3. Builds and persists the ReviewReport.
655
+ * 4. Presents the report to the human and waits for /approve or /reject.
656
+ *
657
+ * @returns The completed ReviewReport (includes human decision in calling context).
658
+ * @throws If required doc files cannot be read.
659
+ */
660
+ async review(context) {
661
+ const designDoc = this.readDoc(context.designDocPath);
662
+ const implDoc = this.readDoc(context.implementationDocPath);
663
+ const designFindings = this.designChecker.check(context.diff, implDoc, designDoc);
664
+ const qualityFindings = this.qualityChecker.check(context.diff);
665
+ const owaspFindings = this.owaspChecker.check(context.diff);
666
+ const allFindings = [...designFindings, ...qualityFindings, ...owaspFindings];
667
+ const report = this.reportBuilder.build(context.featureName, allFindings);
668
+ this.persistReport(report, context);
669
+ await this.confirmationGate.wait(report);
670
+ return report;
671
+ }
672
+ readDoc(docPath) {
673
+ try {
674
+ return fs.readFileSync(docPath, "utf-8");
675
+ } catch {
676
+ return "";
677
+ }
678
+ }
679
+ persistReport(report, context) {
680
+ const ts = report.timestamp.replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
681
+ const reviewDir = path.join(
682
+ path.dirname(path.dirname(context.designDocPath)),
683
+ "review"
684
+ );
685
+ try {
686
+ fs.mkdirSync(reviewDir, { recursive: true });
687
+ const reportPath = path.join(reviewDir, `feature-${context.featureName}-${ts}.md`);
688
+ fs.writeFileSync(reportPath, report.summary, "utf-8");
689
+ } catch {
690
+ console.warn(`[DevLeadAgent] Could not persist review report to ${reviewDir}`);
691
+ }
692
+ }
693
+ };
12
694
  export {
695
+ AgentStatus,
13
696
  AntigravityWriter,
697
+ BaseAgent,
14
698
  ConfigGenerator,
15
699
  CopilotWriter,
16
700
  CursorWriter,
17
701
  DEFAULT_PERSONAS,
702
+ DevLeadAgent,
18
703
  Logger,
704
+ MasterAgent,
19
705
  QwenWriter,
20
706
  buildPersonas,
21
707
  resolveWriters