@kevinrabun/judges 3.113.0 → 3.114.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/README.md +9 -0
- package/agents/accessibility.judge.md +37 -0
- package/agents/agent-instructions.judge.md +37 -0
- package/agents/ai-code-safety.judge.md +48 -0
- package/agents/api-contract.judge.md +30 -0
- package/agents/api-design.judge.md +39 -0
- package/agents/authentication.judge.md +37 -0
- package/agents/backwards-compatibility.judge.md +37 -0
- package/agents/caching.judge.md +37 -0
- package/agents/ci-cd.judge.md +37 -0
- package/agents/cloud-readiness.judge.md +37 -0
- package/agents/code-structure.judge.md +48 -0
- package/agents/compliance.judge.md +40 -0
- package/agents/concurrency.judge.md +39 -0
- package/agents/configuration-management.judge.md +37 -0
- package/agents/cost-effectiveness.judge.md +40 -0
- package/agents/cybersecurity.judge.md +36 -0
- package/agents/data-security.judge.md +34 -0
- package/agents/data-sovereignty.judge.md +58 -0
- package/agents/database.judge.md +41 -0
- package/agents/dependency-health.judge.md +39 -0
- package/agents/documentation.judge.md +39 -0
- package/agents/error-handling.judge.md +37 -0
- package/agents/ethics-bias.judge.md +39 -0
- package/agents/false-positive-review.judge.md +73 -0
- package/agents/framework-safety.judge.md +40 -0
- package/agents/hallucination-detection.judge.md +33 -0
- package/agents/iac-security.judge.md +38 -0
- package/agents/intent-alignment.judge.md +31 -0
- package/agents/internationalization.judge.md +42 -0
- package/agents/logging-privacy.judge.md +37 -0
- package/agents/logic-review.judge.md +34 -0
- package/agents/maintainability.judge.md +37 -0
- package/agents/model-fingerprint.judge.md +31 -0
- package/agents/multi-turn-coherence.judge.md +29 -0
- package/agents/observability.judge.md +37 -0
- package/agents/over-engineering.judge.md +48 -0
- package/agents/performance.judge.md +44 -0
- package/agents/portability.judge.md +37 -0
- package/agents/rate-limiting.judge.md +37 -0
- package/agents/reliability.judge.md +39 -0
- package/agents/scalability.judge.md +41 -0
- package/agents/security.judge.md +31 -0
- package/agents/software-practices.judge.md +44 -0
- package/agents/testing.judge.md +39 -0
- package/agents/ux.judge.md +37 -0
- package/dist/api.d.ts +9 -1
- package/dist/api.js +9 -1
- package/dist/commands/fix.d.ts +10 -0
- package/dist/commands/fix.js +52 -0
- package/dist/commands/llm-benchmark.d.ts +13 -4
- package/dist/commands/llm-benchmark.js +39 -8
- package/dist/commands/review.d.ts +51 -1
- package/dist/commands/review.js +213 -7
- package/dist/evaluators/index.js +61 -35
- package/dist/github-app.d.ts +35 -0
- package/dist/github-app.js +125 -4
- package/dist/judges/index.d.ts +23 -61
- package/dist/judges/index.js +49 -63
- package/dist/patches/apply.d.ts +15 -0
- package/dist/patches/apply.js +37 -0
- package/dist/tools/prompts.d.ts +2 -2
- package/dist/tools/prompts.js +21 -10
- package/docs/skills.md +7 -0
- package/package.json +15 -3
- package/scripts/generate-agents-from-judges.ts +111 -0
- package/scripts/generate-skills-docs.ts +26 -0
- package/scripts/validate-agents.ts +104 -0
- package/server.json +2 -2
- package/skills/ai-code-review.skill.md +57 -0
- package/skills/release-gate.skill.md +27 -0
- package/skills/security-review.skill.md +32 -0
- package/src/agent-loader.ts +324 -0
- package/src/skill-loader.ts +199 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Generate `.judge.md` files for all existing judges registered in the default registry.
|
|
4
|
+
* (Legacy `.agent.md` is still supported for reading.)
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx tsx scripts/generate-agents-from-judges.ts [--force]
|
|
8
|
+
*/
|
|
9
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
10
|
+
import { join, dirname, relative } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
import { defaultRegistry } from "../src/judge-registry.js";
|
|
14
|
+
import { loadJudges } from "../src/judges/index.js";
|
|
15
|
+
import type { JudgeDefinition } from "../src/types.js";
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
|
|
21
|
+
const AGENTS_DIR = join(__dirname, "..", "agents");
|
|
22
|
+
|
|
23
|
+
function ensureDir(p: string) {
|
|
24
|
+
if (!existsSync(p)) mkdirSync(p, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// naive heuristic to map judge id -> evaluator file path
|
|
28
|
+
const evaluatorPathFor = (judge: JudgeDefinition): string | undefined => {
|
|
29
|
+
// some judge IDs use dashes; evaluator files are the same id with .ts
|
|
30
|
+
// special cases can be added here if any diverge
|
|
31
|
+
const candidate = join(__dirname, "..", "src", "evaluators", `${judge.id}.ts`);
|
|
32
|
+
if (existsSync(candidate)) return relative(AGENTS_DIR, candidate).replace(/\\/g, "/");
|
|
33
|
+
|
|
34
|
+
// fallback: try rulePrefix lowercased? or analyze fn name
|
|
35
|
+
const analyzeFnName = judge.analyze?.name;
|
|
36
|
+
if (analyzeFnName?.startsWith("analyze")) {
|
|
37
|
+
const inferred = analyzeFnName
|
|
38
|
+
.replace(/^analyze/, "")
|
|
39
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
const fallback = join(__dirname, "..", "src", "evaluators", `${inferred}.ts`);
|
|
42
|
+
if (existsSync(fallback)) return relative(AGENTS_DIR, fallback).replace(/\\/g, "/");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function toYamlFrontmatter(judge: JudgeDefinition): string {
|
|
49
|
+
const lines: string[] = ["---"];
|
|
50
|
+
const fields: Record<string, string | number | undefined> = {
|
|
51
|
+
id: judge.id,
|
|
52
|
+
name: judge.name,
|
|
53
|
+
domain: judge.domain,
|
|
54
|
+
rulePrefix: judge.rulePrefix,
|
|
55
|
+
description: judge.description,
|
|
56
|
+
tableDescription: judge.tableDescription,
|
|
57
|
+
promptDescription: judge.promptDescription,
|
|
58
|
+
script: evaluatorPathFor(judge),
|
|
59
|
+
priority: judge.id === "false-positive-review" ? 999 : judge.id === "tribunal" ? 1000 : 10,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
63
|
+
if (value === undefined || value === null || value === "") continue;
|
|
64
|
+
// quote values that contain ':' or '#'
|
|
65
|
+
const needsQuotes = typeof value === "string" && /[:#]/.test(value);
|
|
66
|
+
lines.push(`${key}: ${needsQuotes ? JSON.stringify(value) : value}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("---", "");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizePrompt(prompt: string): string {
|
|
73
|
+
// Existing prompts are plain strings; some use backticks. Preserve formatting.
|
|
74
|
+
// Trim leading/trailing whitespace but retain internal newlines.
|
|
75
|
+
return prompt.replace(/^\s+|\s+$/g, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
await loadJudges(); // ensure all TS judges registered
|
|
80
|
+
const judges = defaultRegistry.getJudges();
|
|
81
|
+
ensureDir(AGENTS_DIR);
|
|
82
|
+
|
|
83
|
+
const results: { id: string; path: string; skipped: boolean; reason?: string }[] = [];
|
|
84
|
+
|
|
85
|
+
for (const judge of judges) {
|
|
86
|
+
const targetPath = join(AGENTS_DIR, `${judge.id}.judge.md`);
|
|
87
|
+
|
|
88
|
+
if (!FORCE && existsSync(targetPath)) {
|
|
89
|
+
results.push({ id: judge.id, path: targetPath, skipped: true, reason: "exists" });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fm = toYamlFrontmatter(judge);
|
|
94
|
+
const body = normalizePrompt(judge.systemPrompt ?? "");
|
|
95
|
+
const content = `${fm}${body}\n`;
|
|
96
|
+
|
|
97
|
+
ensureDir(dirname(targetPath));
|
|
98
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
99
|
+
results.push({ id: judge.id, path: targetPath, skipped: false });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("Generated agent files:");
|
|
103
|
+
for (const r of results) {
|
|
104
|
+
console.log(`- ${r.id}: ${r.path}${r.skipped ? ` (skipped: ${r.reason})` : ""}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main().catch((err) => {
|
|
109
|
+
console.error(err);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { listSkills } from "../src/skill-loader.js";
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const skillsDir = join(process.cwd(), "skills");
|
|
8
|
+
const skills = listSkills(skillsDir);
|
|
9
|
+
const lines: string[] = [];
|
|
10
|
+
lines.push(`# Skills Catalog`);
|
|
11
|
+
lines.push("");
|
|
12
|
+
lines.push("| ID | Name | Description | Tags | Agents |");
|
|
13
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
14
|
+
for (const s of skills) {
|
|
15
|
+
lines.push(`| ${s.id} | ${s.name} | ${s.description} | ${(s.tags || []).join(", ")} | ${s.agents.join(", ")} |`);
|
|
16
|
+
}
|
|
17
|
+
lines.push("");
|
|
18
|
+
const outPath = join(process.cwd(), "docs", "skills.md");
|
|
19
|
+
writeFileSync(outPath, lines.join("\n"), "utf-8");
|
|
20
|
+
console.log(`Wrote skills catalog to ${outPath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch((err) => {
|
|
24
|
+
console.error(err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Validate that generated `.judge.md` files round-trip with the current registry.
|
|
4
|
+
* (Legacy `.agent.md` is still accepted for backward compatibility.)
|
|
5
|
+
* Fails with exit code 1 if mismatches are found.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { parseAgentFile, agentToJudgeDefinition } from "../src/agent-loader.js";
|
|
10
|
+
import { loadJudges } from "../src/judges/index.js";
|
|
11
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
function stripWhitespace(s?: string) {
|
|
14
|
+
return (s ?? "").trim().replace(/\s+/g, " ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List agent files in a directory. Prefers `.judge.md`, but accepts legacy
|
|
19
|
+
* `.agent.md` for backward compatibility.
|
|
20
|
+
*/
|
|
21
|
+
export function listAgentFiles(agentsDir: string): string[] {
|
|
22
|
+
if (!existsSync(agentsDir)) {
|
|
23
|
+
throw new Error(`agents directory not found: ${agentsDir}`);
|
|
24
|
+
}
|
|
25
|
+
const files = readdirSync(agentsDir).filter((f) => f.endsWith(".judge.md") || f.endsWith(".agent.md"));
|
|
26
|
+
if (files.length === 0) {
|
|
27
|
+
throw new Error(`No agent files found in ${agentsDir}. Expected .judge.md files (legacy .agent.md tolerated).`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Sort with .judge.md first (canonical)
|
|
31
|
+
files.sort((a, b) => {
|
|
32
|
+
const aLegacy = a.endsWith(".agent.md");
|
|
33
|
+
const bLegacy = b.endsWith(".agent.md");
|
|
34
|
+
if (aLegacy === bLegacy) return a.localeCompare(b);
|
|
35
|
+
return aLegacy ? 1 : -1; // prefer .judge.md
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate agent files against the current registry. Useful in CI.
|
|
43
|
+
* @param agentsDir Directory containing `.judge.md` agent files
|
|
44
|
+
*/
|
|
45
|
+
export async function validateAgents(
|
|
46
|
+
agentsDir: string = join(process.cwd(), "agents"),
|
|
47
|
+
): Promise<{ filesChecked: number }> {
|
|
48
|
+
const judges = await loadJudges();
|
|
49
|
+
const map = new Map(judges.map((j) => [j.id, j]));
|
|
50
|
+
const missing: string[] = [];
|
|
51
|
+
const mismatches: string[] = [];
|
|
52
|
+
|
|
53
|
+
const files = listAgentFiles(agentsDir);
|
|
54
|
+
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
const agent = parseAgentFile(join(agentsDir, file));
|
|
57
|
+
const judge = map.get(agent.frontmatter.id);
|
|
58
|
+
if (!judge) {
|
|
59
|
+
missing.push(agent.frontmatter.id);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const converted = agentToJudgeDefinition(agent);
|
|
63
|
+
const fields: (keyof typeof judge)[] = [
|
|
64
|
+
"id",
|
|
65
|
+
"name",
|
|
66
|
+
"domain",
|
|
67
|
+
"rulePrefix",
|
|
68
|
+
"description",
|
|
69
|
+
"tableDescription",
|
|
70
|
+
"promptDescription",
|
|
71
|
+
];
|
|
72
|
+
const convertedRecord = converted as unknown as Record<string, unknown>;
|
|
73
|
+
const judgeRecord = judge as unknown as Record<string, unknown>;
|
|
74
|
+
for (const field of fields) {
|
|
75
|
+
if (convertedRecord[field as string] !== judgeRecord[field as string]) {
|
|
76
|
+
mismatches.push(`${agent.frontmatter.id}: field ${String(field)} mismatch`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (stripWhitespace(converted.systemPrompt) !== stripWhitespace(judge.systemPrompt)) {
|
|
80
|
+
mismatches.push(`${agent.frontmatter.id}: systemPrompt mismatch`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (missing.length || mismatches.length) {
|
|
85
|
+
const errors: string[] = [];
|
|
86
|
+
if (missing.length) errors.push(`Missing judges in agent files: ${missing.join(", ")}`);
|
|
87
|
+
if (mismatches.length) errors.push(`Mismatches: \n- ${mismatches.join("\n- ")}`);
|
|
88
|
+
throw new Error(errors.join("\n"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { filesChecked: files.length };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
const { filesChecked } = await validateAgents();
|
|
96
|
+
console.log(`Agent files validated against registry (${filesChecked} agents).`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
100
|
+
main().catch((err) => {
|
|
101
|
+
console.error(err);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
}
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/kevinrabun/judges",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "3.
|
|
10
|
+
"version": "3.114.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@kevinrabun/judges",
|
|
15
|
-
"version": "3.
|
|
15
|
+
"version": "3.114.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ai-code-review
|
|
3
|
+
name: AI Code Review Skill
|
|
4
|
+
description: "Full-spectrum AI-generated code review using the Judges Panel, tuned for minimizing false positives and focusing on AI-specific failure modes."
|
|
5
|
+
tags: [ai-code, code-review, tribunal]
|
|
6
|
+
agents:
|
|
7
|
+
- ai-code-safety
|
|
8
|
+
- hallucination-detection
|
|
9
|
+
- logic-review
|
|
10
|
+
- over-engineering
|
|
11
|
+
- code-structure
|
|
12
|
+
- maintainability
|
|
13
|
+
- performance
|
|
14
|
+
- reliability
|
|
15
|
+
- cybersecurity
|
|
16
|
+
- data-security
|
|
17
|
+
- authentication
|
|
18
|
+
- api-design
|
|
19
|
+
- api-contract
|
|
20
|
+
- database
|
|
21
|
+
- caching
|
|
22
|
+
- observability
|
|
23
|
+
- logging-privacy
|
|
24
|
+
- configuration-management
|
|
25
|
+
- dependency-health
|
|
26
|
+
- framework-safety
|
|
27
|
+
- testing
|
|
28
|
+
- ci-cd
|
|
29
|
+
- intent-alignment
|
|
30
|
+
- multi-turn-coherence
|
|
31
|
+
- model-fingerprint
|
|
32
|
+
- agent-instructions
|
|
33
|
+
- cloud-readiness
|
|
34
|
+
- cost-effectiveness
|
|
35
|
+
- ethics-bias
|
|
36
|
+
- accessibility
|
|
37
|
+
- internationalization
|
|
38
|
+
- data-sovereignty
|
|
39
|
+
- iac-security
|
|
40
|
+
- rate-limiting
|
|
41
|
+
- portability
|
|
42
|
+
- ux
|
|
43
|
+
- backwards-compatibility
|
|
44
|
+
- security
|
|
45
|
+
- false-positive-review
|
|
46
|
+
priority: 10
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
You are the AI Code Review Skill. Your job is to orchestrate the Judges Panel to review AI-generated code safely and reliably for production deployment.
|
|
50
|
+
|
|
51
|
+
## Orchestration Guidance
|
|
52
|
+
- Run the listed judges in parallel; aggregate findings.
|
|
53
|
+
- Apply the **Precision Mandate** and **False Positive Cost** guidance; default to "no finding" unless evidence is clear.
|
|
54
|
+
- Highlight AI-specific risks: hallucinated APIs, insecure defaults, missing validation, under-specified logic, misaligned intent.
|
|
55
|
+
- For conflicting guidance, prefer security, data, and safety judges over style-only advice.
|
|
56
|
+
- Summarize top 5 actionable findings with rule IDs and remediation steps.
|
|
57
|
+
- If code passes with zero findings, explicitly state coverage across security, data, auth, and error paths.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: release-gate
|
|
3
|
+
name: Release Gate Skill
|
|
4
|
+
description: "Pre-deploy release gate combining reliability, observability, CI/CD, and security checks."
|
|
5
|
+
tags: [release, sre, reliability, deployment]
|
|
6
|
+
agents:
|
|
7
|
+
- reliability
|
|
8
|
+
- observability
|
|
9
|
+
- performance
|
|
10
|
+
- ci-cd
|
|
11
|
+
- testing
|
|
12
|
+
- cloud-readiness
|
|
13
|
+
- cost-effectiveness
|
|
14
|
+
- security
|
|
15
|
+
- data-security
|
|
16
|
+
- cybersecurity
|
|
17
|
+
- false-positive-review
|
|
18
|
+
priority: 7
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
You are the Release Gate Skill. Act as the final reviewer before production deployment.
|
|
22
|
+
|
|
23
|
+
## Orchestration Guidance
|
|
24
|
+
- Verify health checks, structured logging, metrics/tracing coverage, circuit breakers.
|
|
25
|
+
- Confirm CI/CD checks exist and are enforced (tests, lint, vulnerability scans, IaC policies).
|
|
26
|
+
- Ensure rollback strategies and deployment safety (blue/green or canary where applicable).
|
|
27
|
+
- Provide a go/no-go recommendation with rationale and list of blocking findings.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: security-review
|
|
3
|
+
name: Security Review Skill
|
|
4
|
+
description: "Security-focused review for production readiness, covering AppSec, DataSec, AuthZ, and IaC."
|
|
5
|
+
tags: [security, appsec, datasec]
|
|
6
|
+
agents:
|
|
7
|
+
- cybersecurity
|
|
8
|
+
- data-security
|
|
9
|
+
- authentication
|
|
10
|
+
- logging-privacy
|
|
11
|
+
- api-contract
|
|
12
|
+
- database
|
|
13
|
+
- iac-security
|
|
14
|
+
- framework-safety
|
|
15
|
+
- dependency-health
|
|
16
|
+
- configuration-management
|
|
17
|
+
- rate-limiting
|
|
18
|
+
- compliance
|
|
19
|
+
- data-sovereignty
|
|
20
|
+
- security
|
|
21
|
+
- ai-code-safety
|
|
22
|
+
- false-positive-review
|
|
23
|
+
priority: 5
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
You are the Security Review Skill. Ensure safe-by-default deployment readiness.
|
|
27
|
+
|
|
28
|
+
## Orchestration Guidance
|
|
29
|
+
- Enforce OWASP Top 10, SLSA-style supply chain checks, and least privilege.
|
|
30
|
+
- Flag hardcoded secrets, missing auth, insecure transport, injection risks, and misconfigurations.
|
|
31
|
+
- Cross-check IaC templates for public exposure, missing encryption, and permissive IAM.
|
|
32
|
+
- Deduplicate findings across judges; prefer the most specific rule ID.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Markdown Loader — reads `.judge.md` files (legacy `.agent.md` also
|
|
3
|
+
* accepted) and converts them into JudgeDefinition objects that register with
|
|
4
|
+
* the unified JudgeRegistry.
|
|
5
|
+
*
|
|
6
|
+
* This is the bridge between the file-based agent paradigm and the existing
|
|
7
|
+
* TypeScript judge system. Agent files use YAML frontmatter for metadata
|
|
8
|
+
* and markdown body for the system prompt (persona + evaluation criteria).
|
|
9
|
+
*
|
|
10
|
+
* ## File Format
|
|
11
|
+
*
|
|
12
|
+
* ```markdown
|
|
13
|
+
* ---
|
|
14
|
+
* id: cybersecurity
|
|
15
|
+
* name: Judge Cybersecurity
|
|
16
|
+
* domain: Cybersecurity & Threat Defense
|
|
17
|
+
* rulePrefix: CYBER
|
|
18
|
+
* description: Evaluates code for vulnerability...
|
|
19
|
+
* tableDescription: "Injection attacks, XSS, CSRF, auth flaws"
|
|
20
|
+
* promptDescription: Deep cybersecurity review
|
|
21
|
+
* script: ../src/evaluators/cybersecurity.ts # optional
|
|
22
|
+
* priority: 10 # optional, default 10
|
|
23
|
+
* ---
|
|
24
|
+
*
|
|
25
|
+
* You are Judge Cybersecurity — a principal application security engineer...
|
|
26
|
+
*
|
|
27
|
+
* ## Evaluation Criteria
|
|
28
|
+
* ...
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* - `script` is a relative path to the evaluator module (must export a
|
|
32
|
+
* function matching `(code: string, language: string, context?) => Finding[]`).
|
|
33
|
+
* If omitted, the judge is LLM-only (no deterministic layer).
|
|
34
|
+
* - `priority` controls ordering. Higher = later. 999 is reserved for
|
|
35
|
+
* false-positive-review (always last). Default is 10.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
39
|
+
import { join, resolve, dirname } from "node:path";
|
|
40
|
+
import { createRequire } from "node:module";
|
|
41
|
+
import type { JudgeDefinition, Finding, AnalyzeContext } from "./types.js";
|
|
42
|
+
|
|
43
|
+
// ─── Frontmatter Types ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** Parsed YAML frontmatter from a `.judge.md` file (legacy `.agent.md`). */
|
|
46
|
+
export interface AgentFrontmatter {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
domain: string;
|
|
50
|
+
rulePrefix: string;
|
|
51
|
+
description: string;
|
|
52
|
+
tableDescription: string;
|
|
53
|
+
promptDescription: string;
|
|
54
|
+
script?: string;
|
|
55
|
+
priority?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A parsed agent file — metadata + the markdown body (system prompt). */
|
|
59
|
+
export interface ParsedAgent {
|
|
60
|
+
frontmatter: AgentFrontmatter;
|
|
61
|
+
/** The markdown body below the frontmatter — becomes the systemPrompt. */
|
|
62
|
+
body: string;
|
|
63
|
+
/** Absolute path of the source `.judge.md` file (legacy `.agent.md`). */
|
|
64
|
+
sourcePath: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Frontmatter Parser ─────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse YAML frontmatter from a string. Handles the subset of YAML used
|
|
71
|
+
* by agent files: simple key-value pairs, quoted strings, and multi-line
|
|
72
|
+
* `>` folded scalars. No arrays, nested objects, or anchors.
|
|
73
|
+
*/
|
|
74
|
+
export function parseFrontmatter(raw: string): { meta: Record<string, string>; body: string } {
|
|
75
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
76
|
+
if (!match) {
|
|
77
|
+
return { meta: {}, body: raw };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const yamlBlock = match[1];
|
|
81
|
+
const body = match[2].trim();
|
|
82
|
+
const meta: Record<string, string> = {};
|
|
83
|
+
|
|
84
|
+
const lines = yamlBlock.split(/\r?\n/);
|
|
85
|
+
let i = 0;
|
|
86
|
+
|
|
87
|
+
while (i < lines.length) {
|
|
88
|
+
const line = lines[i];
|
|
89
|
+
|
|
90
|
+
// Skip empty lines and comments
|
|
91
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)/);
|
|
97
|
+
if (!kvMatch) {
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const key = kvMatch[1];
|
|
103
|
+
let value = kvMatch[2].trim();
|
|
104
|
+
|
|
105
|
+
// Handle folded scalar (>)
|
|
106
|
+
if (value === ">") {
|
|
107
|
+
const parts: string[] = [];
|
|
108
|
+
i++;
|
|
109
|
+
while (i < lines.length && (lines[i].startsWith(" ") || lines[i].trim() === "")) {
|
|
110
|
+
if (lines[i].trim() === "") {
|
|
111
|
+
parts.push("");
|
|
112
|
+
} else {
|
|
113
|
+
parts.push(lines[i].trimStart());
|
|
114
|
+
}
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
// Folded scalar: join non-empty lines with spaces, blank lines become newlines
|
|
118
|
+
value = parts
|
|
119
|
+
.reduce<string[]>((acc, part) => {
|
|
120
|
+
if (part === "") {
|
|
121
|
+
acc.push("\n");
|
|
122
|
+
} else if (acc.length > 0 && acc[acc.length - 1] !== "\n") {
|
|
123
|
+
acc[acc.length - 1] += " " + part;
|
|
124
|
+
} else {
|
|
125
|
+
acc.push(part);
|
|
126
|
+
}
|
|
127
|
+
return acc;
|
|
128
|
+
}, [])
|
|
129
|
+
.join("")
|
|
130
|
+
.trim();
|
|
131
|
+
} else {
|
|
132
|
+
// Strip surrounding quotes
|
|
133
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
134
|
+
value = value.slice(1, -1);
|
|
135
|
+
}
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
meta[key] = value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { meta, body };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
const REQUIRED_FIELDS: (keyof AgentFrontmatter)[] = [
|
|
148
|
+
"id",
|
|
149
|
+
"name",
|
|
150
|
+
"domain",
|
|
151
|
+
"rulePrefix",
|
|
152
|
+
"description",
|
|
153
|
+
"tableDescription",
|
|
154
|
+
"promptDescription",
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validate and coerce parsed frontmatter into a typed AgentFrontmatter.
|
|
159
|
+
* Throws on missing required fields.
|
|
160
|
+
*/
|
|
161
|
+
export function validateFrontmatter(meta: Record<string, string>, sourcePath: string): AgentFrontmatter {
|
|
162
|
+
for (const field of REQUIRED_FIELDS) {
|
|
163
|
+
if (!meta[field]) {
|
|
164
|
+
throw new Error(`Agent file ${sourcePath} is missing required field: "${field}"`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
id: meta.id,
|
|
170
|
+
name: meta.name,
|
|
171
|
+
domain: meta.domain,
|
|
172
|
+
rulePrefix: meta.rulePrefix,
|
|
173
|
+
description: meta.description,
|
|
174
|
+
tableDescription: meta.tableDescription,
|
|
175
|
+
promptDescription: meta.promptDescription,
|
|
176
|
+
script: meta.script || undefined,
|
|
177
|
+
priority: meta.priority ? parseInt(meta.priority, 10) : 10,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Agent File Parsing ──────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse a single `.judge.md` file into its frontmatter and body (legacy `.agent.md`).
|
|
185
|
+
*/
|
|
186
|
+
export function parseAgentFile(filePath: string): ParsedAgent {
|
|
187
|
+
const absPath = resolve(filePath);
|
|
188
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
189
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
190
|
+
const frontmatter = validateFrontmatter(meta, absPath);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
frontmatter,
|
|
194
|
+
body,
|
|
195
|
+
sourcePath: absPath,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Evaluator Resolution ────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the `script` path to an analyze function.
|
|
203
|
+
*
|
|
204
|
+
* Requirements:
|
|
205
|
+
* - Synchronous (to support existing synchronous evaluation paths)
|
|
206
|
+
* - Works both from source (`tsx`/ts-node) and compiled `dist`
|
|
207
|
+
*/
|
|
208
|
+
type AnalyzeFn = (code: string, language: string, context?: AnalyzeContext) => Finding[];
|
|
209
|
+
|
|
210
|
+
export function resolveEvaluator(agent: ParsedAgent): AnalyzeFn | undefined {
|
|
211
|
+
if (!agent.frontmatter.script) return undefined;
|
|
212
|
+
|
|
213
|
+
const scriptPath = resolve(dirname(agent.sourcePath), agent.frontmatter.script);
|
|
214
|
+
const candidatePaths: string[] = [
|
|
215
|
+
scriptPath,
|
|
216
|
+
scriptPath.replace(/\.ts$/, ".js"),
|
|
217
|
+
scriptPath
|
|
218
|
+
.replace(/\\src\\/g, "\\dist\\")
|
|
219
|
+
.replace(/\/src\//g, "/dist/")
|
|
220
|
+
.replace(/\.ts$/, ".js"),
|
|
221
|
+
resolve(process.cwd(), "dist", "evaluators", `${agent.frontmatter.id}.js`),
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const req = createRequire(import.meta.url);
|
|
225
|
+
for (const candidate of candidatePaths) {
|
|
226
|
+
try {
|
|
227
|
+
const mod = req(candidate) as Record<string, unknown>;
|
|
228
|
+
const pascalId = agent.frontmatter.id
|
|
229
|
+
.split("-")
|
|
230
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
231
|
+
.join("");
|
|
232
|
+
const fnName = `analyze${pascalId}`;
|
|
233
|
+
const maybeFn = mod?.[fnName];
|
|
234
|
+
if (typeof maybeFn === "function") return maybeFn as AnalyzeFn;
|
|
235
|
+
for (const key of Object.keys(mod || {})) {
|
|
236
|
+
const candidateFn = mod[key];
|
|
237
|
+
if (typeof candidateFn === "function" && key.startsWith("analyze")) return candidateFn as AnalyzeFn;
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// swallow and try next
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── Conversion to JudgeDefinition ───────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Convert a parsed agent file to a JudgeDefinition, reconstructing the
|
|
250
|
+
* systemPrompt from the markdown body with the standard adversarial
|
|
251
|
+
* mandate appended.
|
|
252
|
+
*/
|
|
253
|
+
export function agentToJudgeDefinition(
|
|
254
|
+
agent: ParsedAgent,
|
|
255
|
+
analyze?: (code: string, language: string, context?: AnalyzeContext) => Finding[],
|
|
256
|
+
): JudgeDefinition {
|
|
257
|
+
const fm = agent.frontmatter;
|
|
258
|
+
|
|
259
|
+
// The markdown body IS the system prompt content. We prepend the persona
|
|
260
|
+
// line (which is typically the first line of the body) and leave the
|
|
261
|
+
// rest as structured evaluation criteria.
|
|
262
|
+
const systemPrompt = agent.body;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
id: fm.id,
|
|
266
|
+
name: fm.name,
|
|
267
|
+
domain: fm.domain,
|
|
268
|
+
description: fm.description,
|
|
269
|
+
rulePrefix: fm.rulePrefix,
|
|
270
|
+
tableDescription: fm.tableDescription,
|
|
271
|
+
promptDescription: fm.promptDescription,
|
|
272
|
+
systemPrompt,
|
|
273
|
+
...(analyze ? { analyze } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Directory Loading ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Load all `.judge.md` files from a directory (legacy `.agent.md` supported)
|
|
281
|
+
* and return parsed agents sorted by priority (ascending — lower number =
|
|
282
|
+
* earlier in pipeline).
|
|
283
|
+
*/
|
|
284
|
+
export function loadAgentDirectory(dirPath: string): ParsedAgent[] {
|
|
285
|
+
const absDir = resolve(dirPath);
|
|
286
|
+
if (!existsSync(absDir)) return [];
|
|
287
|
+
|
|
288
|
+
const files = readdirSync(absDir).filter((f) => /\.(agent|judge)\.md$/i.test(f));
|
|
289
|
+
|
|
290
|
+
return files
|
|
291
|
+
.map((f) => parseAgentFile(join(absDir, f)))
|
|
292
|
+
.sort((a, b) => (a.frontmatter.priority ?? 10) - (b.frontmatter.priority ?? 10));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Load all agent files from a directory and register them with the
|
|
297
|
+
* JudgeRegistry. This is the main entry point for the hybrid phase.
|
|
298
|
+
*
|
|
299
|
+
* Returns the number of agents loaded.
|
|
300
|
+
*/
|
|
301
|
+
export function loadAndRegisterAgents(
|
|
302
|
+
dirPath: string,
|
|
303
|
+
registry: {
|
|
304
|
+
register: (judge: JudgeDefinition) => void;
|
|
305
|
+
getJudge: (id: string) => JudgeDefinition | undefined;
|
|
306
|
+
},
|
|
307
|
+
): number {
|
|
308
|
+
const agents = loadAgentDirectory(dirPath);
|
|
309
|
+
let count = 0;
|
|
310
|
+
|
|
311
|
+
for (const agent of agents) {
|
|
312
|
+
// Skip if a judge with this ID already exists (built-ins or previously loaded agents)
|
|
313
|
+
if (registry.getJudge(agent.frontmatter.id)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const analyze = resolveEvaluator(agent);
|
|
318
|
+
const judge = agentToJudgeDefinition(agent, analyze);
|
|
319
|
+
registry.register(judge);
|
|
320
|
+
count++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return count;
|
|
324
|
+
}
|